Browse Source

refactor(file): destroy FileTime facade (#22090)

Kit Langton 5 days ago
parent
commit
82a4292934

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

@@ -1,6 +1,5 @@
 import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { AppFileSystem } from "@/filesystem"
 import { Flag } from "@/flag/flag"
 import type { SessionID } from "@/session/schema"
@@ -112,22 +111,4 @@ export namespace FileTime {
   ).pipe(Layer.orDie)
 
   export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export function read(sessionID: SessionID, file: string) {
-    return runPromise((s) => s.read(sessionID, file))
-  }
-
-  export function get(sessionID: SessionID, file: string) {
-    return runPromise((s) => s.get(sessionID, file))
-  }
-
-  export async function assert(sessionID: SessionID, filepath: string) {
-    return runPromise((s) => s.assert(sessionID, filepath))
-  }
-
-  export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
-    return runPromise((s) => s.withLock(filepath, () => Effect.promise(fn)))
-  }
 }

+ 52 - 0
packages/opencode/test/AGENTS.md

@@ -79,3 +79,55 @@ await using tmp = await tmpdir({
 - Directories are created in the system temp folder with prefix `opencode-test-`
 - Use `await using` for automatic cleanup when the variable goes out of scope
 - Paths are sanitized to strip null bytes (defensive fix for CI environments)
+
+## Testing With Effects
+
+Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.
+
+### Core Pattern
+
+```typescript
+import { describe, expect } from "bun:test"
+import { Effect, Layer } from "effect"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
+
+describe("my service", () => {
+  it.live("does the thing", () =>
+    provideTmpdirInstance(() =>
+      Effect.gen(function* () {
+        const svc = yield* MyService.Service
+        const out = yield* svc.run()
+        expect(out).toEqual("ok")
+      }),
+    ),
+  )
+})
+```
+
+### `it.effect` vs `it.live`
+
+- Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`.
+- Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
+- Most integration-style tests in this package use `it.live(...)`.
+
+### Effect Fixtures
+
+Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a manual runtime in each test.
+
+- `tmpdirScoped(options?)` creates a scoped temp directory and cleans it up when the Effect scope closes.
+- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it just runs an Effect with `Instance.current` bound to `dir`.
+- `provideTmpdirInstance((dir) => effect, options?)` is the convenience helper. It creates a temp directory, binds it as the active instance, and disposes the instance on cleanup.
+- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
+
+Use `provideTmpdirInstance(...)` by default when a test only needs one temp instance. Use `tmpdirScoped()` plus `provideInstance(...)` when a test needs multiple directories, custom setup before binding, or needs to switch instance context within one test.
+
+### Style
+
+- Define `const it = testEffect(...)` near the top of the file.
+- Keep the test body inside `Effect.gen(function* () { ... })`.
+- Yield services directly with `yield* MyService.Service` or `yield* MyTool`.
+- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers when `testEffect(...)` already provides the runtime.
+- When a test needs instance-local state, prefer `provideTmpdirInstance(...)` or `provideInstance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

+ 371 - 394
packages/opencode/test/file/time.test.ts

@@ -1,445 +1,422 @@
-import { describe, test, expect, afterEach } from "bun:test"
-import path from "path"
+import { afterEach, describe, expect } from "bun:test"
 import fs from "fs/promises"
+import path from "path"
+import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { FileTime } from "../../src/file/time"
 import { Instance } from "../../src/project/instance"
 import { SessionID } from "../../src/session/schema"
 import { Filesystem } from "../../src/util/filesystem"
-import { tmpdir } from "../fixture/fixture"
+import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 
 afterEach(async () => {
   await Instance.disposeAll()
 })
 
-async function touch(file: string, time: number) {
-  const date = new Date(time)
-  await fs.utimes(file, date, date)
-}
+const it = testEffect(Layer.mergeAll(FileTime.defaultLayer, CrossSpawnSpawner.defaultLayer))
+
+const id = SessionID.make("ses_00000000000000000000000001")
 
-function gate() {
-  let open!: () => void
-  const wait = new Promise<void>((resolve) => {
-    open = resolve
+const put = (file: string, text: string) => Effect.promise(() => fs.writeFile(file, text, "utf-8"))
+
+const touch = (file: string, time: number) =>
+  Effect.promise(() => {
+    const date = new Date(time)
+    return fs.utimes(file, date, date)
   })
-  return { open, wait }
-}
 
-describe("file/time", () => {
-  const sessionID = SessionID.make("ses_00000000000000000000000001")
+const read = (id: SessionID, file: string) => FileTime.Service.use((svc) => svc.read(id, file))
+
+const get = (id: SessionID, file: string) => FileTime.Service.use((svc) => svc.get(id, file))
+
+const check = (id: SessionID, file: string) => FileTime.Service.use((svc) => svc.assert(id, file))
+
+const lock = <A>(file: string, fn: () => Effect.Effect<A>) => FileTime.Service.use((svc) => svc.withLock(file, fn))
+
+const fail = Effect.fn("FileTimeTest.fail")(function* <A, E, R>(self: Effect.Effect<A, E, R>) {
+  const exit = yield* self.pipe(Effect.exit)
+  if (Exit.isFailure(exit)) {
+    const err = Cause.squash(exit.cause)
+    return err instanceof Error ? err : new Error(String(err))
+  }
+  throw new Error("expected file time effect to fail")
+})
 
+describe("file/time", () => {
   describe("read() and get()", () => {
-    test("stores read timestamp", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-      await fs.writeFile(filepath, "content", "utf-8")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          const before = await FileTime.get(sessionID, filepath)
+    it.live("stores read timestamp", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+
+          const before = yield* get(id, file)
           expect(before).toBeUndefined()
 
-          await FileTime.read(sessionID, filepath)
+          yield* read(id, file)
 
-          const after = await FileTime.get(sessionID, filepath)
+          const after = yield* get(id, file)
           expect(after).toBeInstanceOf(Date)
           expect(after!.getTime()).toBeGreaterThan(0)
-        },
-      })
-    })
-
-    test("tracks separate timestamps per session", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-      await fs.writeFile(filepath, "content", "utf-8")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await FileTime.read(SessionID.make("ses_00000000000000000000000002"), filepath)
-          await FileTime.read(SessionID.make("ses_00000000000000000000000003"), filepath)
-
-          const time1 = await FileTime.get(SessionID.make("ses_00000000000000000000000002"), filepath)
-          const time2 = await FileTime.get(SessionID.make("ses_00000000000000000000000003"), filepath)
-
-          expect(time1).toBeDefined()
-          expect(time2).toBeDefined()
-        },
-      })
-    })
-
-    test("updates timestamp on subsequent reads", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-      await fs.writeFile(filepath, "content", "utf-8")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await FileTime.read(sessionID, filepath)
-          const first = await FileTime.get(sessionID, filepath)
-
-          await FileTime.read(sessionID, filepath)
-          const second = await FileTime.get(sessionID, filepath)
+        }),
+      ),
+    )
+
+    it.live("tracks separate timestamps per session", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+
+          const one = SessionID.make("ses_00000000000000000000000002")
+          const two = SessionID.make("ses_00000000000000000000000003")
+          yield* read(one, file)
+          yield* read(two, file)
+
+          const first = yield* get(one, file)
+          const second = yield* get(two, file)
+
+          expect(first).toBeDefined()
+          expect(second).toBeDefined()
+        }),
+      ),
+    )
+
+    it.live("updates timestamp on subsequent reads", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+
+          yield* read(id, file)
+          const first = yield* get(id, file)
+
+          yield* read(id, file)
+          const second = yield* get(id, file)
 
           expect(second!.getTime()).toBeGreaterThanOrEqual(first!.getTime())
-        },
-      })
-    })
-
-    test("isolates reads by directory", async () => {
-      await using one = await tmpdir()
-      await using two = await tmpdir()
-      await using shared = await tmpdir()
-      const filepath = path.join(shared.path, "file.txt")
-      await fs.writeFile(filepath, "content", "utf-8")
-
-      await Instance.provide({
-        directory: one.path,
-        fn: async () => {
-          await FileTime.read(sessionID, filepath)
-        },
-      })
-
-      await Instance.provide({
-        directory: two.path,
-        fn: async () => {
-          expect(await FileTime.get(sessionID, filepath)).toBeUndefined()
-        },
-      })
-    })
+        }),
+      ),
+    )
+
+    it.live("isolates reads by directory", () =>
+      Effect.gen(function* () {
+        const one = yield* tmpdirScoped()
+        const two = yield* tmpdirScoped()
+        const shared = yield* tmpdirScoped()
+        const file = path.join(shared, "file.txt")
+        yield* put(file, "content")
+
+        yield* provideInstance(one)(read(id, file))
+        const result = yield* provideInstance(two)(get(id, file))
+        expect(result).toBeUndefined()
+      }),
+    )
   })
 
   describe("assert()", () => {
-    test("passes when file has not been modified", 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)
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await FileTime.read(sessionID, filepath)
-          await FileTime.assert(sessionID, filepath)
-        },
-      })
-    })
-
-    test("throws when file was not read first", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-      await fs.writeFile(filepath, "content", "utf-8")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow("You must read file")
-        },
-      })
-    })
-
-    test("throws when file was modified after read", 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)
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          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")
-        },
-      })
-    })
-
-    test("includes timestamps in error message", 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)
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await FileTime.read(sessionID, filepath)
-          await fs.writeFile(filepath, "modified", "utf-8")
-          await touch(filepath, 2_000)
-
-          let error: Error | undefined
-          try {
-            await FileTime.assert(sessionID, filepath)
-          } catch (e) {
-            error = e as Error
-          }
-          expect(error).toBeDefined()
-          expect(error!.message).toContain("Last modification:")
-          expect(error!.message).toContain("Last read:")
-        },
-      })
-    })
+    it.live("passes when file has not been modified", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+          yield* touch(file, 1_000)
+
+          yield* read(id, file)
+          yield* check(id, file)
+        }),
+      ),
+    )
+
+    it.live("throws when file was not read first", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+
+          const err = yield* fail(check(id, file))
+          expect(err.message).toContain("You must read file")
+        }),
+      ),
+    )
+
+    it.live("throws when file was modified after read", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+          yield* touch(file, 1_000)
+
+          yield* read(id, file)
+          yield* put(file, "modified content")
+          yield* touch(file, 2_000)
+
+          const err = yield* fail(check(id, file))
+          expect(err.message).toContain("modified since it was last read")
+        }),
+      ),
+    )
+
+    it.live("includes timestamps in error message", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+          yield* touch(file, 1_000)
+
+          yield* read(id, file)
+          yield* put(file, "modified")
+          yield* touch(file, 2_000)
+
+          const err = yield* fail(check(id, file))
+          expect(err.message).toContain("Last modification:")
+          expect(err.message).toContain("Last read:")
+        }),
+      ),
+    )
   })
 
   describe("withLock()", () => {
-    test("executes function within lock", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          let executed = false
-          await FileTime.withLock(filepath, async () => {
-            executed = true
-            return "result"
-          })
-          expect(executed).toBe(true)
-        },
-      })
-    })
-
-    test("returns function result", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          const result = await FileTime.withLock(filepath, async () => {
-            return "success"
-          })
+    it.live("executes function within lock", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          let hit = false
+
+          yield* lock(file, () =>
+            Effect.sync(() => {
+              hit = true
+              return "result"
+            }),
+          )
+
+          expect(hit).toBe(true)
+        }),
+      ),
+    )
+
+    it.live("returns function result", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          const result = yield* lock(file, () => Effect.succeed("success"))
           expect(result).toBe("success")
-        },
-      })
-    })
-
-    test("serializes concurrent operations on same file", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
+        }),
+      ),
+    )
+
+    it.live("serializes concurrent operations on same file", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
           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)
-          })
+          const hold = yield* Deferred.make<void>()
+          const ready = yield* Deferred.make<void>()
+
+          const one = yield* lock(file, () =>
+            Effect.gen(function* () {
+              order.push(1)
+              yield* Deferred.succeed(ready, void 0)
+              yield* Deferred.await(hold)
+              order.push(2)
+            }),
+          ).pipe(Effect.forkScoped)
 
-          await ready.wait
+          yield* Deferred.await(ready)
 
-          const op2 = FileTime.withLock(filepath, async () => {
-            order.push(3)
-            order.push(4)
-          })
+          const two = yield* lock(file, () =>
+            Effect.sync(() => {
+              order.push(3)
+              order.push(4)
+            }),
+          ).pipe(Effect.forkScoped)
 
-          hold.open()
+          yield* Deferred.succeed(hold, void 0)
+          yield* Fiber.join(one)
+          yield* Fiber.join(two)
 
-          await Promise.all([op1, op2])
           expect(order).toEqual([1, 2, 3, 4])
-        },
-      })
-    })
-
-    test("allows concurrent operations on different files", async () => {
-      await using tmp = await tmpdir()
-      const filepath1 = path.join(tmp.path, "file1.txt")
-      const filepath2 = path.join(tmp.path, "file2.txt")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          let started1 = false
-          let started2 = false
-          const hold = gate()
-          const ready = gate()
-
-          const op1 = FileTime.withLock(filepath1, async () => {
-            started1 = true
-            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])
-          expect(started1).toBe(true)
-          expect(started2).toBe(true)
-        },
-      })
-    })
-
-    test("releases lock even if function throws", async () => {
-      await using tmp = await tmpdir()
-      const filepath = path.join(tmp.path, "file.txt")
-
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await expect(
-            FileTime.withLock(filepath, async () => {
-              throw new Error("Test error")
+        }),
+      ),
+    )
+
+    it.live("allows concurrent operations on different files", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const onefile = path.join(dir, "file1.txt")
+          const twofile = path.join(dir, "file2.txt")
+          let one = false
+          let two = false
+          const hold = yield* Deferred.make<void>()
+          const ready = yield* Deferred.make<void>()
+
+          const a = yield* lock(onefile, () =>
+            Effect.gen(function* () {
+              one = true
+              yield* Deferred.succeed(ready, void 0)
+              yield* Deferred.await(hold)
+              expect(two).toBe(true)
             }),
-          ).rejects.toThrow("Test error")
-
-          let executed = false
-          await FileTime.withLock(filepath, async () => {
-            executed = true
-          })
-          expect(executed).toBe(true)
-        },
-      })
-    })
+          ).pipe(Effect.forkScoped)
+
+          yield* Deferred.await(ready)
+
+          const b = yield* lock(twofile, () =>
+            Effect.sync(() => {
+              two = true
+            }),
+          ).pipe(Effect.forkScoped)
+
+          yield* Fiber.join(b)
+          yield* Deferred.succeed(hold, void 0)
+          yield* Fiber.join(a)
+
+          expect(one).toBe(true)
+          expect(two).toBe(true)
+        }),
+      ),
+    )
+
+    it.live("releases lock even if function throws", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          const err = yield* fail(lock(file, () => Effect.die(new Error("Test error"))))
+          expect(err.message).toContain("Test error")
+
+          let hit = false
+          yield* lock(file, () =>
+            Effect.sync(() => {
+              hit = true
+            }),
+          )
+          expect(hit).toBe(true)
+        }),
+      ),
+    )
   })
 
   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)
+    it.live("read with forward slashes, assert with backslashes", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+          yield* touch(file, 1_000)
+
+          const forward = file.replaceAll("\\", "/")
+          yield* read(id, forward)
+          yield* check(id, file)
+        }),
+      ),
+    )
+
+    it.live("read with backslashes, assert with forward slashes", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+          yield* touch(file, 1_000)
+
+          const forward = file.replaceAll("\\", "/")
+          yield* read(id, file)
+          yield* check(id, forward)
+        }),
+      ),
+    )
+
+    it.live("get returns timestamp regardless of slash direction", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+
+          const forward = file.replaceAll("\\", "/")
+          yield* read(id, forward)
+
+          const result = yield* get(id, file)
           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 () => {
+        }),
+      ),
+    )
+
+    it.live("withLock serializes regardless of slash direction", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          const forward = file.replaceAll("\\", "/")
           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)
-          })
+          const hold = yield* Deferred.make<void>()
+          const ready = yield* Deferred.make<void>()
+
+          const one = yield* lock(file, () =>
+            Effect.gen(function* () {
+              order.push(1)
+              yield* Deferred.succeed(ready, void 0)
+              yield* Deferred.await(hold)
+              order.push(2)
+            }),
+          ).pipe(Effect.forkScoped)
 
-          await ready.wait
+          yield* Deferred.await(ready)
 
-          // Use forward-slash variant -- should still serialize against op1
-          const op2 = FileTime.withLock(forwardSlash, async () => {
-            order.push(3)
-            order.push(4)
-          })
+          const two = yield* lock(forward, () =>
+            Effect.sync(() => {
+              order.push(3)
+              order.push(4)
+            }),
+          ).pipe(Effect.forkScoped)
 
-          hold.open()
+          yield* Deferred.succeed(hold, void 0)
+          yield* Fiber.join(one)
+          yield* Fiber.join(two)
 
-          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()
-      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)
-
-          const stats = Filesystem.stat(filepath)
-          expect(stats?.mtime).toBeInstanceOf(Date)
-          expect(stats!.mtime.getTime()).toBeGreaterThan(0)
-
-          await FileTime.assert(sessionID, filepath)
-        },
-      })
-    })
-
-    test("detects modification via stat mtime", async () => {
-      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 () => {
-          await FileTime.read(sessionID, filepath)
-
-          const originalStat = Filesystem.stat(filepath)
-
-          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())
-
-          await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow()
-        },
-      })
-    })
+    it.live("reads file modification time via Filesystem.stat()", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "content")
+          yield* touch(file, 1_000)
+
+          yield* read(id, file)
+
+          const stat = Filesystem.stat(file)
+          expect(stat?.mtime).toBeInstanceOf(Date)
+          expect(stat!.mtime.getTime()).toBeGreaterThan(0)
+
+          yield* check(id, file)
+        }),
+      ),
+    )
+
+    it.live("detects modification via stat mtime", () =>
+      provideTmpdirInstance((dir) =>
+        Effect.gen(function* () {
+          const file = path.join(dir, "file.txt")
+          yield* put(file, "original")
+          yield* touch(file, 1_000)
+
+          yield* read(id, file)
+
+          const first = Filesystem.stat(file)
+
+          yield* put(file, "modified")
+          yield* touch(file, 2_000)
+
+          const second = Filesystem.stat(file)
+          expect(second!.mtime.getTime()).toBeGreaterThan(first!.mtime.getTime())
+
+          yield* fail(check(id, file))
+        }),
+      ),
+    )
   })
 })