For LLM agents: Load the
writing-testsskill (.opencode/skills/writing-tests/SKILL.md) before writing or modifying any test file. It contains the full mock isolation rules, CI pipeline structure, and anti-patterns. For agent operational safety and the broader engineering invariants of this repo (especially thetest_runnerbroad-scope restriction and the_internalsDI-seam pattern for mock isolation), readAGENTS.mdat the repo root.
⚠️ Do NOT use the OpenCodetest_runnertool to validate the full repo. It is for targeted agent validation with explicitfiles: [...]or small targeted scopes.scope: 'all'requiresallow_full_suite: trueand is intended for opt-in CI mirrors only. Broad scopes can stall or kill OpenCode before theMAX_SAFE_TEST_FILES = 50(src/tools/test-runner.ts:26) guard fires. For repo validation, use the shell commands below — per-file isolation loops match CI behavior. SeeAGENTS.mdinvariant 6 for the full contract.
All tests use bun:test. No Jest, Vitest, or other frameworks.
import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';# Single file (always safe)
bun --smol test tests/unit/tools/diff.test.ts --timeout 30000
# Per-file loop (required for tools, services, agents — prevents mock poisoning)
for f in tests/unit/tools/*.test.ts; do bun --smol test "$f" --timeout 30000; done
# Batch run (safe only for directories without mock conflicts)
bun --smol test tests/unit/hooks --timeout 120000
bun --smol test tests/unit/cli --timeout 120000Do not run bun --smol test tests/unit/tools as a single batch. Mock modules leak across files in Bun's --smol mode, causing false failures. The CI uses per-file isolation loops for steps 4-6 (tools, services, state/agents).
Bun's --smol mode shares module cache between test files. A mock.module() call replaces the module globally for all files in the same process.
Always spread the real module when mocking:
import * as realChildProcess from 'node:child_process';
const mockExecFileSync = mock(() => '');
mock.module('node:child_process', () => ({
...realChildProcess, // preserve all exports
execFileSync: mockExecFileSync, // override only what you need
}));Use lazy binding in source code so mocks can intercept:
// Good — mockable
import * as child_process from 'node:child_process';
function run() { return child_process.execFileSync('git', ['status']); }
// Bad — binds at load time, mock can't intercept
import { execFileSync } from 'node:child_process';| Step | Directories | Isolation |
|---|---|---|
| 1 | hooks (Linux/macOS only) | Batch per-group |
| 2 | cli | Batch |
| 3 | commands, config | Batch |
| 4 | tools | Per-file loop |
| 5 | services, build, quality, sast, sbom, scripts | Per-file loop |
| 6 | adversarial, agents, background, context, diff, evidence, git, helpers, knowledge, lang, output, parallel, plan, session, skills, types, utils | Per-file loop |
- Use
path.join(), never string concatenation with/ - Use
os.tmpdir(), never hardcoded/tmp - Mock
validateDirectoryfrompath-security.tswhen tests use Windows temp paths
See .opencode/skills/writing-tests/SKILL.md for the complete guide including:
- All mock isolation rules and patterns
- File placement conventions
- Test quality standards (DO and DO NOT)
- Cross-platform process spawning rules
- Pre-submission checklist