Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/codex/shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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 "";
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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");
Expand Down
30 changes: 29 additions & 1 deletion tests/codex-shim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<typeof autoRestoreCodexShim>;
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}`));
Expand Down
Loading