From 060b779e7b72c0d29479b6ac209295f2a0afb08b Mon Sep 17 00:00:00 2001 From: lars-hagen Date: Mon, 30 Mar 2026 15:36:33 +0200 Subject: [PATCH] fix(windows): canonicalize FileTime paths to prevent false overwrite rejections --- packages/opencode/src/file/time.ts | 5 +++++ packages/opencode/test/file/time.test.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index d33848000d76..0151e1d3e8a8 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -64,6 +64,7 @@ export namespace FileTime { ) const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) { + filepath = Filesystem.normalizePath(filepath) const locks = (yield* InstanceState.get(state)).locks const lock = locks.get(filepath) if (lock) return lock @@ -74,18 +75,21 @@ export namespace FileTime { }) const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) { + file = Filesystem.normalizePath(file) const reads = (yield* InstanceState.get(state)).reads log.info("read", { sessionID, file }) session(reads, sessionID).set(file, yield* stamp(file)) }) const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) { + file = Filesystem.normalizePath(file) const reads = (yield* InstanceState.get(state)).reads return reads.get(sessionID)?.get(file)?.read }) const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) { if (disableCheck) return + filepath = Filesystem.normalizePath(filepath) const reads = (yield* InstanceState.get(state)).reads const time = reads.get(sessionID)?.get(filepath) @@ -101,6 +105,7 @@ export namespace FileTime { }) const withLock = Effect.fn("FileTime.withLock")(function* (filepath: string, fn: () => Promise) { + filepath = Filesystem.normalizePath(filepath) return yield* Effect.promise(fn).pipe((yield* getLock(filepath)).withPermits(1)) }) diff --git a/packages/opencode/test/file/time.test.ts b/packages/opencode/test/file/time.test.ts index db7eaaae0d8b..be3ee0edc574 100644 --- a/packages/opencode/test/file/time.test.ts +++ b/packages/opencode/test/file/time.test.ts @@ -11,6 +11,8 @@ afterEach(async () => { await Instance.disposeAll() }) +const wintest = process.platform === "win32" ? test : test.skip + async function touch(file: string, time: number) { const date = new Date(time) await fs.utimes(file, date, date) @@ -181,6 +183,23 @@ describe("file/time", () => { }, }) }) + + wintest("treats equivalent Windows path variants as the same file", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "file.txt") + const variant = filepath.replace(/^[A-Za-z]:/, "").replaceAll("\\", "/").toLowerCase() + await fs.writeFile(filepath, "content", "utf-8") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await FileTime.read(sessionID, variant) + + expect(await FileTime.get(sessionID, filepath)).toBeInstanceOf(Date) + await FileTime.assert(sessionID, filepath) + }, + }) + }) }) describe("withLock()", () => {