From 95d503256b7b3be39031171828e0d991dfb33cfb Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 13 Aug 2026 09:33:00 +0900 Subject: [PATCH] fix(codex): preserve concurrent obsolete shim replacement --- src/codex/shim.ts | 12 +++++++++--- tests/codex-shim.test.ts | 30 +++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/codex/shim.ts b/src/codex/shim.ts index 312c946a0..4e2db95d1 100644 --- a/src/codex/shim.ts +++ b/src/codex/shim.ts @@ -644,6 +644,7 @@ let codexShimProbeHookForTests: (() => void) | null = null; let codexShimProbeShellForTests: string | null = null; let codexShimGuardedWriteHookForTests: (() => void) | null = null; let codexShimFreshWriteHookForTests: (() => void) | null = null; +let codexShimObsoleteWriteHookForTests: (() => void) | null = null; let codexShimProbeObservationMs = CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS; /** Narrow deterministic seam for transaction rollback tests. */ @@ -671,6 +672,11 @@ export function setCodexShimFreshWriteHookForTests(hook: (() => void) | null): v codexShimFreshWriteHookForTests = hook; } +/** @internal Test-only hook for races immediately after an obsolete shim write. */ +export function setCodexShimObsoleteWriteHookForTests(hook: (() => void) | null): void { + codexShimObsoleteWriteHookForTests = hook; +} + function readProbeMetadata(path: string, maxBytes: number): string | null { try { if (!existsSync(path)) return ""; @@ -1596,9 +1602,8 @@ function rollbackObsoleteUnixShimRefresh(journal: readonly ObsoleteUnixShimJourn const wrapper = stableShimPathProbe(entry.file.wrapperPath); const ownsWrapper = entry.wrapperWriteStarted && wrapper !== null - && (entry.writtenWrapperFingerprint - ? sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint) - : wrapper.prefix.includes(UNIX_SHIM_REVISION_MARKER)); + && entry.writtenWrapperFingerprint !== undefined + && sameFingerprint(wrapper.fingerprint, entry.writtenWrapperFingerprint); if (ownsWrapper) unlinkSync(entry.file.wrapperPath); }); attempt(() => { @@ -1654,6 +1659,7 @@ function refreshObsoleteUnixShims(files: readonly ShimFileState[]): ObsoleteUnix } entry.wrapperWriteStarted = true; const writtenInode = writeShim(file.wrapperPath, file.realPath ?? file.backupPath); + codexShimObsoleteWriteHookForTests?.(); const writtenWrapper = stableShimPathProbe(file.wrapperPath); if (!writtenWrapper || !isCurrentUnixShimProbe(writtenWrapper)) { throw new Error("Codex autostart shim upgrade could not fingerprint the regenerated wrapper"); diff --git a/tests/codex-shim.test.ts b/tests/codex-shim.test.ts index dbf74d38a..4e2cd49fd 100644 --- a/tests/codex-shim.test.ts +++ b/tests/codex-shim.test.ts @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, utimesSync, writeFileSync } from "node:fs"; import { delimiter, dirname, join } from "node:path"; import { tmpdir } from "node:os"; -import { autoRestoreCodexShim, buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim, diagnoseCodexShim, findCodexOnPath, installCodexShim, isWindowsInteropDir, lastCodexDiscoveryError, setCodexShimFreshWriteHookForTests, setCodexShimGuardedWriteHookForTests, setCodexShimProbeHookForTests, setCodexShimProbeObservationMsForTests, setCodexShimProbeShellForTests, uninstallCodexShim } from "../src/codex/shim"; +import { autoRestoreCodexShim, buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim, diagnoseCodexShim, findCodexOnPath, installCodexShim, isWindowsInteropDir, lastCodexDiscoveryError, setCodexShimFreshWriteHookForTests, setCodexShimGuardedWriteHookForTests, setCodexShimObsoleteWriteHookForTests, setCodexShimProbeHookForTests, setCodexShimProbeObservationMsForTests, setCodexShimProbeShellForTests, uninstallCodexShim } from "../src/codex/shim"; const SHIM_MARKER = "opencodex codex autostart shim"; const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2"; @@ -1285,6 +1285,34 @@ printf '%s\\n' child-codex }); }); + test("obsolete Unix shim rollback preserves a concurrent current shim", () => { + if (process.platform === "win32") return; + withInstalledShim(({ binDir, wrappers, backups, statePath }) => { + const current = readFileSync(wrappers[0], "utf8"); + const obsolete = obsoleteUnixShim(current); + const replacement = join(binDir, ".concurrent-current-shim"); + const oldBackup = readFileSync(backups[0]); + const oldState = readFileSync(statePath); + writeFileSync(wrappers[0], obsolete, "utf8"); + writeFileSync(replacement, current, "utf8"); + chmodSync(replacement, 0o755); + setCodexShimObsoleteWriteHookForTests(() => renameSync(replacement, wrappers[0])); + + let result!: ReturnType; + try { + result = autoRestoreCodexShim({ enabled: () => true, stabilitySleep: skipStabilityWait }); + } finally { + setCodexShimObsoleteWriteHookForTests(null); + } + + expect(result.status).toBe("deferred"); + expect(readFileSync(wrappers[0], "utf8")).toBe(current); + expect(readFileSync(backups[0])).toEqual(oldBackup); + expect(readFileSync(statePath)).toEqual(oldState); + expect(readdirSync(binDir).some(name => name.includes(".upgrade-"))).toBe(false); + }); + }); + test("stable shim replacement restores through the shared install transaction", () => { withInstalledShim(({ wrappers, backups }) => { const replacements = wrappers.map((wrapper, index) => successfulLauncher(`replacement-${index}`));