Skip to content

Commit 87ce262

Browse files
committed
fix: cover remaining review regressions
1 parent 55de5e8 commit 87ce262

3 files changed

Lines changed: 101 additions & 3 deletions

File tree

scripts/codex.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ let shadowHomeCleanupBusyFailuresRemaining = Number.parseInt(
2828
process.env.CODEX_MULTI_AUTH_TEST_SHADOW_CLEANUP_BUSY_FAILURES ?? "0",
2929
10,
3030
);
31+
let shadowHomeCleanupPreflightReadBusyFailuresRemaining = Number.parseInt(
32+
process.env.CODEX_MULTI_AUTH_TEST_SHADOW_PREFLIGHT_READ_BUSY_FAILURES ?? "0",
33+
10,
34+
);
3135

3236
function isRetryableShadowHomeCleanupError(error) {
3337
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
@@ -233,11 +237,22 @@ function maybeThrowSimulatedShadowHomeBusyError() {
233237
}
234238
}
235239

240+
function maybeThrowSimulatedShadowHomePreflightReadBusyError() {
241+
if (shadowHomeCleanupPreflightReadBusyFailuresRemaining > 0) {
242+
shadowHomeCleanupPreflightReadBusyFailuresRemaining -= 1;
243+
const error = new Error("simulated busy shadow-home preflight read");
244+
error.code = "EBUSY";
245+
throw error;
246+
}
247+
}
248+
236249
function ensureShadowHomeDestinationMatchesSnapshot(destinationPath, expectedState) {
237250
if (!expectedState) {
238251
return;
239252
}
240-
const currentState = captureShadowHomeState(destinationPath);
253+
const currentState = captureShadowHomeState(destinationPath, {
254+
rethrowRetryableReadErrors: true,
255+
});
241256
if (!shadowHomeStateMatches(currentState, expectedState)) {
242257
const error = new Error("shadow-home destination changed during sync-back retry");
243258
error.code = "EEXIST";
@@ -526,16 +541,22 @@ function ensureTrailingNewline(value) {
526541
return value.endsWith("\n") ? value : `${value}\n`;
527542
}
528543

529-
function captureShadowHomeState(filePath) {
544+
function captureShadowHomeState(filePath, options = {}) {
530545
try {
531546
if (!existsSync(filePath)) {
532547
return { exists: false, content: null };
533548
}
549+
if (options.rethrowRetryableReadErrors) {
550+
maybeThrowSimulatedShadowHomePreflightReadBusyError();
551+
}
534552
return {
535553
exists: true,
536554
content: readFileSync(filePath, "utf8"),
537555
};
538-
} catch {
556+
} catch (error) {
557+
if (options.rethrowRetryableReadErrors && isRetryableShadowHomeCleanupError(error)) {
558+
throw error;
559+
}
539560
return { exists: true, content: null, unreadable: true };
540561
}
541562
}

test/codex-bin-wrapper.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ function injectShadowCleanupBusyFailures(
9494
};
9595
}
9696

97+
function injectShadowPreflightReadBusyFailures(
98+
failuresBeforeSuccess = 2,
99+
): NodeJS.ProcessEnv {
100+
return {
101+
CODEX_MULTI_AUTH_TEST_SHADOW_PREFLIGHT_READ_BUSY_FAILURES: String(
102+
failuresBeforeSuccess,
103+
),
104+
};
105+
}
106+
97107
function createFakeGlobalCodexInstall(rootDir: string): string {
98108
const fakeBin = join(rootDir, "@openai", "codex", "bin", "codex.js");
99109
mkdirSync(dirname(fakeBin), { recursive: true });
@@ -600,6 +610,47 @@ describe("codex bin wrapper", () => {
600610
).toBe('{"last":"external-during-retry"}');
601611
});
602612

613+
it("retries preflight destination reads when the sync-back target is transiently locked", () => {
614+
const fixtureRoot = createWrapperFixture();
615+
const fakeBin = createCustomFakeCodexBin(fixtureRoot, [
616+
"#!/usr/bin/env node",
617+
'const fs = require("node:fs");',
618+
'const path = require("node:path");',
619+
'const home = process.env.CODEX_HOME ?? "";',
620+
'fs.writeFileSync(path.join(home, "auth.json"), \'{"token":"shadow"}\\n\', "utf8");',
621+
'fs.writeFileSync(path.join(home, "accounts.json"), \'{"accounts":["shadow"]}\\n\', "utf8");',
622+
'fs.writeFileSync(path.join(home, ".codex-global-state.json"), \'{"last":"shadow"}\\n\', "utf8");',
623+
"process.exit(0);",
624+
]);
625+
const originalHome = join(fixtureRoot, "codex-home");
626+
const controlledTmp = join(fixtureRoot, "tmp");
627+
mkdirSync(originalHome, { recursive: true });
628+
mkdirSync(controlledTmp, { recursive: true });
629+
writeFileSync(join(originalHome, "auth.json"), '{"token":"original"}\n', "utf8");
630+
writeFileSync(join(originalHome, "accounts.json"), '{"accounts":["original"]}\n', "utf8");
631+
writeFileSync(join(originalHome, ".codex-global-state.json"), '{"last":"original"}\n', "utf8");
632+
writeFileSync(join(originalHome, "config.toml"), 'model_reasoning_effort = "xhigh"\n', "utf8");
633+
634+
const result = runWrapper(
635+
fixtureRoot,
636+
["exec", "status", "--model", "gpt-5.1"],
637+
{
638+
CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin,
639+
CODEX_HOME: originalHome,
640+
TMP: controlledTmp,
641+
TEMP: controlledTmp,
642+
TMPDIR: controlledTmp,
643+
...injectShadowCleanupBusyFailures(1),
644+
...injectShadowPreflightReadBusyFailures(2),
645+
},
646+
);
647+
648+
expect(result.status).toBe(0);
649+
expect(readFileSync(join(originalHome, "auth.json"), "utf8").trim()).toBe('{"token":"shadow"}');
650+
expect(readFileSync(join(originalHome, "accounts.json"), "utf8").trim()).toBe('{"accounts":["shadow"]}');
651+
expect(readFileSync(join(originalHome, ".codex-global-state.json"), "utf8").trim()).toBe('{"last":"shadow"}');
652+
});
653+
603654
it("rewrites unquoted config reasoning effort values for mini compatibility models", () => {
604655
const fixtureRoot = createWrapperFixture();
605656
const fakeBin = createCustomFakeCodexBin(fixtureRoot, [

test/unified-settings.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,32 @@ describe("unified settings", () => {
426426
});
427427
});
428428

429+
it("overwrites invalid primary settings with sync plugin saves when no usable backup exists", async () => {
430+
const {
431+
getUnifiedSettingsPath,
432+
saveUnifiedPluginConfigSync,
433+
loadUnifiedPluginConfigSync,
434+
} = await import("../lib/unified-settings.js");
435+
436+
await fs.writeFile(getUnifiedSettingsPath(), "{ invalid json", "utf8");
437+
438+
saveUnifiedPluginConfigSync({ codexMode: true, fetchTimeoutMs: 45_000 });
439+
440+
expect(loadUnifiedPluginConfigSync()).toEqual({
441+
codexMode: true,
442+
fetchTimeoutMs: 45_000,
443+
});
444+
const parsed = JSON.parse(
445+
await fs.readFile(getUnifiedSettingsPath(), "utf8"),
446+
) as {
447+
pluginConfig?: Record<string, unknown>;
448+
};
449+
expect(parsed.pluginConfig).toEqual({
450+
codexMode: true,
451+
fetchTimeoutMs: 45_000,
452+
});
453+
});
454+
429455
it("returns null for missing pluginConfig section", async () => {
430456
const { getUnifiedSettingsPath, loadUnifiedPluginConfigSync } =
431457
await import("../lib/unified-settings.js");

0 commit comments

Comments
 (0)