Document instance Effect test helper (#25443)

This commit is contained in:
Kit Langton 2026-05-02 13:27:29 -04:00 committed by GitHub
parent 913321e4b6
commit 895275eb1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 40 deletions

View file

@ -89,20 +89,17 @@ Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect s
```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.instance("does the thing", () =>
Effect.gen(function* () {
const svc = yield* MyService.Service
const out = yield* svc.run()
expect(out).toEqual("ok")
}),
)
})
```
@ -111,6 +108,7 @@ describe("my service", () => {
- 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.
- Use `it.instance(...)` for live Effect tests that need a scoped temporary directory and instance context.
- Most integration-style tests in this package use `it.live(...)`.
### Effect Fixtures
@ -122,7 +120,20 @@ Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a
- `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.
Use `it.instance(...)` by default when a test only needs one temp instance. Yield `TestInstance` from `fixture/fixture.ts` when the test needs the temp directory path:
```typescript
import { TestInstance } from "../fixture/fixture"
it.instance("uses the temp directory", () =>
Effect.gen(function* () {
const test = yield* TestInstance
expect(test.directory).toContain("opencode-test-")
}),
)
```
Use `provideTmpdirInstance(...)` or `tmpdirScoped()` plus `provideInstance(...)` when a test needs multiple directories, custom setup before binding, needs to switch instance context within one test, or explicitly tests instance disposal/reload lifetime.
### Style
@ -130,4 +141,4 @@ Use `provideTmpdirInstance(...)` by default when a test only needs one temp inst
- 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.
- When a test needs instance-local state, prefer `it.instance(...)` over manual `Instance.provide(...)` inside Promise-style tests.

View file

@ -13,7 +13,7 @@ import { Tool } from "@/tool/tool"
import { Agent } from "../../src/agent/agent"
import { SessionID, MessageID } from "../../src/session/schema"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
import { disposeAllInstances, provideTmpdirInstance, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const ctx = {
@ -58,42 +58,39 @@ const run = Effect.fn("WriteToolTest.run")(function* (
describe("tool.write", () => {
describe("new file creation", () => {
it.live("writes content to new file", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const filepath = path.join(dir, "newfile.txt")
const result = yield* run({ filePath: filepath, content: "Hello, World!" })
it.instance("writes content to new file", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "newfile.txt")
const result = yield* run({ filePath: filepath, content: "Hello, World!" })
expect(result.output).toContain("Wrote file successfully")
expect(result.metadata.exists).toBe(false)
expect(result.output).toContain("Wrote file successfully")
expect(result.metadata.exists).toBe(false)
const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8"))
expect(content).toBe("Hello, World!")
}),
),
const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8"))
expect(content).toBe("Hello, World!")
}),
)
it.live("creates parent directories if needed", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const filepath = path.join(dir, "nested", "deep", "file.txt")
yield* run({ filePath: filepath, content: "nested content" })
it.instance("creates parent directories if needed", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "nested", "deep", "file.txt")
yield* run({ filePath: filepath, content: "nested content" })
const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8"))
expect(content).toBe("nested content")
}),
),
const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8"))
expect(content).toBe("nested content")
}),
)
it.live("handles relative paths by resolving to instance directory", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
yield* run({ filePath: "relative.txt", content: "relative content" })
it.instance("handles relative paths by resolving to instance directory", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* run({ filePath: "relative.txt", content: "relative content" })
const content = yield* Effect.promise(() => fs.readFile(path.join(dir, "relative.txt"), "utf-8"))
expect(content).toBe("relative content")
}),
),
const content = yield* Effect.promise(() => fs.readFile(path.join(test.directory, "relative.txt"), "utf-8"))
expect(content).toBe("relative content")
}),
)
})