summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoscha Götzer <[email protected]>2026-04-01 23:45:50 +0200
committerGitHub <[email protected]>2026-04-02 07:45:50 +1000
commit880c0a7477f716998db3f25afeab42bc937272e3 (patch)
treea6ff95d17b19fc82d289fcfd5bad68a4b241c262
parenteabf3caeb9ff70bc8a8efcb03210547e3c875a94 (diff)
downloadopencode-880c0a7477f716998db3f25afeab42bc937272e3.tar.gz
opencode-880c0a7477f716998db3f25afeab42bc937272e3.zip
fix: normalize filepath in FileTime to prevent Windows path mismatch (#20367)
Co-authored-by: JosXa <[email protected]> Co-authored-by: Luke Parker <[email protected]>
-rw-r--r--packages/opencode/src/file/time.ts5
-rw-r--r--packages/opencode/test/file/time.test.ts91
2 files changed, 96 insertions, 0 deletions
diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts
index 08f7e9a95..bd2b5f04f 100644
--- a/packages/opencode/src/file/time.ts
+++ b/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)
diff --git a/packages/opencode/test/file/time.test.ts b/packages/opencode/test/file/time.test.ts
index db7eaaae0..ab7659c59 100644
--- a/packages/opencode/test/file/time.test.ts
+++ b/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()