Przeglądaj źródła

fix: normalize filepath in FileTime to prevent Windows path mismatch (#20367)

Co-authored-by: JosXa <info@josxa.dev>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Joscha Götzer 5 miesięcy temu
rodzic
commit
880c0a7477

+ 5 - 0
packages/opencode/src/file/time.ts

@@ -4,6 +4,7 @@ import { makeRuntime } from "@/effect/run-service"
 import { AppFileSystem } from "@/filesystem"
 import { Flag } from "@/flag/flag"
 import type { SessionID } from "@/session/schema"
+import { Filesystem } from "@/util/filesystem"
 import { Log } from "../util/log"
 
 export namespace FileTime {
@@ -62,6 +63,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
@@ -72,18 +74,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)

+ 91 - 0
packages/opencode/test/file/time.test.ts

@@ -306,6 +306,97 @@ describe("file/time", () => {
     })
   })
 
+  describe("path normalization", () => {
+    test("read with forward slashes, assert with backslashes", async () => {
+      await using tmp = await tmpdir()
+      const filepath = path.join(tmp.path, "file.txt")
+      await fs.writeFile(filepath, "content", "utf-8")
+      await touch(filepath, 1_000)
+
+      const forwardSlash = filepath.replaceAll("\\", "/")
+
+      await Instance.provide({
+        directory: tmp.path,
+        fn: async () => {
+          await FileTime.read(sessionID, forwardSlash)
+          // assert with the native backslash path should still work
+          await FileTime.assert(sessionID, filepath)
+        },
+      })
+    })
+
+    test("read with backslashes, assert with forward slashes", async () => {
+      await using tmp = await tmpdir()
+      const filepath = path.join(tmp.path, "file.txt")
+      await fs.writeFile(filepath, "content", "utf-8")
+      await touch(filepath, 1_000)
+
+      const forwardSlash = filepath.replaceAll("\\", "/")
+
+      await Instance.provide({
+        directory: tmp.path,
+        fn: async () => {
+          await FileTime.read(sessionID, filepath)
+          // assert with forward slashes should still work
+          await FileTime.assert(sessionID, forwardSlash)
+        },
+      })
+    })
+
+    test("get returns timestamp regardless of slash direction", async () => {
+      await using tmp = await tmpdir()
+      const filepath = path.join(tmp.path, "file.txt")
+      await fs.writeFile(filepath, "content", "utf-8")
+
+      const forwardSlash = filepath.replaceAll("\\", "/")
+
+      await Instance.provide({
+        directory: tmp.path,
+        fn: async () => {
+          await FileTime.read(sessionID, forwardSlash)
+          const result = await FileTime.get(sessionID, filepath)
+          expect(result).toBeInstanceOf(Date)
+        },
+      })
+    })
+
+    test("withLock serializes regardless of slash direction", async () => {
+      await using tmp = await tmpdir()
+      const filepath = path.join(tmp.path, "file.txt")
+
+      const forwardSlash = filepath.replaceAll("\\", "/")
+
+      await Instance.provide({
+        directory: tmp.path,
+        fn: async () => {
+          const order: number[] = []
+          const hold = gate()
+          const ready = gate()
+
+          const op1 = FileTime.withLock(filepath, async () => {
+            order.push(1)
+            ready.open()
+            await hold.wait
+            order.push(2)
+          })
+
+          await ready.wait
+
+          // Use forward-slash variant -- should still serialize against op1
+          const op2 = FileTime.withLock(forwardSlash, async () => {
+            order.push(3)
+            order.push(4)
+          })
+
+          hold.open()
+
+          await Promise.all([op1, op2])
+          expect(order).toEqual([1, 2, 3, 4])
+        },
+      })
+    })
+  })
+
   describe("stat() Filesystem.stat pattern", () => {
     test("reads file modification time via Filesystem.stat()", async () => {
       await using tmp = await tmpdir()