Move tests that exercise Effect services out of Promise-land and into the
shared testEffect pattern.
This file is guidance, not a live inventory. Before claiming a migration,
search current dev for the exact anti-pattern and update any PR notes
with what you actually changed.
Every Effect service test should have one local runner near the top:
const it = testEffect(layer)
Use the runner method that matches the behavior:
it.effect("pure service behavior", () =>
Effect.gen(function* () {
const service = yield* SomeService.Service
expect(yield* service.run()).toEqual("ok")
}),
)
it.instance("instance-local behavior", () =>
Effect.gen(function* () {
const test = yield* TestInstance
expect(test.directory).toContain("opencode-test-")
}),
)
it.live("live filesystem or process behavior", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
// real clock / fs / git / process work
}),
)
it.effect(...) — pure Effect behavior with TestClock and
TestConsole.it.instance(...) — service behavior that needs one scoped opencode
instance.it.live(...) — real time, filesystem mtimes, child processes, git,
locks, servers, watchers, or OS behavior.Most integration-style tests use it.live(...) or it.instance(...).
Compose tests from open service layers when a dependency needs replacing.
Do not use a closed defaultLayer and then try to override an inner
dependency after it has already been provided.
Prefer small reusable fake boundary layers in test/fake/*:
AuthTest.empty
AccountTest.empty
NpmTest.noop
SkillTest.empty
ProviderTest.fake().layer
Use Layer.mock for partial service stubs. Missing methods should fail
loudly if the test accidentally calls them.
Do not add generic test-layer builders until repeated local compositions prove the need.
Use Effect-aware fixtures from test/fixture/fixture.ts:
TestInstance inside it.instance(...) for the current temp instance.tmpdirScoped(...) inside Effect.gen for extra temp directories.provideInstance(dir)(effect) when one test needs to switch instance
context.provideTmpdirInstance((dir) => effect, options) when a live test needs
custom instance setup or multiple instance scopes.disposeAllInstances() in afterEach only for integration tests that
intentionally touch shared instance registries.Avoid mutable global setup. If a global mutation is unavoidable during a migration, scope it with acquire/release and treat it as temporary.
Long term, tests should not toggle process.env, Global.Path, or
mutable flags when behavior can be modeled with services. Prefer layers
such as RuntimeFlags.layer(...) or focused fake services.
test(..., async () => Effect.runPromise(...))run(...), load(...), svc(...), or runtime.runPromise(...)
wrappers that only provide a layertmpdir() plus legacy instance provision in Promise test bodiesManagedRuntime.make(...) in test filestry/catch around Effect failuresPromise.withResolvers, Bun.sleep, or setTimeout for synchronization
when events, Deferred, fibers, or deterministic state checks fitPromise helpers are acceptable at non-Effect boundaries, but yield them from
inside an Effect body with Effect.promise(...) rather than making them the
test harness.
layer or
closed defaultLayer is appropriate.layer with real dependencies where relevant and
fake layers at slow or external boundaries.test(..., async () => { ... }) to it.effect, it.instance,
or it.live.await calls inside Effect.gen as yield* calls.await using tmp = await tmpdir(...) with
yield* tmpdirScoped(...) when the temp directory lives inside the
Effect test.Effect.exit, Effect.flip, or
focused assertion helpers.Deferred, and
Effect.all(..., { concurrency: "unbounded" }); do not accidentally
serialize formerly parallel behavior.bun typecheck from packages/opencode.Use current examples as patterns, but re-check them before copying because test migrations are active:
test/effect/instance-state.test.ts — scoped directories, instance
switching, disposal, and concurrency.test/bus/bus-effect.test.ts — Deferred, streams, scoped fibers.test/agent/plugin-agent-regression.test.ts — real service layers plus
fake boundary layers.test/account/service.test.ts — service-level live tests, typed errors,
fake HTTP clients.Do not maintain a long file checklist here. It goes stale quickly.
When looking for the next target, search for current anti-patterns:
git grep -n "Effect.runPromise\|ManagedRuntime\|Promise.withResolvers\|Bun.sleep\|withTestInstance" -- packages/kirincode/test
Then choose one file or one small cluster, keep the PR focused, and mention the focused verification in the PR body.
Exit / Cause can get verbose. Add helpers
only after the same shape repeats across multiple files.Effect.promise(...) around Node/Bun APIs. Prefer
Effect platform services when the surrounding code already uses them, but
do not block useful migrations on perfect abstraction.test/fake/* layers before inventing
larger builders.