Skip to content

Commit 24de47f

Browse files
committed
test: close remaining Interactions coverage gaps from PR review
- Streaming builders: malformed tool-call arguments degrade to a valid '{}' arguments_delta fragment and emit a warning. - Tool-call and content+tools streams: assert usage propagates and the terminal status is requires_action on interaction.completed (unit + e2e). - Collapser: pin the ordering behavior when an arguments_delta arrives before its step.start — the early fragment is flagged via droppedChunks rather than mis-attributed, and the call still surfaces with empty args.
1 parent 78d00b6 commit 24de47f

1 file changed

Lines changed: 79 additions & 1 deletion

File tree

src/__tests__/gemini-interactions.test.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, afterEach, beforeEach } from "vitest";
1+
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
22
import * as http from "node:http";
33
import type { Fixture } from "../types.js";
44
import { createServer, type ServerInstance } from "../server.js";
@@ -937,6 +937,63 @@ describe("SSE event builders", () => {
937937
expect((deltas[1].delta as Record<string, unknown>).text).toBe("DEF");
938938
expect((deltas[2].delta as Record<string, unknown>).text).toBe("GH");
939939
});
940+
941+
it("falls back to empty args and warns on malformed tool-call arguments (streaming)", () => {
942+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
943+
try {
944+
const events = buildInteractionsToolCallSSEEvents(
945+
[{ name: "fn", arguments: "not-json", id: "call_1" }],
946+
"aimock-int-0",
947+
new Logger("warn"),
948+
);
949+
const argDelta = events.find(
950+
(e) =>
951+
e.event_type === "step.delta" &&
952+
(e.delta as Record<string, unknown>).type === "arguments_delta",
953+
)!;
954+
// Malformed args degrade to a valid empty-object fragment, not garbage.
955+
expect((argDelta.delta as Record<string, unknown>).arguments).toBe("{}");
956+
expect(warnSpy).toHaveBeenCalled();
957+
} finally {
958+
warnSpy.mockRestore();
959+
}
960+
});
961+
962+
it("emits usage and requires_action status on interaction.completed for tool calls", () => {
963+
const events = buildInteractionsToolCallSSEEvents(
964+
[{ name: "fn", arguments: '{"a":1}', id: "call_1" }],
965+
"aimock-int-0",
966+
logger,
967+
{ usage: { input_tokens: 3, output_tokens: 7 } },
968+
);
969+
const completed = events.find((e) => e.event_type === "interaction.completed")!;
970+
const interaction = completed.interaction as Record<string, unknown>;
971+
expect(interaction.status).toBe("requires_action");
972+
expect(interaction.usage).toEqual({
973+
total_input_tokens: 3,
974+
total_output_tokens: 7,
975+
total_tokens: 10,
976+
});
977+
});
978+
979+
it("emits usage and requires_action status on interaction.completed for content+tools", () => {
980+
const events = buildInteractionsContentWithToolCallsSSEEvents(
981+
"Text",
982+
[{ name: "fn", arguments: "{}", id: "call_1" }],
983+
"aimock-int-0",
984+
100,
985+
logger,
986+
{ usage: { input_tokens: 2, output_tokens: 4 } },
987+
);
988+
const completed = events.find((e) => e.event_type === "interaction.completed")!;
989+
const interaction = completed.interaction as Record<string, unknown>;
990+
expect(interaction.status).toBe("requires_action");
991+
expect(interaction.usage).toEqual({
992+
total_input_tokens: 2,
993+
total_output_tokens: 4,
994+
total_tokens: 6,
995+
});
996+
});
940997
});
941998

942999
// ─── Integration tests: non-streaming ───────────────────────────────────
@@ -1155,6 +1212,10 @@ describe("Gemini Interactions — streaming", () => {
11551212
expect(argDeltas).toHaveLength(1);
11561213
const argsStr = (argDeltas[0].delta as Record<string, unknown>).arguments as string;
11571214
expect(JSON.parse(argsStr)).toEqual({ city: "NYC" });
1215+
1216+
// The streamed interaction terminates in requires_action for a tool call.
1217+
const completed = events.find((e) => e.event_type === "interaction.completed")!;
1218+
expect((completed.interaction as Record<string, unknown>).status).toBe("requires_action");
11581219
});
11591220

11601221
it("assigns correct indices for content+tools stream", async () => {
@@ -1412,6 +1473,23 @@ describe("collapseGeminiInteractionsSSE", () => {
14121473
expect(result.firstDroppedSample).toMatch(/not valid JSON/);
14131474
});
14141475

1476+
it("drops an arguments_delta that arrives before its step.start (ordering)", () => {
1477+
// The SDK emits step.start before its arguments_delta fragments; a fragment
1478+
// arriving first can't correlate, so it is flagged rather than mis-attributed.
1479+
const sse = [
1480+
'data: {"event_type":"step.delta","index":0,"delta":{"type":"arguments_delta","arguments":"{\\"x\\":1}"},"event_id":"evt_1"}',
1481+
'data: {"event_type":"step.start","index":0,"step":{"type":"function_call","id":"call_1","name":"fn","arguments":{}},"event_id":"evt_2"}',
1482+
'data: {"event_type":"step.stop","index":0,"event_id":"evt_3"}',
1483+
].join("\n\n");
1484+
const result = collapseGeminiInteractionsSSE(sse);
1485+
expect(result.droppedChunks).toBe(1);
1486+
expect(result.firstDroppedSample).toMatch(/no correlating step\.start/);
1487+
// The call still surfaces (identity preserved), opening with empty args.
1488+
expect(result.toolCalls).toHaveLength(1);
1489+
expect(result.toolCalls![0].name).toBe("fn");
1490+
expect(result.toolCalls![0].arguments).toBe("{}");
1491+
});
1492+
14151493
it("preserves identity of a function_call step.start that arrives without an index", () => {
14161494
const sse = [
14171495
'data: {"event_type":"step.start","step":{"type":"function_call","id":"call_1","name":"fn","arguments":{"x":1}},"event_id":"evt_1"}',

0 commit comments

Comments
 (0)