Test coverage matrix¶
The idea and convention below are harvested from openhuman
(GPL-3.0) — description only, no code; see the openhuman research must stay clean-room decision
note in the brain. Implementation here is original.
This page maps every capability to the test file(s) that actually exercise it, so "is this covered?" is a lookup instead of tribal knowledge. It is honest by design: a row marked ✅ means a real test failed when the behavior was broken while writing this page, not that a file with a plausible name exists. Where coverage is thin or absent, that is stated plainly, with why it matters and — for anything only verifiable by hand — what an automated version would need.
Convention (see CLAUDE.md): a PR that changes a capability's behavior updates that
capability's row in this matrix in the same PR.
Status vocabulary¶
| Symbol | Meaning |
|---|---|
| ✅ Full | Automated test(s) cover the happy path and at least one failure/edge path, exercising real behavior (real git, real filesystem, real child processes where relevant — not mocked away). |
| 🟡 Partial | Automated, but a specific gap is named in the Notes column. |
| ⚠️ Manual-by-necessity | Cannot be exercised by this suite without infrastructure it doesn't have (a second physical machine, a live external host). A human must verify it; what an automated version would need is stated. |
| ❌ Missing | Implemented with no automated test. |
Verified 2026-09-02 against pnpm test: 104 test files, 1122 tests, all green.
Concurrency-sensitive paths (CLAUDE.md definition-of-done)¶
These are the paths ADR-0003's "concurrent writes union-merge without conflict" claim actually rests on. They get named individually because a silent regression here is the one that most directly breaks the project's core pitch.
| Capability | Implementation | Test(s) | Status | Notes |
|---|---|---|---|---|
| Atomic note writes (no reader ever sees a partial file) | packages/core/src/notes.ts (writeNote: write to .tmp, publish via fs.link) |
packages/core/test/notes.test.ts |
✅ Full | — |
| Id collision fails closed, never silently overwrites | packages/core/src/notes.ts (fs.link throws EEXIST, not fs.rename) |
packages/core/test/notes.test.ts (writeNote refuses to overwrite an existing note on an id collision (#101)) |
✅ Full | — |
Id collision-resistance (ids.ts) |
packages/core/src/ids.ts (makeNoteId, 4-char random suffix, ~1.7M values) |
packages/core/test/concurrency.test.ts (makeNoteId uses a random suffix so repeated calls differ) |
🟡 Partial | The test is a 50-call uniqueness spot-check, not a statistical collision-rate proof. Acceptable given the fail-closed backstop above, but note it's a sample, not a bound. |
| Two writers, same title+date, merge as a union with zero git conflict | packages/core/src/notes.ts + ids.ts |
packages/core/test/concurrency.test.ts (same title+date on two branches produce distinct files that merge cleanly) |
✅ Full | Simulated via two git branches merged sequentially, not literally simultaneous processes — see the cross-cutting note below. |
| True simultaneous writers (real race, not turn-taking) | packages/curate/src/capture.ts (contributor person resolution; supersession) |
packages/curate/test/capture-autopromote.test.ts (converges concurrent name-only and email-backed first writes, uses Promise.all), packages/curate/test/capture-supersede-lock.test.ts (two concurrent captures superseding one target never disagree with disk, uses Promise.all) |
✅ Full | The two places in the suite that fire two writers with genuine Promise.all concurrency rather than sequential turns. Everything else in this table is turn-taking simulation of the outcome of a race (see cross-cutting note). |
Sync engine: single-writer lock serializes concurrent syncOnce() on one brain |
packages/sync/src/lock.ts, packages/sync/src/engine.ts |
packages/sync/test/lock.test.ts, packages/sync/test/engine.test.ts (skips (no-op) when another live process holds the sync lock (#100)), packages/core/test/clone.test.ts (survives a concurrent race — both callers converge, neither corrupts) |
✅ Full | This lock is the actual mechanism that makes same-machine concurrency safe — the design serializes rather than relying on atomic-everything. |
| Sync engine: crash recovery (stranded rebase from a killed process) | packages/sync/src/engine.ts |
packages/sync/test/engine.test.ts (recovers a rebase stranded by a prior crashed pass, then converges (#100)) |
✅ Full | — |
| MCP-only sync races a hook-driven sync on one brain (ADR-0040) | packages/mcp/src/sync.ts, packages/core/src/lock.ts |
packages/mcp/test/sync.test.ts (defers while a concurrent hook sync holds the lock, and the note lands on the next pass, runs simultaneously with a hook sync without corrupting the repo or deadlocking) |
✅ Full | Two components now sync the same brain (#290). The second test is one of the few genuine Promise.all races in the suite — two separate SyncEngine instances, so only the cross-process file lock orders them — and asserts convergence with no conflict markers in either teammate-visible note. |
Conflict handling — sibling-file resolution (packages/sync/src/conflict.ts) |
packages/sync/src/conflict.ts (resolveConflictsAsSiblings) |
packages/sync/test/conflict.test.ts |
✅ Full | Only 2 tests, but both substantive: same-file conflict resolves to two sibling files with no markers, and a secret present during conflict resolution is never committed/pushed (#98). |
| Curation gate: scope (deny/allow routing before capture) | packages/curate/src/scope.ts, packages/curate/src/capture.ts |
packages/curate/test/scope.test.ts, packages/curate/test/capture-scope.test.ts |
✅ Full | — |
| Curation gate: secret detection | packages/core/src/secrets.ts, secrets-gitleaks.generated.ts |
packages/core/test/secrets.test.ts, packages/core/test/secrets-gitleaks.test.ts, packages/curate/test/secrets.test.ts (gate-level: title/tags/kind-specific fields, entropy opt-in) |
✅ Full | Gitleaks-derived patterns are tested against a benign corpus for false positives, not just true positives. |
| Curation gate: lexical dedup | packages/curate/src/curate.ts |
packages/curate/test/curate.test.ts (rejects a candidate near-identical to an existing canon note, stages only one of two near-identical candidates in a batch) |
✅ Full | — |
| Curation gate: semantic dedup (ADR-0021) | packages/curate/src/curate.ts (embedding path) |
packages/curate/test/semantic-dedup.test.ts |
✅ Full | Covers flag-off passthrough, flag-on rejection, no-vectors-yet fallback, and a true-negative (unrelated novel note still stages). |
| Staging → promote | packages/curate/src/staging.ts, packages/curate/src/promote-pr.ts |
packages/curate/test/staging.test.ts, packages/curate/test/promote-pr.test.ts |
🟡 Partial | staging.ts itself has exactly one test (writes land under staging/<dir>/, stay out of canon). promote-pr.ts is thoroughly covered (branch/commit/push/PR body, partial-selection, post-merge reconciliation ADR-0008, closed-PR re-promote, secret scrub parity #98/#16, no-remote / no-gh graceful refusals). The thin spot is staging.ts's own edge cases (e.g. re-staging an id already pending) rather than the promote flow. |
Supersede semantics (status + superseded_by, never delete) |
packages/core/src/notes.ts (supersedeNote/overwriteNote), packages/curate/src/consolidate.ts, packages/curate/src/tombstone.ts |
packages/core/test/notes.test.ts (marks a memory note superseded in place, keeping the file), packages/curate/test/consolidate.test.ts (supersede-not-delete on near-duplicate consolidation, single-writer lock respected), packages/curate/test/tombstone.test.ts (reject-tombstones union-merge across teammates, corrupt-file isolation, idempotent re-reject) |
✅ Full | — |
| Supersession is single-writer on the capture path (#281) | packages/curate/src/capture.ts (acquireSyncLock around the ADR-0030 supersession loop) |
packages/curate/test/capture-supersede-lock.test.ts (concurrent Promise.all supersessions of one target, contended-lock defer + persisted supersession-deferred receipt, lock released on the throwing path, no lock taken when there is nothing to supersede) |
✅ Full | supersedeNote is a read-modify-write of a pre-existing note, so it is the one capture write that atomic-one-fact-per-file does not protect. Both concurrency tests fail if the lock is removed. |
| Auto-promote flag (ADR-0014): default-on straight-to-canon vs. gated review queue | packages/curate/src/capture.ts, packages/curate/src/graduate.ts |
packages/curate/test/capture-autopromote.test.ts, packages/curate/test/graduate.test.ts (stays in staging even when the org-brain has autoPromote=true (manual review by default)) |
✅ Full | — |
Cross-cutting honesty note on "concurrency" tests: most tests above simulate concurrency by
turn-taking — two git branches created and merged in sequence, or syncOnce() called on alice
then bob — rather than firing genuinely simultaneous processes. This is a reasonable choice for
most of them: the design's actual safety net for same-machine concurrency is the sync lock
(tested directly), and for cross-machine concurrency the real race is decided by git's own
atomic ref update at the remote (whoever pushes first wins; the loser's syncOnce sees a
rejected push and runs conflict resolution) — the sequential alice-then-bob shape in
conflict.test.ts and sync/test/engine.test.ts is what that race actually reduces to once one
push has landed, so it is faithful, not a shortcut. The one place a literal Promise.all race is
exercised is capture-autopromote.test.ts's contributor-identity test, joined by
capture-supersede-lock.test.ts's concurrent supersessions (#281). What a stronger
automated test would need: a fixture that spawns two real child processes against the same
brain directory with Promise.all and asserts convergence — most valuable for the id-collision
path (writing the exact same title+date+random-suffix collision is currently only reachable by
mocking shortId(), which no test does) and for the sync lock under true OS-level concurrent
syncOnce() calls rather than sequential ones.
packages/core¶
| Capability | Test file(s) | Status | Notes |
|---|---|---|---|
| Note parse/serialize round-trip, unknown-key preservation (#81) | notes.test.ts |
✅ Full | — |
| Path containment (no escaping the brain dir, #76/#77) | notes.test.ts |
✅ Full | — |
Malformed note isolation (one bad file doesn't fail listNotes, #80) |
notes.test.ts |
✅ Full | — |
Project provenance layout (<project>/<kind>/, ADR-0015) |
notes.test.ts |
✅ Full | — |
| Derived-file detection (hub/MOC vs. user-owned README, ADR-0034) | notes.test.ts |
✅ Full | — |
ask retrieval (citation-anchored, coverage signal, ADR-0020) |
ask.test.ts |
✅ Full | — |
| Attribution / responsibility linking (ADR-0029) | attribution.test.ts |
✅ Full | — |
| Capture-coverage accounting | capture-coverage.test.ts |
✅ Full | — |
| Brain clone / ensure-cloned, incl. concurrent race | clone.test.ts |
✅ Full | — |
| Config IO, rule resolution | config.test.ts, rule-resolution.test.ts, shared-rules.test.ts |
✅ Full | — |
| Embeddings | embed.test.ts, vectors.test.ts |
✅ Full | — |
| Derived-index emit / rebuild | emit.test.ts |
✅ Full | — |
| Org-brain graduation policy | graduate-policy.test.ts |
✅ Full | — |
doctor-style health checks |
health.test.ts |
✅ Full | — |
| Vault-hygiene lint (dead links, metadata gaps, stale derived views, #258) | hygiene.test.ts |
✅ Full | Read-only (asserted on mtimes, not just content). bodyWikilinks code-stripping is table-driven over the fence forms real notes use — indented, blockquoted, ~~~, nested backtick runs. |
| Derived SQLite index + hybrid (FTS5 + vector) search | index-db.test.ts, hybrid-search.test.ts |
✅ Full | Includes planDerived (write-free content plan) and the derived-file prune contract: regenerateDerived prunes ghost MOCs / legacy INDEX.md by default, and { prune: false } (used by the --fix self-heals) deletes nothing — a hand-written .md at a brain/project root would otherwise be classified as derived and removed. |
| MOC / map generation | map.test.ts |
✅ Full | — |
| Registry (brain path resolution), org-brain registry | registry.test.ts, registry-orgbrain.test.ts |
✅ Full | — |
| Project resolution | projects.test.ts |
✅ Full | — |
| Note source/provenance stamping | source.test.ts |
✅ Full | — |
| Status/summary reporting | status.test.ts |
✅ Full | — |
verify (derived-vs-source consistency check) |
verify.test.ts |
✅ Full | — |
packages/curate¶
Gate-level rows are in the concurrency table above. Additional capabilities:
| Capability | Test file(s) | Status | Notes |
|---|---|---|---|
| Adopt (bring an external note into canon) | adopt.test.ts |
✅ Full | — |
| Auto-ADR proposal on decision-shaped captures | autoadr.test.ts |
✅ Full | — |
| Consolidate near-duplicate canon notes | consolidate.test.ts |
✅ Full | — |
| Context assembly for the curator | context.test.ts |
✅ Full | — |
| Contradiction detection (ADR-0033) | contradiction.test.ts |
✅ Full | — |
| Nearest-neighbor ranking | neighbors.test.ts |
✅ Full | — |
| Reclassify memory → decision (#265) | reclassify.test.ts |
✅ Full | — |
| Relevance scoring | relevance.test.ts |
✅ Full | — |
| Human review-queue listing | review.test.ts |
✅ Full | — |
| Verdict capture (per-candidate accept/reject bookkeeping) | verdict-capture.test.ts |
✅ Full | — |
| Smoke (built curate binary runs) | smoke.test.ts |
✅ Full | — |
| Project stamping on capture | capture-project.test.ts |
✅ Full | — |
Project relayout (move files to <project>/<kind>/, ADR-0035) |
relayout.test.ts |
✅ Full | — |
| Project rename (id + folders, one commit, #304) | project-rename.test.ts |
✅ Full | Alias-only + declared-frontmatter rename, collision/invalid/unknown refusals, write-nothing dry-run |
packages/sync¶
Lock, conflict, engine convergence, and secret-scrub rows are in the concurrency table above. Additional:
| Capability | Test file(s) | Status | Notes |
|---|---|---|---|
| Git plumbing (commit, non-ASCII paths #99) | git.test.ts |
✅ Full | — |
| Daemon lifecycle (start/stop, single-instance guard #100, self-trigger avoidance) | daemon.test.ts |
✅ Full | Daemon itself is legacy per ADR-0032 (daemonless is now the default lifecycle) — see packages/plugin row below. Still exercised because the code path remains in the tree. |
| Derived-file formatting | format.test.ts |
✅ Full | — |
| Sync queue | queue.test.ts |
✅ Full | — |
| Push/pull retry policy | retry.test.ts |
✅ Full | — |
| Smoke (built sync binary runs) | smoke.test.ts |
✅ Full | — |
packages/mcp¶
| Capability | Test file(s) | Status | Notes |
|---|---|---|---|
| Brain-dir resolution for the MCP server | brain.test.ts |
✅ Full | — |
Prompt rendering + drift guard against packages/plugin/commands/*.md (#216) |
prompts.test.ts |
✅ Full | — |
| Resource listing | resources.test.ts |
✅ Full | — |
| Server construction, tool registration, no-brain / corrupt-config error surfacing (#210) | server.test.ts |
✅ Full | — |
| Tool implementations (the six MCP tools) | tools.test.ts |
✅ Full | — |
| MCP-only sync on hookless hosts: publish-on-write, pull-on-start, flag default, honest failure reporting (#290, ADR-0040) | sync.test.ts |
✅ Full | Asserts git state, not file existence: the remembered note is tracked, the tree is clean, and a fresh clone of the remote has it. Failure paths covered: unreachable remote (committed locally + "publishing FAILED"), no remote at all, a wedged pass hitting the time cap, a staged note that can never publish, and lock contention. The flag-off case pins the pre-ADR-0040 response text byte-for-byte so Claude Code's write path cannot drift. |
| stdio protocol handshake against the built binary (lists tools over real stdio) | smoke.test.ts |
✅ Full | This is the deepest automated handshake test — real process, real MCP stdio transport. |
| Live handshake against an actual Claude Code / Codex client | — | ⚠️ Manual-by-necessity | No automated test drives a real Claude Code or Codex process against the MCP server; smoke.test.ts proves the wire protocol works, not that Claude Code's specific client behavior (tool-call formatting, prompt argument passing) matches. An automated version would need a scripted Claude Code CLI session (or the Codex equivalent) as a subprocess — out of reach for a unit/integration suite without a live model call. |
packages/cli¶
| Capability | Test file(s) | Status | Notes |
|---|---|---|---|
add |
add.test.ts |
✅ Full | — |
ask |
ask.test.ts |
✅ Full | — |
| Capture logging | capture-log.test.ts |
✅ Full | — |
demo (scripted recall walkthrough) |
demo.test.ts |
✅ Full | — |
| Dependency checks | deps.test.ts |
✅ Full | — |
| History discovery / missed-capture analysis | discover.test.ts |
✅ Full | — |
doctor (incl. vault-hygiene links: dead links, metadata gaps, stale derived views, orphans, #258) |
doctor.test.ts |
✅ Full | — |
emit (derived-file regeneration CLI) |
emit.test.ts |
✅ Full | — |
| Host integration (writing Claude Code / Codex config) | host-integration.test.ts |
✅ Full | — |
init |
init.test.ts |
✅ Full | — |
lint (vault-hygiene lint, #258) |
lint.test.ts |
✅ Full | Includes the --fix guarantee: it regenerates drifted derived views and never deletes a hand-written markdown file at a brain/project root. |
| Interactive onboarding wizard | onboard.test.ts, wizard.test.ts |
✅ Full | — |
| Org-brain commands | org-brain.test.ts |
✅ Full | — |
| Prompt templating | prompt.test.ts |
✅ Full | — |
| Registry commands | registry.test.ts |
✅ Full | — |
| Background service management | service.test.ts |
✅ Full | — |
| Statusline integration | statusline.test.ts |
✅ Full | — |
synthesize |
synthesize.test.ts |
✅ Full | — |
update |
update.test.ts |
✅ Full | — |
verify CLI wrapper |
verify.test.ts |
✅ Full | — |
| Smoke (built CLI binary runs) | smoke.test.ts |
✅ Full | — |
| Claude/Codex onboarding + update parity (#226) | host-parity.e2e.test.ts |
✅ Full | Genuine e2e: runs the real CLI against fixture host directories for both hosts. |
packages/seed¶
| Capability | Test file(s) | Status | Notes |
|---|---|---|---|
| Config importer (seeding from an existing tool's config) | config-importer.test.ts |
✅ Full | — |
| Git-history mining for seed candidates | git-miner.test.ts |
✅ Full | — |
| Seed orchestration | seed.test.ts |
✅ Full | — |
| Smoke (built seed binary runs) | smoke.test.ts |
✅ Full | — |
packages/plugin (the hook layer — Claude Code + Codex)¶
This is the layer most likely to look untested from file names alone, because the hooks/*.mjs
entrypoints are thin CLI wrappers. In fact the logic they wrap is unit-tested directly, and the
wrappers themselves are exercised as real subprocesses:
| Capability | Implementation | Test(s) | Status | Notes |
|---|---|---|---|---|
| Session lifecycle logic (start/prompt-submit injection, scope gate, corrupt-config surfacing #210) | hooks/lib.mjs |
test/lib.test.ts, test/realdeps.test.ts |
✅ Full | realdeps.test.ts runs the real hook entrypoints as subprocesses with DISABLE_HOOKS toggled, a real curate binary over stdin, and a hard-killed wedged extraction child (#104) — not just the imported functions. |
| Transcript extraction (Claude + Codex schemas, oversized-transcript handling #84) | hooks/extraction.mjs |
test/extraction.test.ts, test/realdeps.test.ts (multi-MB transcript via stdin not argv, #84) |
✅ Full | — |
| LLM classification of captured candidates (batched, fail-open) | hooks/classify.mjs |
test/classify.test.ts, test/curation-pipeline.test.ts |
✅ Full | — |
| Contradiction guard (PreToolUse, ADR-0033) | hooks/contradiction-guard.mjs |
test/contradiction-guard.test.ts |
✅ Full | — |
| Reclassify judge (fail-closed per row, #265) | hooks/reclassify.mjs |
test/reclassify.test.ts |
✅ Full | — |
| Codex lifecycle adapter (event mapping, recursion guard, PreCompact/Stop throttling, #225) | hooks/codex-hook.mjs |
test/codex-hooks.test.ts |
✅ Full | — |
Detached capture worker (survives /clear teardown, #190) |
hooks/capture-worker.mjs |
test/realdeps.test.ts (finishes its work after the launcher's process group is killed) |
✅ Full | — |
| Daemonless lifecycle sync end-to-end (SessionEnd commits+pushes, offline-commit flush at next SessionStart, ADR-0032) | hooks/session-end.mjs, hooks/session-start.mjs |
test/lifecycle-sync.e2e.test.ts |
✅ Full | Real git remote, real worker process — the strongest e2e in the repo for the plugin layer. |
| Packaged/standalone runtime (marketplace payload has no monorepo or vendor leakage, every host hook loads and runs from the copied install) | packages/plugin/vendor/** (generated) |
test/standalone-smoke.test.ts, test/vendor-smoke.test.ts |
✅ Full | — |
hooks.json / codex-hooks.json / .claude-plugin/plugin.json / .codex-plugin/plugin.json wiring |
manifests | test/manifest.test.ts |
✅ Full | Checks the manifests are internally consistent (versions agree, referenced scripts exist) — not that Claude Code itself reads them correctly (see MCP live-handshake row above; the same caveat applies to plugin manifest consumption). |
pre-compact.mjs / user-prompt-submit.mjs throttling and DISABLE_HOOKS no-op |
hooks/pre-compact.mjs, hooks/user-prompt-submit.mjs |
test/realdeps.test.ts |
✅ Full | — |
| Live installation into a real Claude Code / Codex settings dir, restart, and observed hook firing | — | ⚠️ Manual-by-necessity | Everything above proves the hooks behave correctly when invoked with the right stdin/argv shape. Nothing drives an actual Claude Code or Codex session that fires the hooks itself. An automated version would need a scripted host CLI session — same class of gap as the MCP live-handshake row. |
Named gaps (summary)¶
Ranked by how much it would hurt if the untested/thinly-tested thing broke silently:
- True simultaneous-process id collision on
writeNote— only reachable today by mockingshortId(); the fail-closedfs.linkpath is proven correct in isolation (#101) but never driven by two real racing processes writing the exact same id. staging.tsedge cases (e.g., re-staging an id already pending) — the promote flow around it is thoroughly tested, but the staging primitive itself has one test.- Live handshake against a real Claude Code / Codex client (both MCP stdio and the plugin
hook manifests) — manual-by-necessity, not a suite gap, but worth a standing manual check
before each release per
docs/release-checklist.md. - Statistical bound on id collision-resistance — the 4-char suffix space is only spot-checked for uniqueness over 50 draws, not proven against the actual daily write volume a busy brain would see.
None of these are "broken and marked green" — they are real, named absences of automation, which is the entire point of this page.