feat(aircoding): AirCoding V2 baseline — deterministic multi-agent architecture
Forked from OpenCode v1.17.4 with multi-agent system: - 5 agents: aircoding, scheduler, worker, architect, reviewer - Deterministic DAG scheduling engine (coordinator_tick) - Tool whitelists as hard enforcement - AirCoding validation plugin - System prompt injection for routing - V1 requirements: C4 docs, ADR, AGENTS.md, debug-log.md - Design documents in docs/
This commit is contained in:
169
packages/opencode/test/EFFECT_TEST_MIGRATION.md
Normal file
169
packages/opencode/test/EFFECT_TEST_MIGRATION.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Effect Test Migration
|
||||
|
||||
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.
|
||||
|
||||
## Target Pattern
|
||||
|
||||
Every Effect service test should have one local runner near the top:
|
||||
|
||||
```ts
|
||||
const it = testEffect(layer)
|
||||
```
|
||||
|
||||
Use the runner method that matches the behavior:
|
||||
|
||||
```ts
|
||||
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
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
## Choosing The Runner
|
||||
|
||||
- `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(...)`.
|
||||
|
||||
## Layer Rules
|
||||
|
||||
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/*`:
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
## Fixture Rules
|
||||
|
||||
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.
|
||||
|
||||
## Anti-Patterns To Remove
|
||||
|
||||
- `test(..., async () => Effect.runPromise(...))`
|
||||
- local `run(...)`, `load(...)`, `svc(...)`, or `runtime.runPromise(...)`
|
||||
wrappers that only provide a layer
|
||||
- `tmpdir()` plus legacy instance provision in Promise test bodies
|
||||
- custom `ManagedRuntime.make(...)` in test files
|
||||
- Promise `try/catch` around Effect failures
|
||||
- `Promise.withResolvers`, `Bun.sleep`, or `setTimeout` for synchronization
|
||||
when events, `Deferred`, fibers, or deterministic state checks fit
|
||||
- mutable env/global/flag changes after layers are built
|
||||
|
||||
Promise 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.
|
||||
|
||||
## Conversion Recipe
|
||||
|
||||
1. Identify the real service under test and whether its open `layer` or
|
||||
closed `defaultLayer` is appropriate.
|
||||
2. Build one top-level `layer` with real dependencies where relevant and
|
||||
fake layers at slow or external boundaries.
|
||||
3. Replace local Promise wrappers with Effect helpers.
|
||||
4. Convert `test(..., async () => { ... })` to `it.effect`, `it.instance`,
|
||||
or `it.live`.
|
||||
5. Move `await` calls inside `Effect.gen` as `yield*` calls.
|
||||
6. Replace `await using tmp = await tmpdir(...)` with
|
||||
`yield* tmpdirScoped(...)` when the temp directory lives inside the
|
||||
Effect test.
|
||||
7. Replace Promise failure assertions with `Effect.exit`, `Effect.flip`, or
|
||||
focused assertion helpers.
|
||||
8. Preserve concurrency with fibers, `Deferred`, and
|
||||
`Effect.all(..., { concurrency: "unbounded" })`; do not accidentally
|
||||
serialize formerly parallel behavior.
|
||||
9. Run the focused test file and `bun typecheck` from `packages/opencode`.
|
||||
|
||||
## Good Examples
|
||||
|
||||
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.
|
||||
|
||||
## Migration Queue Policy
|
||||
|
||||
Do not maintain a long file checklist here. It goes stale quickly.
|
||||
|
||||
When looking for the next target, search for current anti-patterns:
|
||||
|
||||
```bash
|
||||
git grep -n "Effect.runPromise\|ManagedRuntime\|Promise.withResolvers\|Bun.sleep\|withTestInstance" -- packages/opencode/test
|
||||
```
|
||||
|
||||
Then choose one file or one small cluster, keep the PR focused, and mention
|
||||
the focused verification in the PR body.
|
||||
|
||||
## Rough Edges To Watch
|
||||
|
||||
- Failure assertions against `Exit` / `Cause` can get verbose. Add helpers
|
||||
only after the same shape repeats across multiple files.
|
||||
- Some tests still need `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.
|
||||
- Layer composition can be noisy when a test needs real service subtrees plus
|
||||
fake boundaries. Extract small `test/fake/*` layers before inventing
|
||||
larger builders.
|
||||
- Concurrency tests can get harder to read after replacing Promise
|
||||
resolvers. Look for repeated patterns that deserve named helpers.
|
||||
Reference in New Issue
Block a user