Skip to content
Open
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
113 changes: 113 additions & 0 deletions src/api/providers/__tests__/openai-codex-native-tool-calls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,4 +523,117 @@ describe("OpenAiCodexHandler native tool calls", () => {
}),
)
})

describe("createMessage abort signal", () => {
it("should bridge the external abortSignal into the internal AbortController", async () => {
vi.spyOn(openAiCodexOAuthManager, "getAccessToken").mockResolvedValue("test-token")
vi.spyOn(openAiCodexOAuthManager, "getAccountId").mockResolvedValue("acct_test")

// The mock transport pauses mid-flight until the request-local signal aborts
const mockCreate = vi.fn().mockImplementation(async (_body: unknown, init?: { signal?: AbortSignal }) => {
return {
async *[Symbol.asyncIterator]() {
yield { type: "response.text.delta", delta: "test" }
await new Promise<void>((resolve) => {
const signal = init?.signal
if (!signal || signal.aborted) {
resolve()
return
}
signal.addEventListener("abort", () => resolve(), { once: true })
})
yield {
type: "response.completed",
response: {
id: "resp_1",
status: "completed",
output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }],
usage: { input_tokens: 1, output_tokens: 1 },
},
}
// Arrives after the abort resolves the pause: the loop guard must break before
// processing the completed event and this delta.
yield { type: "response.text.delta", delta: "post-abort" }
},
}
})
Object.assign(handler, {
client: {
responses: { create: mockCreate },
},
})

const controller = new AbortController()
const stream = handler.createMessage("system", [{ role: "user", content: "hello" }], {
taskId: "t",
abortSignal: controller.signal,
})

// Consume the stream (the mock transport pauses mid-flight)
const collected = collectStream(stream)

// Wait until the request has started; the bridge listener is registered before
// the SDK call, so aborting now lands mid-flight
await vi.waitFor(() => expect(mockCreate).toHaveBeenCalled())

// Abort the external signal mid-flight; the bridge must abort the request-local controller
controller.abort()

const chunks = await collected
// Exactly the pre-abort delta: the completed event and the post-abort delta only arrive
// once the abort resolves the transport's pause, so the loop guard must break before
// processing either.
expect(chunks).toEqual([{ type: "text", text: "test" }])

expect(mockCreate).toHaveBeenCalled()
const createCallArgs = mockCreate.mock.calls[0][1] as { signal?: AbortSignal }
// The captured (request-local) signal passed to the SDK must now be aborted
expect(createCallArgs.signal).toBeDefined()
expect(createCallArgs.signal).toBeInstanceOf(AbortSignal)
expect(createCallArgs.signal?.aborted).toBe(true)
})

it("should immediately abort when the external signal is already aborted", async () => {
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.text.delta", delta: "test" }
yield {
type: "response.completed",
response: {
id: "resp_1",
status: "completed",
output: [{ type: "message", content: [{ type: "output_text", text: "test" }] }],
usage: { input_tokens: 1, output_tokens: 1 },
},
}
},
})
Object.assign(handler, {
client: {
responses: { create: mockCreate },
},
})

const controller = new AbortController()
controller.abort() // Pre-abort

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

// Consume the stream to trigger the request; the request-local controller is already
// aborted, so the loop must break before the first event is processed.
const chunks = await collectStream(stream)
expect(chunks).toEqual([])

expect(mockCreate).toHaveBeenCalled()
const createCallArgs = mockCreate.mock.calls[0][1] as { signal?: AbortSignal }
// The internal signal should already be aborted since the external one was pre-aborted
expect(createCallArgs.signal?.aborted).toBe(true)
})
})
})
Loading
Loading