Skip to content

Commit 430e483

Browse files
committed
✅ test(providers): add abort signal assertions and coverage tests (#404)
1 parent 3d43548 commit 430e483

56 files changed

Lines changed: 4581 additions & 2256 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/api/providers/__tests__/anthropic-vertex.spec.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ describe("VertexHandler", () => {
197197
tools: expect.any(Array),
198198
tool_choice: expect.any(Object),
199199
}),
200-
undefined,
200+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
201201
)
202202
})
203203

@@ -412,7 +412,7 @@ describe("VertexHandler", () => {
412412
}),
413413
],
414414
}),
415-
undefined,
415+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
416416
)
417417
})
418418

@@ -765,18 +765,21 @@ describe("VertexHandler", () => {
765765

766766
const result = await handler.completePrompt("Test prompt")
767767
expect(result).toBe("Test response")
768-
expect(handler["client"].messages.create).toHaveBeenCalledWith({
769-
model: "claude-3-5-sonnet-v2@20241022",
770-
max_tokens: 8192,
771-
temperature: 0,
772-
messages: [
773-
{
774-
role: "user",
775-
content: [{ type: "text", text: "Test prompt", cache_control: { type: "ephemeral" } }],
776-
},
777-
],
778-
stream: false,
779-
})
768+
expect(handler["client"].messages.create).toHaveBeenCalledWith(
769+
{
770+
model: "claude-3-5-sonnet-v2@20241022",
771+
max_tokens: 8192,
772+
temperature: 0,
773+
messages: [
774+
{
775+
role: "user",
776+
content: [{ type: "text", text: "Test prompt", cache_control: { type: "ephemeral" } }],
777+
},
778+
],
779+
stream: false,
780+
},
781+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
782+
)
780783
})
781784

782785
it("should handle API errors for Claude", async () => {
@@ -795,6 +798,19 @@ describe("VertexHandler", () => {
795798
)
796799
})
797800

801+
it("should rethrow non-Error values without wrapping", async () => {
802+
handler = new AnthropicVertexHandler({
803+
apiModelId: "claude-3-5-sonnet-v2@20241022",
804+
vertexProjectId: "test-project",
805+
vertexRegion: "us-central1",
806+
})
807+
808+
const mockCreate = vitest.fn().mockRejectedValue("raw error")
809+
;(handler["client"].messages as any).create = mockCreate
810+
811+
await expect(handler.completePrompt("Test prompt")).rejects.toBe("raw error")
812+
})
813+
798814
it("should handle non-text content for Claude", async () => {
799815
handler = new AnthropicVertexHandler({
800816
apiModelId: "claude-3-5-sonnet-v2@20241022",
@@ -1087,7 +1103,10 @@ describe("VertexHandler", () => {
10871103
}
10881104

10891105
// Verify the API was called without the beta header
1090-
expect(mockCreate).toHaveBeenCalledWith(expect.anything(), undefined)
1106+
expect(mockCreate).toHaveBeenCalledWith(
1107+
expect.anything(),
1108+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
1109+
)
10911110
})
10921111
})
10931112

@@ -1177,7 +1196,7 @@ describe("VertexHandler", () => {
11771196
thinking: { type: "enabled", budget_tokens: 4096 },
11781197
temperature: 1.0, // Thinking requires temperature 1.0
11791198
}),
1180-
undefined,
1199+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
11811200
)
11821201
})
11831202

@@ -1204,7 +1223,7 @@ describe("VertexHandler", () => {
12041223
expect.objectContaining({
12051224
thinking: { type: "adaptive" },
12061225
}),
1207-
undefined,
1226+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
12081227
)
12091228

12101229
const request = mockCreate.mock.calls[0][0]
@@ -1233,7 +1252,7 @@ describe("VertexHandler", () => {
12331252
expect.objectContaining({
12341253
thinking: { type: "adaptive" },
12351254
}),
1236-
undefined,
1255+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
12371256
)
12381257

12391258
const request = mockCreate.mock.calls[0][0]
@@ -1324,7 +1343,7 @@ describe("VertexHandler", () => {
13241343
]),
13251344
tool_choice: { type: "auto", disable_parallel_tool_use: false },
13261345
}),
1327-
undefined,
1346+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
13281347
)
13291348
})
13301349

@@ -1377,7 +1396,7 @@ describe("VertexHandler", () => {
13771396
}),
13781397
]),
13791398
}),
1380-
undefined,
1399+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
13811400
)
13821401
})
13831402

src/api/providers/__tests__/anthropic.spec.ts

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -428,20 +428,77 @@ describe("AnthropicHandler", () => {
428428
expect(requestBody?.max_tokens).toBe(32768)
429429
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
430430
})
431+
432+
it("should pass signal for models outside the prompt-caching list", async () => {
433+
// Mock getModel to return an ID not in the createMessage outer switch,
434+
// so it hits the default (non-caching) path which passes { signal }.
435+
const modelInfo = handler.getModel()
436+
vitest.spyOn(handler, "getModel").mockReturnValue({
437+
...modelInfo,
438+
id: "non-cached-model",
439+
} as any)
440+
441+
const stream = handler.createMessage(systemPrompt, [
442+
{
443+
role: "user",
444+
content: [{ type: "text" as const, text: "Hello" }],
445+
},
446+
])
447+
448+
for await (const _chunk of stream) {
449+
// Consume stream
450+
}
451+
452+
// Verify messages.create was called with { signal } (no prompt-caching headers)
453+
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
454+
expect(requestOptions).toEqual(expect.objectContaining({ signal: expect.any(AbortSignal) }))
455+
expect(requestOptions?.headers).toBeUndefined()
456+
})
457+
458+
it("should pass signal when inner prompt-caching switch hits default", async () => {
459+
// Mock getModel to return the coverage-inner-default model, which
460+
// is in the outer prompt-caching switch but NOT in the inner
461+
// prompt-caching beta switch, so the inner default returns { signal }.
462+
const modelInfo = handler.getModel()
463+
vitest.spyOn(handler, "getModel").mockReturnValue({
464+
...modelInfo,
465+
id: "coverage-inner-default",
466+
} as any)
467+
468+
const stream = handler.createMessage(systemPrompt, [
469+
{
470+
role: "user",
471+
content: [{ type: "text" as const, text: "Hello" }],
472+
},
473+
])
474+
475+
for await (const _chunk of stream) {
476+
// Consume stream
477+
}
478+
479+
// Verify messages.create was called with { signal } and no
480+
// prompt-caching beta header (the inner switch default path).
481+
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
482+
expect(requestOptions).toEqual(expect.objectContaining({ signal: expect.any(AbortSignal) }))
483+
expect(requestOptions?.headers).toBeUndefined()
484+
})
431485
})
432486

433487
describe("completePrompt", () => {
434488
it("should complete prompt successfully", async () => {
435489
const result = await handler.completePrompt("Test prompt")
436490
expect(result).toBe("Test response")
437-
expect(mockCreate).toHaveBeenCalledWith({
438-
model: mockOptions.apiModelId,
439-
messages: [{ role: "user", content: "Test prompt" }],
440-
max_tokens: 8192,
441-
temperature: 0,
442-
thinking: undefined,
443-
stream: false,
444-
})
491+
expect(mockCreate).toHaveBeenCalledWith(
492+
{
493+
model: mockOptions.apiModelId,
494+
messages: [{ role: "user", content: "Test prompt" }],
495+
max_tokens: 8192,
496+
temperature: 0,
497+
thinking: undefined,
498+
stream: false,
499+
},
500+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
501+
)
445502
})
446503

447504
it("should handle API errors", async () => {
@@ -1057,4 +1114,76 @@ describe("AnthropicHandler", () => {
10571114
})
10581115
})
10591116
})
1117+
1118+
describe("content_block_start and content_block_delta coverage", () => {
1119+
it("should handle thinking content_block_start", async () => {
1120+
mockCreate.mockImplementationOnce(async () => ({
1121+
async *[Symbol.asyncIterator]() {
1122+
yield { type: "message_start", message: { usage: { input_tokens: 1, output_tokens: 1 } } }
1123+
yield {
1124+
type: "content_block_start",
1125+
index: 0,
1126+
content_block: { type: "thinking", thinking: "let me think" },
1127+
}
1128+
yield { type: "message_stop" }
1129+
},
1130+
}))
1131+
1132+
const stream = handler.createMessage("sys", [{ role: "user", content: "hi" }])
1133+
const chunks: any[] = []
1134+
for await (const c of stream) {
1135+
chunks.push(c)
1136+
}
1137+
const reasoning = chunks.filter((c) => c.type === "reasoning")
1138+
expect(reasoning.some((c) => c.text === "let me think")).toBe(true)
1139+
})
1140+
1141+
it("should insert newline for second text content_block_start", async () => {
1142+
mockCreate.mockImplementationOnce(async () => ({
1143+
async *[Symbol.asyncIterator]() {
1144+
yield { type: "message_start", message: { usage: { input_tokens: 1, output_tokens: 1 } } }
1145+
yield {
1146+
type: "content_block_start",
1147+
index: 0,
1148+
content_block: { type: "text", text: "first" },
1149+
}
1150+
yield {
1151+
type: "content_block_start",
1152+
index: 1,
1153+
content_block: { type: "text", text: "" },
1154+
}
1155+
yield { type: "message_stop" }
1156+
},
1157+
}))
1158+
1159+
const stream = handler.createMessage("sys", [{ role: "user", content: "hi" }])
1160+
const chunks: any[] = []
1161+
for await (const c of stream) {
1162+
chunks.push(c)
1163+
}
1164+
const textChunks = chunks.filter((c) => c.type === "text")
1165+
expect(textChunks.some((c) => c.text === "\n")).toBe(true)
1166+
})
1167+
1168+
it("should handle thinking_delta", async () => {
1169+
mockCreate.mockImplementationOnce(async () => ({
1170+
async *[Symbol.asyncIterator]() {
1171+
yield { type: "message_start", message: { usage: { input_tokens: 1, output_tokens: 1 } } }
1172+
yield {
1173+
type: "content_block_delta",
1174+
delta: { type: "thinking_delta", thinking: "hmm" },
1175+
}
1176+
yield { type: "message_stop" }
1177+
},
1178+
}))
1179+
1180+
const stream = handler.createMessage("sys", [{ role: "user", content: "hi" }])
1181+
const chunks: any[] = []
1182+
for await (const c of stream) {
1183+
chunks.push(c)
1184+
}
1185+
const reasoning = chunks.filter((c) => c.type === "reasoning")
1186+
expect(reasoning.some((c) => c.text === "hmm")).toBe(true)
1187+
})
1188+
})
10601189
})

0 commit comments

Comments
 (0)