From 44cf6abab9c47ff5065e2fc34ca4dedc718e57ad Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sun, 30 Aug 2026 06:17:54 +0900 Subject: [PATCH] test(codex): isolate the tombstone guard with a credential-carrying tombstone Closes an over-claim in #2934's merge record. Its mutation table said removing the `alias.deletedAt != null` check in commitRefreshedCodexCredentialWithAliases turns the resurrection test red. It does not: tombstoneCodexAccount drops the credential, so the separate `!alias.credential` guard already skips that record and the assertion passes with `deletedAt` deleted. The two guards overlap on the only fixture that exercised them, so neither was independently proven. A tombstone that still carries a credential is the only shape that reaches the deletedAt check, and it is reachable: a store written by an older build, or a tombstone raced by a concurrent save. `tokenful tombstone is treated as absent` already pins that shape for the read path, so this uses the same construction for the propagation path. Every other eligibility field matches the owner in this fixture -- same fingerprint, same access token, same expiry, same chatgptAccountId -- so deletedAt is the only thing that can skip it. Removing that check now turns this test red while the other 41 stay green. No production change. --- tests/codex-account-store.test.ts | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/codex-account-store.test.ts b/tests/codex-account-store.test.ts index fe7274496d..3e2a9f3f77 100644 --- a/tests/codex-account-store.test.ts +++ b/tests/codex-account-store.test.ts @@ -1100,6 +1100,50 @@ describe("codex-account-store CRUD", () => { } }); + test("a TOKENFUL tombstone is not resurrected either, isolating the deletedAt guard (#2892 gap 3)", async () => { + // The sibling test above cannot prove the `deletedAt` check is load-bearing: `tombstoneCodexAccount` + // drops the credential, so the separate `!alias.credential` guard already skips that record and the + // assertion passes with `deletedAt` removed. A tombstone that still CARRIES a credential is the only + // shape that reaches the `deletedAt` check, and it is reachable — a store written by an older build, + // or a tombstone raced by a concurrent save, produces exactly this record. `tokenful tombstone is + // treated as absent` earlier in this file pins the same shape for the read path. + const { getValidCodexToken, readCodexAccountRecord, saveCodexAccountCredential } = + await import("../src/codex/account-store"); + const shared = { + accessToken: "tokenful-old", + refreshToken: "tokenful-grant", + expiresAt: 0, + chatgptAccountId: "tokenful-acc", + }; + saveCodexAccountCredential("tokenful-owner", { ...shared }); + const ownerGeneration = readCodexAccountRecord("tokenful-owner")!.generation; + // Written directly: no public API produces a tombstone that retains its credential. + writeFileSync(ACCOUNTS_PATH, JSON.stringify({ + "tokenful-owner": { credential: { ...shared }, generation: ownerGeneration }, + "tokenful-deleted": { credential: { ...shared }, generation: ownerGeneration, deletedAt: Date.now() }, + }, null, 2)); + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => Response.json({ + access_token: "tokenful-new", + refresh_token: "tokenful-rotated", + expires_in: 3600, + })) as typeof fetch; + try { + await getValidCodexToken("tokenful-owner"); + // The owner rotated. + expect(readCodexAccountRecord("tokenful-owner")!.credential!.refreshToken).toBe("tokenful-rotated"); + // The tombstone kept its stale grant and stayed deleted: propagation skipped it on `deletedAt` + // alone, since its credential was present and every other eligibility field matched the owner. + const deleted = readCodexAccountRecord("tokenful-deleted")!; + expect(deleted.deletedAt).toBeGreaterThan(0); + expect(deleted.credential!.refreshToken).toBe("tokenful-grant"); + expect(deleted.generation).toBe(ownerGeneration); + } finally { + globalThis.fetch = originalFetch; + } + }); + test("a same-grant sibling on a DIFFERENT upstream identity is never adopted (#2892 review)", async () => { const { forceRefreshCodexPoolToken, getCodexAccountCredential, readCodexAccountRecord, saveCodexAccountCredential } =