.agents/skills/opencode-qa/references/testing-harness.md
This is reference material for writing and running tests against the opencode source. The skill's own QA scripts (CLI, curl, sqlite) do not require this, but it is the authoritative pattern when you need a unit or integration test.
The runner is bun test (Bun built-in, not vitest or jest).
Tests cannot run from the repo root. Two guards enforce this:
bunfig.toml at repo root sets root = "./do-not-run-tests-from-root"package.json has "test": "echo 'do not run tests from root' && exit 1"Run from a package directory instead:
cd packages/opencode && bun test --timeout 30000
Run a single file:
bun test test/tool/read.test.ts
Filter by test name:
bun test --grep "truncates large file"
CI variant:
bun run test:ci
Turbo dependency: opencode#test depends on ^build.
The preload file is packages/opencode/test/preload.ts. It is wired via packages/opencode/bunfig.toml:
[test]
preload = ["@opentui/solid/preload", "./test/preload.ts"]
What it does:
XDG_DATA_HOME, XDG_CACHE_HOME, XDG_CONFIG_HOME, and XDG_STATE_HOME to temp directoriesOPENCODE_TEST_HOMEOPENCODE_DB=":memory:" (SQLite in-memory)process.envOPENCODE_EXPERIMENTAL_EVENT_SYSTEM=trueOPENCODE_EXPERIMENTAL_WORKSPACES=trueLog.init({ print: false })initProjectors()The it factory wraps bun:test with three variants:
it.effect(name, body) ... TestClock + TestConsole (isolated time)it.live(name, body) ... real clock + TestConsoleit.instance(name, body, opts) ... real clock + scoped tmpdir + a real Instance contexttestEffect(layer) builds an it bound to an Effect layer:
const it = testEffect(Layer.mergeAll(readLayer(), testInstanceStoreLayer))
tmpdirScoped(options?) ... scoped temp directory. Optional git: true, optional config (writes opencode.json), optional init.provideInstance(directory)(effect) ... runs an Effect inside a real instance for that directory.withTmpdirInstance({ git?, config?, init? })(effect) ... one-liner: make tmpdir, optional git init + config, provide instance.testInstanceStoreLayer ... instance store with a no-op bootstrap.cliIt.live(name, body, timeoutMs?) and cliIt.concurrent(...) spawn the real CLI (bun run --conditions=browser src/index.ts) in an isolated environment.
Exposed helpers:
opencode.run()opencode.serve()opencode.acp()expectExitparseJsonEventsIsolation environment keys:
OPENCODE_TEST_HOMEOPENCODE_CONFIG_CONTENT (inline provider config)OPENCODE_DISABLE_PROJECT_CONFIG=1OPENCODE_PURE=1OPENCODE_DISABLE_AUTOUPDATE=1OPENCODE_DISABLE_AUTOCOMPACT=1OPENCODE_DISABLE_MODELS_FETCH=1Real example from packages/opencode/test/cli/serve/serve-process.test.ts:
cliIt.live("spawns serve and health responds", async ({ opencode, expectExit }) => {
const server = await opencode.serve()
expect(server.port).toBeGreaterThan(0)
const res = await fetch(`${server.url}/global/health`)
expect(res.status).toBe(200)
})
TestLLMServer is an in-process OpenAI-compatible SSE server to mock model responses deterministically.
Methods:
llm.text("hello")llm.tool("read", { filePath: "x" })llm.pushMatch(matchFn, reply)This is how tests avoid real provider calls.
From packages/opencode/test/tool/read.test.ts:
const it = testEffect(Layer.mergeAll(readLayer(), testInstanceStoreLayer))
it.instance("truncates large file over maxReadFileSize", () =>
Effect.gen(function* () {
const test = yield* TestInstance
// ... exercise the read tool, assert truncation
})
)
From packages/opencode/test/session/session.test.ts:
test("session.created fires after session.create", async () => {
const deferred = Deferred.unsafeMake<void>(FiberId.none)
// ... listen for session.created event
await session.create({})
// ... assert deferred resolves
})
From packages/opencode/test/cli/run/runtime.boot.test.ts:
import { describe, expect, mock, spyOn, test } from "bun:test"
test("boots runtime without errors", () => {
// ... standard assertions, no Effect
})
The app lives in packages/app (SolidJS). Config is packages/app/playwright.config.ts. It starts the Vite dev server via webServer; the backend is expected at localhost:4096.
Commands (run from packages/app):
bunx playwright install chromium
bun run test:e2e:local
Filter with grep:
bun run test:e2e:local -- --grep "settings"
UI mode:
bun run test:e2e:ui
App unit tests:
bun run test:unit
This equals bun test --preload ./happydom.ts ./src.
Per opencode AGENTS.md:
bun typecheck from the package directory (uses tsgo). Never run bare tsc.For runtime or scriptable QA without writing tests, use the opencode-qa scripts (Cases A-D in SKILL.md).