From 370beb9ea6c297d722859c384bdd3a66bab3ff2b Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 12:04:13 +0900 Subject: [PATCH] fix(codex): retire main short lock on explicit absent primary --- .../ko/reference/cli/providers-accounts.md | 12 +- .../docs/reference/cli/providers-accounts.md | 10 +- scripts/test-layout/layout.json | 1 + src/codex/main-account-hard-lock.ts | 4 +- src/codex/quota.ts | 7 +- structure/providers/openai-tiers.md | 20 +-- .../main-account-hard-lock-recovery.test.ts | 2 +- .../main-account-hard-lock-retirement.test.ts | 125 ++++++++++++++++++ .../main-quota-provenance.test.ts | 2 +- tests/fixtures/test-layout-expected.json | 1 + 10 files changed, 159 insertions(+), 25 deletions(-) create mode 100644 tests/codex-integration/main-account-hard-lock-retirement.test.ts 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 7f8c672ca7f..12ddcad4aac 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 @@ -96,17 +96,19 @@ Reserve입니다. 차단 중에는 그 메인 계정의 Reserve를 활성화할 예정된 리셋 시간이 지났다는 이유만으로 풀지는 않습니다. 차단 중에는 1분 주기 점검이 알려진 차단 창의 리셋 시각까지 기다린 뒤 계정의 실제 사용량을 확인합니다. 이후에도 차단 상태이거나 미래 리셋 시각을 모르면 5·10·20·40·60분 간격으로 재시도하며, 더 긴 `Retry-After`가 있으면 -프로필 및 토큰 준비도 그 시각까지 미룹니다. 유효한 최신 수치만 차단을 해제합니다. +프로필 및 토큰 준비도 그 시각까지 미룹니다. 유효한 최신 수치나 창이 없음을 명시한 응답만 차단을 해제합니다. Pool 모드에서 사용량 조회의 `--refresh`는 캐시 유효기간을 무시하지만 실패 후 대기 시간은 지킵니다. 조회가 연기되면 새 진단 시도로 기록하지 않습니다. 일시정지, 재인증, 서버의 사용량 제한은 별도로 적용됩니다. -새로운 유효한 WHAM 사용량 응답 한 건에서 1차 창의 기간이 **24시간 이상**으로 명시되고 유효한 사용량 수치가 있으며, -2차·3차 창이 명시적 `null`이거나 그 기간도 24시간 이상으로 명시되고 사용량 수치도 함께 오면 이전 5h 수치를 대체합니다. +새로운 유효한 WHAM 응답에서 1차 창이 **24시간 이상**이고 사용량 수치가 있으면 이전 5h 수치를 대체할 수 있습니다. +1차 창이 명시적 `null`이고 2차 창에 유효한 주간 사용량이 있어도 같습니다. 어느 경우든 2차·3차 창은 +명시적 `null`이거나 기간이 24시간 이상이고 유효한 사용량 수치가 있어야 합니다. 파서의 단기·장기 구분 기준을 따르므로 주간·월간뿐 아니라 하루짜리 창도 해당합니다. 현재 창에는 동일한 98% 기준을 적용합니다. 이 판단은 응답 한 건의 정보에 의존하며 연속 관측을 -요구하지 않습니다. 2차·3차 필드가 생략되었거나, 1차 창의 기간을 모르거나, 응답 헤더만 일부 -도착한 경우에는 이전 차단을 해제하지 않습니다. +요구하지 않습니다. 창 필드가 하나라도 생략되었거나, 창의 기간을 모르거나, 응답 헤더만 일부 +도착한 경우에는 이전 차단을 해제하지 않습니다. 모든 창이 `null`이고 크레딧만 있거나, 보조 월간 수치만 +있는 응답도 차단을 풀지 않습니다. 지연 응답을 반영하기 전에 저장된 인증정보를 다시 확인합니다. 파일을 읽을 수 없거나 같은 계정의 인증 토큰이 교체되었다면 별도 사용량 조회가 없어도 이전 응답은 사용량 캐시나 차단 상태를 갱신하거나 새 토큰을 재인증 대상으로 표시하지 않습니다. 해당 요청자에게 파싱된 조회 결과를 반환할 수는 있지만, 공유 상태나 차단 해제 근거에는 반영하지 않습니다. 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 46a973c1265..49cf4d0f763 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -159,16 +159,18 @@ An unreadable 5h reading cannot hide a weekly block. Unknown usage does not fabr not erase an already measured blocking tuple. A predicted reset time alone does not unlock it. While blocked, the minute sweep waits for the latest known blocking reset, then checks owned usage. If no future reset is known or a check remains blocked, recovery uses a capped 5/10/20/40/60-minute -schedule; a longer `Retry-After` also delays profile and token preparation. Only a fresh valid reading +schedule; a longer `Retry-After` also delays profile and token preparation. Only fresh valid usage or authoritative window-absence evidence can lift the block. In Pool mode, a quota `--refresh` bypasses cache freshness but still honors failed-read pacing; a deferred read makes no new diagnostic attempt. 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 and report their usage. This follows the parser's short/long boundary, so a +its primary window explicitly lasts **at least 24 hours** and reports valid usage, or its primary window is +explicitly `null` and a measured secondary supplies the weekly usage. In either case, secondary/tertiary +windows must be explicitly `null` or explicitly last at least 24 hours and report valid usage. 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 98% 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. +Any omitted window field, an unknown duration, or partial response headers cannot clear a previous block. +All-null responses carrying only credits and supplementary monthly-only readings also cannot establish recovery. The proxy checks the stored credential again before applying a delayed response. An unreadable file or replaced bearer cannot update the usage cache, release the lock, or quarantine the new credential, even for the same account with no second quota read. diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 3fc00931e08..4954cefa06a 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1055,6 +1055,7 @@ "main-device-reauth-ui.test.ts": "gui", "main-device-reauth.test.ts": "codex-integration", "main-quota-evidence-validation.test.ts": "codex-integration", + "main-account-hard-lock-retirement.test.ts": "codex-integration", "main-quota-provenance.test.ts": "codex-integration", "main-quota-window-observation.test.ts": "codex-integration", "management-anthropic-reset-grants.test.ts": "server", diff --git a/src/codex/main-account-hard-lock.ts b/src/codex/main-account-hard-lock.ts index 700cffb2111..9216ddffffc 100644 --- a/src/codex/main-account-hard-lock.ts +++ b/src/codex/main-account-hard-lock.ts @@ -66,9 +66,9 @@ export function getMainAccountHardLockStatus( const windows = governingWindows(quota); const blocking = windows.filter(w => validPercent(w.percent) && w.percent >= MAIN_ACCOUNT_HARD_LOCK_PERCENT); if (blocking.length > 0) { - // The lock holds until every blocking window reads lower, so the earliest possible unlock is + // The lock holds until every blocking window reads lower or is authoritatively absent, so the earliest possible unlock is // the latest blocking reset. One blocking window without a future reset makes it unknowable. - // A predicted reset is not evidence of recovery either way: only a fresh lower reading releases. + // A predicted reset is not evidence of recovery; fresh lower usage or validated WHAM absence releases. const resets = blocking.map(w => resetTimestamp(w.resetAt)); const resetAt = resets.every(r => r !== undefined && r > now) ? Math.max(...(resets as number[])) : undefined; return { enabled: true, state: "blocked", ...(resetAt !== undefined ? { resetAt } : {}) }; diff --git a/src/codex/quota.ts b/src/codex/quota.ts index 57444410a85..7720c33c678 100644 --- a/src/codex/quota.ts +++ b/src/codex/quota.ts @@ -827,8 +827,8 @@ function filterMainPolicyMonthlyQuota( /** * 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. + * A measured long primary, or explicitly absent primary with measured weekly secondary, + * proves replacement only with complete long/null topology. Null supplies no usable evidence. */ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null { const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window]; @@ -840,7 +840,8 @@ export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQu // carries a valid usage reading: a long window without used_percent leaves that // window's usage unknown, and unknown usage must never release a block. // Headers never supply this proof, and reset time alone still cannot release a block. - if (quota && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary) + if (quota && (isMeasuredLongWindow(primary) + || (primary === null && isMeasuredLongWindow(secondary) && quota.weeklyPercent !== undefined)) && (secondary === null || isMeasuredLongWindow(secondary)) && (tertiary === null || isMeasuredLongWindow(tertiary))) { return { ...quota, shortWindowAbsent: true }; diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index 861722e63df..95b3e318112 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -304,7 +304,7 @@ This stops partial weekly/Spark or credits-only refreshes from renewing obsolete The separately retained main-policy snapshot preserves omitted blocking short evidence even after 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. A validated long-primary WHAM snapshot can also retire the short tuple as described below. +it. A validated complete 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 @@ -320,14 +320,14 @@ Regression coverage lives in `tests/codex-integration/codex-quota-parser-parity. `MAIN_ACCOUNT_HARD_LOCK_PERCENT` = 98%. The 5h/short window and the weekly window each govern on their own: either one at 98% blocks, and an unknown or invalid reading in one never hides a block in the other (unknown still admits). Monthly governs only a monthly-only account. A block holds -until every blocking window reads lower, so its reported `resetAt` is the latest blocking reset, +until every blocking window reads lower or is authoritatively absent, so `resetAt` is the latest blocking reset, omitted when any blocking window has none. In the policy snapshot a reset-only weekly observation keeps a blocking weekly tuple, mirroring the short-window rule; monthly-primary evidence still replaces it. It blocks newly admitted identity-matched main-account requests. Pool alternatives remain eligible; explicit main selection and stored Direct substitution do not override it. It neither pauses the account nor clears upstream cooldown/reauth state, and management quota refresh remains available. -Only a fresh valid reading below 98%, including 0%, releases a measured block; passing a reset -timestamp alone does not. The minute sweep waits locally until the latest known blocking reset; +Fresh valid usage below 98%, including 0%, or validated WHAM absence retires a measured short block; +passing a reset timestamp alone does not. The minute sweep waits locally until the latest known blocking reset; when no future reset is known or reads remain blocked, main recovery uses the same capped 5/10/20/40/60-minute delay calculation as usage-query failures. Skipped ticks do not extend it; the physical bearer is reconciled before checking the delay, and late results cannot charge @@ -336,7 +336,7 @@ current credential before the recovery worker takes a profile lease or prepares credential has a separate key and may proceed immediately. Nonterminal 401/403 responses do not arm the successful-but-blocked recovery delay; the next sweep may retry, while terminal authentication failure keeps its reauth quarantine. -Only fresh lower usage releases the lock; no inference or reset-credit consumption is added. Failed, +Only fresh lower usage or validated window absence releases the lock; no reset-credit consumption is added. Failed, missing, non-finite or out-of-range readings do not release the block. Policy validation precedes legacy clamping. Supplementary monthly data cannot become the fallback governing window without a monthly-only plan or explicit primary-monthly evidence. Previously unobserved usage is unknown, not @@ -361,16 +361,18 @@ boundary, the admission consequence, and the settings opt-out round trip are cov hard-lock tests registered in `scripts/test-layout/layout.json`, including `tests/config/settings-main-account-hard-lock.test.ts`. -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 with a valid usage reading. +A single fresh valid WHAM response with a measured long primary, or an explicitly null primary and +measured secondary that supplies parsed weekly usage, can replace an obsolete short-window tuple. +Secondary and tertiary must each be explicitly null or explicitly long with a valid usage reading. 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, a long auxiliary window without a usage reading, 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; +Any omitted window field, an unreadable long window, an unknown duration, partial headers, or invalid usage +cannot prove that the short window disappeared. All-null credits-only and tertiary-only Go/Free responses remain insufficient. Replacement proof belongs only to that observation and is never persisted; the resulting weekly/monthly window still blocks at 98%. 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-account-hard-lock-retirement.test.ts`, `tests/codex-integration/main-quota-provenance.test.ts`, and `tests/codex-integration/main-account-hard-lock-recovery.test.ts`. 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 823cc27d22e..17dbb5f6736 100644 --- a/tests/codex-integration/main-account-hard-lock-recovery.test.ts +++ b/tests/codex-integration/main-account-hard-lock-recovery.test.ts @@ -677,7 +677,7 @@ 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, + primary_window: null, secondary_window: { used_percent: 35, limit_window_seconds: 604_800 }, tertiary_window: null, } })); await runMainAccountHardLockRecovery(config()); expect(calls).toEqual([whamUrl]); diff --git a/tests/codex-integration/main-account-hard-lock-retirement.test.ts b/tests/codex-integration/main-account-hard-lock-retirement.test.ts new file mode 100644 index 00000000000..21db57c6527 --- /dev/null +++ b/tests/codex-integration/main-account-hard-lock-retirement.test.ts @@ -0,0 +1,125 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +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 { + applyAccountQuotaFromUpstreamHeaders, clearAccountQuota, getMainPolicyQuota, + parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed, type WhamUsageResponse, +} from "../../src/codex/quota"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +const account = "retirement-main-fixture"; +const weekly = { used_percent: 0, limit_window_seconds: 604_800 }; +let home: string; +let previousHome: string | undefined; + +beforeEach(() => { + previousHome = process.env.OPENCODEX_HOME; + home = mkdtempSync(join(tmpdir(), "ocx-main-retirement-")); + process.env.OPENCODEX_HOME = home; + clearAccountQuota(); + clearMainAccountInfoCache(); + observeMainQuotaIdentity(account); + const writer = captureMainQuotaWriter(account)!; + const old = Date.now() - 19 * 24 * 60 * 60_000; + writeFileSync(join(home, "codex-quota-cache.json"), JSON.stringify({ version: 1, quotas: {}, + mainPolicyQuota: { identityKey: writer.identityKey, quota: { + shortPercent: 100, shortObservedAt: old, shortResetAt: old / 1000 + 18_000, + shortWindowSeconds: 18_000, weeklyPercent: 0, updatedAt: old, + } }, + })); + expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); +}); + +afterEach(() => { + clearAccountQuota(); + clearMainAccountInfoCache(); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + removeTreeWithRetry(home); +}); + +/** Exercise the production display/policy projections with one captured live identity writer. */ +function publish(data: WhamUsageResponse): void { + setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, + captureMainQuotaWriter(account), parseMainPolicyUsageQuota(data)); +} + +describe("authoritative main 5h window retirement (#6244)", () => { + test.each([0, 35, 97.99, 98, 100])("explicit absent primary retires old 5h while weekly %s still governs", percent => { + publish({ rate_limit: { + primary_window: null, secondary_window: { ...weekly, used_percent: percent }, tertiary_window: null, + } }); + const policy = getMainPolicyQuota(); + expect(policy?.weeklyPercent).toBe(percent); + for (const field of ["shortPercent", "shortObservedAt", "shortResetAt", "shortWindowSeconds"] as const) { + expect(policy?.[field]).toBeUndefined(); + } + expect(policy).not.toHaveProperty("shortWindowAbsent"); + expect(getMainAccountHardLockStatus({}).state).toBe(percent < 98 ? "ready" : "blocked"); + }); + + test.each([ + { rate_limit: { secondary_window: weekly, tertiary_window: null } }, + { rate_limit: { primary_window: null, secondary_window: weekly } }, + { rate_limit: { primary_window: null, secondary_window: {}, tertiary_window: null } }, + { rate_limit: { primary_window: {}, secondary_window: weekly, tertiary_window: null } }, + { rate_limit: { primary_window: null, secondary_window: { used_percent: 0 }, tertiary_window: null } }, + { rate_limit: { primary_window: null, secondary_window: { ...weekly, used_percent: 101 }, tertiary_window: null } }, + { rate_limit: { primary_window: null, secondary_window: { ...weekly, used_percent: -1 }, tertiary_window: null } }, + { rate_limit: { primary_window: null, secondary_window: { ...weekly, used_percent: NaN }, tertiary_window: null } }, + { rate_limit: { primary_window: null, secondary_window: weekly, tertiary_window: { limit_window_seconds: 2_592_000 } } }, + { rate_limit: { primary_window: null, secondary_window: weekly, + tertiary_window: { used_percent: 0, limit_window_seconds: 18_000 } } }, + { rate_limit: { primary_window: { limit_window_seconds: 18_000 }, secondary_window: weekly, tertiary_window: null } }, + { rate_limit: { primary_window: null, secondary_window: null, tertiary_window: null }, + rate_limit_reset_credits: { available_count: 2 } }, + { rate_limit_reset_credits: { available_count: 2 } }, + ])("partial or unreadable observation cannot retire 19-day blocking evidence: %j", data => { + const before = getMainPolicyQuota()!; + expect(parseMainPolicyUsageQuota(data)?.shortWindowAbsent).toBeUndefined(); + publish(data); + expect(getMainPolicyQuota()).toMatchObject({ + shortPercent: 100, shortObservedAt: before.shortObservedAt, + shortResetAt: before.shortResetAt, shortWindowSeconds: 18_000, + }); + expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); + }); + + test.each(["go", "free"])("%s tertiary-only monthly usage cannot prove governing recovery", plan_type => { + const data = { plan_type, rate_limit: { primary_window: null, secondary_window: null, + tertiary_window: { used_percent: 0, limit_window_seconds: 2_592_000 } } }; + expect(parseMainPolicyUsageQuota(data)?.shortWindowAbsent).toBeUndefined(); + publish(data); + expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); + }); + + test("weekly headers and elapsed reset clocks retain the old short block", () => { + const before = getMainPolicyQuota()!; + applyAccountQuotaFromUpstreamHeaders(MAIN, new Headers({ + "x-codex-secondary-used-percent": "0", "x-codex-secondary-window-minutes": "10080", + }), undefined, captureMainQuotaWriter(account)); + expect(getMainPolicyQuota()?.shortObservedAt).toBe(before.shortObservedAt); + expect(getMainAccountHardLockStatus({}, Date.now() + 30 * 24 * 60 * 60_000).state).toBe("blocked"); + }); + + test("a stale identity writer cannot publish otherwise authoritative absence", () => { + const staleWriter = captureMainQuotaWriter(account)!; + clearMainAccountInfoCache(); + observeMainQuotaIdentity(account); + const data = { rate_limit: { primary_window: null, secondary_window: weekly, tertiary_window: null } }; + setAccountQuotaFromParsed(MAIN, parseUsageQuota(data), undefined, staleWriter, parseMainPolicyUsageQuota(data)); + expect(getMainPolicyQuota()?.shortPercent).toBe(100); + expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); + }); + + test("fresh lower short usage releases and the next blocking reading rearms", () => { + publish({ rate_limit: { primary_window: { used_percent: 0, limit_window_seconds: 18_000 } } }); + expect(getMainAccountHardLockStatus({}).state).toBe("ready"); + publish({ rate_limit: { primary_window: { used_percent: 98, limit_window_seconds: 18_000 } } }); + expect(getMainAccountHardLockStatus({}).state).toBe("blocked"); + }); +}); diff --git a/tests/codex-integration/main-quota-provenance.test.ts b/tests/codex-integration/main-quota-provenance.test.ts index e132854b38f..b73680c2e03 100644 --- a/tests/codex-integration/main-quota-provenance.test.ts +++ b/tests/codex-integration/main-quota-provenance.test.ts @@ -313,7 +313,7 @@ test("window replacement persists without carrying its proof into later partial 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, + primary_window: null, secondary_window: { used_percent: 35, limit_window_seconds: 604_800 }, tertiary_window: null, } }); // Execute quota's actual debounced serializer through the existing deterministic clock. const persisted = flushPersistence(); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index fa709f82902..814bd1e300e 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1068,6 +1068,7 @@ "main-device-reauth-ui.test.ts": "gui", "main-device-reauth.test.ts": "codex-integration", "main-quota-evidence-validation.test.ts": "codex-integration", + "main-account-hard-lock-retirement.test.ts": "codex-integration", "main-quota-provenance.test.ts": "codex-integration", "main-quota-window-observation.test.ts": "codex-integration", "management-anthropic-reset-grants.test.ts": "server",