diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 05a70f80e8..e5055b6718 100644 --- a/packages/opencode/src/file/time.ts +++ b/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(filepath: string, fn: () => Promise): Promise { - return runPromise((s) => s.withLock(filepath, () => Effect.promise(fn))) - } } diff --git a/packages/opencode/test/AGENTS.md b/packages/opencode/test/AGENTS.md index 209cf72da4..00564a17bf 100644 --- a/packages/opencode/test/AGENTS.md +++ b/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. diff --git a/packages/opencode/test/file/time.test.ts b/packages/opencode/test/file/time.test.ts index ab7659c591..7f65d05ead 100644 --- a/packages/opencode/test/file/time.test.ts +++ b/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)) -function gate() { - let open!: () => void - const wait = new Promise((resolve) => { - open = resolve +const id = SessionID.make("ses_00000000000000000000000001") + +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 } -} + +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 = (file: string, fn: () => Effect.Effect) => FileTime.Service.use((svc) => svc.withLock(file, fn)) + +const fail = Effect.fn("FileTimeTest.fail")(function* (self: Effect.Effect) { + 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", () => { - const sessionID = SessionID.make("ses_00000000000000000000000001") - 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") + it.live("stores read timestamp", () => + provideTmpdirInstance((dir) => + Effect.gen(function* () { + const file = path.join(dir, "file.txt") + yield* put(file, "content") - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const before = await FileTime.get(sessionID, filepath) + 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") + it.live("tracks separate timestamps per session", () => + provideTmpdirInstance((dir) => + Effect.gen(function* () { + const file = path.join(dir, "file.txt") + yield* put(file, "content") - 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 one = SessionID.make("ses_00000000000000000000000002") + const two = SessionID.make("ses_00000000000000000000000003") + yield* read(one, file) + yield* read(two, file) - const time1 = await FileTime.get(SessionID.make("ses_00000000000000000000000002"), filepath) - const time2 = await FileTime.get(SessionID.make("ses_00000000000000000000000003"), filepath) + const first = yield* get(one, file) + const second = yield* get(two, file) - expect(time1).toBeDefined() - expect(time2).toBeDefined() - }, - }) - }) + expect(first).toBeDefined() + expect(second).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") + it.live("updates timestamp on subsequent reads", () => + provideTmpdirInstance((dir) => + Effect.gen(function* () { + const file = path.join(dir, "file.txt") + yield* put(file, "content") - await Instance.provide({ - directory: tmp.path, - fn: async () => { - await FileTime.read(sessionID, filepath) - const first = await FileTime.get(sessionID, filepath) + yield* read(id, file) + const first = yield* get(id, file) - await FileTime.read(sessionID, filepath) - const second = await FileTime.get(sessionID, filepath) + 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") + 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") - 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() - }, - }) - }) + 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) + 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) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - await FileTime.read(sessionID, filepath) - await FileTime.assert(sessionID, filepath) - }, - }) - }) + yield* read(id, file) + yield* check(id, file) + }), + ), + ) - 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") + 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") - await Instance.provide({ - directory: tmp.path, - fn: async () => { - await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow("You must read file") - }, - }) - }) + const err = yield* fail(check(id, file)) + expect(err.message).toContain("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) + 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) - 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") - }, - }) - }) + yield* read(id, file) + yield* put(file, "modified content") + yield* touch(file, 2_000) - 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) + const err = yield* fail(check(id, file)) + expect(err.message).toContain("modified since it was last read") + }), + ), + ) - 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) + 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) - 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:") - }, - }) - }) + 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") + it.live("executes function within lock", () => + provideTmpdirInstance((dir) => + Effect.gen(function* () { + const file = path.join(dir, "file.txt") + let hit = false - 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" - }) - 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 () => { - 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 - - 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]) - }, - }) - }) - - 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") + yield* lock(file, () => + Effect.sync(() => { + hit = true + return "result" }), - ).rejects.toThrow("Test error") + ) - let executed = false - await FileTime.withLock(filepath, async () => { - executed = true - }) - expect(executed).toBe(true) - }, - }) - }) + 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") + }), + ), + ) + + 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 = yield* Deferred.make() + const ready = yield* Deferred.make() + + 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) + + yield* Deferred.await(ready) + + const two = yield* lock(file, () => + Effect.sync(() => { + order.push(3) + order.push(4) + }), + ).pipe(Effect.forkScoped) + + yield* Deferred.succeed(hold, void 0) + yield* Fiber.join(one) + yield* Fiber.join(two) + + expect(order).toEqual([1, 2, 3, 4]) + }), + ), + ) + + 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() + const ready = yield* Deferred.make() + + const a = yield* lock(onefile, () => + Effect.gen(function* () { + one = true + yield* Deferred.succeed(ready, void 0) + yield* Deferred.await(hold) + expect(two).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) + 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 forwardSlash = filepath.replaceAll("\\", "/") + const forward = file.replaceAll("\\", "/") + yield* read(id, forward) + yield* check(id, file) + }), + ), + ) - 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) - }, - }) - }) + 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) - 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 forward = file.replaceAll("\\", "/") + yield* read(id, file) + yield* check(id, forward) + }), + ), + ) - const forwardSlash = filepath.replaceAll("\\", "/") + 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") - 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) - }, - }) - }) + const forward = file.replaceAll("\\", "/") + yield* read(id, forward) - 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) + 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 hold = yield* Deferred.make() + const ready = yield* Deferred.make() - const op1 = FileTime.withLock(filepath, async () => { - order.push(1) - ready.open() - await hold.wait - order.push(2) - }) + 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) + 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) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - await FileTime.read(sessionID, filepath) + yield* read(id, file) - const stats = Filesystem.stat(filepath) - expect(stats?.mtime).toBeInstanceOf(Date) - expect(stats!.mtime.getTime()).toBeGreaterThan(0) + const stat = Filesystem.stat(file) + expect(stat?.mtime).toBeInstanceOf(Date) + expect(stat!.mtime.getTime()).toBeGreaterThan(0) - await FileTime.assert(sessionID, filepath) - }, - }) - }) + yield* check(id, file) + }), + ), + ) - 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) + 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) - await Instance.provide({ - directory: tmp.path, - fn: async () => { - await FileTime.read(sessionID, filepath) + yield* read(id, file) - const originalStat = Filesystem.stat(filepath) + const first = Filesystem.stat(file) - await fs.writeFile(filepath, "modified", "utf-8") - await touch(filepath, 2_000) + yield* put(file, "modified") + yield* touch(file, 2_000) - const newStat = Filesystem.stat(filepath) - expect(newStat!.mtime.getTime()).toBeGreaterThan(originalStat!.mtime.getTime()) + const second = Filesystem.stat(file) + expect(second!.mtime.getTime()).toBeGreaterThan(first!.mtime.getTime()) - await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow() - }, - }) - }) + yield* fail(check(id, file)) + }), + ), + ) }) })