The tmpdir function in fixture/fixture.ts creates temporary directories for tests with automatic cleanup.
import { tmpdir } from "./fixture/fixture"
test("example", async () => {
await using tmp = await tmpdir()
// tmp.path is the temp directory path
// automatically cleaned up when test ends
})
git?: boolean - Initialize a git repo with a root commitconfig?: Partial<Config.Info> - Write an opencode.json config fileinit?: (dir: string) => Promise<T> - Custom setup function, returns value accessible as tmp.extradispose?: (dir: string) => Promise<T> - Custom cleanup functionGit repository:
await using tmp = await tmpdir({ git: true })
With config file:
await using tmp = await tmpdir({
config: { model: "test/model", username: "testuser" },
})
Custom initialization (returns extra data):
await using tmp = await tmpdir<string>({
init: async (dir) => {
await Bun.write(path.join(dir, "file.txt"), "content")
return "extra data"
},
})
// Access extra data via tmp.extra
console.log(tmp.extra) // "extra data"
With cleanup:
await using tmp = await tmpdir({
init: async (dir) => {
const specialDir = path.join(dir, "special")
await fs.mkdir(specialDir)
return specialDir
},
dispose: async (dir) => {
// Custom cleanup logic
await fs.rm(path.join(dir, "special"), { recursive: true })
},
})
path: string - Absolute path to the temp directory (realpath resolved)extra: T - Value returned by the init function[Symbol.asyncDispose] - Enables automatic cleanup via await usingopencode-test-await using for automatic cleanup when the variable goes out of scopeUse testEffect(...) from test/lib/effect.ts for tests that exercise Effect services or Effect-based workflows.
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
describe("my service", () => {
it.instance("does the thing", () =>
Effect.gen(function* () {
const svc = yield* MyService.Service
const out = yield* svc.run()
expect(out).toEqual("ok")
}),
)
})
it.effect vs it.liveit.effect(...) when the test should run with TestClock and TestConsole.it.live(...) when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.it.instance(...) for live Effect tests that need a scoped temporary directory and instance context.it.live(...).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 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:
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.
const it = testEffect(...) near the top of the file.Effect.gen(function* () { ... }).yield* MyService.Service or yield* MyTool.ManagedRuntime, attach(...), or ad hoc run(...) wrappers when testEffect(...) already provides the runtime.it.instance(...) over manual Instance.provide(...) inside Promise-style tests.When a test only needs to override one or two methods of a service, prefer Layer.mock over a hand-rolled Layer.succeed(Service, Service.of({ ... })). Layer.mock lets you supply just the methods that matter — anything else throws an UnimplementedError defect if the test accidentally calls it, which is exactly the signal you want.
import { Effect, Layer } from "effect"
import { Account } from "@/account/account"
const failingAccountLayer = Layer.mock(Account.Service, {
orgsByAccount: () =>
Effect.fail(new Account.AccountServiceError({ message: "simulated upstream failure" })),
})
This is much shorter than stubbing every method with Effect.void / Effect.succeed(...) placeholders, and it keeps the test focused on the behaviour under test.