diff --git a/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts b/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts index 2ad5dc8d61..d9fcdcb967 100644 --- a/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts +++ b/src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts @@ -7,6 +7,7 @@ import type { ApiHandlerOptions } from "../../../shared/api" import { NativeToolCallParser } from "../../../core/assistant-message/NativeToolCallParser" import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" import { Package } from "../../../shared/package" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" describe("OpenAiCodexHandler native tool calls", () => { let handler: OpenAiCodexHandler @@ -31,9 +32,9 @@ describe("OpenAiCodexHandler native tool calls", () => { // Mock OpenAI SDK streaming (preferred path). ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.output_item.added", item: { type: "function_call", @@ -42,15 +43,14 @@ describe("OpenAiCodexHandler native tool calls", () => { arguments: "", }, output_index: 0, - } - yield { + }, + { type: "response.function_call_arguments.delta", delta: '{"result":"hi"}', - // Note: intentionally omit call_id + name to simulate tool-call-only streams. item_id: "fc_1", output_index: 0, - } - yield { + }, + { type: "response.completed", response: { id: "resp_1", @@ -65,9 +65,9 @@ describe("OpenAiCodexHandler native tool calls", () => { ], usage: { input_tokens: 1, output_tokens: 1 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -104,9 +104,9 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.output_item.done", item: { type: "message", @@ -114,8 +114,8 @@ describe("OpenAiCodexHandler native tool calls", () => { content: [{ type: "output_text", text: "hello from spark" }], }, output_index: 0, - } - yield { + }, + { type: "response.completed", response: { id: "resp_done_only", @@ -129,9 +129,9 @@ describe("OpenAiCodexHandler native tool calls", () => { ], usage: { input_tokens: 1, output_tokens: 2 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -140,10 +140,7 @@ describe("OpenAiCodexHandler native tool calls", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const textChunks = chunks.filter((c) => c.type === "text") expect(textChunks.length).toBeGreaterThan(0) @@ -155,9 +152,9 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.completed", response: { id: "resp_completed_only", @@ -171,9 +168,9 @@ describe("OpenAiCodexHandler native tool calls", () => { ], usage: { input_tokens: 1, output_tokens: 2 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -182,10 +179,7 @@ describe("OpenAiCodexHandler native tool calls", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const textChunks = chunks.filter((c) => c.type === "text") expect(textChunks.length).toBeGreaterThan(0) @@ -197,13 +191,13 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.output_text.done", text: "done-event text only", - } - yield { + }, + { type: "response.completed", response: { id: "resp_done_text_only", @@ -211,9 +205,9 @@ describe("OpenAiCodexHandler native tool calls", () => { output: [], usage: { input_tokens: 1, output_tokens: 2 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -222,10 +216,7 @@ describe("OpenAiCodexHandler native tool calls", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const textChunks = chunks.filter((c) => c.type === "text") expect(textChunks.length).toBeGreaterThan(0) @@ -237,9 +228,9 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.output_item.done", item: { type: "function_call", @@ -248,8 +239,8 @@ describe("OpenAiCodexHandler native tool calls", () => { arguments: '{"result":"ok"}', }, output_index: 0, - } - yield { + }, + { type: "response.completed", response: { id: "resp_done_tool_only", @@ -257,9 +248,9 @@ describe("OpenAiCodexHandler native tool calls", () => { output: [], usage: { input_tokens: 1, output_tokens: 2 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -268,10 +259,7 @@ describe("OpenAiCodexHandler native tool calls", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const toolCalls = chunks.filter((c) => c.type === "tool_call") expect(toolCalls.length).toBeGreaterThan(0) @@ -287,9 +275,9 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.content_part.added", part: { type: "output_text", @@ -297,8 +285,8 @@ describe("OpenAiCodexHandler native tool calls", () => { }, output_index: 0, content_index: 0, - } - yield { + }, + { type: "response.completed", response: { id: "resp_content_part", @@ -306,9 +294,9 @@ describe("OpenAiCodexHandler native tool calls", () => { output: [], usage: { input_tokens: 1, output_tokens: 2 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -317,10 +305,7 @@ describe("OpenAiCodexHandler native tool calls", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const textChunks = chunks.filter((c) => c.type === "text") expect(textChunks.length).toBeGreaterThan(0) @@ -332,12 +317,12 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { type: "response.output_text.delta", delta: "hello " } - yield { type: "response.output_text.delta", delta: "world" } - yield { type: "response.output_text.done", text: "hello world" } - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.output_text.delta", delta: "hello " }, + { type: "response.output_text.delta", delta: "world" }, + { type: "response.output_text.done", text: "hello world" }, + { type: "response.completed", response: { id: "resp_delta_done", @@ -345,9 +330,9 @@ describe("OpenAiCodexHandler native tool calls", () => { output: [], usage: { input_tokens: 1, output_tokens: 2 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -356,10 +341,7 @@ describe("OpenAiCodexHandler native tool calls", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const textChunks = chunks.filter((c) => c.type === "text") expect(textChunks.map((c) => c.text).join("")).toBe("hello world") @@ -370,16 +352,16 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { type: "response.output_text.delta", delta: "hello world" } - yield { + create: vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.output_text.delta", delta: "hello world" }, + { type: "response.content_part.added", part: { type: "output_text", text: "hello world" }, output_index: 0, content_index: 0, - } - yield { + }, + { type: "response.completed", response: { id: "resp_delta_content_part", @@ -387,9 +369,9 @@ describe("OpenAiCodexHandler native tool calls", () => { output: [], usage: { input_tokens: 1, output_tokens: 2 }, }, - } - }, - }), + }, + ]), + ), }, } @@ -398,10 +380,7 @@ describe("OpenAiCodexHandler native tool calls", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const textChunks = chunks.filter((c) => c.type === "text") expect(textChunks.map((c) => c.text).join("")).toBe("hello world") @@ -411,10 +390,10 @@ describe("OpenAiCodexHandler native tool calls", () => { vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token") vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test") - const mockCreate = vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - yield { type: "response.output_text.delta", delta: "ok" } - yield { + const mockCreate = vi.fn().mockResolvedValue( + asyncStreamFrom([ + { type: "response.output_text.delta", delta: "ok" }, + { type: "response.completed", response: { id: "resp_sdk_headers", @@ -422,18 +401,16 @@ describe("OpenAiCodexHandler native tool calls", () => { output: [], usage: { input_tokens: 1, output_tokens: 1 }, }, - } - }, - }) + }, + ]), + ) ;(handler as any).client = { responses: { create: mockCreate } } const stream = handler.createMessage("system", [{ role: "user", content: "headers" } as any], { taskId: "task-123", tools: [], }) - for await (const _chunk of stream) { - // drain stream - } + await collectStream(stream) expect(mockCreate).toHaveBeenCalledWith( expect.anything(), @@ -479,9 +456,7 @@ describe("OpenAiCodexHandler native tool calls", () => { taskId: "task-456", tools: [], }) - for await (const _chunk of stream) { - // drain stream - } + await collectStream(stream) expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining("/responses"), diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index dbe13d576b..9a256535c1 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -12,27 +12,20 @@ import { Anthropic } from "@anthropic-ai/sdk" import { OPEN_AI_CODEX_SERVICE_TIER_KEY, OpenAiCodexServiceTier, SERVICE_TIER_KEY } from "@roo-code/types" import { OpenAiCodexHandler, transformLunaResponsesLiteBody } from "../openai-codex" import { openAiCodexOAuthManager } from "../../../integrations/openai-codex/oauth" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" function createCompletedStream() { - return { - async *[Symbol.asyncIterator]() { - yield { - type: "response.completed", - response: { - id: "response-1", - status: "completed", - output: [], - usage: { input_tokens: 1, output_tokens: 1 }, - }, - } + return asyncStreamFrom([ + { + type: "response.completed", + response: { + id: "response-1", + status: "completed", + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + }, }, - } -} - -async function drainStream(stream: AsyncIterable) { - for await (const _chunk of stream) { - // Drain the response stream. - } + ]) } describe("OpenAiCodexHandler.getModel", () => { @@ -92,7 +85,7 @@ describe("OpenAiCodexHandler.createMessage", () => { const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) Reflect.set(handler, "client", { responses: { create: mockCreate } }) - await drainStream(handler.createMessage("System prompt", [])) + await collectStream(handler.createMessage("System prompt", [])) const [body] = mockCreate.mock.calls[0] expect(body).toMatchObject({ @@ -117,7 +110,7 @@ describe("OpenAiCodexHandler.createMessage", () => { const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) Reflect.set(handler, "client", { responses: { create: mockCreate } }) - await drainStream(handler.createMessage("System prompt", [])) + await collectStream(handler.createMessage("System prompt", [])) expect(mockCreate.mock.calls[0][0]).not.toHaveProperty(SERVICE_TIER_KEY) }) @@ -147,7 +140,7 @@ describe("OpenAiCodexHandler.createMessage", () => { }) vitest.stubGlobal("fetch", mockFetch) - await drainStream(handler.createMessage("System prompt", [])) + await collectStream(handler.createMessage("System prompt", [])) expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchObject({ stream: true, @@ -166,19 +159,17 @@ describe("OpenAiCodexHandler.createMessage", () => { responses: { create: vitest.fn().mockImplementation(async (body: any) => { capturedInput.push(...(body.input ?? [])) - return { - async *[Symbol.asyncIterator]() { - yield { - type: "response.completed", - response: { - id: "r1", - status: "completed", - output: [], - usage: { input_tokens: 1, output_tokens: 1 }, - }, - } + return asyncStreamFrom([ + { + type: "response.completed", + response: { + id: "r1", + status: "completed", + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + }, }, - } + ]) }), }, } @@ -193,10 +184,7 @@ describe("OpenAiCodexHandler.createMessage", () => { }, ] - const stream = handler.createMessage("system", messages) - for await (const _ of stream) { - // consume - } + await collectStream(handler.createMessage("system", messages)) // URL image is skipped; only the text input_text block should be present const userMsg = capturedInput.find((item: any) => item.role === "user") @@ -215,19 +203,17 @@ describe("OpenAiCodexHandler.createMessage", () => { responses: { create: vitest.fn().mockImplementation(async (body: any) => { capturedInput.push(...(body.input ?? [])) - return { - async *[Symbol.asyncIterator]() { - yield { - type: "response.completed", - response: { - id: "r1", - status: "completed", - output: [], - usage: { input_tokens: 1, output_tokens: 1 }, - }, - } + return asyncStreamFrom([ + { + type: "response.completed", + response: { + id: "r1", + status: "completed", + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + }, }, - } + ]) }), }, } @@ -242,10 +228,7 @@ describe("OpenAiCodexHandler.createMessage", () => { }, ] - const stream = handler.createMessage("system", messages) - for await (const _ of stream) { - // consume - } + await collectStream(handler.createMessage("system", messages)) const userMsg = capturedInput.find((item: any) => item.role === "user") expect(userMsg?.content).toContainEqual({ @@ -423,7 +406,7 @@ describe("OpenAiCodexHandler Luna Responses Lite requests", () => { const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) ;(handler as any).client = { responses: { create: mockCreate } } - await drainStream( + await collectStream( handler.createMessage("Luna instructions", [{ role: "user", content: "Hello" }], { taskId: "task-luna", tools: [ @@ -498,7 +481,7 @@ describe("OpenAiCodexHandler Luna Responses Lite requests", () => { }) vitest.stubGlobal("fetch", mockFetch) - await drainStream( + await collectStream( handler.createMessage("Instructions", [{ role: "user", content: "Fallback" }], { taskId: "task-fallback", tools: [], @@ -549,7 +532,7 @@ describe("OpenAiCodexHandler Luna Responses Lite requests", () => { }) vitest.stubGlobal("fetch", mockFetch) - await drainStream( + await collectStream( handler.createMessage("Instructions", [{ role: "user", content: "Retry" }], { taskId: "task-retry", tools: [], @@ -591,7 +574,7 @@ describe("OpenAiCodexHandler Luna Responses Lite requests", () => { const mockCreate = vitest.fn().mockResolvedValue(createCompletedStream()) ;(handler as any).client = { responses: { create: mockCreate } } - await drainStream( + await collectStream( handler.createMessage("Normal instructions", [{ role: "user", content: "Hello" }], { taskId: "task-normal", tools: [], diff --git a/src/api/providers/__tests__/openai-native-tools.spec.ts b/src/api/providers/__tests__/openai-native-tools.spec.ts index 5d7a77a0c5..8bacca0dbd 100644 --- a/src/api/providers/__tests__/openai-native-tools.spec.ts +++ b/src/api/providers/__tests__/openai-native-tools.spec.ts @@ -3,16 +3,13 @@ import OpenAI from "openai" import { OpenAiHandler } from "../openai" import { OpenAiNativeHandler } from "../openai-native" import type { ApiHandlerOptions } from "../../../shared/api" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" describe("OpenAiHandler native tools", () => { it("includes tools in request when tools are provided via metadata (regression test)", async () => { - const mockCreate = vi.fn().mockImplementationOnce(() => ({ - [Symbol.asyncIterator]: async function* () { - yield { - choices: [{ delta: { content: "Test response" } }], - } - }, - })) + const mockCreate = vi + .fn() + .mockImplementationOnce(() => asyncStreamFrom([{ choices: [{ delta: { content: "Test response" } }] }])) // Set openAiCustomModelInfo without any tool capability flags; tools should // still be passed whenever metadata.tools is present. @@ -82,17 +79,15 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { responses: { create: vi.fn().mockImplementation((body: any) => { capturedRequestBody = body - return { - [Symbol.asyncIterator]: async function* () { - yield { - type: "response.done", - response: { - output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }], - usage: { input_tokens: 10, output_tokens: 5 }, - }, - } + return asyncStreamFrom([ + { + type: "response.done", + response: { + output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }], + usage: { input_tokens: 10, output_tokens: 5 }, + }, }, - } + ]) }), }, } @@ -120,10 +115,7 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { tools: mcpTools, }) - // Consume the stream - for await (const _ of stream) { - // Just consume - } + await collectStream(stream) // Verify the request body expect(capturedRequestBody.tools).toBeDefined() @@ -149,17 +141,15 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { responses: { create: vi.fn().mockImplementation((body: any) => { capturedRequestBody = body - return { - [Symbol.asyncIterator]: async function* () { - yield { - type: "response.done", - response: { - output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }], - usage: { input_tokens: 10, output_tokens: 5 }, - }, - } + return asyncStreamFrom([ + { + type: "response.done", + response: { + output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }], + usage: { input_tokens: 10, output_tokens: 5 }, + }, }, - } + ]) }), }, } @@ -187,10 +177,7 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { tools: regularTools, }) - // Consume the stream - for await (const _ of stream) { - // Just consume - } + await collectStream(stream) // Verify the request body expect(capturedRequestBody.tools).toBeDefined() @@ -216,17 +203,15 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { responses: { create: vi.fn().mockImplementation((body: any) => { capturedRequestBody = body - return { - [Symbol.asyncIterator]: async function* () { - yield { - type: "response.done", - response: { - output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }], - usage: { input_tokens: 10, output_tokens: 5 }, - }, - } + return asyncStreamFrom([ + { + type: "response.done", + response: { + output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }], + usage: { input_tokens: 10, output_tokens: 5 }, + }, }, - } + ]) }), }, } @@ -268,10 +253,7 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { tools: mcpToolsWithNestedObjects, }) - // Consume the stream - for await (const _ of stream) { - // Just consume - } + await collectStream(stream) // Verify the request body const tool = capturedRequestBody.tools[0] @@ -290,43 +272,34 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { const mockClient = { responses: { create: vi.fn().mockImplementation(function () { - return { - [Symbol.asyncIterator]: async function* () { - // 1. Emit output_item.added with tool identity - yield { - type: "response.output_item.added", - item: { - type: "function_call", - call_id: "call_123", - name: "read_file", - arguments: "", - }, - } - - // 2. Emit tool_call_arguments.delta WITHOUT identity (just args) - yield { - type: "response.function_call_arguments.delta", - delta: '{"path":', - } - - // 3. Emit another delta - yield { - type: "response.function_call_arguments.delta", - delta: '"/tmp/test.txt"}', - } - - // 4. Emit output_item.done - yield { - type: "response.output_item.done", - item: { - type: "function_call", - call_id: "call_123", - name: "read_file", - arguments: '{"path":"/tmp/test.txt"}', - }, - } + return asyncStreamFrom([ + { + type: "response.output_item.added", + item: { + type: "function_call", + call_id: "call_123", + name: "read_file", + arguments: "", + }, + }, + { + type: "response.function_call_arguments.delta", + delta: '{"path":', + }, + { + type: "response.function_call_arguments.delta", + delta: '"/tmp/test.txt"}', + }, + { + type: "response.output_item.done", + item: { + type: "function_call", + call_id: "call_123", + name: "read_file", + arguments: '{"path":"/tmp/test.txt"}', + }, }, - } + ]) }), }, } @@ -336,12 +309,7 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { taskId: "test-task-id", }) - const chunks: any[] = [] - for await (const chunk of stream) { - if (chunk.type === "tool_call_partial") { - chunks.push(chunk) - } - } + const chunks = (await collectStream(stream)).filter((chunk) => chunk.type === "tool_call_partial") expect(chunks.length).toBe(2) expect(chunks[0]).toEqual({ @@ -370,13 +338,7 @@ describe("OpenAiNativeHandler done-event fallbacks", () => { ;(handler as any).client = { responses: { - create: vi.fn().mockResolvedValue({ - async *[Symbol.asyncIterator]() { - for (const event of events) { - yield event - } - }, - }), + create: vi.fn().mockResolvedValue(asyncStreamFrom(events)), }, } @@ -390,11 +352,7 @@ describe("OpenAiNativeHandler done-event fallbacks", () => { tools: [], }) - const chunks: any[] = [] - for await (const chunk of stream) { - chunks.push(chunk) - } - return chunks + return collectStream(stream) } it.each([ diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index b21d409d0a..254cd1dad4 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -20,6 +20,7 @@ import OpenAI from "openai" import { OpenRouterHandler } from "../openrouter" import { ApiHandlerOptions } from "../../../shared/api" import { Package } from "../../../shared/package" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" vitest.mock("openai") vitest.mock("delay", () => ({ @@ -224,19 +225,17 @@ describe("OpenRouterHandler", () => { it("generates correct stream chunks", async () => { const handler = new OpenRouterHandler(mockOptions) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { - id: mockOptions.openRouterModelId, - choices: [{ delta: { content: "test response" } }], - } - yield { - id: "test-id", - choices: [{ delta: {} }], - usage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 }, - } + const mockStream = asyncStreamFrom([ + { + id: mockOptions.openRouterModelId, + choices: [{ delta: { content: "test response" } }], }, - } + { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20, cost: 0.001 }, + }, + ]) // Mock OpenAI chat.completions.create const mockCreate = vitest.fn().mockResolvedValue(mockStream) @@ -248,12 +247,7 @@ describe("OpenRouterHandler", () => { const systemPrompt = "test system prompt" const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }] - const generator = handler.createMessage(systemPrompt, messages) - const chunks = [] - - for await (const chunk of generator) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage(systemPrompt, messages)) // Verify stream chunks expect(chunks).toHaveLength(2) // One text chunk and one usage chunk @@ -292,14 +286,12 @@ describe("OpenRouterHandler", () => { openRouterModelId: "anthropic/claude-3.5-sonnet", }) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { - id: "test-id", - choices: [{ delta: { content: "test response" } }], - } + const mockStream = asyncStreamFrom([ + { + id: "test-id", + choices: [{ delta: { content: "test response" } }], }, - } + ]) const mockCreate = vitest.fn().mockResolvedValue(mockStream) ;(OpenAI as any).prototype.chat = { @@ -331,11 +323,7 @@ describe("OpenRouterHandler", () => { it("handles API errors and captures telemetry", async () => { const handler = new OpenRouterHandler(mockOptions) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { error: { message: "API Error", code: 500 } } - }, - } + const mockStream = asyncStreamFrom([{ error: { message: "API Error", code: 500 } }]) const mockCreate = vitest.fn().mockResolvedValue(mockStream) ;(OpenAI as any).prototype.chat = { @@ -444,11 +432,7 @@ describe("OpenRouterHandler", () => { it("passes 429 rate limit errors from stream to telemetry (filtering happens in PostHogTelemetryClient)", async () => { const handler = new OpenRouterHandler(mockOptions) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { error: { message: "Rate limit exceeded", code: 429 } } - }, - } + const mockStream = asyncStreamFrom([{ error: { message: "Rate limit exceeded", code: 429 } }]) const mockCreate = vitest.fn().mockResolvedValue(mockStream) ;(OpenAI as any).prototype.chat = { @@ -479,38 +463,36 @@ describe("OpenRouterHandler", () => { const handler = new OpenRouterHandler(mockOptions) - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { - id: "test-id", - choices: [ - { - delta: { - tool_calls: [ - { - index: 0, - id: "call_openrouter_test", - function: { name: "read_file", arguments: '{"path":"test.ts"}' }, - }, - ], - }, - index: 0, - }, - ], - } - yield { - id: "test-id", - choices: [ - { - delta: {}, - finish_reason: "tool_calls", - index: 0, + const mockStream = asyncStreamFrom([ + { + id: "test-id", + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call_openrouter_test", + function: { name: "read_file", arguments: '{"path":"test.ts"}' }, + }, + ], }, - ], - usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, - } + index: 0, + }, + ], }, - } + { + id: "test-id", + choices: [ + { + delta: {}, + finish_reason: "tool_calls", + index: 0, + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + }, + ]) const mockCreate = vitest.fn().mockResolvedValue(mockStream) ;(OpenAI as any).prototype.chat = { diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index cdab485080..ca07e0a6fa 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -206,7 +206,7 @@ }, "api/providers/__tests__/openai-codex-native-tool-calls.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 30 + "count": 23 } }, "api/providers/__tests__/openai-codex.spec.ts": { @@ -216,7 +216,7 @@ }, "api/providers/__tests__/openai-native-tools.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 16 + "count": 14 } }, "api/providers/__tests__/openai-native-usage.spec.ts": {