5.6 KiB
Testing policy
How this repo tests, tier by tier, and the rules that keep a green suite meaningful. Commands live in root AGENTS.md; linked RFCs carry the rationale.
Tiers
- Unit (
pnpm run test): vitest overpackages|examples/*/tests/**/*.spec.ts, colocated with what they test. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (seepackages/core/agent-loop/tests/contract-regressions.spec.ts). - Coverage gate (
pnpm run test:coverage): the gating run, per-file 100% onpackages/*/*/src. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. - Real-API e2e (
pnpm run test:e2e): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (EXA_API_KEY,PERPLEXITY_API_KEY, …); each suite self-skips without its key so keyless CI stays green (real-API e2e RFC). - Snapshot (
pnpm run test:snapshot): real example subprocesses replay recorded model sessions keylessly and compare normalized stdout plus re-persisted logs (snapshot RFC). The primary suite pins ACP JSON-RPC; the headless projection reusesadvanced-toolchainforstream-json. Usepnpm run test:snapshot:recordwhen the model transcript changes andpnpm run test:snapshot:refreshwhen only replay outputs change; review the golden diff. One scenario per header class pins system-prompt/tool-schema content; other fixtures tokenize it (pinned-header RFC).
The with-key policy: inference is cheap here
We are DeepSeek — do not ration real-API tests. A no-key test proves plumbing; only a with-key run proves the agent works against a real model. Write many: file-writing prompts, multi-turn conversations, tool use, cancellation mid-stream. Highest-value are smoke tests that boot the real example, send one real prompt, and check the world — they catch the "green unit tests, broken product" class that mocks structurally cannot (postmortem 0001). The self-skip exists only so secretless CI and keyless contributors aren't blocked; it is not a cost signal. Every example ships a keyless smoke and — unless keyless-by-nature — a with-key smoke (examples/AGENTS.md).
Prefer the real implementation over a mock
Mock only the genuinely expensive or non-deterministic boundary (the LLM adapter, the network, the clock); keep everything downstream real. A hand-rolled stand-in proves the bridge moves bytes, not that the shipping tool behaves as asserted — the two drift while the test stays green. Example: bridge tool-call tests run the scripted mock MODEL but the real tool + real executor (makeBridgeHarness({ withBash: true }) plugs dsh-bash-local + dsh-tool-bash and runs an actual echo).
Verify the world, not the self-report
An e2e assertion re-runs the command or re-reads the file externally; a keyword probe on the agent's own output lets a cheating agent pass. Assert untouched files are byte-identical. e2e tests own their resources: create the harness in the test, dispose in afterEach (even on failure/retry/timeout); shared fixtures live in a plain tests/harness.ts, never another *.e2e.ts (importing a spec re-registers its describe and duplicates real API calls).
Test the real entry path
- Product-visible plugins require a non-unit REAL-composition test. Hand-built
ctx.plugin(...)suites are insufficient: boot test-onlycordis.ymlthrough Loader and app/process, mock only external/nondeterministic boundaries, and assert model-visible request/log, durable state, or user-visible output. Keep opt-ins out of shipped defaults. - A guard only guards if the regression actually fails it. For a plugin without
inject(bundle/composition plugins), a Loader smoke stays green under a broken export shape — add an explicitexpect('default' in mod).toBe(false)plus anunwrapExportsround-trip assertion, and prove it: introduce the regression, watch red, revert. - "Real entry path" means the published artifact: the package
binpoints at builtlib/bin.jsunder plainnode, which tsx masks (settle races, module resolution, a swallowed load failure exiting 0). The same applies to any non-index runtime entry the built package resolves at run time (the worker-thread runtime's siblinglib/worker.cjs). Keep the built-artifact smokes green (packages/ui/*/tests/built-bin.e2e.ts,packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts), and assert a genuinely-missing config exits non-zero. - An e2e that spawns an example from a temp cwd sets
TSX_TSCONFIG_PATHto the repo-root tsconfig, or it silently falls back to stale builtlib/(examples/AGENTS.md).
When a snapshot test is required
A change affecting an editor transcript, headless event stream, or agent UX adds or updates the owning examples/<name>/tests/snapshots/ scenario, or explains its omission in the PR. examples/acp-agent hosts the primary dsh-acp-snapshot table and the headless stream-json projection. Plans for new capability seams, lifecycle shapes, or transcript surfaces identify every test tier and any required harness work before implementation.