Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 166 additions & 10 deletions src/api/providers/__tests__/anthropic-vertex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,13 @@ describe("VertexHandler", () => {
],
stream: true,
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
tools: expect.any(Array),
tool_choice: expect.any(Object),
tools: [],
tool_choice: {
disable_parallel_tool_use: false,
type: "auto",
},
}),
undefined,
{},
)
})

Expand Down Expand Up @@ -481,7 +484,7 @@ describe("VertexHandler", () => {
}),
],
}),
undefined,
{},
)
})

Expand Down Expand Up @@ -1156,7 +1159,7 @@ describe("VertexHandler", () => {
}

// Verify the API was called without the beta header
expect(mockCreate).toHaveBeenCalledWith(expect.anything(), undefined)
expect(mockCreate).toHaveBeenCalledWith(expect.anything(), {})
})
})

Expand Down Expand Up @@ -1246,7 +1249,7 @@ describe("VertexHandler", () => {
thinking: { type: "enabled", budget_tokens: 4096 },
temperature: 1.0, // Thinking requires temperature 1.0
}),
undefined,
{},
)
})

Expand All @@ -1273,7 +1276,7 @@ describe("VertexHandler", () => {
expect.objectContaining({
thinking: { type: "adaptive" },
}),
undefined,
{},
)

const request = mockCreate.mock.calls[0][0]
Expand Down Expand Up @@ -1302,7 +1305,7 @@ describe("VertexHandler", () => {
expect.objectContaining({
thinking: { type: "adaptive" },
}),
undefined,
{},
)

const request = mockCreate.mock.calls[0][0]
Expand Down Expand Up @@ -1393,7 +1396,7 @@ describe("VertexHandler", () => {
]),
tool_choice: { type: "auto", disable_parallel_tool_use: false },
}),
undefined,
{},
)
})

Expand Down Expand Up @@ -1446,7 +1449,7 @@ describe("VertexHandler", () => {
}),
]),
}),
undefined,
{},
)
})

Expand Down Expand Up @@ -1611,4 +1614,157 @@ describe("VertexHandler", () => {
})
})
})

describe("abort signal", () => {
it("should handle abort signal triggered during request", async () => {
const controller = new AbortController()
const handler = new AnthropicVertexHandler({
apiModelId: "claude-3-sonnet",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const mockStream = async function* () {
await new Promise((resolve) => setTimeout(resolve, 10))
if (controller.signal.aborted) {
throw new Error("AbortError: The operation was aborted")
}
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 0 } },
}
}

;(handler["client"].messages as any).create = vitest.fn().mockResolvedValue(mockStream())

const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
taskId: "test",
tools: [],
abortSignal: controller.signal,
})

const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

expect(chunks.length).toBeGreaterThan(0)
})
Comment on lines +1619 to +1652

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Abort test never triggers the abort path.

This test never calls controller.abort(), so it validates normal streaming, not cancellation behavior.

Suggested fix
 it("should handle abort signal triggered during request", async () => {
   const controller = new AbortController()
   const handler = new AnthropicVertexHandler({
@@
   const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
@@
     abortSignal: controller.signal,
   })
-
-  const chunks: any[] = []
-  for await (const chunk of stream) {
-    chunks.push(chunk)
-  }
-
-  expect(chunks.length).toBeGreaterThan(0)
+  setTimeout(() => controller.abort(), 20)
+  await expect(async () => {
+    for await (const _chunk of stream) {
+      // consume stream
+    }
+  }).rejects.toThrow(/abort/i)
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should handle abort signal triggered during request", async () => {
const controller = new AbortController()
const handler = new AnthropicVertexHandler({
apiModelId: "claude-3-sonnet",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})
const mockStream = async function* () {
await new Promise((resolve) => setTimeout(resolve, 10))
if (controller.signal.aborted) {
throw new Error("AbortError: The operation was aborted")
}
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 0 } },
}
}
;(handler["client"].messages as any).create = vitest.fn().mockResolvedValue(mockStream())
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
taskId: "test",
tools: [],
abortSignal: controller.signal,
})
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(chunks.length).toBeGreaterThan(0)
})
it("should handle abort signal triggered during request", async () => {
const controller = new AbortController()
const handler = new AnthropicVertexHandler({
apiModelId: "claude-3-sonnet",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})
const mockStream = async function* () {
await new Promise((resolve) => setTimeout(resolve, 10))
if (controller.signal.aborted) {
throw new Error("AbortError: The operation was aborted")
}
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 0 } },
}
}
;(handler["client"].messages as any).create = vitest.fn().mockResolvedValue(mockStream())
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
taskId: "test",
tools: [],
abortSignal: controller.signal,
})
setTimeout(() => controller.abort(), 20)
await expect(async () => {
for await (const _chunk of stream) {
// consume stream
}
}).rejects.toThrow(/abort/i)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/anthropic-vertex.spec.ts` around lines 1616 -
1649, The test "should handle abort signal triggered during request" never
actually triggers the abort path because controller.abort() is never called. To
fix this, add a call to controller.abort() within the loop that iterates through
the stream chunks (the for await loop iterating over stream) to actually trigger
the abort signal and validate that the handler properly handles the aborted
signal. Consider aborting the controller after collecting one or more chunks to
test the cancellation behavior, then verify that the AbortError is properly
caught or that the streaming stops as expected.


it("should not pass signal when abortSignal is undefined", async () => {
const handler = new AnthropicVertexHandler({
apiModelId: "claude-3-sonnet",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const mockStream = async function* () {
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 5 } },
}
yield {
type: "content_block_start",
content_block: { type: "text", text: "" },
}
yield {
type: "content_block_delta",
delta: { type: "text_delta", text: "response" },
}
}

;(handler["client"].messages as any).create = vitest.fn().mockResolvedValue(mockStream())

const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }])

const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

expect(chunks.length).toBeGreaterThan(0)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("should abort immediately if signal is already aborted", async () => {
const controller = new AbortController()
controller.abort()

const testHandler = new AnthropicVertexHandler({
apiModelId: "claude-3-sonnet",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

testHandler["client"].messages.create = vitest.fn().mockImplementation(async (options, requestOptions) => {
// Verify that the signal was passed and is already aborted
expect(requestOptions).toHaveProperty("signal", controller.signal)
expect(controller.signal.aborted).toBe(true)

return {
[Symbol.asyncIterator]: async function* () {
if (controller.signal.aborted) {
throw new Error("AbortError: The operation was aborted")
}
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 5 } },
}
},
}
})

const stream = testHandler.createMessage("system", [{ role: "user", content: "Hello" }], {
taskId: "test",
tools: [],
abortSignal: controller.signal,
})

await expect(async () => {
for await (const _chunk of stream) {
// consume stream
}
}).rejects.toThrow(/abort/i)
})

it("should pass signal when provided", async () => {
const controller = new AbortController()
let capturedRequestOptions: any

const testHandler = new AnthropicVertexHandler({
apiModelId: "claude-3-sonnet",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

testHandler["client"].messages.create = vitest.fn().mockImplementation(async (options, requestOptions) => {
capturedRequestOptions = requestOptions
return {
[Symbol.asyncIterator]: async function* () {
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 5 } },
}
yield {
type: "content_block_delta",
delta: { type: "text_delta", text: "response" },
}
},
}
})

const stream = testHandler.createMessage("system", [{ role: "user", content: "Hello" }], {
taskId: "test",
tools: [],
abortSignal: controller.signal,
})

const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

expect(chunks.length).toBeGreaterThan(0)
expect(capturedRequestOptions).toHaveProperty("signal", controller.signal)
})
})
})
74 changes: 74 additions & 0 deletions src/api/providers/__tests__/anthropic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,4 +1057,78 @@ describe("AnthropicHandler", () => {
})
})
})

describe("abort signal", () => {
it("should pass abortSignal to the SDK options", async () => {
const controller = new AbortController()

mockCreate.mockImplementation(async (options, requestOptions) => {
// Verify that the signal was passed
expect(requestOptions).toHaveProperty("signal", controller.signal)
return {
async *[Symbol.asyncIterator]() {
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 5 } },
}
yield {
type: "content_block_delta",
delta: { type: "text_delta", text: "response" },
}
},
}
})

const handler = new AnthropicHandler(mockOptions)
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }], {
taskId: "test",
tools: [],
abortSignal: controller.signal,
})

const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

expect(chunks.length).toBeGreaterThan(0)
})

it("should work normally without abortSignal", async () => {
const handler = new AnthropicHandler(mockOptions)
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }])

const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

expect(chunks.length).toBeGreaterThan(0)
})

it("should not pass signal when abortSignal is undefined", async () => {
mockCreate.mockImplementation(async (options, requestOptions) => {
// When no abortSignal is provided, requestOptions should be undefined or not have signal
expect(requestOptions).toBeUndefined()
return {
Comment on lines +1109 to +1113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert “no signal”, not “no options object”.

Line 1112 is brittle: provider request options may include non-signal fields (e.g., headers) even when abortSignal is absent. Assert only that signal is missing.

Suggested test fix
- expect(requestOptions).toBeUndefined()
+ expect(requestOptions?.signal).toBeUndefined()

As per coding guidelines, "Use package-local unit tests for pure logic, parsing, state transitions, validation, serialization, request construction, retry decisions, and error handling."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should not pass signal when abortSignal is undefined", async () => {
mockCreate.mockImplementation(async (options, requestOptions) => {
// When no abortSignal is provided, requestOptions should be undefined or not have signal
expect(requestOptions).toBeUndefined()
return {
it("should not pass signal when abortSignal is undefined", async () => {
mockCreate.mockImplementation(async (options, requestOptions) => {
// When no abortSignal is provided, requestOptions should be undefined or not have signal
expect(requestOptions?.signal).toBeUndefined()
return {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/anthropic.spec.ts` around lines 1109 - 1113, The
test assertion in the mockCreate implementation is overly strict by checking
that the entire requestOptions object is undefined, when it should only verify
that the signal property is absent. Modify the expect statement to assert that
requestOptions.signal is undefined rather than asserting that requestOptions
itself is undefined, as other request option fields (like headers) may still be
present even when abortSignal is not provided.

Source: Coding guidelines

async *[Symbol.asyncIterator]() {
yield {
type: "message_start",
message: { usage: { input_tokens: 10, output_tokens: 5 } },
}
},
}
})

const handler = new AnthropicHandler(mockOptions)
const stream = handler.createMessage("system", [{ role: "user", content: "Hello" }])

const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

expect(chunks.length).toBeGreaterThan(0)
})
})
})
Loading
Loading