Skip to content

Commit 60597f8

Browse files
committed
Add runtime persistence failure coverage
1 parent bafd2c8 commit 60597f8

3 files changed

Lines changed: 92 additions & 1 deletion

File tree

lib/runtime/account-check.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export async function runRuntimeAccountCheck(
328328
deps.invalidateAccountManagerCache();
329329
} else {
330330
if (state.flaggedChanged) {
331-
await deps.saveFlaggedAccounts(state.flaggedStorage);
331+
await deps.saveFlaggedAccounts(state.flaggedStorage);
332332
}
333333
if (state.storageChanged) {
334334
await deps.saveAccounts(workingStorage);

test/runtime-account-check.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,51 @@ describe("runRuntimeAccountCheck", () => {
263263
expect(saveFlaggedAccounts).not.toHaveBeenCalled();
264264
});
265265

266+
it("propagates EBUSY from combined persistence without partial writes", async () => {
267+
const persistAccountAndFlaggedStorage = vi.fn(async () => {
268+
const error = new Error("busy") as Error & { code?: string };
269+
error.code = "EBUSY";
270+
throw error;
271+
});
272+
const saveAccounts = vi.fn(async () => {});
273+
const saveFlaggedAccounts = vi.fn(async () => {});
274+
275+
await expect(
276+
runRuntimeAccountCheck(true, {
277+
hydrateEmails: async (storage) => storage,
278+
loadAccounts: async () => ({
279+
version: 3,
280+
accounts: [{ email: "one@example.com", refreshToken: "refresh-1", accessToken: undefined, addedAt: 1, lastUsed: 1 }],
281+
activeIndex: 0,
282+
activeIndexByFamily: { codex: 0 },
283+
}),
284+
createEmptyStorage: () => ({ version: 3, accounts: [], activeIndex: 0, activeIndexByFamily: {} }),
285+
loadFlaggedAccounts: async () => ({ version: 1, accounts: [] }),
286+
createAccountCheckWorkingState: (flaggedStorage) => ({ flaggedStorage, removeFromActive: new Set(), storageChanged: false, flaggedChanged: false, ok: 0, errors: 0, disabled: 0 }),
287+
lookupCodexCliTokensByEmail: async () => null,
288+
extractAccountId: () => undefined,
289+
shouldUpdateAccountIdFromToken: () => false,
290+
sanitizeEmail: (email) => email,
291+
extractAccountEmail: () => undefined,
292+
queuedRefresh: async () => ({ type: "failed", reason: "invalid_grant", message: "refresh failed" }),
293+
isRuntimeFlaggableFailure: () => true,
294+
fetchCodexQuotaSnapshot: async () => { throw new Error("should not probe quota in deep mode"); },
295+
resolveRequestAccountId: () => undefined,
296+
formatCodexQuotaLine: () => "quota",
297+
clampRuntimeActiveIndices: vi.fn(),
298+
MODEL_FAMILIES: ["codex"],
299+
saveAccounts,
300+
invalidateAccountManagerCache: vi.fn(),
301+
saveFlaggedAccounts,
302+
persistAccountAndFlaggedStorage,
303+
showLine: vi.fn(),
304+
}),
305+
).rejects.toThrow("busy");
306+
307+
expect(saveAccounts).not.toHaveBeenCalled();
308+
expect(saveFlaggedAccounts).not.toHaveBeenCalled();
309+
});
310+
266311
it("treats cache lookup failures as a cache miss and still refreshes", async () => {
267312
const saveAccounts = vi.fn(async () => {});
268313
await runRuntimeAccountCheck(false, {

test/runtime-verify-flagged.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,52 @@ describe("verifyRuntimeFlaggedAccounts", () => {
345345
expect(saveFlaggedAccounts).not.toHaveBeenCalled();
346346
});
347347

348+
it("propagates EBUSY from persistAccountsAndFlagged without partial writes", async () => {
349+
const now = 10_000;
350+
const persistAccountsAndFlagged = vi.fn(async () => {
351+
const error = new Error("busy") as Error & { code?: string };
352+
error.code = "EBUSY";
353+
throw error;
354+
});
355+
const persistAccounts = vi.fn(async () => {});
356+
const saveFlaggedAccounts = vi.fn(async () => {});
357+
358+
await expect(
359+
verifyRuntimeFlaggedAccounts({
360+
loadFlaggedAccounts: async () => ({
361+
version: 1,
362+
accounts: [
363+
{
364+
email: "refresh@example.com",
365+
refreshToken: "refresh-token",
366+
addedAt: 1,
367+
lastUsed: 1,
368+
},
369+
],
370+
}),
371+
lookupCodexCliTokensByEmail: async () => {
372+
throw new Error("busy");
373+
},
374+
queuedRefresh: async () => ({
375+
type: "success" as const,
376+
access: "new-access",
377+
refresh: "new-refresh",
378+
expires: now + 60_000,
379+
}),
380+
resolveTokenSuccessAccount: (tokens) => ({ ...tokens }) as never,
381+
persistAccounts,
382+
persistAccountsAndFlagged,
383+
invalidateAccountManagerCache: vi.fn(),
384+
saveFlaggedAccounts,
385+
now: () => now,
386+
showLine: vi.fn(),
387+
}),
388+
).rejects.toThrow("busy");
389+
390+
expect(persistAccounts).not.toHaveBeenCalled();
391+
expect(saveFlaggedAccounts).not.toHaveBeenCalled();
392+
});
393+
348394
it("leaves flagged state untouched when persistAccounts throws EBUSY", async () => {
349395
const now = 10_000;
350396
const saveFlaggedAccounts = vi.fn(async () => {});

0 commit comments

Comments
 (0)