From bf29126a43e84ac0d629a89b2dbe08def0edd9bd Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 14 Sep 2026 04:51:52 +0900 Subject: [PATCH] test(server): cover live-outcome booking for invalid answers and alias failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #4512 (maoxin1234), which recorded the real upstream status before body handling on the audio/live routes but landed without the two live regressions CodeRabbit asked for — pushing to the contributor branch would have reset its completed readiness gate. Two cases in the /v1/live fixture: an invalid live answer (upstream 200, no location, empty body) gives the client 502 while recordCodexUpstreamOutcome books 200 for the creating account, and an alias registration failure (valid 200 answer, binding create returns null) gives the client 503 while the same 200 is booked. The pre-fetch hasCapacity 503 is not the path under test; the alias case asserts the response message so it cannot pass through the capacity branch. Co-authored-by: maoxin1234 <275637173+maoxin1234@users.noreply.github.com> --- tests/server/audio-dictation.test.ts | 35 +++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/server/audio-dictation.test.ts b/tests/server/audio-dictation.test.ts index 1c9020e270..24d50d2cd6 100644 --- a/tests/server/audio-dictation.test.ts +++ b/tests/server/audio-dictation.test.ts @@ -10,6 +10,7 @@ import { saveConfig } from "../../src/config"; import { startServer } from "../../src/server"; import { createDictationFrameValidator } from "../../src/server/audio-dictation"; import { abortAndReleaseAllTurns, resetLifecycleDrainStateForTests } from "../../src/server/lifecycle"; +import { LiveCallBindings } from "../../src/server/live-call-bindings"; import type { OcxConfig } from "../../src/types"; import { fakeChatGptJwt } from "../helpers/fake-chatgpt-jwt"; import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; @@ -32,7 +33,7 @@ const startEvent = { type: "session.start", config: { vad: { type: "server_vad", threshold: 0.5, prefix_padding_ms: 300, silence_duration_ms: 500 }, } }; -function createFixture(options: { failDictation?: boolean } = {}) { +function createFixture(options: { failDictation?: boolean; answer?: "invalid" | "ok200" } = {}) { const creates: Headers[] = []; const handshakes: Array<{ url: string; headers: Headers; protocols?: string[] }> = []; const frames: string[] = []; @@ -71,6 +72,8 @@ function createFixture(options: { failDictation?: boolean } = {}) { if (["chatgpt.com", "api.openai.com"].includes(new URL(req.url).hostname)) { if (new URL(req.url).pathname.endsWith("/realtime/calls") || new URL(req.url).pathname === "/v1/live") { creates.push(new Headers(req.headers)); + if (options.answer === "invalid") return new Response("", { status: 200 }); + if (options.answer === "ok200") return new Response("v=0\r\n", { status: 200, headers: { "content-type": "application/sdp", location: `https://api.openai.com/v1/live/rtc_upstream_${creates.length}` } }); return new Response("v=0\r\n", { status: 201, headers: { "content-type": "application/sdp", location: `https://api.openai.com/v1/live/rtc_upstream_${creates.length}` } }); } return Response.json({}); @@ -270,6 +273,36 @@ describe("external audio sockets", () => { } expect(fixture.handshakes).toHaveLength(0); }); + test("invalid live answer books the upstream 200 while the client gets 502", async () => { + fixture = createFixture({ answer: "invalid" }); + const outcomes = spyOn(routing, "recordCodexUpstreamOutcome"); + try { + const response = await fetchOriginal(new URL("/v1/live", fixture.server.url), { + method: "POST", headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" }, body: JSON.stringify({ sdp: "v=0\r\n" }), + }); + expect(response.status).toBe(502); + const body = await response.text(); + expect(body).toContain("invalid call answer"); + const accountId = fixture.creates[0]!.get("chatgpt-account-id") === "acct-b" ? "pool-b" : "pool-a"; + expect(outcomes.mock.calls.filter(call => call[1] === accountId).map(call => call[2])).toEqual([200]); + } finally { outcomes.mockRestore(); } + }); + test("alias registration failure books the upstream 200 while the client gets 503", async () => { + fixture = createFixture({ answer: "ok200" }); + const outcomes = spyOn(routing, "recordCodexUpstreamOutcome"); + const create = spyOn(LiveCallBindings.prototype, "create").mockReturnValue(null); + try { + const response = await fetchOriginal(new URL("/v1/live", fixture.server.url), { + method: "POST", headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" }, body: JSON.stringify({ sdp: "v=0\r\n" }), + }); + expect(response.status).toBe(503); + const body = await response.text(); + expect(body).toContain("Live call could not be registered"); + expect(body).not.toContain("Live call capacity reached"); + const accountId = fixture.creates[0]!.get("chatgpt-account-id") === "acct-b" ? "pool-b" : "pool-a"; + expect(outcomes.mock.calls.filter(call => call[1] === accountId).map(call => call[2])).toEqual([200]); + } finally { outcomes.mockRestore(); create.mockRestore(); } + }); test("missing reserved aliases never become legacy native joins", async () => { fixture = createFixture(); const response = await fetchOriginal(new URL("/v1/live/rtc_ocx_expired", fixture.server.url), { headers: { upgrade: "websocket" } });