diff --git a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md index bc618edfbea..27d172b549f 100644 --- a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md @@ -88,6 +88,13 @@ ocx login anthropic 실제 사용량을 다시 확인하며, 조회 실패나 잘못된 수치는 차단을 풀지 않습니다. 일시정지, 재인증, 서버의 사용량 제한은 별도로 적용됩니다. +새로운 유효한 WHAM 사용량 응답 한 건에서 1차 창의 기간이 **24시간 이상**으로 명시되고, +2차·3차 창이 명시적 `null`이거나 그 기간도 24시간 이상으로 명시되면 이전 5h 수치를 대체합니다. +파서의 단기·장기 구분 기준을 따르므로 주간·월간뿐 아니라 하루짜리 창도 해당합니다. +현재 창에는 동일한 99% 기준을 적용합니다. 이 판단은 응답 한 건의 정보에 의존하며 연속 관측을 +요구하지 않습니다. 2차·3차 필드가 생략되었거나, 1차 창의 기간을 모르거나, 응답 헤더만 일부 +도착한 경우에는 이전 차단을 해제하지 않습니다. + 저장되는 옵션은 OpenCodex의 `config.json`에 있는 `"codexMainAccountHardLock": true`이며, 기본값은 꺼짐입니다. 식별된 메인 계정의 새 요청을 막는 기능이지 마지막 1%를 예약하는 기능은 아닙니다. 진행 중 요청, 식별되지 않은 키링 계정, 프록시 밖 요청은 사용량을 더 쓸 수 있습니다. diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 11d568ab92f..dd6dfae95ce 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -151,6 +151,13 @@ not erase an already measured blocking tuple. A predicted reset time alone does While blocked, the existing once-per-minute background cycle checks fresh owned usage; failed or invalid readings retain the block. Other pause, reauthentication, and upstream limits remain independent. +Protection treats one fresh valid WHAM usage response as a replacement for the old 5h reading when +its primary window explicitly lasts **at least 24 hours** and secondary/tertiary windows are explicitly `null` +or also explicitly last at least 24 hours. This follows the parser's short/long boundary, so a +one-day window qualifies as well as weekly/monthly windows. The current window still uses the same +99% threshold. This relies on the single reported snapshot; repeated observations are not required. +Omitted secondary/tertiary fields, an unknown primary duration, or partial response headers cannot clear a previous block. + The persisted option is `"codexMainAccountHardLock": true` in OpenCodex's `config.json`; it is off by default. This protects new requests using the identified main account, not the last 1% itself: already-running requests, unmatched caller-owned keyring credentials, and traffic outside the diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 56ba7abdd36..276d3df02c7 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -28,6 +28,8 @@ type QuotaDiskFile = { }; type MainPolicyQuota = { identityKey: string; quota: StoredAccountQuota }; +/** Fresh WHAM topology proof is consumed by the merge, never retained in a cache or DTO. */ +type MainPolicyQuotaObservation = Omit & { shortWindowAbsent?: true }; let mainPolicyQuota: MainPolicyQuota | null = null; let diskHydrated = false; let persistTimer: ReturnType | null = null; @@ -198,6 +200,12 @@ function isExplicitMonthlyWindow(window: WhamUsageWindow | null | undefined): bo && seconds >= MONTHLY_WINDOW_MIN_SECONDS; } +/** Same 24h short/long boundary as the parser; this includes a declared one-day window. */ +function isExplicitLongWindow(window: WhamUsageWindow | null | undefined): boolean { + const seconds = window?.limit_window_seconds; + return typeof seconds === "number" && Number.isFinite(seconds) && seconds >= WEEKLY_WINDOW_MIN_SECONDS; +} + function isExplicitMonthlyWindowMinutes(windowMinutes: unknown): boolean { const minutes = windowMinutes_(windowMinutes); return minutes !== undefined && minutes >= MONTHLY_WINDOW_MIN_MINUTES; @@ -266,12 +274,17 @@ function snapshotHasCustom(quota: Omit): boolea function snapshotHasUsage(quota: Omit): boolean { return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota) || snapshotHasCustom(quota); } +/** + * Publish parsed display quota and separately validated main-policy evidence after writer checks. + * A null policy observation retains only the matching main identity's previous evidence; + * transient replacement markers are consumed during merging and never enter stored snapshots. + */ export function setAccountQuotaFromParsed( accountId: string, quota: Omit | null, writerGeneration = captureConfigGeneration(), mainWriter?: MainQuotaWriter, - policyQuota: Omit | null = quota, + policyQuota: MainPolicyQuotaObservation | null = quota, historyEvidence?: QuotaObservationEvidence, ): void { quota = withoutRetiredCodexQuota(quota); @@ -312,9 +325,13 @@ export function setAccountQuotaFromParsed( } } -/** One partial-window merge contract for legacy quota and identity-bound policy evidence. */ +/** + * Merge a partial observation into the legacy or identity-bound policy snapshot. + * Policy mode retains omitted blocking short usage unless this observation authorizes replacement; + * the returned snapshot contains quota fields only, without the transient replacement marker. + */ function mergeAccountQuota( - quota: Omit, + quota: MainPolicyQuotaObservation, existing: StoredAccountQuota | undefined, updatedAt: number, policyEvidence = false, @@ -376,7 +393,7 @@ function mergeAccountQuota( } if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt; if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds; - } else { + } else if (!policyEvidence || quota.shortWindowAbsent !== true) { // Unknown usage is not a lower reading. Retain the entire known tuple: pairing // its percentage with new metadata would silently extend or shorten its reset. // An elapsed reset is the exception. It describes a window that has already rolled over, @@ -791,13 +808,32 @@ function filterMainPolicyMonthlyQuota( return hasKnownQuotaValue(filtered) || filtered.resetCredits !== undefined ? filtered : null; } -/** Ordinary main policy rejects an entire message containing any invalid numeric window. */ -export function parseMainPolicyUsageQuota(data: WhamUsageResponse): Omit | null { +/** + * Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages. + * Mark a valid primary of at least 24h as replacement evidence only when both other windows + * are explicitly null or at least 24h. A null result supplies no usable policy observation. + */ +export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null { const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window]; if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null; - return filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type)); + const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type)); + const [primary, secondary, tertiary] = windows; + // WHAM explicitly reports absent windows as null; omissions cannot prove replacement. + // Policy trusts one complete snapshot only when every non-null window is >=24h. + // Headers never supply this proof, and reset time alone still cannot release a block. + if (quota && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary) + && (secondary === null || isExplicitLongWindow(secondary)) + && (tertiary === null || isExplicitLongWindow(tertiary))) { + return { ...quota, shortWindowAbsent: true }; + } + return quota; } +/** + * Normalize WHAM windows into the display snapshot, preserving declared short-window shape. + * Finite percentages are clamped for compatibility; policy callers must validate raw readings + * separately. Return null when neither a quota value/window nor reset credits are available. + */ export function parseUsageQuota(data: WhamUsageResponse): Omit | null { const resetCredits = typeof data.rate_limit_reset_credits?.available_count === "number" ? data.rate_limit_reset_credits.available_count diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index e6f650c78d3..000727ac2d9 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -274,10 +274,10 @@ This stops partial weekly/Spark or credits-only refreshes from renewing obsolete 5h rows through the cache-wide `updatedAt` timestamp. Plan labels do not suppress real windows. The separately retained main-policy snapshot preserves omitted blocking short evidence even after -its reset clock passes. Credits-only, weekly-only, and metadata-only updates cannot remove an +its reset clock passes. Credits-only, partial weekly-only, and metadata-only updates cannot remove an existing blocking short usage reading or release its hard lock; a fresh short reading can replace -it. Expired non-blocking short evidence is dropped, so it cannot take priority over a fresh blocking -weekly reading. +it. A validated long-primary WHAM snapshot can also retire the short tuple as described below. +Expired non-blocking short evidence is dropped, so it cannot take priority over a fresh blocking weekly reading. The Codex writer explicitly asks `src/quota/reset-observer.ts` to retain an absent short window in `src/quota/reset-seen-store.ts`, with its original observation time. Detection compares only @@ -301,6 +301,19 @@ release the block. Policy validation precedes legacy clamping. Supplementary mon become the fallback governing window without a monthly-only plan or explicit primary-monthly evidence. Previously unobserved usage is unknown, not fabricated headroom. +A single fresh valid WHAM response with an explicitly long primary window can replace an obsolete +short-window tuple when secondary and tertiary windows are explicitly null or also explicitly long. +Long means **at least 24 hours**, matching the parser's short/long discriminator; a one-day primary +qualifies, not only a seven-day or monthly window. The policy trusts that one reported topology; +it does not require repeated observations or independently confirm upstream window completeness. +Omitted secondary/tertiary fields, an unknown primary duration, partial headers, or invalid usage cannot prove that the +short window disappeared. Replacement proof belongs only to that observation and is never persisted; +the resulting weekly/monthly window still blocks at 99%. This prevents old short-window exhaustion +from surviving indefinitely on a now weekly/monthly account. Coverage lives in +`tests/codex-integration/main-quota-evidence-validation.test.ts`, +`tests/codex-integration/main-quota-provenance.test.ts`, and +`tests/codex-integration/main-account-hard-lock-recovery.test.ts`. + The policy reads a separately retained identity-tagged quota snapshot, so the legacy rotation cache's six-hour expiry does not silently release a known block. A confirmed account transition invalidates old evidence. Request-owned bearers are matched only against a credential and effective diff --git a/tests/codex-integration/main-account-hard-lock-recovery.test.ts b/tests/codex-integration/main-account-hard-lock-recovery.test.ts index f0a803ef764..5517374e4de 100644 --- a/tests/codex-integration/main-account-hard-lock-recovery.test.ts +++ b/tests/codex-integration/main-account-hard-lock-recovery.test.ts @@ -30,10 +30,12 @@ let previousHome: string | undefined; let previousCodexHome: string | undefined; let previousFetch: typeof fetch; +/** Build the minimal proxy configuration with main-account hard-lock recovery enabled. */ function config(): OcxConfig { return { port: 10100, defaultProvider: "openai", providers: {}, codexMainAccountHardLock: true }; } +/** Encode synthetic account and expiry claims for the fixture; this is not a signed credential. */ function bearer(expired = false): string { const payload = Buffer.from(JSON.stringify({ exp: Math.floor(Date.now() / 1000) + (expired ? -120 : 86_400), @@ -42,6 +44,7 @@ function bearer(expired = false): string { return `header.${payload}.signature`; } +/** Write fixture credentials into the isolated home and reconcile the active main identity. */ function writeMain(expired = false): void { writeFileSync(join(home, "auth.json"), JSON.stringify({ tokens: { access_token: bearer(expired), refresh_token: "fixture-refresh", account_id: accountId, @@ -49,6 +52,7 @@ function writeMain(expired = false): void { reconcileMainCodexAccountRuntimeState(); } +/** Seed a 99% short-window block for the observed fixture identity, even though its reset elapsed. */ function block(): void { const writer = captureMainQuotaWriter(accountId); if (!writer) throw new Error("Fixture identity must be observed"); @@ -61,6 +65,10 @@ function usage(percent = 0): Response { } }); } +/** + * Stub recovery HTTP calls, requiring a known metadata/token URL and an active native-main drain. + * Return the captured URL list so tests can verify the requests made by background recovery. + */ function fetchWith(handler: (url: string, init?: RequestInit) => Promise) { const calls: string[] = []; globalThis.fetch = Object.assign(async (input: Parameters[0], init?: RequestInit) => { @@ -123,6 +131,18 @@ afterEach(async () => { }); describe("main hard-lock background recovery", () => { + test("owned metadata recovery replaces an obsolete short block with the current weekly window", async () => { + const calls = fetchWith(async () => Response.json({ plan_type: "pro", rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, tertiary_window: null, + } })); + await runMainAccountHardLockRecovery(config()); + expect(calls).toEqual([whamUrl]); + expect(getMainAccountHardLockStatus(config())).toEqual({ enabled: true, state: "ready" }); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + expect(getMainPolicyQuota()?.weeklyPercent).toBe(35); + expect(getNativeMainProfileRequestCount()).toBe(0); + }); + test("existing sweep hook forces fresh WHAM past cache/reset without adding a timer", async () => { let percent = 99; const calls = fetchWith(async () => usage(percent)); diff --git a/tests/codex-integration/main-quota-evidence-validation.test.ts b/tests/codex-integration/main-quota-evidence-validation.test.ts index 1ed42205fed..2ca37630c7a 100644 --- a/tests/codex-integration/main-quota-evidence-validation.test.ts +++ b/tests/codex-integration/main-quota-evidence-validation.test.ts @@ -6,7 +6,7 @@ import { MAIN_CODEX_ACCOUNT_ID as MAIN } from "../../src/codex/account-id"; import { getMainAccountHardLockStatus } from "../../src/codex/main-account-hard-lock"; import { captureMainQuotaWriter, clearMainAccountInfoCache, observeMainQuotaIdentity } from "../../src/codex/main-account-cache"; import { - clearAccountQuota, getAccountQuota, getMainPolicyQuota, parseMainPolicyUsageQuota, + applyAccountQuotaFromUpstreamHeaders, clearAccountQuota, getAccountQuota, getMainPolicyQuota, parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed, updateAccountQuota, type WhamUsageResponse, } from "../../src/codex/quota"; import { removeTreeWithRetry } from "../helpers/remove-tree"; @@ -30,6 +30,7 @@ afterEach(() => { removeTreeWithRetry(home); }); +/** Observe a synthetic main identity and capture the live writer used to publish its fixture quota. */ function writerFor(accountId = "fixture-main-a") { observeMainQuotaIdentity(accountId); const writer = captureMainQuotaWriter(accountId); @@ -144,6 +145,129 @@ describe("raw policy evidence validation", () => { }); }); +describe("main policy window replacement", () => { + const cfg = { codexMainAccountHardLock: true }; + const weeklySeconds = 7 * 24 * 60 * 60; + const monthlySeconds = 30 * 24 * 60 * 60; + + /** Publish the same fixture through display normalization and strict policy validation. */ + function publish(data: WhamUsageResponse) { + setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, writerFor(), parseMainPolicyUsageQuota(data)); + } + + /** Hydrate a sixteen-day-old short-window block and assert the replacement test's initial state. */ + function retainedShort() { + const old = Date.now() - 16 * 24 * 60 * 60_000; + writeColdPolicy({ shortPercent: 100, shortWindowSeconds: 18_000, + shortObservedAt: old, shortResetAt: old / 1000 + 300, weeklyPercent: 35 }); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + } + + test.each([86_399, 86_400, 86_401])("primary duration %s follows the exact 24h parser boundary", seconds => { + retainedShort(); + const data = { rate_limit: { + primary_window: { used_percent: 20, limit_window_seconds: seconds }, secondary_window: null, tertiary_window: null, + } }; + const parsed = parseMainPolicyUsageQuota(data); + expect(parsed?.shortWindowAbsent).toBe(seconds >= 86_400 ? true : undefined); + publish(data); + expect(getMainPolicyQuota()?.shortPercent).toBe(seconds < 86_400 ? 20 : undefined); + expect(getMainPolicyQuota()?.weeklyPercent).toBe(seconds < 86_400 ? 35 : 20); + expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); + }); + + for (const [field, seconds] of [["weeklyPercent", weeklySeconds], ["monthlyPercent", monthlySeconds]] as const) { + test.each([0, 35, 98.99, 99, 100])(`fresh ${field}=%s replaces a retired persisted short window`, percent => { + retainedShort(); + publish({ rate_limit: { + primary_window: { used_percent: percent, limit_window_seconds: seconds }, secondary_window: null, tertiary_window: null, + } }); + const policy = getMainPolicyQuota(); + expect(policy?.[field]).toBe(percent); + for (const key of ["shortPercent", "shortResetAt", "shortObservedAt", "shortWindowSeconds"] as const) { + expect(policy?.[key]).toBeUndefined(); + } + // Replacement proof is per-observation, never a persisted permission to drop future evidence. + expect(policy).not.toHaveProperty("shortWindowAbsent"); + expect(getMainAccountHardLockStatus(cfg).state).toBe(percent < 99 ? "ready" : "blocked"); + publish({ rate_limit: { primary_window: { used_percent: 99, limit_window_seconds: 18_000 } } }); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + } + + test.each([ + { primary_window: { used_percent: 35 } }, + { primary_window: { limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null }, + { primary_window: { used_percent: -1, limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null }, + { primary_window: { used_percent: 101, limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null }, + { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: {}, tertiary_window: null }, + { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, + secondary_window: { used_percent: 99, limit_window_seconds: 18_000 }, tertiary_window: null }, + { primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, + tertiary_window: { used_percent: 99, limit_window_seconds: 18_000 } }, + ])("partial, invalid or short-bearing metadata retains the old block: %j", rate_limit => { + retainedShort(); + publish({ rate_limit }); + expect(getMainPolicyQuota()?.shortPercent).toBe(100); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test("a declared long secondary cannot hide the current weekly limit", () => { + retainedShort(); + publish({ rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: monthlySeconds }, + secondary_window: { used_percent: 99, limit_window_seconds: weeklySeconds }, tertiary_window: null, + } }); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test.each([ + {}, + { secondary_window: null }, + { tertiary_window: null }, + { secondary_window: { used_percent: 20, limit_window_seconds: weeklySeconds } }, + { tertiary_window: { used_percent: 20, limit_window_seconds: monthlySeconds } }, + ])("omitted secondary or tertiary windows cannot retire a short block: %j", windows => { + retainedShort(); + const data = { rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, ...windows, + } }; + expect(parseMainPolicyUsageQuota(data)?.shortWindowAbsent).toBeUndefined(); + publish(data); + expect(getMainPolicyQuota()?.shortPercent).toBe(100); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test("an explicit null secondary and long tertiary permit replacement", () => { + retainedShort(); + publish({ rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, + tertiary_window: { used_percent: 20, limit_window_seconds: monthlySeconds }, + } }); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); + }); + + test("long-window response headers alone do not retire a known short block", () => { + retainedShort(); + applyAccountQuotaFromUpstreamHeaders(MAIN, new Headers({ + "x-codex-primary-used-percent": "35", "x-codex-primary-window-minutes": "10080", + }), undefined, writerFor()); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); + + test("a superseded identity cannot retire the current account's short block", () => { + const staleWriter = writerFor("fixture-main-b"); + retainedShort(); + const data = { rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: weeklySeconds }, secondary_window: null, tertiary_window: null, + } }; + setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, staleWriter, parseMainPolicyUsageQuota(data)); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + }); +}); + describe("cold partial writers hydrate only the surviving legacy cache", () => { for (const writerKind of ["parsed", "legacy"] as const) { for (const expired of [false, true]) { diff --git a/tests/codex-integration/main-quota-provenance.test.ts b/tests/codex-integration/main-quota-provenance.test.ts index c6a26280a09..e132854b38f 100644 --- a/tests/codex-integration/main-quota-provenance.test.ts +++ b/tests/codex-integration/main-quota-provenance.test.ts @@ -28,10 +28,12 @@ import { getAccountQuota, getMainPolicyQuota, listAccountQuotas, + parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed, updateAccountQuota, type StoredAccountQuota, + type WhamUsageResponse, } from "../../src/codex/quota"; import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmModuleGraph } from "../helpers/cold-spawn-warmup"; import { repoPath, repoRoot } from "../helpers/repo-root"; @@ -49,8 +51,10 @@ let previousCodexHome: string | undefined; let pendingPersist: { run: () => void; timer: ReturnType } | undefined; let timerSpy: ReturnType; -// Exercise the real debounced serializer deterministically, without sleeping or exporting -// a production flush hook. Only quota's 250ms timeout is captured; all others stay native. +/** + * Capture quota's 250ms persistence callback for explicit flushing; leave other timers native. + * Return the timer spy so teardown restores scheduling after exercising the real serializer. + */ function installPersistenceClock() { const nativeSetTimeout = globalThis.setTimeout; return spyOn(globalThis, "setTimeout").mockImplementation((( @@ -63,6 +67,7 @@ function installPersistenceClock() { }) as typeof setTimeout); } +/** Run the captured quota persistence callback and read its actual disk snapshot without a sleep. */ function flushPersistence(): string { if (!pendingPersist) throw new Error("Expected a scheduled quota persistence"); const pending = pendingPersist; @@ -72,6 +77,7 @@ function flushPersistence(): string { return readFileSync(join(testDir, "codex-quota-cache.json"), "utf8"); } +/** Bind a synthetic main identity and return its current generation-scoped quota writer. */ function writerFor(accountId = "fixture-main-a"): MainQuotaWriter { observeMainQuotaIdentity(accountId); const writer = captureMainQuotaWriter(accountId); @@ -297,6 +303,31 @@ describe("main policy quota writes", () => { }); }); +test("window replacement persists without carrying its proof into later partial updates", () => { + const cfg = { codexMainAccountHardLock: true }; + const writer = writerFor(); + /** Publish both parsed projections with the captured writer throughout the simulated restart. */ + const publish = (data: WhamUsageResponse) => setAccountQuotaFromParsed( + MAIN, parseUsageQuota(data), undefined, writer, parseMainPolicyUsageQuota(data), + ); + setAccountQuotaFromParsed(MAIN, { shortPercent: 100, shortWindowSeconds: 18_000, shortResetAt: 1 }, undefined, writer); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); + publish({ rate_limit: { + primary_window: { used_percent: 35, limit_window_seconds: 604_800 }, secondary_window: null, tertiary_window: null, + } }); + // Execute quota's actual debounced serializer through the existing deterministic clock. + const persisted = flushPersistence(); + expect(JSON.parse(persisted).mainPolicyQuota.quota.weeklyPercent).toBe(35); + expect(persisted).not.toContain("shortWindowAbsent"); + clearAccountQuota(); + writeFileSync(join(testDir, "codex-quota-cache.json"), persisted); + expect(getMainAccountHardLockStatus(cfg).state).toBe("ready"); + expect(getMainPolicyQuota()?.shortPercent).toBeUndefined(); + publish({ rate_limit: { primary_window: { used_percent: 99, limit_window_seconds: 18_000 } } }); + publish({ rate_limit: { primary_window: { used_percent: 0 } } }); + expect(getMainAccountHardLockStatus(cfg).state).toBe("blocked"); +}); + describe("main policy quota durability and lifecycle", () => { // The first loop iteration is this graph's cold child; warm quota provenance imports before its // spawn timeout starts measuring the restart behavior.