| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 13 | 18 | 0% |
| Commands | 0 | 3 | 1 | 0% |
| Section tags | 3 | 4 | 2 | 33% |
What each file covers
Sections
0 shared · 13 only in A · 18 only in B- − Branch Names
- − Commits and PR Titles
- − Style Guide
- − General Principles
- − Destructuring
- − Imports
- − Variables
- − Control Flow
- − Complex Logic
- − Schema Definitions (Drizzle)
- − Testing
- − Type Checking
- − V2 Session Core
- + Test Fixtures Guide
- + Temporary Directory Fixture
- + Basic Usage
- + Options
- + Examples
- + Returned Object
- + Notes
- + Testing With Effects
- + Core Pattern
- + `it.effect` vs `it.live`
- + Effect Fixtures
- + Style
- + Partial Service Stubs
- + Synchronizing With Concurrent Work
- + The Anti-Pattern
- + The Fix
- + Example
- + When Fixed Sleeps Are OK
Commands
0 shared · 3 only in A · 1 only in B- − bun run generate
- − bun typecheck
- − tsc
- + git?: boolean
Section tags
3 shared · 4 only in A · 2 only in B- − lint-format
- − types
- − git-pr
- − database
- + architecture
- + testing-strategy
- test
- code-style
- do-not
Line diff
anomalyco/opencode · AGENTS.md
@@ −1 @@
1- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
2- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
3- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
4- The default branch in this repo is `dev`.
5- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
6
7## Branch Names
8
9Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`.
10
11Examples: `session-recovery`, `fix-scroll-state`, `regenerate-sdk`.
12
13## Commits and PR Titles
14
15Use conventional commit-style messages and PR titles: `type(scope): summary`.
16
17Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes are optional; use the affected package or area when helpful, e.g. `core`, `opencode`, `tui`, `app`, `desktop`, `sdk`, or `plugin`.
18
19Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
20
21## Style Guide
22
23### General Principles
24
25- Keep things in one function unless composable or reusable
26- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
27- Avoid `try`/`catch` where possible
28- Avoid using the `any` type
29- Use Bun APIs when possible, like `Bun.file()`
30- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
31- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
32- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
33- In Effect generators, bind services to named variables before calling methods. Do not use nested service yields such as `yield* (yield* Foo.Service).bar()`.
34
35Reduce total variable count by inlining when a value is only used once.
36
37```ts
38// Good
39const journal = await Bun.file(path.join(dir, "journal.json")).json()
40
41// Bad
42const journalPath = path.join(dir, "journal.json")
43const journal = await Bun.file(journalPath).json()
44```
45
46### Destructuring
47
48Avoid unnecessary destructuring. Use dot notation to preserve context.
49
50```ts
51// Good
52obj.a
53obj.b
54
55// Bad
56const { a, b } = obj
57```
58
59### Imports
60
61- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
62- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
63- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
64- Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as `await import("./module").then((mod) => mod.value())` or `(await import("./module")).value()`. Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
65
66### Variables
67
68Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
69
70```ts
71// Good
72const foo = condition ? 1 : 2
73
74// Bad
75let foo
76if (condition) foo = 1
77else foo = 2
78```
79
80### Control Flow
81
82Avoid `else` statements. Prefer early returns.
83
84```ts
85// Good
86function foo() {
87 if (condition) return 1
88 return 2
89}
90
91// Bad
92function foo() {
93 if (condition) return 1
94 else return 2
95}
96```
97
98### Complex Logic
99
100When a function has several validation branches or supporting details, make the main function read as the happy path and move supporting details into small helpers below it.
101
102```ts
103// Good
104export function loadThing(input: unknown) {
105 const config = requireConfig(input)
106 const metadata = readMetadata(input)
107 return createThing({ config, metadata })
108}
109
110function requireConfig(input: unknown) {
111 ...
112}
113```
114
115- Keep helpers close to the code they support, below the main export when that improves readability.
116- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like `requireConfig` or `readMetadata`.
117- Do not return `Effect` from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
118- Prefer Effect schema helpers such as `Schema.UnknownFromJsonString` and `Schema.decodeUnknownOption` over manual `JSON.parse` wrapped in `Effect.try` when parsing untrusted JSON strings.
119- Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
120
121### Schema Definitions (Drizzle)
122
123Use snake_case for field names so column names don't need to be redefined as strings.
124
125```ts
126// Good
127const table = sqliteTable("session", {
128 id: text().primaryKey(),
129 project_id: text().notNull(),
130 created_at: integer().notNull(),
131})
132
133// Bad
134const table = sqliteTable("session", {
135 id: text("id").primaryKey(),
136 projectID: text("project_id").notNull(),
137 createdAt: integer("created_at").notNull(),
138})
139```
140
141## Testing
142
143- Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.
144- Test actual implementation, do not duplicate logic into tests
145- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
146
147## Type Checking
148
149- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
150
151## V2 Session Core
152
153- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
154- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
155- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
156- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
157- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
158- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
159- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
160- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
161- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.
162
anomalyco/opencode · packages/opencode/test/AGENTS.md
@@ +1 @@
1# Test Fixtures Guide
2
3## Temporary Directory Fixture
4
5The `tmpdir` function in `fixture/fixture.ts` creates temporary directories for tests with automatic cleanup.
6
7### Basic Usage
8
9```typescript
10import { tmpdir } from "./fixture/fixture"
11
12test("example", async () => {
13 await using tmp = await tmpdir()
14 // tmp.path is the temp directory path
15 // automatically cleaned up when test ends
16})
17```
18
19### Options
20
21- `git?: boolean` - Initialize a git repo with a root commit
22- `config?: Partial<Config.Info>` - Write an `opencode.json` config file
23- `init?: (dir: string) => Promise<T>` - Custom setup function, returns value accessible as `tmp.extra`
24- `dispose?: (dir: string) => Promise<T>` - Custom cleanup function
25
26### Examples
27
28**Git repository:**
29
30```typescript
31await using tmp = await tmpdir({ git: true })
32```
33
34**With config file:**
35
36```typescript
37await using tmp = await tmpdir({
38 config: { model: "test/model", username: "testuser" },
39})
40```
41
42**Custom initialization (returns extra data):**
43
44```typescript
45await using tmp = await tmpdir<string>({
46 init: async (dir) => {
47 await Bun.write(path.join(dir, "file.txt"), "content")
48 return "extra data"
49 },
50})
51// Access extra data via tmp.extra
52console.log(tmp.extra) // "extra data"
53```
54
55**With cleanup:**
56
57```typescript
58await using tmp = await tmpdir({
59 init: async (dir) => {
60 const specialDir = path.join(dir, "special")
61 await fs.mkdir(specialDir)
62 return specialDir
63 },
64 dispose: async (dir) => {
65 // Custom cleanup logic
66 await fs.rm(path.join(dir, "special"), { recursive: true })
67 },
68})
69```
70
71### Returned Object
72
73- `path: string` - Absolute path to the temp directory (realpath resolved)
74- `extra: T` - Value returned by the `init` function
75- `[Symbol.asyncDispose]` - Enables automatic cleanup via `await using`
76
77### Notes
78
79- Directories are created in the system temp folder with prefix `opencode-test-`
80- Use `await using` for automatic cleanup when the variable goes out of scope
81- Paths are sanitized to strip null bytes (defensive fix for CI environments)
82
83## Testing With Effects
84
85Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.
86
87### Core Pattern
88
89```typescript
90import { describe, expect } from "bun:test"
91import { Effect, Layer } from "effect"
92import { testEffect } from "../lib/effect"
93
94const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
95
96describe("my service", () => {
97 it.instance("does the thing", () =>
98 Effect.gen(function* () {
99 const svc = yield* MyService.Service
100 const out = yield* svc.run()
101 expect(out).toEqual("ok")
102 }),
103 )
104})
105```
106
107### `it.effect` vs `it.live`
108
109- Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`.
110- Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
111- Use `it.instance(...)` for live Effect tests that need a scoped temporary directory and instance context.
112- Most integration-style tests in this package use `it.live(...)`.
113
114### Effect Fixtures
115
116Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a manual runtime in each test.
117
118- `tmpdirScoped(options?)` creates a scoped temp directory and cleans it up when the Effect scope closes.
119- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it runs an Effect with `InstanceRef` provided for `dir`.
120- `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.
121- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
122
123Use `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:
124
125```typescript
126import { TestInstance } from "../fixture/fixture"
127
128it.instance("uses the temp directory", () =>
129 Effect.gen(function* () {
130 const test = yield* TestInstance
131 expect(test.directory).toContain("opencode-test-")
132 }),
133)
134```
135
136Use `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.
137
138### Style
139
140- Define `const it = testEffect(...)` near the top of the file.
141- Keep the test body inside `Effect.gen(function* () { ... })`.
142- Yield services directly with `yield* MyService.Service` or `yield* MyTool`.
143- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers when `testEffect(...)` already provides the runtime.
144- When a test needs instance-local state, prefer `it.instance(...)` over manual `Instance.provide(...)` inside Promise-style tests.
145
146### Partial Service Stubs
147
148When 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.
149
150```typescript
151import { Effect, Layer } from "effect"
152import { Account } from "@/account/account"
153
154const failingAccountLayer = Layer.mock(Account.Service, {
155 orgsByAccount: () => Effect.fail(new Account.AccountServiceError({ message: "simulated upstream failure" })),
156})
157```
158
159This is much shorter than stubbing every method with `Effect.void` / `Effect.succeed(...)` placeholders, and it keeps the test focused on the behaviour under test.
160
161## Synchronizing With Concurrent Work
162
163### The Anti-Pattern
164
165Using `Effect.sleep(N)` or `setTimeout` as a "wait for the forked fiber to be ready" hack races the scheduler. The forked fiber may not have reached the synchronization point within `N` ms on a slow CI host, and the test fails intermittently. See PR #27622 for a concrete flake that fell out of this exact pattern.
166
167### The Fix
168
169Wait on a **published readiness signal**, not wall-clock time. Available affordances:
170
171- `pollWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — repeatedly run a predicate effect until it returns a non-`undefined` value, with a timeout.
172- `awaitWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — wrap any effect with `Effect.timeoutOrElse` and a custom error message.
173- `llm.wait(n)` from `test/lib/llm-server.ts` — wait until the mock LLM has received `n` HTTP calls.
174- `SessionStatus.Service` `.get(sessionID)` — observable per-session state (`{ type: "busy" | "idle" | ... }`).
175- `BackgroundJob.wait({ id, timeout })` from `src/background/job.ts` — wait for a background job to complete.
176- Bus subscriptions — fork `Stream.runForEach(bus.subscribe(Event), ...)` and open a `Latch` inside the callback to signal first-event readiness.
177- `Deferred.await(deferred).pipe(Effect.timeoutOrElse(...))` for one-shot signals.
178
179### Example
180
181```ts
182// Antipattern — race
183yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
184yield * Effect.sleep(50)
185yield * prompt.cancel(chat.id)
186
187// Fix — wait for a published readiness signal
188yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
189yield *
190 pollWithTimeout(
191 Effect.gen(function* () {
192 const s = yield* (yield* SessionStatus.Service).get(chat.id)
193 return s.type === "busy" ? (true as const) : undefined
194 }),
195 "session never became busy",
196 )
197yield * prompt.cancel(chat.id)
198```
199
200### When Fixed Sleeps Are OK
201
202- Testing debounce or throttle behavior, where the sleep **is** the test.
203- Letting real wall-clock advance past a genuine timestamp resolution boundary (e.g. mtime granularity).
204- Simulating network latency in race-regression tests that intentionally exercise ordering.
205
@@ −1 +1 @@
1−- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
2−- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
3−- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
4−- The default branch in this repo is `dev`.
5−- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
1+# Test Fixtures Guide
62
7−## Branch Names
3+## Temporary Directory Fixture
84
9−Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`.
5+The `tmpdir` function in `fixture/fixture.ts` creates temporary directories for tests with automatic cleanup.
106
11−Examples: `session-recovery`, `fix-scroll-state`, `regenerate-sdk`.
7+### Basic Usage
128
13−## Commits and PR Titles
9+```typescript
10+import { tmpdir } from "./fixture/fixture"
1411
15−Use conventional commit-style messages and PR titles: `type(scope): summary`.
12+test("example", async () => {
13+ await using tmp = await tmpdir()
14+ // tmp.path is the temp directory path
15+ // automatically cleaned up when test ends
16+})
17+```
1618
17−Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes are optional; use the affected package or area when helpful, e.g. `core`, `opencode`, `tui`, `app`, `desktop`, `sdk`, or `plugin`.
19+### Options
1820
19−Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`.
21+- `git?: boolean` - Initialize a git repo with a root commit
22+- `config?: Partial<Config.Info>` - Write an `opencode.json` config file
23+- `init?: (dir: string) => Promise<T>` - Custom setup function, returns value accessible as `tmp.extra`
24+- `dispose?: (dir: string) => Promise<T>` - Custom cleanup function
2025
21−## Style Guide
26+### Examples
2227
23−### General Principles
28+**Git repository:**
2429
25−- Keep things in one function unless composable or reusable
26−- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
27−- Avoid `try`/`catch` where possible
28−- Avoid using the `any` type
29−- Use Bun APIs when possible, like `Bun.file()`
30−- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
31−- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
32−- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
33−- In Effect generators, bind services to named variables before calling methods. Do not use nested service yields such as `yield* (yield* Foo.Service).bar()`.
30+```typescript
31+await using tmp = await tmpdir({ git: true })
32+```
3433
35−Reduce total variable count by inlining when a value is only used once.
34+**With config file:**
3635
37−```ts
38−// Good
39−const journal = await Bun.file(path.join(dir, "journal.json")).json()
40−
41−// Bad
42−const journalPath = path.join(dir, "journal.json")
43−const journal = await Bun.file(journalPath).json()
36+```typescript
37+await using tmp = await tmpdir({
38+ config: { model: "test/model", username: "testuser" },
39+})
4440 ```
4541
46−### Destructuring
42+**Custom initialization (returns extra data):**
4743
48−Avoid unnecessary destructuring. Use dot notation to preserve context.
44+```typescript
45+await using tmp = await tmpdir<string>({
46+ init: async (dir) => {
47+ await Bun.write(path.join(dir, "file.txt"), "content")
48+ return "extra data"
49+ },
50+})
51+// Access extra data via tmp.extra
52+console.log(tmp.extra) // "extra data"
53+```
4954
50−```ts
51−// Good
52−obj.a
53−obj.b
55+**With cleanup:**
5456
55−// Bad
56−const { a, b } = obj
57+```typescript
58+await using tmp = await tmpdir({
59+ init: async (dir) => {
60+ const specialDir = path.join(dir, "special")
61+ await fs.mkdir(specialDir)
62+ return specialDir
63+ },
64+ dispose: async (dir) => {
65+ // Custom cleanup logic
66+ await fs.rm(path.join(dir, "special"), { recursive: true })
67+ },
68+})
5769 ```
5870
59−### Imports
71+### Returned Object
6072
61−- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
62−- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
63−- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
64−- Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as `await import("./module").then((mod) => mod.value())` or `(await import("./module")).value()`. Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
73+- `path: string` - Absolute path to the temp directory (realpath resolved)
74+- `extra: T` - Value returned by the `init` function
75+- `[Symbol.asyncDispose]` - Enables automatic cleanup via `await using`
6576
66−### Variables
77+### Notes
6778
68−Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
79+- Directories are created in the system temp folder with prefix `opencode-test-`
80+- Use `await using` for automatic cleanup when the variable goes out of scope
81+- Paths are sanitized to strip null bytes (defensive fix for CI environments)
6982
70−```ts
71−// Good
72−const foo = condition ? 1 : 2
83+## Testing With Effects
7384
74−// Bad
75−let foo
76−if (condition) foo = 1
77−else foo = 2
78−```
85+Use `testEffect(...)` from `test/lib/effect.ts` for tests that exercise Effect services or Effect-based workflows.
7986
80−### Control Flow
87+### Core Pattern
8188
82−Avoid `else` statements. Prefer early returns.
89+```typescript
90+import { describe, expect } from "bun:test"
91+import { Effect, Layer } from "effect"
92+import { testEffect } from "../lib/effect"
8393
84−```ts
85−// Good
86−function foo() {
87− if (condition) return 1
88− return 2
89−}
94+const it = testEffect(Layer.mergeAll(MyService.defaultLayer))
9095
91−// Bad
92−function foo() {
93− if (condition) return 1
94− else return 2
95−}
96+describe("my service", () => {
97+ it.instance("does the thing", () =>
98+ Effect.gen(function* () {
99+ const svc = yield* MyService.Service
100+ const out = yield* svc.run()
101+ expect(out).toEqual("ok")
102+ }),
103+ )
104+})
96105 ```
97106
98−### Complex Logic
107+### `it.effect` vs `it.live`
99108
100−When a function has several validation branches or supporting details, make the main function read as the happy path and move supporting details into small helpers below it.
109+- Use `it.effect(...)` when the test should run with `TestClock` and `TestConsole`.
110+- Use `it.live(...)` when the test depends on real time, filesystem mtimes, child processes, git, locks, or other live OS behavior.
111+- Use `it.instance(...)` for live Effect tests that need a scoped temporary directory and instance context.
112+- Most integration-style tests in this package use `it.live(...)`.
101113
102−```ts
103−// Good
104−export function loadThing(input: unknown) {
105− const config = requireConfig(input)
106− const metadata = readMetadata(input)
107− return createThing({ config, metadata })
108−}
114+### Effect Fixtures
109115
110−function requireConfig(input: unknown) {
111− ...
112−}
116+Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a manual runtime in each test.
117+
118+- `tmpdirScoped(options?)` creates a scoped temp directory and cleans it up when the Effect scope closes.
119+- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it runs an Effect with `InstanceRef` provided for `dir`.
120+- `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.
121+- `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
122+
123+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:
124+
125+```typescript
126+import { TestInstance } from "../fixture/fixture"
127+
128+it.instance("uses the temp directory", () =>
129+ Effect.gen(function* () {
130+ const test = yield* TestInstance
131+ expect(test.directory).toContain("opencode-test-")
132+ }),
133+)
113134 ```
114135
115−- Keep helpers close to the code they support, below the main export when that improves readability.
116−- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like `requireConfig` or `readMetadata`.
117−- Do not return `Effect` from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
118−- Prefer Effect schema helpers such as `Schema.UnknownFromJsonString` and `Schema.decodeUnknownOption` over manual `JSON.parse` wrapped in `Effect.try` when parsing untrusted JSON strings.
119−- Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
136+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.
120137
121−### Schema Definitions (Drizzle)
138+### Style
122139
123−Use snake_case for field names so column names don't need to be redefined as strings.
140+- Define `const it = testEffect(...)` near the top of the file.
141+- Keep the test body inside `Effect.gen(function* () { ... })`.
142+- Yield services directly with `yield* MyService.Service` or `yield* MyTool`.
143+- Avoid custom `ManagedRuntime`, `attach(...)`, or ad hoc `run(...)` wrappers when `testEffect(...)` already provides the runtime.
144+- When a test needs instance-local state, prefer `it.instance(...)` over manual `Instance.provide(...)` inside Promise-style tests.
124145
125−```ts
126−// Good
127−const table = sqliteTable("session", {
128− id: text().primaryKey(),
129− project_id: text().notNull(),
130− created_at: integer().notNull(),
131−})
146+### Partial Service Stubs
132147
133−// Bad
134−const table = sqliteTable("session", {
135− id: text("id").primaryKey(),
136− projectID: text("project_id").notNull(),
137− createdAt: integer("created_at").notNull(),
148+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.
149+
150+```typescript
151+import { Effect, Layer } from "effect"
152+import { Account } from "@/account/account"
153+
154+const failingAccountLayer = Layer.mock(Account.Service, {
155+ orgsByAccount: () => Effect.fail(new Account.AccountServiceError({ message: "simulated upstream failure" })),
138156 })
139157 ```
140158
141−## Testing
159+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.
142160
143−- Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.
144−- Test actual implementation, do not duplicate logic into tests
145−- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
161+## Synchronizing With Concurrent Work
146162
147−## Type Checking
163+### The Anti-Pattern
148164
149−- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
165+Using `Effect.sleep(N)` or `setTimeout` as a "wait for the forked fiber to be ready" hack races the scheduler. The forked fiber may not have reached the synchronization point within `N` ms on a slow CI host, and the test fails intermittently. See PR #27622 for a concrete flake that fell out of this exact pattern.
150166
151−## V2 Session Core
167+### The Fix
152168
153−- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
154−- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
155−- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
156−- Keep `SessionRunner`, model resolution, tool registry, permissions, and filesystem Location-scoped. Omitted `Location.workspaceID` means implicit-local placement; explicit workspace identity remains reserved for future placement semantics.
157−- Preserve one explicit `llm.stream(request)` call per provider turn and reload projected history before durable continuation. Do not bridge through legacy `SessionPrompt.loop(...)` or delegate orchestration to an in-memory tool loop.
158−- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
159−- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
160−- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
161−- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.
169+Wait on a **published readiness signal**, not wall-clock time. Available affordances:
170+
171+- `pollWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — repeatedly run a predicate effect until it returns a non-`undefined` value, with a timeout.
172+- `awaitWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — wrap any effect with `Effect.timeoutOrElse` and a custom error message.
173+- `llm.wait(n)` from `test/lib/llm-server.ts` — wait until the mock LLM has received `n` HTTP calls.
174+- `SessionStatus.Service` `.get(sessionID)` — observable per-session state (`{ type: "busy" | "idle" | ... }`).
175+- `BackgroundJob.wait({ id, timeout })` from `src/background/job.ts` — wait for a background job to complete.
176+- Bus subscriptions — fork `Stream.runForEach(bus.subscribe(Event), ...)` and open a `Latch` inside the callback to signal first-event readiness.
177+- `Deferred.await(deferred).pipe(Effect.timeoutOrElse(...))` for one-shot signals.
178+
179+### Example
180+
181+```ts
182+// Antipattern — race
183+yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
184+yield * Effect.sleep(50)
185+yield * prompt.cancel(chat.id)
186+
187+// Fix — wait for a published readiness signal
188+yield * prompt.shell({ command: "sleep 30" }).pipe(Effect.forkChild)
189+yield *
190+ pollWithTimeout(
191+ Effect.gen(function* () {
192+ const s = yield* (yield* SessionStatus.Service).get(chat.id)
193+ return s.type === "busy" ? (true as const) : undefined
194+ }),
195+ "session never became busy",
196+ )
197+yield * prompt.cancel(chat.id)
198+```
199+
200+### When Fixed Sleeps Are OK
201+
202+- Testing debounce or throttle behavior, where the sleep **is** the test.
203+- Letting real wall-clock advance past a genuine timestamp resolution boundary (e.g. mtime granularity).
204+- Simulating network latency in race-regression tests that intentionally exercise ordering.
162205
