// tests/integration/combo-matrix/context-relay-codex.test.ts // // Deterministic in-process test for context-relay CODEX-SPECIFIC HANDOFF // (combo.ts ~2143-2183): the quota-aware handoff that fires after a successful // codex response when the quota exceeds the threshold. // // Code path: // if (strategy === "context-relay" && relayOptions?.sessionId && relayConfig && // relayConfig.handoffProviders.includes(provider) && provider === "codex") { // const connectionId = getSessionConnection(relayOptions.sessionId); // if (connectionId) { // const quotaInfo = await fetchCodexQuota(connectionId).catch(() => null); // if (quotaInfo) { ... maybeGenerateHandoff({ ..., expiresAt: resetCandidates[0] }); } // } // } // // PRIMARY observable: getHandoff(sessionId, comboName).expiresAt === session-window // reset time from the codex quota response. This is the CODEX-SPECIFIC proof: // the universal handoff path does NOT set expiresAt from a codex quota fetch. // // SECONDARY observables: // - The codex usage URL was fetched (proves the quota path ran). // - The handoff record has a non-empty .summary. // // CONTROL: combo targeting openai (not codex) → codex block NEVER runs → // codex usage URL is never fetched → expiresAt from quota doesn't appear. // // Seams used: // - registerCodexConnection / unregisterCodexConnection (codexQuotaFetcher.ts) // - clearSessions (sessionManager.ts) — cleanup between tests // - buildCodexResponsesSse helper (mirrors codex-stream-false.test.ts) // - Custom fetch mock distinguishes quota URL / summary dispatch / main dispatch import test from "node:test"; import assert from "node:assert/strict"; import { createComboRoutingHarness } from "../_comboRoutingHarness.ts"; import { registerCodexConnection, unregisterCodexConnection, } from "../../../open-sse/services/codexQuotaFetcher.ts"; import { clearSessions } from "../../../open-sse/services/sessionManager.ts"; // ── Harness setup ───────────────────────────────────────────────────────────── // Each harness owns an isolated DATA_DIR so DB handles don't clash. const h = await createComboRoutingHarness("combo-relay-codex"); const { BaseExecutor, combosDb, handleChat, buildRequest, seedConnection, resetStorage, buildOpenAIResponse, waitFor, toPlainHeaders, } = h; // Import DB helpers AFTER harness creation so they share the same DB instance // (DATA_DIR is set by the harness before any import triggers DB init). const { getHandoff } = await import("../../../src/lib/db/contextHandoffs.ts"); // ── Constants ───────────────────────────────────────────────────────────────── const CODEX_COMBO_NAME = "m-relay-codex-quota"; const SESSION_HEADER_VALUE = "relay-codex-quota-001"; const SESSION_ID = `ext:${SESSION_HEADER_VALUE}`; // Codex endpoint URLs (must match codexQuotaFetcher.ts and the executor config). const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"; const CODEX_RESPONSES_HOST = "chatgpt.com/backend-api/codex/responses"; // Summary JSON that parseHandoffJSON will successfully parse. const CODEX_SUMMARY_JSON = JSON.stringify({ summary: "User is implementing a TypeScript context-relay codex quota-handoff test using TDD.", keyDecisions: ["codex provider selected", "quota threshold at 90%"], taskProgress: "writing deterministic integration test for codex handoff", activeEntities: ["combo.ts", "codexQuotaFetcher.ts", "contextHandoff.ts"], }); // ── SSE builder (mirrors codex-stream-false.test.ts) ───────────────────────── function buildCodexResponsesSse(text = "codex assistant reply") { return new Response( [ "event: response.created", 'data: {"type":"response.created","response":{"id":"resp_codex_1","model":"gpt-5.3-codex","status":"in_progress","output":[]}}', "", "event: response.output_text.delta", `data: ${JSON.stringify({ type: "response.output_text.delta", output_index: 0, delta: text, })}`, "", "event: response.completed", `data: ${JSON.stringify({ type: "response.completed", response: { id: "resp_codex_1", object: "response", model: "gpt-5.3-codex", status: "completed", output: [ { type: "message", role: "assistant", content: [{ type: "output_text", text }], }, ], usage: { input_tokens: 6, output_tokens: 4 }, }, })}`, "", "data: [DONE]", "", ].join("\n"), { status: 200, headers: { "Content-Type": "text/event-stream" }, } ); } // ── Quota response builder ──────────────────────────────────────────────────── // Produces a codex usage API response that parseCodexUsageResponse accepts. // primary_window → session → percentUsed = 0.90 (90%) — triggers handoff (threshold 0.85) // secondary_window → weekly → percentUsed = 0.85 (85%) — below threshold for weekly // Both are below HANDOFF_EXHAUSTION_THRESHOLD (0.95) so maybeGenerateHandoff proceeds. function buildCodexUsageBody( sessionResetAtUnix: number, weeklyResetAtUnix: number ): Record { return { rate_limit: { primary_window: { used_percent: 90, reset_at: sessionResetAtUnix }, secondary_window: { used_percent: 85, reset_at: weeklyResetAtUnix }, }, }; } // ── Request builder ─────────────────────────────────────────────────────────── function codexRequest(withSessionId = true) { return buildRequest({ headers: withSessionId ? { "x-session-id": SESSION_HEADER_VALUE } : {}, body: { model: CODEX_COMBO_NAME, stream: false, messages: [{ role: "user", content: "Write a TypeScript hello world." }], }, }); } // ── Fetch mock ──────────────────────────────────────────────────────────────── // Handles three URL classes: // 1. CODEX_USAGE_URL → codex quota JSON // 2. CODEX_RESPONSES_HOST (call 1) → main request Responses API SSE // 3. CODEX_RESPONSES_HOST (call 2) → summary request Responses API SSE (with CODEX_SUMMARY_JSON) // // The second codex-responses call is the handoff summary generation, identified by // call count (the first is always the main request). function installCodexHandoffFetch( sessionResetAtUnix: number, weeklyResetAtUnix: number, seenUrls: string[] ) { let codexResponsesCallCount = 0; globalThis.fetch = async (url: unknown, init: unknown = {}) => { const u = String(url); seenUrls.push(u); const headers = init != null && typeof init === "object" ? toPlainHeaders((init as Record).headers) : {}; void headers; // recorded for debugging; not needed in assertions here if (u === CODEX_USAGE_URL) { // Codex quota endpoint — return structured usage JSON. return new Response( JSON.stringify(buildCodexUsageBody(sessionResetAtUnix, weeklyResetAtUnix)), { status: 200, headers: { "Content-Type": "application/json" } } ); } if (u.includes(CODEX_RESPONSES_HOST)) { codexResponsesCallCount++; if (codexResponsesCallCount === 1) { // Main codex request — return a successful Responses API SSE. return buildCodexResponsesSse("codex assistant reply ok"); } // Subsequent call(s) are from the handoff summary generation. // Return Responses API SSE whose output_text is CODEX_SUMMARY_JSON so that // generateHandoffAsync → parseHandoffJSON succeeds. return buildCodexResponsesSse(CODEX_SUMMARY_JSON); } // Fallback — should not be reached in these tests. return new Response(JSON.stringify({ error: { message: "unexpected URL in test" } }), { status: 400, headers: { "Content-Type": "application/json" }, }); }; } // ── Lifecycle hooks ─────────────────────────────────────────────────────────── test.beforeEach(async () => { BaseExecutor.RETRY_CONFIG.delayMs = 0; await resetStorage(); }); test.afterEach(async () => { BaseExecutor.RETRY_CONFIG.delayMs = h.originalRetryDelayMs; // Clear in-memory session state so test 2 doesn't see stale connections from test 1. clearSessions(); await resetStorage(); }); test.after(async () => { await h.cleanup(); }); // ── Test 1 (PRIMARY): codex quota handoff fires ─────────────────────────────── // // Flow: // 1. Seed a codex DB connection + register with accessToken. // 2. Create context-relay combo → codex. // 3. Send request with x-session-id header. // 4. During request processing, chat.ts calls touchSession(sessionId, codexConnId). // 5. After the successful codex response, combo.ts enters the codex block: // - getSessionConnection → codexConnId (set in step 4). // - fetchCodexQuota(codexConnId) → hits our mock → returns 90% usage. // - resetCandidates[0] = session-window resetAt ISO string. // - maybeGenerateHandoff({ ..., expiresAt: sessionResetISO }) fires via setImmediate. // 6. generateHandoffAsync dispatches a second codex fetch for the summary. // 7. upsertHandoff writes to DB with expiresAt = sessionResetISO. // // Assertion: handoff.expiresAt === sessionResetISO (codex-specific, not TTL-derived). test("context-relay codex quota handoff: fires and expiresAt matches session-window reset from quota", async () => { // 1. Seed codex connection. const conn = await seedConnection("codex", { apiKey: "sk-codex-handoff-test-1" }); const codexConnId = conn.id; // 2. Register connection meta so fetchCodexQuota can find credentials. // chat.ts also calls registerCodexConnection during request processing for OAuth // tokens; for API-key connections the accessToken may not be set there, so we // pre-register with the API key as the token. This is the documented test seam. registerCodexConnection(codexConnId, { accessToken: "sk-codex-handoff-test-1" }); // 3. Create a context-relay combo targeting codex. // Default handoffProviders: ["codex"] and handoffThreshold: 0.85 apply. await combosDb.createCombo({ name: CODEX_COMBO_NAME, strategy: "context-relay", config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 }, models: [ { id: "rc-codex-1", kind: "model", providerId: "codex", model: "gpt-5.3-codex" }, ], }); // 4. Compute quota reset times (future timestamps). const sessionResetAtUnix = Math.floor((Date.now() + 5 * 60 * 60 * 1000) / 1000); // +5h const weeklyResetAtUnix = Math.floor((Date.now() + 7 * 24 * 60 * 60 * 1000) / 1000); // +7d // Expected expiresAt: parseWindowReset converts reset_at (unix seconds) → ISO string. // resetCandidates[0] = earliest ISO string = session window reset (5h < 7d). const expectedExpiresAt = new Date(sessionResetAtUnix * 1000).toISOString(); // 5. Install fetch mock and send request. const seenUrls: string[] = []; installCodexHandoffFetch(sessionResetAtUnix, weeklyResetAtUnix, seenUrls); const r = await handleChat(codexRequest(true)); assert.equal(r.status, 200, "main codex request must return 200"); // 6. Wait for setImmediate → generateHandoffAsync → upsertHandoff (up to 3s). const handoff = await waitFor(() => getHandoff(SESSION_ID, CODEX_COMBO_NAME), 3000); // ── Primary assertion: handoff record exists ────────────────────────────── assert.ok( handoff !== null, "codex quota handoff record must be written to DB when quota ≥ threshold" ); // ── CODEX-SPECIFIC proof: expiresAt == session-window reset from quota ──── // The universal handoff path never calls fetchCodexQuota, so it cannot produce // this specific expiresAt value. The value below can only come from the codex block. assert.equal( handoff!.expiresAt, expectedExpiresAt, `handoff.expiresAt must equal the session-window reset from codex quota (${expectedExpiresAt}); ` + `got ${handoff!.expiresAt}` ); // ── Secondary assertion: codex usage URL was fetched ───────────────────── assert.ok( seenUrls.includes(CODEX_USAGE_URL), `codex usage URL must have been fetched to produce the expiresAt; seen URLs: ${JSON.stringify(seenUrls)}` ); // ── Secondary assertion: summary was generated (non-empty) ─────────────── assert.ok( typeof handoff!.summary === "string" && handoff!.summary.length > 0, `handoff.summary must be non-empty; got ${JSON.stringify(handoff!.summary)}` ); // ── Cleanup ─────────────────────────────────────────────────────────────── unregisterCodexConnection(codexConnId); }); // ── Test 2 (CONTROL): non-codex provider never triggers the codex block ─────── // // The codex block gates on `provider === "codex"`. A combo targeting openai // will never enter it, so the codex usage URL is never fetched and there is no // quota-derived expiresAt in the DB. // // This proves the primary test is not trivially green (i.e., the block is // actually gated on `provider === "codex"`). test("context-relay codex quota handoff: does NOT fire when provider is openai (control)", async () => { // Seed openai (not codex). await seedConnection("openai", { apiKey: "sk-openai-control-no-codex-block" }); await combosDb.createCombo({ name: CODEX_COMBO_NAME, strategy: "context-relay", config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 }, models: [ { id: "rc-openai-ctrl", kind: "model", providerId: "openai", model: "gpt-4o-mini" }, ], }); const seenUrls: string[] = []; globalThis.fetch = async (url: unknown, _init: unknown = {}) => { seenUrls.push(String(url)); return buildOpenAIResponse("assistant reply ok"); }; const r = await handleChat(codexRequest(true)); assert.equal(r.status, 200, "openai request must return 200"); // Give setImmediate time to fire if the block were incorrectly entered. await new Promise((res) => setTimeout(res, 400)); // The codex block requires provider === "codex", so it never runs for openai. // The codex usage URL must NOT have been fetched. assert.ok( !seenUrls.includes(CODEX_USAGE_URL), `codex usage URL must NOT be fetched for openai provider; seen: ${JSON.stringify(seenUrls)}` ); // No codex quota handoff record in DB. // (The universal handoff also does not fire because no prior model is seeded, // so getLastSessionModel returns null → no model switch detected.) const handoff = getHandoff(SESSION_ID, CODEX_COMBO_NAME); assert.equal( handoff, null, "no handoff record must exist when provider is openai (codex block never entered)" ); });