diff --git a/src/codex/history-manifest.ts b/src/codex/history-manifest.ts index 01e649b306..047dd036c4 100644 --- a/src/codex/history-manifest.ts +++ b/src/codex/history-manifest.ts @@ -54,8 +54,23 @@ export type CodexHistoryManifestValidation = readonly scope: "manifest" | "entry-shape" | "entry-provenance"; }; +/** + * Strips Windows extended-length and device prefixes (e.g. \\?\, \\.\, //?/, //./, \\?\UNC\, //?/UNC/) + * to allow deterministic path comparison across tools. Pure string operation independent of host OS. + */ +export function stripWindowsPathPrefix(path: string): string { + if (/^(\\\\(\?|\.)\\UNC\\|\/\/(\?|\.)\/UNC\/)/i.test(path)) { + return "\\\\" + path.slice(8); + } + if (/^(\\\\(\?|\.)\\|\/\/(\?|\.)\/)/.test(path)) { + return path.slice(4); + } + return path; +} + function codexHistoryPathIdentity(path: string): string { - const canonical = resolve(path); + const stripped = stripWindowsPathPrefix(path); + const canonical = resolve(stripped); return process.platform === "win32" ? canonical.toLowerCase() : canonical; } diff --git a/tests/codex-integration/codex-history-provider.test.ts b/tests/codex-integration/codex-history-provider.test.ts index 972245846f..53f298bac3 100644 --- a/tests/codex-integration/codex-history-provider.test.ts +++ b/tests/codex-integration/codex-history-provider.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { Database } from "bun:sqlite"; import { afterEach, describe, expect, setDefaultTimeout, test } from "bun:test"; import { classifyRecoverableHistoryError, countPendingOpencodexHistory, historyBackupPathFor, isRecoverableHistoryError, migrateHistoryToOpenai, restoreLegacyOpenaiHistory, restoredUserEventFor, setAfterNoopPendingCountForTests, setAfterStrictHistoryRolloutAppendForTests, setBeforeHistoryApplyTransactionForTests, setBeforeHistoryBackupConsumeForTests, setBeforeStrictHistoryRolloutAppendForTests, setHistoryDbBusyTimeoutForTests, snapshotCodexHistoryNoop, syncCodexHistoryProvider, withHistoryRetry } from "../../src/codex/history-provider"; +import { sameCodexHistoryPath, stripWindowsPathPrefix } from "../../src/codex/history-manifest"; import { INVALID_HISTORY_BACKUP_FIXTURES, validHistoryBackupFixture } from "../helpers/codex-history-manifest-fixtures"; import { preflightCodexHistoryInjection, setHistoryAppendHooksForTests } from "../../src/codex/history-provider"; @@ -1513,4 +1514,36 @@ describe("Design B migration helpers", () => { expect(db.query("SELECT model_provider FROM threads WHERE id = 'thread-1'").get()).toEqual({ model_provider: "openai" }); db.close(); }); + + test("stripWindowsPathPrefix normalizes Windows extended prefixes platform-independently", () => { + expect(stripWindowsPathPrefix("\\\\?\\C:\\Users\\test\\state.db")).toBe("C:\\Users\\test\\state.db"); + expect(stripWindowsPathPrefix("\\\\.\\C:\\Users\\test\\state.db")).toBe("C:\\Users\\test\\state.db"); + expect(stripWindowsPathPrefix("//?/C:/Users/test/state.db")).toBe("C:/Users/test/state.db"); + expect(stripWindowsPathPrefix("//./C:/Users/test/state.db")).toBe("C:/Users/test/state.db"); + expect(stripWindowsPathPrefix("\\\\?\\UNC\\server\\share\\path")).toBe("\\\\server\\share\\path"); + expect(stripWindowsPathPrefix("\\\\?\\unc\\server\\share\\path")).toBe("\\\\server\\share\\path"); + expect(stripWindowsPathPrefix("//?/UNC/server/share/path")).toBe("\\\\server/share/path"); + expect(stripWindowsPathPrefix("C:\\standard\\path")).toBe("C:\\standard\\path"); + expect(stripWindowsPathPrefix("/unix/style/path")).toBe("/unix/style/path"); + expect(stripWindowsPathPrefix("")).toBe(""); + }); + + test("sameCodexHistoryPath normalizes Windows extended-length and device path prefixes", () => { + if (process.platform !== "win32") return; + const std = "C:\\Users\\test\\state.db"; + const ext = "\\\\?\\C:\\Users\\test\\state.db"; + const dev = "\\\\.\\C:\\Users\\test\\state.db"; + const fwd = "//?/C:/Users/test/state.db"; + expect(sameCodexHistoryPath(std, ext)).toBe(true); + expect(sameCodexHistoryPath(ext, std)).toBe(true); + expect(sameCodexHistoryPath(std, dev)).toBe(true); + expect(sameCodexHistoryPath(std, fwd)).toBe(true); + }); + + test("sameCodexHistoryPath normalizes Windows UNC paths with extended prefix", () => { + if (process.platform !== "win32") return; + const unc = "\\\\server\\share\\rollout.jsonl"; + const extUnc = "\\\\?\\UNC\\server\\share\\rollout.jsonl"; + expect(sameCodexHistoryPath(unc, extUnc)).toBe(true); + }); });