Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/opencode/src/file/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
91 changes: 91 additions & 0 deletions packages/opencode/test/file/time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading