From 6ebc81ab1b015a237e37d94d14eccfee63bb9026 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 01:45:13 +0900 Subject: [PATCH 1/5] fix(codex): probe reset-derived cooldowns without waiting to be selected (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reset-derived cooldown is a prediction, and OpenAI can reset earlier than predicted. The probe that would notice was unreachable: cooled accounts are filtered out before every strategy runs, and resolveCodexAuthContext() selects an account first and only then checks that account's lease. With another account eligible, the cooled one was never selected, never probed, and never recovered. Adds a background claim/settle pair that enumerates accounts independently of selection, on the state sweeper's existing tick rather than a new timer. Claims require reset-derived exactly — a default cooldown is the 60s headerless fallback, so there is no prediction to be early against — and are ordered oldest-first so a bounded pass cannot starve later accounts. Settle clears only the exact scope entry, and only while the lease, cooldown generation, and credential generation all still match; any mismatch releases the lease and retains the cooldown. Recovery does not route through recordCodexUpstreamOutcome(200), which would also mutate account-wide failure state, and never uses clearCodexAccountCooldown(), which clears every scope. Spark is skipped at the claim site: WHAM takes no scope parameter, so a generic result can never prove a spark recovery. Claiming it would spend the account's one claim per pass to settle false and leave the shared scope cooled behind it. The main account is excluded from this first cut — it has no WHAM single-flight, so a sweeper probe could race a dashboard refresh into parallel requests. Fixes #915 --- src/codex/auth-api.ts | 53 ++++- src/codex/quota.ts | 12 ++ src/codex/routing.ts | 125 ++++++++++++ src/server/index.ts | 2 + tests/codex-cooldown-recovery.test.ts | 282 ++++++++++++++++++++++++++ 5 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 tests/codex-cooldown-recovery.test.ts diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index fa0472267..518ba20ce 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -16,11 +16,13 @@ import { import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle"; import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause"; import { + claimDueCodexQuotaRecoveryProbes, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, reconcileCodexActiveAfterExclusion, resetCodexRoutingForManualSelection, + settleCodexQuotaRecoveryProbe, } from "./routing"; import { normalizeAccountPoolStickyLimit, @@ -35,6 +37,7 @@ import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } import { clearAccountQuota, getAccountQuota, + isCompleteCodexQuotaRecoverySnapshot, isCodexQuotaExhausted, listAccountQuotas, parseUsageQuota, @@ -53,7 +56,7 @@ export { } from "./quota"; import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt"; import { getMainAccountPlan, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account"; -import { captureConfigGeneration } from "../lib/state-store-sweeper"; +import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper"; import { reconcileLiveStateStores } from "../lib/state-store-registrations"; import { clearMainAccountInfoCache, @@ -689,6 +692,49 @@ async function fetchPoolAccountQuota(accountId: string, forceRefresh = false, co } let primeInFlight: Promise | null = null; +let cooldownRecoveryInFlight: Promise | null = null; + +export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Date.now()): Promise { + const openai = config.providers[OPENAI_CODEX_PROVIDER_ID]; + if (!openai + || openai.disabled === true + || !isCanonicalOpenAiForwardProvider(openai) + || providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool") return; + if (cooldownRecoveryInFlight) return cooldownRecoveryInFlight; + cooldownRecoveryInFlight = (async () => { + const claims = claimDueCodexQuotaRecoveryProbes(config, POOL_QUOTA_REFRESH_CONCURRENCY, now); + await mapWithConcurrency(claims, POOL_QUOTA_REFRESH_CONCURRENCY, async claim => { + const account = configuredPoolAccount(config, claim.accountId); + if (!account) { + settleCodexQuotaRecoveryProbe(claim, false, {}, now); + return; + } + try { + const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan); + // Defence in depth: `spark` is already excluded at the claim site, since generic WHAM + // cannot prove a spark recovery. Keep the settle-side guard so a future claim change + // cannot silently start clearing spark on generic evidence. + const recovered = claim.scope !== "spark" + && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan); + settleCodexQuotaRecoveryProbe(claim, recovered, { + credentialGeneration: result.freshCredentialGeneration, + }, now); + } catch { + settleCodexQuotaRecoveryProbe(claim, false, {}, now); + } + }); + })().catch(() => { + // Background recovery is best-effort; routing keeps the cooldown on failure. + }).finally(() => { cooldownRecoveryInFlight = null; }); + return cooldownRecoveryInFlight; +} + +export function registerCodexCooldownRecoveryProbeWorker(config: OcxConfig): void { + registerStateSweepAfterTick({ + name: "codex-cooldown-recovery", + afterTick: () => { void runCodexCooldownRecoveryProbes(config); }, + }); +} /** * Best-effort prime of pool-account (and main) quota so the rotation engine has @@ -750,6 +796,11 @@ export function clearCodexQuotaPrimeState(): void { primeInFlight = null; } +/** Test-only reset for the worker-level single-flight. */ +export function clearCodexCooldownRecoveryProbeState(): void { + cooldownRecoveryInFlight = null; +} + export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise { const runtimeConfig = getRuntimeConfig(config); const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount); diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 7d4f60563..04bac22bd 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -76,6 +76,18 @@ export function isCodexQuotaExhausted( && value >= CODEX_EXHAUSTED_USAGE_PERCENT); } +export function isCompleteCodexQuotaRecoverySnapshot( + quota: Pick | null, + plan?: string | null, +): boolean { + if (!quota || isCodexQuotaExhausted(quota, plan)) return false; + const normalizedPlan = plan?.trim().toLowerCase(); + const required = normalizedPlan === "go" || normalizedPlan === "free" + ? quota.monthlyPercent + : quota.weeklyPercent; + return typeof required === "number" && Number.isFinite(required); +} + export function normalizeUsagePercent(value: unknown): number | undefined { const numeric = typeof value === "number" ? value diff --git a/src/codex/routing.ts b/src/codex/routing.ts index 1ed7ece50..3331bc7d0 100644 --- a/src/codex/routing.ts +++ b/src/codex/routing.ts @@ -133,6 +133,18 @@ export type CodexCooldownSource = "retry-after" | "reset-derived" | "default"; */ export type CodexQuotaScope = "shared" | "spark"; +export type CodexQuotaRecoveryProbeClaim = { + accountId: string; + scope?: CodexQuotaScope; + leaseId: string; + cooldownGeneration: number; + credentialGeneration: number; +}; + +export type CodexQuotaRecoveryProbeProof = { + credentialGeneration?: number; +}; + /** * Requests without a resolved native model retain the historic one-account-per- * thread behavior. Requests with a known quota scope get an independent @@ -411,6 +423,119 @@ function canAcquireQuotaProbeLease(health: CodexUpstreamHealth | undefined, now: return now - origin >= CODEX_QUOTA_PROBE_INTERVAL_MS; } +/** + * Claim due reset-derived cooldown probes without consulting account selection. + * Pool credentials only: the main account has no quota-refresh single-flight. + */ +export function claimDueCodexQuotaRecoveryProbes( + config: OcxConfig, + limit: number, + now = Date.now(), +): CodexQuotaRecoveryProbeClaim[] { + const boundedLimit = Math.max(0, Math.floor(limit)); + if (boundedLimit === 0) return []; + const candidates: Array<{ + accountId: string; + scope?: CodexQuotaScope; + health: CodexUpstreamHealth; + credentialGeneration: number; + order: number; + }> = []; + for (const [order, account] of (config.codexAccounts ?? []).entries()) { + if (!isSelectableCodexPoolAccount(account) + || isCodexAccountPaused(config, account.id) + || isAccountNeedsReauth(account.id)) continue; + const record = readCodexAccountRecord(account.id); + if (!record?.credential || record.deletedAt != null) continue; + const due = [ + { scope: undefined, health: upstreamHealth.get(account.id) }, + ...[...(quotaScopedHealth.get(account.id) ?? [])].map(([scope, health]) => ({ scope, health })), + ].filter((entry): entry is { scope?: CodexQuotaScope; health: CodexUpstreamHealth } => + // `spark` is deliberately never claimed. `GET /backend-api/wham/usage` takes no scope + // parameter and returns generic weekly/monthly windows, so its result can never prove a + // spark recovery — a claim here would spend an upstream call to settle `false` every + // time, and (with one claim per account per pass) delay the shared scope that CAN recover. + entry.scope !== "spark" + && entry.health?.cooldownSource === "reset-derived" + && canAcquireQuotaProbeLease(entry.health, now)) + .sort((a, b) => + (a.health.lastProbeAt ?? a.health.cooldownSince ?? 0) + - (b.health.lastProbeAt ?? b.health.cooldownSince ?? 0)); + const candidate = due[0]; + if (candidate) candidates.push({ + accountId: account.id, + ...(candidate.scope ? { scope: candidate.scope } : {}), + health: candidate.health, + credentialGeneration: record.generation, + order, + }); + } + candidates.sort((a, b) => { + const age = (a.health.lastProbeAt ?? a.health.cooldownSince ?? 0) + - (b.health.lastProbeAt ?? b.health.cooldownSince ?? 0); + return age || a.order - b.order; + }); + return candidates.slice(0, boundedLimit).map(candidate => { + const leaseId = randomUUID(); + const next = { + ...candidate.health, + probeLeaseId: leaseId, + probeLeaseGeneration: candidate.health.cooldownGeneration ?? 0, + lastProbeAt: now, + }; + if (candidate.scope) setScopedHealth(candidate.accountId, candidate.scope, next); + else upstreamHealth.set(candidate.accountId, next); + return { + accountId: candidate.accountId, + ...(candidate.scope ? { scope: candidate.scope } : {}), + leaseId, + cooldownGeneration: candidate.health.cooldownGeneration ?? 0, + credentialGeneration: candidate.credentialGeneration, + }; + }); +} + +/** Settle one background recovery claim without mutating account-wide outcome state. */ +export function settleCodexQuotaRecoveryProbe( + claim: CodexQuotaRecoveryProbeClaim, + recovered: boolean, + proof: CodexQuotaRecoveryProbeProof, + now = Date.now(), +): boolean { + const health = claim.scope + ? scopedHealthFor(claim.accountId, claim.scope) + : upstreamHealth.get(claim.accountId); + if (!health || health.probeLeaseId !== claim.leaseId) return false; + const fenced = (health.cooldownGeneration ?? 0) === claim.cooldownGeneration + && (health.probeLeaseGeneration ?? 0) === claim.cooldownGeneration + && claim.credentialGeneration === proof.credentialGeneration + && isCodexAccountGenerationLive(claim.accountId, claim.credentialGeneration); + if (!recovered || !fenced) { + const released = withProbeLeaseReleased(health, now); + if (claim.scope) setScopedHealth(claim.accountId, claim.scope, released); + else upstreamHealth.set(claim.accountId, released); + return false; + } + if (claim.scope) { + deleteScopedHealth(claim.accountId, claim.scope); + } else { + const { + cooldownUntil: _until, + cooldownSince: _since, + cooldownSource: _source, + probeLeaseId: _leaseId, + probeLeaseGeneration: _leaseGeneration, + ...rest + } = health; + upstreamHealth.set(claim.accountId, { + ...rest, + cooldownGeneration: claim.cooldownGeneration + 1, + lastProbeAt: now, + }); + } + return true; +} + /** Acquire the recovery probe for one confirmed model-specific quota group. */ export function tryAcquireCodexQuotaScopeProbeLease( accountId: string, diff --git a/src/server/index.ts b/src/server/index.ts index 1ef97c64a..dc7b109fb 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -20,6 +20,7 @@ import { } from "../config"; import { reconcileOAuthProviders } from "../oauth"; import { invalidateCodexModelsCache } from "../codex/catalog"; +import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api"; import { startMemoryWatchdog } from "./memory-watchdog"; import { reconcileLiveStateStores, @@ -327,6 +328,7 @@ export function startServer(port?: number) { registerAppOwnedMemorySweepFallback(); configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb)); enforceAppOwnedMemoryBudget(); + registerCodexCooldownRecoveryProbeWorker(config); startStateStoreSweeper(); // Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly // tick for daily/weekly; startup evaluation is fire-and-forget after listen. diff --git a/tests/codex-cooldown-recovery.test.ts b/tests/codex-cooldown-recovery.test.ts new file mode 100644 index 000000000..bedbd66d7 --- /dev/null +++ b/tests/codex-cooldown-recovery.test.ts @@ -0,0 +1,282 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { + clearCodexCooldownRecoveryProbeState, + clearAccountQuota, + runCodexCooldownRecoveryProbes, + seedCodexAuthAdmissionForTests, +} from "../src/codex/auth-api"; +import { saveCodexAccountCredential } from "../src/codex/account-store"; +import { + CODEX_QUOTA_PROBE_INTERVAL_MS, + clearCodexUpstreamHealth, + getCodexQuotaHealthSnapshot, + recordCodexUpstreamOutcome, + resolveCodexAccountForThread, +} from "../src/codex/routing"; +import type { OcxConfig } from "../src/types"; + +const TEST_DIR = join(import.meta.dir, ".tmp-codex-cooldown-recovery-test"); +const TEST_CODEX_HOME = join(TEST_DIR, "codex"); +const START = 1_800_000_000_000; +let previousOpencodexHome: string | undefined; +let previousCodexHome: string | undefined; +let previousFetch: typeof fetch; + +function makeConfig(ids = ["a", "b"]): OcxConfig { + return { + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "pool", + }, + }, + defaultProvider: "openai", + activeCodexAccountId: ids[0], + accountPoolStrategy: "fill-first", + codexAccounts: ids.map(id => ({ id, email: `${id}@example.test`, plan: "team", isMain: false })), + } as OcxConfig; +} + +function saveCredential(id: string, suffix = ""): void { + saveCodexAccountCredential(id, { + accessToken: `access-${id}${suffix}`, + refreshToken: `refresh-${id}${suffix}`, + expiresAt: Date.now() + 60 * 60_000, + chatgptAccountId: `acct-${id}${suffix}`, + }); +} + +function cool(config: OcxConfig, id: string, scope: "shared" | "spark" = "shared", now = START): void { + recordCodexUpstreamOutcome(config, id, 429, { + now, + resetAt: now + 60 * 60_000, + modelId: scope === "spark" ? "gpt-5.3-codex-spark" : "gpt-5.6-sol", + }); +} + +function usageResponse(percent = 10, body?: unknown): Response { + return new Response(JSON.stringify(body ?? { + plan_type: "team", + rate_limit: { secondary_window: { used_percent: percent, reset_at: 1_900_000_000 } }, + }), { status: 200, headers: { "Content-Type": "application/json" } }); +} + +function due(at = START): number { + return at + CODEX_QUOTA_PROBE_INTERVAL_MS + 1; +} + +describe("Codex cooldown recovery worker", () => { + beforeEach(() => { + previousOpencodexHome = process.env.OPENCODEX_HOME; + previousCodexHome = process.env.CODEX_HOME; + previousFetch = globalThis.fetch; + if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true }); + mkdirSync(TEST_CODEX_HOME, { recursive: true }); + process.env.OPENCODEX_HOME = TEST_DIR; + process.env.CODEX_HOME = TEST_CODEX_HOME; + clearAccountQuota(); + clearCodexUpstreamHealth(); + clearCodexCooldownRecoveryProbeState(); + }); + + afterEach(() => { + globalThis.fetch = previousFetch; + clearAccountQuota(); + clearCodexUpstreamHealth(); + clearCodexCooldownRecoveryProbeState(); + if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousOpencodexHome; + if (previousCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousCodexHome; + if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true }); + }); + + test("recovers cooled A independently while ordinary routing only selects B", async () => { + const config = makeConfig(); + saveCredential("a"); + saveCredential("b"); + cool(config, "a"); + const routed = [resolveCodexAccountForThread("before", config, due())]; + const authorizations: string[] = []; + globalThis.fetch = async (_input, init) => { + authorizations.push(new Headers(init?.headers).get("Authorization") ?? ""); + return usageResponse(12); + }; + + await runCodexCooldownRecoveryProbes(config, due()); + routed.push(resolveCodexAccountForThread("after", config, due() + 1)); + + expect(authorizations).toEqual(["Bearer access-a"]); + expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).toBeNull(); + expect(routed).toEqual(["b", "b"]); + }); + + test.each([ + ["still exhausted", () => usageResponse(100)], + ["credits only", () => usageResponse(0, { plan_type: "team", rate_limit_reset_credits: { available_count: 1 } })], + ["non-2xx", () => new Response("busy", { status: 503 })], + ["parse failure", () => new Response("not-json", { status: 200 })], + ])("retains the cooldown for %s", async (_name, response) => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a"); + globalThis.fetch = async () => response(); + await runCodexCooldownRecoveryProbes(config, due()); + expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); + }); + + test("retains the cooldown after transport timeout", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a"); + globalThis.fetch = async () => { throw new DOMException("timed out", "TimeoutError"); }; + await runCodexCooldownRecoveryProbes(config, due()); + expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); + }); + + test("retains and releases a claim when quota admission is busy", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a"); + const cleanup = seedCodexAuthAdmissionForTests({ quotaFlights: 16 }); + let calls = 0; + globalThis.fetch = async () => { calls += 1; return usageResponse(); }; + try { + await runCodexCooldownRecoveryProbes(config, due()); + } finally { + cleanup(); + } + expect(calls).toBe(0); + expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); + }); + + test("credential replacement during WHAM cannot clear the old cooldown", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a"); + let release!: () => void; + const gate = new Promise(resolve => { release = resolve; }); + globalThis.fetch = async () => { await gate; return usageResponse(); }; + const run = runCodexCooldownRecoveryProbes(config, due()); + await Promise.resolve(); + saveCredential("a", "-new"); + release(); + await run; + expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); + }); + + test("a newer 429 during WHAM cannot be erased by the older probe", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a"); + let release!: () => void; + const gate = new Promise(resolve => { release = resolve; }); + globalThis.fetch = async () => { await gate; return usageResponse(); }; + const run = runCodexCooldownRecoveryProbes(config, due()); + await Promise.resolve(); + recordCodexUpstreamOutcome(config, "a", 429, { + now: due() + 1, + resetAt: due() + 60 * 60_000, + modelId: "gpt-5.6-sol", + }); + release(); + await run; + expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 2)).not.toBeNull(); + }); + + test("concurrent worker passes coalesce into one WHAM request", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a"); + let calls = 0; + globalThis.fetch = async () => { calls += 1; await Promise.resolve(); return usageResponse(); }; + await Promise.all([ + runCodexCooldownRecoveryProbes(config, due()), + runCodexCooldownRecoveryProbes(config, due()), + ]); + expect(calls).toBe(1); + }); + + test("shared recovery leaves Spark cooled", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a", "shared", START); + cool(config, "a", "spark", START + 1); + globalThis.fetch = async () => usageResponse(); + await runCodexCooldownRecoveryProbes(config, due(START + 1)); + expect(getCodexQuotaHealthSnapshot("a", "shared", due(START + 1) + 1)).toBeNull(); + expect(getCodexQuotaHealthSnapshot("a", "spark", due(START + 1) + 1)).not.toBeNull(); + }); + + test("an older Spark cooldown never starves the shared scope that can recover", async () => { + // Spark is skipped at the claim site: generic WHAM carries no scope and can never prove a + // spark recovery. Claiming it would spend the account's one claim per pass to settle false, + // leaving the shared scope — which this evidence CAN clear — cooled behind it. + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a", "spark", START); + cool(config, "a", "shared", START + 1); + globalThis.fetch = async () => usageResponse(); + await runCodexCooldownRecoveryProbes(config, due(START + 1)); + expect(getCodexQuotaHealthSnapshot("a", "spark", due(START + 1) + 1)).not.toBeNull(); + expect(getCodexQuotaHealthSnapshot("a", "shared", due(START + 1) + 1)).toBeNull(); + }); + + test("a Spark-only cooldown makes no upstream call at all", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a", "spark", START); + let calls = 0; + globalThis.fetch = async () => { calls += 1; return usageResponse(); }; + await runCodexCooldownRecoveryProbes(config, due(START)); + expect(calls).toBe(0); + expect(getCodexQuotaHealthSnapshot("a", "spark", due(START) + 1)).not.toBeNull(); + }); + + test.each([ + ["retry-after", { retryAfter: "900" }], + ["default", {}], + ])("never claims %s cooldowns", async (_name, meta) => { + const config = makeConfig(["a"]); + saveCredential("a"); + recordCodexUpstreamOutcome(config, "a", 429, { ...meta, now: START }); + let calls = 0; + globalThis.fetch = async () => { calls += 1; return usageResponse(); }; + await runCodexCooldownRecoveryProbes(config, due()); + expect(calls).toBe(0); + }); + + test("non-pool OpenAI configurations never run recovery probes", async () => { + const config = makeConfig(["a"]); + saveCredential("a"); + cool(config, "a"); + config.providers.openai!.codexAccountMode = "direct"; + let calls = 0; + globalThis.fetch = async () => { calls += 1; return usageResponse(); }; + await runCodexCooldownRecoveryProbes(config, due()); + expect(calls).toBe(0); + expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); + }); + + test("oldest-first fairness probes every account beyond the per-pass limit", async () => { + const ids = ["a", "b", "c", "d", "e", "f", "g", "h", "i"]; + const config = makeConfig(ids); + for (const id of ids) { + saveCredential(id); + cool(config, id); + } + const seen: string[] = []; + globalThis.fetch = async (_input, init) => { + seen.push(new Headers(init?.headers).get("Authorization")?.replace("Bearer access-", "") ?? ""); + return new Response("busy", { status: 503 }); + }; + await runCodexCooldownRecoveryProbes(config, due()); + await runCodexCooldownRecoveryProbes(config, due() + 60_000); + await runCodexCooldownRecoveryProbes(config, due() + 120_000); + expect(new Set(seen)).toEqual(new Set(ids)); + }); +}); From 2b805f8394deec8f64663b9f760877f4ef78f842 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 01:57:48 +0900 Subject: [PATCH 2/5] fix(codex): fail closed on an unrecognized plan, and make three tests load-bearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recovery predicate assumed weekly-window semantics for every plan except go/free, so an unfamiliar plan_type with a finite weekly percentage cleared the cooldown on evidence we could not interpret — routing traffic to an account that may still be restricted in a window we never read. Recognized plans are now enumerated and anything else retains the cooldown, which only costs a delay since it expires on its own. Three tests also passed vacuously. Both lease-release cases asserted only that the cooldown survived, never that the lease came back — a stranded lease means that account is never probed again, which is worse than the bug. They now require a later pass to reach WHAM and clear it. The fairness test spaced its passes 60s apart, inside the 5-minute probe interval, so already-probed accounts dropped out on their own and the ordering was never exercised; it now spaces past the interval so served and unserved accounts genuinely compete, and it fails when the oldest-first sort is removed. --- src/codex/quota.ts | 29 ++++++++++++-- tests/codex-cooldown-recovery.test.ts | 55 +++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 04bac22bd..0a9c4fe1a 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -76,15 +76,38 @@ export function isCodexQuotaExhausted( && value >= CODEX_EXHAUSTED_USAGE_PERCENT); } +/** + * Plans whose usage is reported in the 30-day window rather than the weekly one. + * Mirrors the `thirtyDayOnly` branch in `parseUsageQuota()`. + */ +const CODEX_MONTHLY_WINDOW_PLANS = new Set(["go", "free"]); +/** + * Plans known to report a weekly window. An ABSENT plan is treated as weekly too, + * matching the parser's default — but an unfamiliar non-empty plan string is not, + * because we cannot tell which window is authoritative for it. + */ +const CODEX_WEEKLY_WINDOW_PLANS = new Set([ + "plus", "pro", "team", "business", "enterprise", "edu", +]); + export function isCompleteCodexQuotaRecoverySnapshot( quota: Pick | null, plan?: string | null, ): boolean { if (!quota || isCodexQuotaExhausted(quota, plan)) return false; const normalizedPlan = plan?.trim().toLowerCase(); - const required = normalizedPlan === "go" || normalizedPlan === "free" - ? quota.monthlyPercent - : quota.weeklyPercent; + // Fail CLOSED on an unrecognized plan. This predicate authorizes autonomously clearing a + // cooldown, and assuming weekly semantics for a plan we do not know could route traffic to an + // account that is still restricted in a window we never read. Retaining the cooldown only + // costs a delay: it expires on its own. + let required: number | undefined; + if (normalizedPlan !== undefined && CODEX_MONTHLY_WINDOW_PLANS.has(normalizedPlan)) { + required = quota.monthlyPercent; + } else if (normalizedPlan === undefined || normalizedPlan === "" || CODEX_WEEKLY_WINDOW_PLANS.has(normalizedPlan)) { + required = quota.weeklyPercent; + } else { + return false; + } return typeof required === "number" && Number.isFinite(required); } diff --git a/tests/codex-cooldown-recovery.test.ts b/tests/codex-cooldown-recovery.test.ts index bedbd66d7..b76f3260c 100644 --- a/tests/codex-cooldown-recovery.test.ts +++ b/tests/codex-cooldown-recovery.test.ts @@ -8,6 +8,7 @@ import { seedCodexAuthAdmissionForTests, } from "../src/codex/auth-api"; import { saveCodexAccountCredential } from "../src/codex/account-store"; +import { isCompleteCodexQuotaRecoverySnapshot } from "../src/codex/quota"; import { CODEX_QUOTA_PROBE_INTERVAL_MS, clearCodexUpstreamHealth, @@ -136,6 +137,14 @@ describe("Codex cooldown recovery worker", () => { globalThis.fetch = async () => { throw new DOMException("timed out", "TimeoutError"); }; await runCodexCooldownRecoveryProbes(config, due()); expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); + // Same reasoning as the admission case: prove the timed-out claim released its lease by + // requiring a later pass to succeed. + let calls = 0; + globalThis.fetch = async () => { calls += 1; return usageResponse(); }; + const later = due() + CODEX_QUOTA_PROBE_INTERVAL_MS + 1; + await runCodexCooldownRecoveryProbes(config, later); + expect(calls).toBe(1); + expect(getCodexQuotaHealthSnapshot("a", "shared", later + 1)).toBeNull(); }); test("retains and releases a claim when quota admission is busy", async () => { @@ -152,6 +161,15 @@ describe("Codex cooldown recovery worker", () => { } expect(calls).toBe(0); expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); + + // "Releases" has to be proven, not asserted by the test name. A stranded lease is worse + // than the bug being fixed: that account would never be probed again. So lift the admission + // pressure, advance past the probe interval, and require the NEXT pass to reach WHAM and + // actually clear the cooldown — which is only possible if the failed claim released. + const later = due() + CODEX_QUOTA_PROBE_INTERVAL_MS + 1; + await runCodexCooldownRecoveryProbes(config, later); + expect(calls).toBe(1); + expect(getCodexQuotaHealthSnapshot("a", "shared", later + 1)).toBeNull(); }); test("credential replacement during WHAM cannot clear the old cooldown", async () => { @@ -237,6 +255,22 @@ describe("Codex cooldown recovery worker", () => { expect(getCodexQuotaHealthSnapshot("a", "spark", due(START) + 1)).not.toBeNull(); }); + test("an unrecognized plan retains the cooldown (fails closed)", () => { + // This predicate authorizes autonomously clearing a cooldown. Assuming weekly semantics + // for a plan we do not know could route traffic to an account still restricted in a window + // we never read; retaining it only costs a delay, since the cooldown expires by itself. + expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, "plus")).toBe(true); + expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, undefined)).toBe(true); + expect(isCompleteCodexQuotaRecoverySnapshot({ monthlyPercent: 12 }, "go")).toBe(true); + expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, "some_new_tier")).toBe(false); + expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, "go")).toBe(false); + // Credits-only / windowless payloads carry no usage evidence at all. + expect(isCompleteCodexQuotaRecoverySnapshot({}, "plus")).toBe(false); + expect(isCompleteCodexQuotaRecoverySnapshot(null, "plus")).toBe(false); + // An exhausted snapshot is not a recovery no matter how complete it is. + expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 100 }, "plus")).toBe(false); + }); + test.each([ ["retry-after", { retryAfter: "900" }], ["default", {}], @@ -262,8 +296,8 @@ describe("Codex cooldown recovery worker", () => { expect(getCodexQuotaHealthSnapshot("a", "shared", due() + 1)).not.toBeNull(); }); - test("oldest-first fairness probes every account beyond the per-pass limit", async () => { - const ids = ["a", "b", "c", "d", "e", "f", "g", "h", "i"]; + test("oldest-first fairness: an already-probed account never jumps the queue", async () => { + const ids = ["a", "b", "c", "d", "e", "f"]; const config = makeConfig(ids); for (const id of ids) { saveCredential(id); @@ -274,9 +308,22 @@ describe("Codex cooldown recovery worker", () => { seen.push(new Headers(init?.headers).get("Authorization")?.replace("Bearer access-", "") ?? ""); return new Response("busy", { status: 503 }); }; + // Six accounts, four claims per pass. Pass one takes four; the second pass is spaced PAST + // the probe interval so those four are eligible again and genuinely compete with the two + // that were never reached. That competition is the whole test: under stable config-order + // claims the same four would win again and the tail would starve. Tighter spacing would + // pass on any ordering, because a just-probed account is ineligible for five minutes and + // drops out without the sort doing any work. await runCodexCooldownRecoveryProbes(config, due()); - await runCodexCooldownRecoveryProbes(config, due() + 60_000); - await runCodexCooldownRecoveryProbes(config, due() + 120_000); + const firstPass = [...seen]; + expect(firstPass).toHaveLength(4); + + await runCodexCooldownRecoveryProbes(config, due() + CODEX_QUOTA_PROBE_INTERVAL_MS + 1); + const secondPass = seen.slice(4); + const starved = ids.filter(id => !firstPass.includes(id)); + expect(starved).toHaveLength(2); + // The two that waited must be served before any account gets a second turn. + expect(secondPass.slice(0, 2).sort()).toEqual(starved.sort()); expect(new Set(seen)).toEqual(new Set(ids)); }); }); From 0cbd6720ce98a3f69c14165d715aef59fea64a08 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 02:01:44 +0900 Subject: [PATCH 3/5] fix(codex): classify prolite as a weekly plan (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hand-written weekly set missed prolite, which the upstream model snapshot enumerates alongside the other eight plans. A recovered prolite account would have failed the completeness check and stayed cooled — reintroducing the exact defect this change exists to fix, through an incomplete list rather than a wrong rule. The test now loops every weekly plan the snapshot names. --- src/codex/quota.ts | 8 +++++++- tests/codex-cooldown-recovery.test.ts | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 0a9c4fe1a..c8a4774eb 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -85,9 +85,15 @@ const CODEX_MONTHLY_WINDOW_PLANS = new Set(["go", "free"]); * Plans known to report a weekly window. An ABSENT plan is treated as weekly too, * matching the parser's default — but an unfamiliar non-empty plan string is not, * because we cannot tell which window is authoritative for it. + * + * This is the complement of the monthly set across the plans the upstream snapshot + * actually enumerates (`src/codex/data/upstream-models.json`): plus, pro, prolite, + * team, business, enterprise, edu. `prolite` was missed on the first pass, which + * would have left a recovered account on that plan cooled — the exact defect this + * change exists to fix, reintroduced through an incomplete hand-written list. */ const CODEX_WEEKLY_WINDOW_PLANS = new Set([ - "plus", "pro", "team", "business", "enterprise", "edu", + "plus", "pro", "prolite", "team", "business", "enterprise", "edu", ]); export function isCompleteCodexQuotaRecoverySnapshot( diff --git a/tests/codex-cooldown-recovery.test.ts b/tests/codex-cooldown-recovery.test.ts index b76f3260c..3f4afc2a3 100644 --- a/tests/codex-cooldown-recovery.test.ts +++ b/tests/codex-cooldown-recovery.test.ts @@ -259,7 +259,11 @@ describe("Codex cooldown recovery worker", () => { // This predicate authorizes autonomously clearing a cooldown. Assuming weekly semantics // for a plan we do not know could route traffic to an account still restricted in a window // we never read; retaining it only costs a delay, since the cooldown expires by itself. - expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, "plus")).toBe(true); + // Every weekly plan the upstream snapshot enumerates must recover. `prolite` is the one an + // earlier hand-written list missed, which would have left those accounts cooled forever. + for (const plan of ["plus", "pro", "prolite", "team", "business", "enterprise", "edu"]) { + expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, plan)).toBe(true); + } expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, undefined)).toBe(true); expect(isCompleteCodexQuotaRecoverySnapshot({ monthlyPercent: 12 }, "go")).toBe(true); expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, "some_new_tier")).toBe(false); From 1ab19f381294b2260d743003ba6e499253f5de2d Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 02:05:33 +0900 Subject: [PATCH 4/5] fix(codex): share one window rule instead of maintaining a plan allowlist (#915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The allowlist was the wrong shape, not just incomplete. The upstream snapshot carries 21 distinct plan strings — edu_plus, finserv, k12, quorum, self_serve_business_usage_based and eight more — and CodexAccount.plan is an unrestricted string, so any hand-written list is a list of the plans someone remembered. Twelve real plans would have been refused recovery and stayed cooled forever: the exact defect this unit exists to fix, reintroduced as a typo-shaped hole. Adding prolite fixed one name and left eleven. Extracted codexQuotaWindowForPlan() as the single rule and routed parsing, exhaustion, and recovery through it, so the three cannot drift. Recovery still fails closed on missing EVIDENCE — credits-only and windowless payloads never clear a cooldown — which is the guard that actually protects a restricted account. Failing closed on an unfamiliar plan NAME only ever meant cooled forever. The test now derives its plan set from the snapshot instead of restating a list, so a newly added plan is covered without anyone remembering to update it. --- src/codex/quota.ts | 56 ++++++++++++--------------- tests/codex-cooldown-recovery.test.ts | 43 +++++++++++++------- 2 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/codex/quota.ts b/src/codex/quota.ts index c8a4774eb..739baf986 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -67,8 +67,7 @@ export function isCodexQuotaExhausted( plan?: string | null, ): boolean { if (!quota) return false; - const normalizedPlan = plan?.trim().toLowerCase(); - const values = normalizedPlan === "go" || normalizedPlan === "free" + const values = codexQuotaWindowForPlan(plan) === "monthly" ? [quota.monthlyPercent] : [quota.weeklyPercent, quota.monthlyPercent]; return values.some(value => typeof value === "number" @@ -77,43 +76,36 @@ export function isCodexQuotaExhausted( } /** - * Plans whose usage is reported in the 30-day window rather than the weekly one. - * Mirrors the `thirtyDayOnly` branch in `parseUsageQuota()`. - */ -const CODEX_MONTHLY_WINDOW_PLANS = new Set(["go", "free"]); -/** - * Plans known to report a weekly window. An ABSENT plan is treated as weekly too, - * matching the parser's default — but an unfamiliar non-empty plan string is not, - * because we cannot tell which window is authoritative for it. + * Which usage window a plan reports in. This is the SINGLE rule shared by quota + * parsing, exhaustion, and recovery — they must not diverge. + * + * An allowlist of "known" plans was tried here and was wrong: the upstream model + * snapshot alone carries 21 distinct plan strings (`edu_plus`, `finserv`, `k12`, + * `quorum`, `self_serve_business_usage_based`, ...), and `CodexAccount.plan` is an + * unrestricted string, so any list is a list of the plans someone remembered. + * Twelve real plans would have been refused recovery and stayed cooled forever — + * the very defect this unit exists to fix, reintroduced as a typo-shaped hole. * - * This is the complement of the monthly set across the plans the upstream snapshot - * actually enumerates (`src/codex/data/upstream-models.json`): plus, pro, prolite, - * team, business, enterprise, edu. `prolite` was missed on the first pass, which - * would have left a recovered account on that plan cooled — the exact defect this - * change exists to fix, reintroduced through an incomplete hand-written list. + * The honest rule is the parser's own: Go and Free report a 30-day window, + * everything else (including an absent plan) reports weekly. Recovery reads the + * window the parser actually wrote rather than second-guessing it. */ -const CODEX_WEEKLY_WINDOW_PLANS = new Set([ - "plus", "pro", "prolite", "team", "business", "enterprise", "edu", -]); +export function codexQuotaWindowForPlan(plan?: string | null): "monthly" | "weekly" { + const normalized = plan?.trim().toLowerCase(); + return normalized === "go" || normalized === "free" ? "monthly" : "weekly"; +} export function isCompleteCodexQuotaRecoverySnapshot( quota: Pick | null, plan?: string | null, ): boolean { if (!quota || isCodexQuotaExhausted(quota, plan)) return false; - const normalizedPlan = plan?.trim().toLowerCase(); - // Fail CLOSED on an unrecognized plan. This predicate authorizes autonomously clearing a - // cooldown, and assuming weekly semantics for a plan we do not know could route traffic to an - // account that is still restricted in a window we never read. Retaining the cooldown only - // costs a delay: it expires on its own. - let required: number | undefined; - if (normalizedPlan !== undefined && CODEX_MONTHLY_WINDOW_PLANS.has(normalizedPlan)) { - required = quota.monthlyPercent; - } else if (normalizedPlan === undefined || normalizedPlan === "" || CODEX_WEEKLY_WINDOW_PLANS.has(normalizedPlan)) { - required = quota.weeklyPercent; - } else { - return false; - } + // Recovery still fails closed on MISSING EVIDENCE — a credits-only or windowless payload + // carries no usage reading at all and must never clear a cooldown. What it does not do is + // fail closed on an unfamiliar plan NAME, which only ever meant "cooled forever". + const required = codexQuotaWindowForPlan(plan) === "monthly" + ? quota.monthlyPercent + : quota.weeklyPercent; return typeof required === "number" && Number.isFinite(required); } @@ -408,7 +400,7 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit = {}; - const thirtyDayOnly = data.plan_type?.trim().toLowerCase() === "go" || data.plan_type?.trim().toLowerCase() === "free"; + const thirtyDayOnly = codexQuotaWindowForPlan(data.plan_type) === "monthly"; const primaryWindow = data.rate_limit.primary_window; const secondaryWindow = data.rate_limit.secondary_window; const tertiaryWindow = data.rate_limit.tertiary_window; diff --git a/tests/codex-cooldown-recovery.test.ts b/tests/codex-cooldown-recovery.test.ts index 3f4afc2a3..88d2a428d 100644 --- a/tests/codex-cooldown-recovery.test.ts +++ b/tests/codex-cooldown-recovery.test.ts @@ -8,7 +8,8 @@ import { seedCodexAuthAdmissionForTests, } from "../src/codex/auth-api"; import { saveCodexAccountCredential } from "../src/codex/account-store"; -import { isCompleteCodexQuotaRecoverySnapshot } from "../src/codex/quota"; +import { codexQuotaWindowForPlan, isCompleteCodexQuotaRecoverySnapshot } from "../src/codex/quota"; +import upstreamModels from "../src/codex/data/upstream-models.json"; import { CODEX_QUOTA_PROBE_INTERVAL_MS, clearCodexUpstreamHealth, @@ -255,20 +256,36 @@ describe("Codex cooldown recovery worker", () => { expect(getCodexQuotaHealthSnapshot("a", "spark", due(START) + 1)).not.toBeNull(); }); - test("an unrecognized plan retains the cooldown (fails closed)", () => { - // This predicate authorizes autonomously clearing a cooldown. Assuming weekly semantics - // for a plan we do not know could route traffic to an account still restricted in a window - // we never read; retaining it only costs a delay, since the cooldown expires by itself. - // Every weekly plan the upstream snapshot enumerates must recover. `prolite` is the one an - // earlier hand-written list missed, which would have left those accounts cooled forever. - for (const plan of ["plus", "pro", "prolite", "team", "business", "enterprise", "edu"]) { - expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, plan)).toBe(true); + test("recovery reads the same window the parser wrote, for EVERY plan", () => { + // Derived from the upstream snapshot, not hand-listed. An allowlist was tried here and was + // wrong: 21 distinct plan strings appear in this file alone, and `CodexAccount.plan` is an + // unrestricted string, so any list is a list of the plans someone remembered — and every + // omission means an account cooled forever, which is the defect this unit exists to fix. + const snapshotPlans = new Set(); + const walk = (node: unknown): void => { + if (Array.isArray(node)) { node.forEach(walk); return; } + if (!node || typeof node !== "object") return; + for (const [key, value] of Object.entries(node as Record)) { + if (key === "available_in_plans" && Array.isArray(value)) { + for (const plan of value) if (typeof plan === "string") snapshotPlans.add(plan); + } else walk(value); + } + }; + walk(upstreamModels); + expect(snapshotPlans.size).toBeGreaterThan(9); + + for (const plan of snapshotPlans) { + const monthly = codexQuotaWindowForPlan(plan) === "monthly"; + const filled = monthly ? { monthlyPercent: 12 } : { weeklyPercent: 12 }; + const empty = monthly ? { weeklyPercent: 12 } : { monthlyPercent: 12 }; + expect(isCompleteCodexQuotaRecoverySnapshot(filled, plan)).toBe(true); + // The other window is not evidence for this plan, in either direction. + expect(isCompleteCodexQuotaRecoverySnapshot(empty, plan)).toBe(false); } + + // Absent plan follows the parser's weekly default. expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, undefined)).toBe(true); - expect(isCompleteCodexQuotaRecoverySnapshot({ monthlyPercent: 12 }, "go")).toBe(true); - expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, "some_new_tier")).toBe(false); - expect(isCompleteCodexQuotaRecoverySnapshot({ weeklyPercent: 12 }, "go")).toBe(false); - // Credits-only / windowless payloads carry no usage evidence at all. + // Missing EVIDENCE still fails closed — that is the guard that matters. expect(isCompleteCodexQuotaRecoverySnapshot({}, "plus")).toBe(false); expect(isCompleteCodexQuotaRecoverySnapshot(null, "plus")).toBe(false); // An exhausted snapshot is not a recovery no matter how complete it is. From 493329df011f031ac5581fa652e7cec545e221ee Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 4 Aug 2026 02:07:44 +0900 Subject: [PATCH 5/5] test(codex): assert the window rule in literals, not via the function under test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-derived loop computed its own expectation from codexQuotaWindowForPlan(), so it only proved the function agrees with itself — ablating the rule to always return weekly still passed 18/18. The contract is now stated in literals first (go/free monthly; plus, pro, prolite, team, business, enterprise, edu, finserv, k12, absent, empty weekly; free_workspace is not free), and that ablation now fails. --- tests/codex-cooldown-recovery.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/codex-cooldown-recovery.test.ts b/tests/codex-cooldown-recovery.test.ts index 88d2a428d..7ce26fd19 100644 --- a/tests/codex-cooldown-recovery.test.ts +++ b/tests/codex-cooldown-recovery.test.ts @@ -274,6 +274,20 @@ describe("Codex cooldown recovery worker", () => { walk(upstreamModels); expect(snapshotPlans.size).toBeGreaterThan(9); + // Pin the rule INDEPENDENTLY first. Deriving the expectation from + // codexQuotaWindowForPlan() below would only prove the loop agrees with itself, so state + // the contract in literals: Go and Free bill on the 30-day window, everything else weekly. + expect(codexQuotaWindowForPlan("go")).toBe("monthly"); + expect(codexQuotaWindowForPlan("free")).toBe("monthly"); + expect(codexQuotaWindowForPlan(" GO ")).toBe("monthly"); + for (const plan of ["plus", "pro", "prolite", "team", "business", "enterprise", "edu", "finserv", "k12"]) { + expect(codexQuotaWindowForPlan(plan)).toBe("weekly"); + } + expect(codexQuotaWindowForPlan(undefined)).toBe("weekly"); + expect(codexQuotaWindowForPlan("")).toBe("weekly"); + // "free_workspace" is not "free": only the exact names take the monthly window. + expect(codexQuotaWindowForPlan("free_workspace")).toBe("weekly"); + for (const plan of snapshotPlans) { const monthly = codexQuotaWindowForPlan(plan) === "monthly"; const filled = monthly ? { monthlyPercent: 12 } : { weeklyPercent: 12 };