diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 08f7e9a95127..bd2b5f04f360 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 db7eaaae0d8b..ab7659c59136 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()