diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 44c04e9e434..cee03e0915a 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -448,7 +448,7 @@ export class FileService extends ServiceMap.Service kick()) }) const status = Effect.fn("FileService.status")(function* () { diff --git a/packages/opencode/test/file/index.test.ts b/packages/opencode/test/file/index.test.ts index 89de5b571b1..8f4cbe8688c 100644 --- a/packages/opencode/test/file/index.test.ts +++ b/packages/opencode/test/file/index.test.ts @@ -681,9 +681,7 @@ describe("file/index Filesystem patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - File.init() - // Give the background scan time to populate - await new Promise((r) => setTimeout(r, 500)) + await File.init() const result = await File.search({ query: "", type: "file" }) expect(result.length).toBeGreaterThan(0) @@ -697,8 +695,7 @@ describe("file/index Filesystem patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - File.init() - await new Promise((r) => setTimeout(r, 500)) + await File.init() const result = await File.search({ query: "", type: "directory" }) expect(result.length).toBeGreaterThan(0) @@ -718,8 +715,7 @@ describe("file/index Filesystem patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - File.init() - await new Promise((r) => setTimeout(r, 500)) + await File.init() const result = await File.search({ query: "main", type: "file" }) expect(result.some((f) => f.includes("main"))).toBe(true) @@ -733,8 +729,7 @@ describe("file/index Filesystem patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - File.init() - await new Promise((r) => setTimeout(r, 500)) + await File.init() const result = await File.search({ query: "", type: "file" }) // Files don't end with / @@ -751,8 +746,7 @@ describe("file/index Filesystem patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - File.init() - await new Promise((r) => setTimeout(r, 500)) + await File.init() const result = await File.search({ query: "", type: "directory" }) // Directories end with / @@ -769,8 +763,7 @@ describe("file/index Filesystem patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - File.init() - await new Promise((r) => setTimeout(r, 500)) + await File.init() const result = await File.search({ query: "", type: "file", limit: 2 }) expect(result.length).toBeLessThanOrEqual(2) @@ -784,8 +777,7 @@ describe("file/index Filesystem patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - File.init() - await new Promise((r) => setTimeout(r, 500)) + await File.init() const result = await File.search({ query: ".hidden", type: "directory" }) expect(result.length).toBeGreaterThan(0) diff --git a/packages/opencode/test/file/time.test.ts b/packages/opencode/test/file/time.test.ts index 2a3c56b2c5f..fbf8d5cd1e2 100644 --- a/packages/opencode/test/file/time.test.ts +++ b/packages/opencode/test/file/time.test.ts @@ -9,6 +9,19 @@ import { tmpdir } from "../fixture/fixture" afterEach(() => Instance.disposeAll()) +async function touch(file: string, time: number) { + const date = new Date(time) + await fs.utimes(file, date, date) +} + +function gate() { + let open!: () => void + const wait = new Promise((resolve) => { + open = resolve + }) + return { open, wait } +} + describe("file/time", () => { const sessionID = SessionID.make("ses_00000000000000000000000001") @@ -25,7 +38,6 @@ describe("file/time", () => { expect(before).toBeUndefined() await FileTime.read(sessionID, filepath) - await Bun.sleep(10) const after = await FileTime.get(sessionID, filepath) expect(after).toBeInstanceOf(Date) @@ -44,7 +56,6 @@ describe("file/time", () => { fn: async () => { await FileTime.read(SessionID.make("ses_00000000000000000000000002"), filepath) await FileTime.read(SessionID.make("ses_00000000000000000000000003"), filepath) - await Bun.sleep(10) const time1 = await FileTime.get(SessionID.make("ses_00000000000000000000000002"), filepath) const time2 = await FileTime.get(SessionID.make("ses_00000000000000000000000003"), filepath) @@ -63,14 +74,10 @@ describe("file/time", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(sessionID, filepath) - await Bun.sleep(10) + await FileTime.read(sessionID, filepath) const first = await FileTime.get(sessionID, filepath) - await Bun.sleep(10) - - FileTime.read(sessionID, filepath) - await Bun.sleep(10) + await FileTime.read(sessionID, filepath) const second = await FileTime.get(sessionID, filepath) expect(second!.getTime()).toBeGreaterThanOrEqual(first!.getTime()) @@ -84,12 +91,12 @@ describe("file/time", () => { 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) await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(sessionID, filepath) - await Bun.sleep(10) + await FileTime.read(sessionID, filepath) await FileTime.assert(sessionID, filepath) }, }) @@ -112,13 +119,14 @@ describe("file/time", () => { 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) await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(sessionID, filepath) - await Bun.sleep(100) + await FileTime.read(sessionID, filepath) await fs.writeFile(filepath, "modified content", "utf-8") + await touch(filepath, 2_000) await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow("modified since it was last read") }, }) @@ -128,13 +136,14 @@ describe("file/time", () => { 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) await Instance.provide({ directory: tmp.path, fn: async () => { await FileTime.read(sessionID, filepath) - await Bun.sleep(100) await fs.writeFile(filepath, "modified", "utf-8") + await touch(filepath, 2_000) let error: Error | undefined try { @@ -191,18 +200,25 @@ describe("file/time", () => { directory: tmp.path, fn: async () => { const order: number[] = [] + const hold = gate() + const ready = gate() const op1 = FileTime.withLock(filepath, async () => { order.push(1) - await Bun.sleep(50) + ready.open() + await hold.wait order.push(2) }) + await ready.wait + const op2 = FileTime.withLock(filepath, async () => { order.push(3) order.push(4) }) + hold.open() + await Promise.all([op1, op2]) expect(order).toEqual([1, 2, 3, 4]) }, @@ -219,15 +235,21 @@ describe("file/time", () => { fn: async () => { let started1 = false let started2 = false + const hold = gate() + const ready = gate() const op1 = FileTime.withLock(filepath1, async () => { started1 = true - await Bun.sleep(50) + ready.open() + await hold.wait expect(started2).toBe(true) }) + await ready.wait + const op2 = FileTime.withLock(filepath2, async () => { started2 = true + hold.open() }) await Promise.all([op1, op2]) @@ -265,12 +287,12 @@ describe("file/time", () => { 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) await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(sessionID, filepath) - await Bun.sleep(10) + await FileTime.read(sessionID, filepath) const stats = Filesystem.stat(filepath) expect(stats?.mtime).toBeInstanceOf(Date) @@ -285,17 +307,17 @@ describe("file/time", () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "file.txt") await fs.writeFile(filepath, "original", "utf-8") + await touch(filepath, 1_000) await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(sessionID, filepath) - await Bun.sleep(10) + await FileTime.read(sessionID, filepath) const originalStat = Filesystem.stat(filepath) - await Bun.sleep(100) await fs.writeFile(filepath, "modified", "utf-8") + await touch(filepath, 2_000) const newStat = Filesystem.stat(filepath) expect(newStat!.mtime.getTime()).toBeGreaterThan(originalStat!.mtime.getTime()) diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 1ebd273d266..e253183d8d0 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -44,6 +44,7 @@ process.env["OPENCODE_TEST_HOME"] = testHome // Set test managed config directory to isolate tests from system managed settings const testManagedConfigDir = path.join(dir, "managed") process.env["OPENCODE_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir +process.env["OPENCODE_DISABLE_DEFAULT_PLUGINS"] = "true" // Write the cache version file to prevent global/index.ts from clearing the cache const cacheDir = path.join(dir, "cache", "opencode") diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index b0ee95ff6f7..7b6784cf49a 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -18,6 +18,11 @@ const ctx = { ask: async () => {}, } +async function touch(file: string, time: number) { + const date = new Date(time) + await fs.utimes(file, date, date) +} + describe("tool.edit", () => { describe("creating new files", () => { test("creates new file when oldString is empty", async () => { @@ -111,7 +116,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() const result = await edit.execute( @@ -138,7 +143,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() await expect( @@ -186,7 +191,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() await expect( @@ -230,18 +235,17 @@ describe("tool.edit", () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "file.txt") await fs.writeFile(filepath, "original content", "utf-8") + await touch(filepath, 1_000) await Instance.provide({ directory: tmp.path, fn: async () => { // Read first - FileTime.read(ctx.sessionID, filepath) - - // Wait a bit to ensure different timestamps - await new Promise((resolve) => setTimeout(resolve, 100)) + await FileTime.read(ctx.sessionID, filepath) // Simulate external modification await fs.writeFile(filepath, "modified externally", "utf-8") + await touch(filepath, 2_000) // Try to edit with the new content const edit = await EditTool.init() @@ -267,7 +271,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() await edit.execute( @@ -294,7 +298,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const { Bus } = await import("../../src/bus") const { File } = await import("../../src/file") @@ -332,7 +336,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() await edit.execute( @@ -358,7 +362,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() await edit.execute( @@ -407,7 +411,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, dirpath) + await FileTime.read(ctx.sessionID, dirpath) const edit = await EditTool.init() await expect( @@ -432,7 +436,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() const result = await edit.execute( @@ -503,7 +507,7 @@ describe("tool.edit", () => { fn: async () => { const edit = await EditTool.init() const filePath = path.join(tmp.path, "test.txt") - FileTime.read(ctx.sessionID, filePath) + await FileTime.read(ctx.sessionID, filePath) await edit.execute( { filePath, @@ -644,7 +648,7 @@ describe("tool.edit", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const edit = await EditTool.init() @@ -659,7 +663,7 @@ describe("tool.edit", () => { ) // Need to read again since FileTime tracks per-session - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const promise2 = edit.execute( { diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index b93ab4e853e..af002a39100 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -99,7 +99,7 @@ describe("tool.write", () => { directory: tmp.path, fn: async () => { const { FileTime } = await import("../../src/file/time") - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const write = await WriteTool.init() const result = await write.execute( @@ -128,7 +128,7 @@ describe("tool.write", () => { directory: tmp.path, fn: async () => { const { FileTime } = await import("../../src/file/time") - FileTime.read(ctx.sessionID, filepath) + await FileTime.read(ctx.sessionID, filepath) const write = await WriteTool.init() const result = await write.execute( @@ -306,7 +306,7 @@ describe("tool.write", () => { directory: tmp.path, fn: async () => { const { FileTime } = await import("../../src/file/time") - FileTime.read(ctx.sessionID, readonlyPath) + await FileTime.read(ctx.sessionID, readonlyPath) const write = await WriteTool.init() await expect(