Skip to content

Commit 47da311

Browse files
committed
fix: address latest review follow-ups
1 parent 87ce262 commit 47da311

5 files changed

Lines changed: 128 additions & 17 deletions

File tree

lib/storage/flagged-storage-io.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export async function loadFlaggedAccountsState(params: {
106106
to: params.path,
107107
error: String(persistError),
108108
});
109+
return recovered;
109110
}
110111
}
111112
params.logInfo("Recovered flagged account storage from backup", {

lib/unified-settings.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,16 @@ function cloneRecord(value: unknown): JsonRecord | null {
5353
return { ...value };
5454
}
5555

56+
class InvalidSettingsRecordError extends Error {
57+
override readonly name = "InvalidSettingsRecordError";
58+
}
59+
5660
function parseSettingsRecord(content: string): JsonRecord {
5761
const parsed = cloneRecord(JSON.parse(content));
5862
if (!parsed) {
59-
throw new Error("Unified settings must contain a JSON object at the root.");
63+
throw new InvalidSettingsRecordError(
64+
"Unified settings must contain a JSON object at the root.",
65+
);
6066
}
6167
return parsed;
6268
}
@@ -157,10 +163,7 @@ function isInvalidSettingsRecordError(error: unknown): boolean {
157163
if (error instanceof SyntaxError) {
158164
return true;
159165
}
160-
return (
161-
error instanceof Error &&
162-
error.message === "Unified settings must contain a JSON object at the root."
163-
);
166+
return error instanceof InvalidSettingsRecordError;
164167
}
165168

166169
/**

scripts/codex.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ let shadowHomeCleanupPreflightReadBusyFailuresRemaining = Number.parseInt(
3232
process.env.CODEX_MULTI_AUTH_TEST_SHADOW_PREFLIGHT_READ_BUSY_FAILURES ?? "0",
3333
10,
3434
);
35+
const shadowHomeCleanupRetryMarkerDir =
36+
(process.env.CODEX_MULTI_AUTH_TEST_SHADOW_RETRY_MARKER_DIR ?? "").trim();
3537

3638
function isRetryableShadowHomeCleanupError(error) {
3739
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
@@ -246,6 +248,25 @@ function maybeThrowSimulatedShadowHomePreflightReadBusyError() {
246248
}
247249
}
248250

251+
function writeShadowHomeCleanupRetryMarker(destinationPath, attempt) {
252+
if (shadowHomeCleanupRetryMarkerDir.length === 0) {
253+
return;
254+
}
255+
try {
256+
mkdirSync(shadowHomeCleanupRetryMarkerDir, { recursive: true });
257+
writeFileSync(
258+
join(
259+
shadowHomeCleanupRetryMarkerDir,
260+
`${basename(destinationPath)}.retry-${attempt + 1}`,
261+
),
262+
`${attempt + 1}\n`,
263+
"utf8",
264+
);
265+
} catch {
266+
// Best-effort test hook only.
267+
}
268+
}
269+
249270
function ensureShadowHomeDestinationMatchesSnapshot(destinationPath, expectedState) {
250271
if (!expectedState) {
251272
return;
@@ -277,6 +298,7 @@ function renameFileWithRetry(sourcePath, destinationPath, expectedDestinationSta
277298
) {
278299
throw error;
279300
}
301+
writeShadowHomeCleanupRetryMarker(destinationPath, attempt);
280302
sleepSync(SHADOW_HOME_CLEANUP_BACKOFF_MS[attempt]);
281303
}
282304
}

test/codex-bin-wrapper.test.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -548,28 +548,41 @@ describe("codex bin wrapper", () => {
548548

549549
it("does not clobber sync-back files that change during rename retry backoff", () => {
550550
const fixtureRoot = createWrapperFixture();
551+
const retryMarkerDir = join(fixtureRoot, "retry-markers");
552+
const accountsRetryMarker = join(retryMarkerDir, "accounts.json.retry-1");
551553
const fakeBin = createCustomFakeCodexBin(fixtureRoot, [
552554
"#!/usr/bin/env node",
553555
'const { spawn } = require("node:child_process");',
554556
'const fs = require("node:fs");',
555557
'const path = require("node:path");',
556558
'const home = process.env.CODEX_HOME ?? "";',
559+
'const retryMarker = process.env.CODEX_MULTI_AUTH_TEST_RETRY_MARKER ?? "";',
557560
'const originalHome = process.env.CODEX_MULTI_AUTH_TEST_EXTERNAL_HOME ?? "";',
558-
'fs.writeFileSync(path.join(home, "auth.json"), \'{"token":"shadow"}\\n\', "utf8");',
559561
'fs.writeFileSync(path.join(home, "accounts.json"), \'{"accounts":["shadow"]}\\n\', "utf8");',
560562
'fs.writeFileSync(path.join(home, ".codex-global-state.json"), \'{"last":"shadow"}\\n\', "utf8");',
561-
"if (originalHome) {",
563+
"if (originalHome && retryMarker) {",
562564
" const mutateScript = [",
563565
' \'const fs = require("node:fs");\',',
564566
' \'const path = require("node:path");\',',
565-
' \'const target = process.argv[1];\',',
566-
' \'setTimeout(() => {\',',
567+
' \'const markerPath = process.argv[1];\',',
568+
' \'const target = process.argv[2];\',',
569+
' \'const startedAt = Date.now();\',',
570+
' \'const waitForMarker = () => {\',',
571+
' \' if (fs.existsSync(markerPath)) {\',',
567572
' \' fs.writeFileSync(path.join(target, \"accounts.json\"), \"{\\\\\"accounts\\\\\":[\\\\\"external-during-retry\\\\\"]}\\\\n\", \"utf8\");\',',
568573
' \' fs.writeFileSync(path.join(target, \".codex-global-state.json\"), \"{\\\\\"last\\\\\":\\\\\"external-during-retry\\\\\"}\\\\n\", \"utf8\");\',',
569574
' \' process.exit(0);\',',
570-
' \'}, 40);\',',
575+
' \' return;\',',
576+
' \' }\',',
577+
' \' if (Date.now() - startedAt > 5000) {\',',
578+
' \' process.exit(2);\',',
579+
' \' return;\',',
580+
' \' }\',',
581+
' \' setTimeout(waitForMarker, 5);\',',
582+
' \'};\',',
583+
' \'waitForMarker();\',',
571584
" ].join(\"\\n\");",
572-
" const mutator = spawn(process.execPath, [\"-e\", mutateScript, originalHome], {",
585+
" const mutator = spawn(process.execPath, [\"-e\", mutateScript, retryMarker, originalHome], {",
573586
" detached: true,",
574587
' stdio: "ignore",',
575588
" });",
@@ -581,6 +594,7 @@ describe("codex bin wrapper", () => {
581594
const controlledTmp = join(fixtureRoot, "tmp");
582595
mkdirSync(originalHome, { recursive: true });
583596
mkdirSync(controlledTmp, { recursive: true });
597+
mkdirSync(retryMarkerDir, { recursive: true });
584598
writeFileSync(join(originalHome, "auth.json"), '{"token":"original"}\n', "utf8");
585599
writeFileSync(join(originalHome, "accounts.json"), '{"accounts":["original"]}\n', "utf8");
586600
writeFileSync(join(originalHome, ".codex-global-state.json"), '{"last":"original"}\n', "utf8");
@@ -593,6 +607,8 @@ describe("codex bin wrapper", () => {
593607
CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin,
594608
CODEX_HOME: originalHome,
595609
CODEX_MULTI_AUTH_TEST_EXTERNAL_HOME: originalHome,
610+
CODEX_MULTI_AUTH_TEST_RETRY_MARKER: accountsRetryMarker,
611+
CODEX_MULTI_AUTH_TEST_SHADOW_RETRY_MARKER_DIR: retryMarkerDir,
596612
TMP: controlledTmp,
597613
TEMP: controlledTmp,
598614
TMPDIR: controlledTmp,
@@ -601,7 +617,7 @@ describe("codex bin wrapper", () => {
601617
);
602618

603619
expect(result.status).toBe(0);
604-
expect(readFileSync(join(originalHome, "auth.json"), "utf8").trim()).toBe('{"token":"shadow"}');
620+
expect(readFileSync(join(originalHome, "auth.json"), "utf8").trim()).toBe('{"token":"original"}');
605621
expect(readFileSync(join(originalHome, "accounts.json"), "utf8").trim()).toBe(
606622
'{"accounts":["external-during-retry"]}',
607623
);

test/storage-flagged.test.ts

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,12 @@ describe("flagged account storage", () => {
333333
return originalReadFile(...args);
334334
});
335335

336-
const flagged = await loadFlaggedAccounts();
337-
expect(flagged.accounts).toHaveLength(1);
338-
expect(flagged.accounts[0]?.refreshToken).toBe("primary-flagged");
339-
expect(existsSync(flaggedPath)).toBe(true);
336+
const flagged = await loadFlaggedAccounts();
337+
expect(flagged.accounts).toHaveLength(1);
338+
expect(flagged.accounts[0]?.refreshToken).toBe("primary-flagged");
339+
expect(existsSync(flaggedPath)).toBe(true);
340340

341-
readSpy.mockRestore();
341+
readSpy.mockRestore();
342342
});
343343

344344
it("retries transient flagged primary read errors before falling back to backup", async () => {
@@ -716,4 +716,73 @@ describe("flagged storage extracted helpers", () => {
716716
expect.objectContaining({ path: "flagged.json" }),
717717
);
718718
});
719+
720+
it("does not log successful backup recovery when persisting the recovery fails", async () => {
721+
const { loadFlaggedAccountsState } = await import(
722+
"../lib/storage/flagged-storage-io.js"
723+
);
724+
const fixtureRoot = join(
725+
tmpdir(),
726+
`codex-flagged-io-${Math.random().toString(36).slice(2)}`,
727+
);
728+
const flaggedPath = join(fixtureRoot, "flagged.json");
729+
const resetMarkerPath = `${flaggedPath}.reset`;
730+
const logError = vi.fn();
731+
const logInfo = vi.fn();
732+
733+
try {
734+
await fs.mkdir(fixtureRoot, { recursive: true });
735+
await fs.writeFile(
736+
`${flaggedPath}.bak`,
737+
JSON.stringify(
738+
{
739+
version: 1,
740+
accounts: [
741+
{
742+
refreshToken: "backup-token",
743+
flaggedAt: 1,
744+
addedAt: 1,
745+
lastUsed: 1,
746+
},
747+
],
748+
},
749+
null,
750+
2,
751+
),
752+
"utf8",
753+
);
754+
755+
await expect(
756+
loadFlaggedAccountsState({
757+
path: flaggedPath,
758+
legacyPath: `${flaggedPath}.legacy`,
759+
resetMarkerPath,
760+
normalizeFlaggedStorage: (data) => data as never,
761+
persistRecoveredBackup: vi.fn(async () => {
762+
throw new Error("persist failed");
763+
}),
764+
saveFlaggedAccounts: vi.fn(async () => {}),
765+
logError,
766+
logInfo,
767+
}),
768+
).resolves.toEqual({
769+
version: 1,
770+
accounts: [
771+
{
772+
refreshToken: "backup-token",
773+
flaggedAt: 1,
774+
addedAt: 1,
775+
lastUsed: 1,
776+
},
777+
],
778+
});
779+
expect(logError).toHaveBeenCalledWith(
780+
"Failed to persist recovered flagged account storage",
781+
expect.objectContaining({ from: `${flaggedPath}.bak`, to: flaggedPath }),
782+
);
783+
expect(logInfo).not.toHaveBeenCalled();
784+
} finally {
785+
await removeWithRetry(fixtureRoot, { recursive: true, force: true });
786+
}
787+
});
719788
});

0 commit comments

Comments
 (0)