Files
HealthLog/src/lib/ai/__tests__/codex-slug-cache.test.ts
T
Marc-André Bombeck dfe34386be fix(ai): read a Codex sign-in's memory only under its own account
Every write to the per-sign-in Codex memory checked the account digest,
but the reads did not. A credential reconnected to another ChatGPT
account therefore tried the old account's working model first, sorted
by its refusals and trusted its model list, until the first write
rebound the entry, and the vision gate kept answering from the old
account in the meantime.

The client reads (working model, refusals, model list) now take the
credential and treat an entry learned under another account as empty.
The vision gate holds no account id, so the connect routes rebind the
entry and the disconnect routes drop it, and the gate reads the entry
as its process last bound it.
2026-10-29 21:36:33 +01:00

196 lines
6.9 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import {
CODEX_CATALOG_RETRY_MS,
CODEX_CATALOG_TTL_MS,
CODEX_CREDENTIAL_CACHE_MAX,
CODEX_REJECTED_SLUG_TTL_MS,
CODEX_SLUG_CACHE_TTL_MS,
clearCodexSlugCache,
codexCredentialCacheSize,
codexCredentialKey,
codexGateMemory,
forgetCodexSignIn,
getCachedCodexSlug,
getCodexCatalogState,
rejectedCodexSlugs,
rememberRejectedCodexSlug,
setCachedCodexSlug,
setCodexCatalogState,
startCodexSignIn,
} from "../codex-slug-cache";
/**
* Per-sign-in memory: the working slug (1 h, spec §7b), refused slugs, and
* the account's model list, each kept under its stored credential.
*/
const A = { key: codexCredentialKey.user("a"), accountId: "acct-a" };
const B = { key: codexCredentialKey.user("b"), accountId: "acct-b" };
const CENTRAL = { key: codexCredentialKey.central, accountId: "acct-op" };
describe("Codex per-sign-in cache", () => {
beforeEach(() => {
clearCodexSlugCache();
});
it("keys a person's connection and the central one apart", () => {
expect(codexCredentialKey.user("a")).toBe("user:a");
expect(codexCredentialKey.central).toBe("central");
});
it("starts empty", () => {
expect(getCachedCodexSlug(A)).toBeNull();
expect(rejectedCodexSlugs(A).size).toBe(0);
expect(getCodexCatalogState(A)).toBeNull();
});
it("holds the working slug for exactly the TTL", () => {
setCachedCodexSlug(A, "gpt-6-sol", 1_000);
expect(getCachedCodexSlug(A, 1_000)).toBe("gpt-6-sol");
expect(getCachedCodexSlug(A, 1_000 + CODEX_SLUG_CACHE_TTL_MS)).toBe(
"gpt-6-sol",
);
expect(
getCachedCodexSlug(A, 1_000 + CODEX_SLUG_CACHE_TTL_MS + 1),
).toBeNull();
expect(CODEX_SLUG_CACHE_TTL_MS).toBe(60 * 60 * 1000);
});
it("keeps every sign-in's working slug to itself", () => {
// Two plans: one account works on terra, the other only on gpt-6.
setCachedCodexSlug(A, "gpt-5.6-terra");
setCachedCodexSlug(B, "gpt-6.1-sol");
setCachedCodexSlug(CENTRAL, "gpt-6-sol");
expect(getCachedCodexSlug(A)).toBe("gpt-5.6-terra");
expect(getCachedCodexSlug(B)).toBe("gpt-6.1-sol");
expect(getCachedCodexSlug(CENTRAL)).toBe("gpt-6-sol");
rememberRejectedCodexSlug(B, "gpt-6.1-sol");
expect(getCachedCodexSlug(B)).toBeNull();
expect(getCachedCodexSlug(A)).toBe("gpt-5.6-terra");
expect(rejectedCodexSlugs(A).size).toBe(0);
});
it("a refusal drops the working slot and is held for its own TTL", () => {
setCachedCodexSlug(A, "gpt-5.6-terra", 0);
rememberRejectedCodexSlug(A, "gpt-5.6-terra", 10);
expect(getCachedCodexSlug(A, 10)).toBeNull();
expect([...rejectedCodexSlugs(A, 10)]).toEqual(["gpt-5.6-terra"]);
expect([...rejectedCodexSlugs(A, 10 + CODEX_REJECTED_SLUG_TTL_MS)]).toEqual(
["gpt-5.6-terra"],
);
expect(
rejectedCodexSlugs(A, 10 + CODEX_REJECTED_SLUG_TTL_MS + 1).size,
).toBe(0);
});
it("a slug that works again leaves the refused set", () => {
rememberRejectedCodexSlug(A, "gpt-6-sol");
setCachedCodexSlug(A, "gpt-6-sol");
expect(rejectedCodexSlugs(A).size).toBe(0);
});
it("a credential reconnected to another account reads as empty before anything is written", () => {
setCachedCodexSlug(A, "gpt-5.6-terra");
rememberRejectedCodexSlug(A, "gpt-6-sol");
setCodexCatalogState(A, { kind: "offered", models: [], at: Date.now() });
// Same stored credential, a different ChatGPT account (another plan):
// its first read finds nothing the old account learned.
const other = { key: A.key, accountId: "acct-other" };
expect(getCachedCodexSlug(other)).toBeNull();
expect(rejectedCodexSlugs(other).size).toBe(0);
expect(getCodexCatalogState(other)).toBeNull();
// Its first write starts the entry afresh, and the old account's memory
// is gone with it.
setCachedCodexSlug(other, "gpt-6-luna");
expect(getCachedCodexSlug(other)).toBe("gpt-6-luna");
expect(rejectedCodexSlugs(other).size).toBe(0);
expect(getCachedCodexSlug(A)).toBeNull();
expect(getCodexCatalogState(A)).toBeNull();
});
it("a connect rebinds the gate's memory and a disconnect forgets it", () => {
setCachedCodexSlug(A, "gpt-5.6-terra");
rememberRejectedCodexSlug(A, "gpt-6-sol");
expect(codexGateMemory(A.key).working).toBe("gpt-5.6-terra");
// A reconnect to the same account keeps what it learned.
startCodexSignIn(A);
expect(codexGateMemory(A.key).working).toBe("gpt-5.6-terra");
// A reconnect to another account starts it afresh.
startCodexSignIn({ key: A.key, accountId: "acct-other" });
expect(codexGateMemory(A.key)).toEqual({
working: null,
rejected: new Set(),
catalog: null,
});
setCachedCodexSlug(B, "gpt-6-sol");
forgetCodexSignIn(B.key);
expect(codexGateMemory(B.key).working).toBeNull();
expect(getCachedCodexSlug(B)).toBeNull();
});
it("holds a list for its TTL and a failed read for the shorter retry window", () => {
setCodexCatalogState(A, { kind: "offered", models: [], at: 0 });
expect(getCodexCatalogState(A, CODEX_CATALOG_TTL_MS)?.kind).toBe("offered");
expect(getCodexCatalogState(A, CODEX_CATALOG_TTL_MS + 1)).toBeNull();
setCodexCatalogState(B, { kind: "unreadable", reason: "timeout", at: 0 });
expect(getCodexCatalogState(B, CODEX_CATALOG_RETRY_MS)?.kind).toBe(
"unreadable",
);
expect(getCodexCatalogState(B, CODEX_CATALOG_RETRY_MS + 1)).toBeNull();
});
it("reports the first unreadable read of a stretch, and re-arms on success", () => {
const unreadable = {
kind: "unreadable" as const,
reason: "timeout" as const,
at: 0,
};
expect(setCodexCatalogState(A, unreadable)).toBe(true);
expect(setCodexCatalogState(A, unreadable)).toBe(false);
expect(setCodexCatalogState(B, unreadable)).toBe(true);
expect(
setCodexCatalogState(A, { kind: "offered", models: [], at: 0 }),
).toBe(false);
expect(setCodexCatalogState(A, unreadable)).toBe(true);
});
it("holds at most the cap, dropping the least recently used sign-in", () => {
for (let i = 0; i < CODEX_CREDENTIAL_CACHE_MAX; i += 1) {
setCachedCodexSlug(
{ key: codexCredentialKey.user(`u${i}`), accountId: `acct-${i}` },
"gpt-6-sol",
);
}
// Touch the oldest so the second-oldest is the one to go.
expect(
getCachedCodexSlug({
key: codexCredentialKey.user("u0"),
accountId: "acct-0",
}),
).toBe("gpt-6-sol");
setCachedCodexSlug(
{ key: codexCredentialKey.user("new"), accountId: "acct-new" },
"gpt-6-sol",
);
expect(codexCredentialCacheSize()).toBe(CODEX_CREDENTIAL_CACHE_MAX);
expect(
getCachedCodexSlug({
key: codexCredentialKey.user("u0"),
accountId: "acct-0",
}),
).toBe("gpt-6-sol");
expect(
getCachedCodexSlug({
key: codexCredentialKey.user("u1"),
accountId: "acct-1",
}),
).toBeNull();
});
});