mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* chore(release): open v3.8.39 development cycle * docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize (ff57be32f) and the merge-to-main (ae6e2342d), so they shipped in the v3.8.38 tag but had no bullet: - feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (#5148) - fix(sse): preserve non-stream reasoning fields (#5155, @rdself) - fix(i18n): add missing English UI labels (#5153, @rdself) - test(combo): gated live smoke (#5151) + release-expectations refresh (#5150, @KooshaPari) (#5129 exact-host Anthropic baseUrl is already covered by the #5130 bullet — same CodeQL #674.) Synced 41 i18n CHANGELOG mirrors. * feat(compression): TOON best-of-N candidate encoder + encoder A/B table (#5163) Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT. * fix(zenmux): normalize vendor-prefixed GLM system roles (#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. * [codex] fix xAI OAuth test and reasoning effort (#5157) Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale. * docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (#5162) Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only. * test(security): guard PII redaction stays opt-in (default off) + Hard Rule #20 (#5159) Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule #20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified. * test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (#5168) Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result. * docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (#5171) Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only. * fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (#5134) (#5170) Integrated into release/v3.8.39. HOSTNAME env override in serve (#5134) + regression test (4/4, TDD flip-proof verified). * fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (#5154) (#5173) Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression). * fix(sse): normalize array user content for Command Code to avoid upstream 400 (#5166) (#5174) Integrated into release/v3.8.39. Normalize array user content for Command Code (#5166, user-array/400 symptom); 4/4 tests pass on merge result. * fix(sse): defer </think> close so it never leaks before tool_calls (#5123) (#5175) Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (#5123); 4/4 tests pass (incl. #4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes. * fix(dashboard): use amber for home update-step warning icon (#5176) Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test. * fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (#5083) (#5177) Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. * fix(api): replace #5083 global middleware CSP with declarative ws: scheme (#5083) Follow-up to PR #5177 (merged): that version implemented the LAN-CSP fix (Bug 1) with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the project's documented architecture — 'No global Next.js middleware — interception is route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs next.config header precedence was never confirmed in a real build). This replaces that approach with the minimal, declarative equivalent: • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the bare `wss:` already allowed) so the dashboard can reach its own Live WS server from a LAN/Tailscale host. No middleware. • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts. • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts does NOT exist, so the global-middleware approach cannot silently return). Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from #5177 are unaffected and remain in place. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> * test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (#5179) Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result. * feat(agent-bridge): graceful cert-install fallback with manual guide for containers (#4546) (#5178) Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (#4546); 6/6 tests pass on merge result. * fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (#5180) Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation. * fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (#5189) Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result. * feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (#5187) Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result. * docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (#5185) Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only. * fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (#5169) (#5191) * fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (#5192) (#5194) * test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (#5195) * test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (#5196) * fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (#5193) Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes #5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39. * feat(oauth): remote Antigravity login via local helper + paste-credentials (#5203) Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39. * fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (#5156) Integrated into release/v3.8.39 * fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (#5206) Integrated into release/v3.8.39 * fix(cli): auto-calibrate server V8 heap from physical RAM (#5172) (#5213) The server was spawned with a fixed --max-old-space-size=512 (omniroute serve) or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under load (Ineffective mark-compacts near heap limit ~500MB) with many providers/ accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem()) defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (#2939 unchanged). Also addresses #5160 (same OOM root); #5152 (docker) benefits via the same knob. Closes #5172 * fix(proxy): coalesce fast-fail health probes (#5208) Integrated into release/v3.8.39 * fix(proxy): close dispatchers when clearing cache (#5202) Integrated into release/v3.8.39 * fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (#5198) Integrated into release/v3.8.39 * fix(auth): allow synthetic no-auth fallback for mimocode (#5205) Integrated into release/v3.8.39 * fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (#3850) (#5214) Google's OAuth refresh tokens are non-rotating: the refresh response usually omits refresh_token and occasionally returns it as an empty string. The Antigravity executor used `typeof tokens.refresh_token === "string" ? ... ` which accepts "" (typeof "" === "string") and overwrote the stored token with empty, nulling it on first refresh. Now treats non-string OR empty as absent and preserves credentials.refreshToken, matching refreshGoogleToken semantics. Closes #3850 * fix(responses): normalize non-array input (#5204) Integrated into release/v3.8.39 * fix(stream): normalize safety finish reasons via shared helper (#5197) Integrated into release/v3.8.39 * fix(request-logger): never render negative '(-100%)' compression badge (#5201) Integrated into release/v3.8.39 * fix(combo): reject empty responses api output (#5207) Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release). * fix(pwa): prefer cached navigation before offline page (#5209) Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (#5165). * chore(release): v3.8.39 — 2026-06-28 * chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39 --------- Co-authored-by: Arthur Bodera <abodera@gmail.com> Co-authored-by: Nguyen Minh <lop123thcs@gmail.com> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com> Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com> Co-authored-by: Ardem2025 <ardemb22@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
368 lines
16 KiB
TypeScript
368 lines
16 KiB
TypeScript
// 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<string, unknown> {
|
|
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<string, unknown>).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)"
|
|
);
|
|
});
|