Skip to content

Commit 4f32169

Browse files
committed
test: comprehensive response override and auto-stringify coverage
55 new tests across 4 test files: - response-overrides.test.ts: override propagation for all providers (streaming + non-streaming), cross-provider finishReason mappings (stop, tool_calls, length, content_filter), usage auto-sum and partial merge per provider, CWTC with overrides/reasoning/webSearches, extractOverrides unit tests - fixture-loader.test.ts: auto-stringify edge cases (arrays, null, mixed types, immutability), validation for all override fields, ContentWithToolCallsResponse validation, unknown field/usage detection - content-with-toolcalls.test.ts: multi-tool-call streaming for OpenAI/Claude/Gemini, type guard mutual exclusivity - llmock.test.ts: programmatic API override passthrough via on()
1 parent 3290c43 commit 4f32169

4 files changed

Lines changed: 2101 additions & 5 deletions

File tree

src/__tests__/content-with-toolcalls.test.ts

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ describe("isContentWithToolCallsResponse", () => {
2727
expect(isContentWithToolCallsResponse(r)).toBe(false);
2828
});
2929

30-
it("existing guards still work for combined response", () => {
30+
it("existing guards are mutually exclusive with combined response", () => {
3131
const r = {
3232
content: "Hello",
3333
toolCalls: [{ name: "get_weather", arguments: "{}" }],
3434
};
35-
// Both existing guards would match — that's why we check combined first
36-
expect(isTextResponse(r)).toBe(true);
37-
expect(isToolCallResponse(r)).toBe(true);
35+
// Guards are mutually exclusive — combined response only matches isContentWithToolCallsResponse
36+
expect(isTextResponse(r)).toBe(false);
37+
expect(isToolCallResponse(r)).toBe(false);
38+
expect(isContentWithToolCallsResponse(r)).toBe(true);
3839
});
3940
});
4041

@@ -426,6 +427,139 @@ describe("Gemini — content + toolCalls", () => {
426427
});
427428
});
428429

430+
describe("Gemini — multi-tool-call CWTC", () => {
431+
let mock: LLMock | null = null;
432+
433+
afterEach(async () => {
434+
if (mock) {
435+
await mock.stop();
436+
mock = null;
437+
}
438+
});
439+
440+
it("Gemini non-streaming multi-tool-call CWTC", async () => {
441+
mock = new LLMock({ port: 0 });
442+
mock.addFixture({
443+
match: { userMessage: "test gemini multi-tc" },
444+
response: {
445+
content: "Sure, let me check.",
446+
toolCalls: [
447+
{ name: "get_weather", arguments: '{"city":"NYC"}' },
448+
{ name: "get_time", arguments: '{"tz":"EST"}' },
449+
],
450+
},
451+
});
452+
await mock.start();
453+
454+
const res = await fetch(`${mock.url}/v1beta/models/gemini-2.0-flash:generateContent`, {
455+
method: "POST",
456+
headers: { "Content-Type": "application/json" },
457+
body: JSON.stringify({
458+
contents: [{ role: "user", parts: [{ text: "test gemini multi-tc" }] }],
459+
}),
460+
});
461+
462+
const body = await res.json();
463+
const parts = body.candidates[0].content.parts;
464+
const fcParts = parts.filter((p: { functionCall?: unknown }) => p.functionCall !== undefined);
465+
expect(fcParts).toHaveLength(2);
466+
expect(fcParts[0].functionCall.name).toBe("get_weather");
467+
expect(fcParts[1].functionCall.name).toBe("get_time");
468+
});
469+
});
470+
471+
describe("Anthropic — multi-tool-call CWTC streaming", () => {
472+
let mock: LLMock | null = null;
473+
474+
afterEach(async () => {
475+
if (mock) {
476+
await mock.stop();
477+
mock = null;
478+
}
479+
});
480+
481+
it("Claude streaming multi-tool-call CWTC", async () => {
482+
mock = new LLMock({ port: 0 });
483+
mock.addFixture({
484+
match: { userMessage: "test claude multi-tc" },
485+
response: {
486+
content: "Checking.",
487+
toolCalls: [
488+
{ name: "get_weather", arguments: '{"city":"NYC"}' },
489+
{ name: "get_time", arguments: '{"tz":"EST"}' },
490+
],
491+
},
492+
});
493+
await mock.start();
494+
495+
const res = await fetch(`${mock.url}/v1/messages`, {
496+
method: "POST",
497+
headers: {
498+
"Content-Type": "application/json",
499+
"x-api-key": "test-key",
500+
"anthropic-version": "2023-06-01",
501+
},
502+
body: JSON.stringify({
503+
model: "claude-sonnet-4-20250514",
504+
max_tokens: 1024,
505+
messages: [{ role: "user", content: "test claude multi-tc" }],
506+
stream: true,
507+
}),
508+
});
509+
510+
const events = parseAnthropicSSEEvents(await res.text());
511+
const toolBlockStarts = events.filter(
512+
(e) =>
513+
e.type === "content_block_start" &&
514+
(e.content_block as { type: string })?.type === "tool_use",
515+
);
516+
expect(toolBlockStarts).toHaveLength(2);
517+
});
518+
});
519+
520+
describe("OpenAI — multi-tool-call CWTC streaming indices", () => {
521+
let mock: LLMock | null = null;
522+
523+
afterEach(async () => {
524+
if (mock) {
525+
await mock.stop();
526+
mock = null;
527+
}
528+
});
529+
530+
it("streams content then multiple tool calls with correct indices", async () => {
531+
mock = new LLMock({ port: 0 });
532+
mock.addFixture({
533+
match: { userMessage: "test multi-tc indices" },
534+
response: {
535+
content: "Here.",
536+
toolCalls: [
537+
{ name: "fn_a", arguments: '{"a":1}' },
538+
{ name: "fn_b", arguments: '{"b":2}' },
539+
],
540+
},
541+
});
542+
await mock.start();
543+
544+
const res = await fetch(`${mock.url}/v1/chat/completions`, {
545+
method: "POST",
546+
headers: { "Content-Type": "application/json", Authorization: "Bearer test" },
547+
body: JSON.stringify({
548+
model: "gpt-4o",
549+
messages: [{ role: "user", content: "test multi-tc indices" }],
550+
stream: true,
551+
}),
552+
});
553+
554+
const chunks = parseSSEChunks(await res.text());
555+
const toolChunks = chunks.filter((c) => c.choices?.[0]?.delta?.tool_calls);
556+
const indices = toolChunks.map((c) => c.choices[0].delta.tool_calls![0].index);
557+
// Should have both index 0 and index 1
558+
expect(indices).toContain(0);
559+
expect(indices).toContain(1);
560+
});
561+
});
562+
429563
import {
430564
collapseOpenAISSE,
431565
collapseAnthropicSSE,

0 commit comments

Comments
 (0)