From ecf51c67f89b45e29303b9c5be49678733008c2e Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 18:26:25 +0900 Subject: [PATCH 1/2] test(auth): seed the pool quota and credential after the clock is pinned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The websocket refresh test still failed on loaded CI runners after #3139, on both macOS and Linux, and dev's own HEAD fails it too — so it was not something any open branch introduced. Two writes stamp real time when they run before the clock is pinned: updateAccountQuota sets updatedAt: Date.now(), and saveCodexAccountCredential sets replacedAt. Everything after the pin reads the pinned 2027 value, so the gap is about 136 days against a 6-hour freshness window (QUOTA_DISK_MAX_AGE_MS, src/codex/quota.ts:491). The seeded state reads as stale no matter how fast the runner is, the startup pool-quota prime refreshes the credential before the first turn is served, and seenAuth[0] is already the new token — which is why the failure diff was always the first element. #3139 pinned the clock and the fetch stub before startServer, closing the window for the prime's own reads. It could not close a window for timestamps written before either was in place. Both seeds now run after the pin. Timing-dependent by nature: the mismatch does not reproduce locally either before or after, so the evidence is the mechanism rather than a local red-to-green. A 136-day gap against a 6-hour window is arithmetic, not a race. Twelve consecutive local runs are clean. --- tests/server-auth.test.ts | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/server-auth.test.ts b/tests/server-auth.test.ts index 129cb8dba4..735614a113 100644 --- a/tests/server-auth.test.ts +++ b/tests/server-auth.test.ts @@ -2125,8 +2125,6 @@ describe("server local API auth", () => { expiresAt: now + CODEX_THREAD_AFFINITY_IDLE_TTL_MS + 10 * 60_000, chatgptAccountId: "acct-pool-a", }); - updateAccountQuota("pool-a", 10, 5); - const originalNow = Date.now; // Pin the clock BEFORE startServer, not after. `startServer` returns synchronously but // arms an async pool-quota prime (src/server/index.ts:2054-2064) that outlives its @@ -2240,14 +2238,6 @@ describe("server local API auth", () => { codexAccountNamespaces: { "ws-refresh": "pool-a" }, activeCodexAccountId: "pool-a", } as OcxConfig); - saveCodexAccountCredential("pool-a", { - accessToken: "old-access-token", - refreshToken: "old-refresh-token", - expiresAt: now + 120_000, - chatgptAccountId: "acct-pool-a", - }); - updateAccountQuota("pool-a", 10, 5); - const originalNow = Date.now; const originalFetch = globalThis.fetch; // Both the clock and the fetch stub go up before `startServer`. The async pool-quota @@ -2258,6 +2248,23 @@ describe("server local API auth", () => { // credential before the first turn was served — so `seenAuth[0]` was already the new // token. The failure diff was always the first element, never the second. Date.now = () => now; + // Seed the credential and quota AFTER the clock is pinned. + // + // Both writes stamp real time when they run before the pin: `updateAccountQuota` sets + // `updatedAt: Date.now()`, and `saveCodexAccountCredential` sets `replacedAt`. The + // startup pool-quota prime then compares those stamps + // against this 2027 `now` and judges stale — so it refreshes the credential before the + // first turn is served and `seenAuth[0]` is already the new token. Pinning the clock + // and the fetch stub first (#3139) closed the window for the prime's own reads, but not + // for a timestamp written before either was in place, which is why this kept flaking on + // loaded runners after that fix. + saveCodexAccountCredential("pool-a", { + accessToken: "old-access-token", + refreshToken: "old-refresh-token", + expiresAt: now + 120_000, + chatgptAccountId: "acct-pool-a", + }); + updateAccountQuota("pool-a", 10, 5); globalThis.fetch = (async (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; if (url === "https://auth.openai.com/oauth/token") { From 0cf5ef7b5331ac05ebae56b236d36b7f9d534ad4 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 22:23:37 +0900 Subject: [PATCH 2/2] test(auth): restore the affinity test's quota seed after the pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit removed `updateAccountQuota("pool-a", 10, 5)` from the `expired thread affinity` test along with the websocket test's own seeds. That seed belongs to the affinity test, and its comment kept pointing at a call that was no longer there. Restore it on the correct side of the clock pin. Note what the comment now claims and what it does not: seeding after the pin is what keeps the startup pool-quota prime quiet, because `primeCodexPoolQuotas` treats a missing entry as stale exactly like an expired one (src/codex/auth-api.ts:1334). It is not a race fix for `expect(upstreamRequests).toBe(3)` — `redirectCanonicalCodexTo` only rewrites `/backend-api/codex`, while the prime's WHAM call goes to `/backend-api/wham/usage` and never reaches the counted upstream. Verified with `bun test tests/server-auth.test.ts`: 91 pass, 0 fail. --- tests/server-auth.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/server-auth.test.ts b/tests/server-auth.test.ts index 735614a113..ac913d4d33 100644 --- a/tests/server-auth.test.ts +++ b/tests/server-auth.test.ts @@ -2129,11 +2129,13 @@ describe("server local API auth", () => { // Pin the clock BEFORE startServer, not after. `startServer` returns synchronously but // arms an async pool-quota prime (src/server/index.ts:2054-2064) that outlives its // return, and that prime decides staleness with `Date.now() - quota.updatedAt >= - // POOL_CACHE_TTL` (src/codex/auth-api.ts:1334-1337). `updateAccountQuota` above stamped - // `updatedAt` with the REAL clock, so a prime that lands after a 2027 fake clock is - // installed sees months of cache age, fetches, and rotates the credential out from under - // the assertions. Installing the clock first closes the window entirely. + // POOL_CACHE_TTL` (src/codex/auth-api.ts:1334-1337), where a MISSING entry is stale too. + // Seeding the quota after the pin is what actually keeps the prime quiet: a seed written + // before the pin stamps `updatedAt` with the real clock, which reads as months of cache + // age against this 2027 `now` and sends the prime off to fetch and rotate the credential + // out from under the assertions. Date.now = () => now; + updateAccountQuota("pool-a", 10, 5); const server = startServer(0); try { for (const threadId of ["expired-http", "expired-compact", "expired-ws"]) {