# Coverage Map & Gap Roadmap

> **What this is.** The state of the testing union on `mgf-hrb` as of
> the latest commit on `main`. Counts. Layer-by-layer. What's tested.
> What's missing. The gap row in each section is the "open" pile a
> contributor can pull from.
>
> **How to use this.** Pick an "Open" row → file a small PR adding
> tests for it → flip the row to "Done" with the commit SHA.
> Updates land in the same commit that closes the gap.

## At-a-glance

| Layer | Count | Location | Marker | State |
|---|---|---|---|---|
| Unit (Zig) | ~240 | `hrb-code/src/*_test.zig` | — | 🟢 healthy |
| Unit (Python) | ~85 | `tests/unit/mgf/hrb/test_*.py` | — | 🟢 healthy |
| Functional | (subset of above) | same files | — | 🟢 healthy |
| Integration | ~10 | `facade_test.zig`, `test_cli_doctor.py` | — | 🟢 healthy |
| Contract | 22 | `tests/contract/` | `contract` | 🟢 healthy |
| Property | ~12 | `cap_test.zig::test "cap U3: minting 100 caps..."`, `ring_test.zig::tier0_5` | — | 🟢 healthy |
| Concurrency | ~6 | `core_test.zig::concurrency`, `test_client.py::test_transport_call_holds_lock` | — | 🟢 healthy |
| Smoke | 15 | `tests/smoke/` | `smoke` | 🟢 healthy |
| End-to-end | 1 (with ~36 internal checks) | `tests/e2e/test_e2e.py` | `e2e` | 🟢 healthy |
| Use-case | 22 | `tests/use_cases/` | `use_case` | 🟢 healthy |
| Fuzz | ~5 | `request_test.zig::S1 fuzz`, `ring_test.zig::tier0_5: fuzzer` | (none — runs in default) | 🟢 healthy |
| Stress | ~6 Zig + 7 Python | `stub_usb_test.zig`, `core_test.zig::stress`, `tests/stress/` | `stress` (Python) | 🟢 healthy |
| Performance | 1 Zig + 5 Python | `ring_test.zig::BENCHMARK`, `tests/perf/` | `perf` (Python) | 🟢 healthy |
| Conformance | ~15 Zig + 14 Python | `conformance.zig` + `tests/conformance/` | `conformance` (Python side) | 🟢 healthy |
| Regression | implicit | scattered, marked in commit messages | — | 🟡 should be tagged in code |

Totals: **252 Zig tests · 132 default + 49 opt-in Python tests · 1 e2e (36 checks)** =
~430 distinct assertions today.

---

## Layer-by-layer detail

### Unit + Functional + Integration

🟢 **State.** Saturated for the surface we've shipped. Every public
method on `Client`, every Core operation, every CapTable verifier, the
diagnostics engine, the identity comparator, and all USB-worker phases
are covered.

**Coverage report (Python, last measured):**

```
src/mgf/hrb/__init__.py           8      0   100%
src/mgf/hrb/_client.py          ~150    11    93%
src/mgf/hrb/cli/__init__.py      43      7    84%
src/mgf/hrb/cli/_doctor.py      ~110    ~10   91%
src/mgf/hrb/cli/_explain.py      47      0   100%
src/mgf/hrb/cli/_info.py         91     27    70%   ← UNDER-COVERED
src/mgf/hrb/cli/_scan.py         61      4    93%
src/mgf/hrb/config.py             6      0   100%
src/mgf/hrb/diagnostics.py       82     13    84%
src/mgf/hrb/domain.py           113      0   100%
src/mgf/hrb/exceptions.py        20      0   100%
src/mgf/hrb/identity.py          88      8    91%
TOTAL                           902    125    86%
```

**Gap rows:**

| Row | Where | Why |
|---|---|---|
| `cli/_info.py` at 70% | Renderer branches | Pure rendering — visual diff > line coverage; could mock subprocess for a contract test |

### Contract

🟢 **State.** Three files, 22 tests in `tests/contract/`. Graduated
from `test_client.py` (still keeps the original tests for in-context
unit coverage). The dedicated dir means a contract regression points
unambiguously at "the wire / surface changed."

**Done rows:**

| File | Tests | Pins |
|---|---|---|
| `tests/contract/test_wire_handshake.py` | 5 | Hello shape (required fields, service name, cap_id positivity, malformed JSON refused). Catches a future broker that renames `hello` → `service`. |
| `tests/contract/test_wire_capabilities.py` | 4 | Six canonical backend fields + nine canonical ops + typed-projection preserves every field + degraded mode carries reason. Catches a renamed `scope_prefix` → `bus_prefix`. |
| `tests/contract/test_wire_errors.py` | 13 | Parametrized: every wire `Err` variant → typed Python class. Catches a new broker `Err` variant added without a Python class. Plus EH-01 (every error descends from MgfHrbError) + unknown-variant fallback. |

**Future additions** (no urgency — driven by new wire ops as they ship):

| Row | Effort |
|---|---|
| `tests/contract/test_wire_control.py` | S — pin every verb name + value-string format |
| `tests/contract/test_wire_drain_events.py` | S — pin the kinds: `added`, `removed`, `state_changed`, `gap`, `state_changed` with `previous_urn` |

### Property

🟢 **State.** Zig side covers the SPSC ring fuzz, the cap-id
non-sequential guarantee, the synth-pending cap policy. Python side
covers `from_wire` defensive coercion across all three typed records.

**Open rows:**

| Row | Effort | Notes |
|---|---|---|
| Add `hypothesis` to dev deps | S | Once we have ≥ 3 property tests that benefit |
| `tests/unit/test_urn_glob_properties.py` | M | URN-matching invariants under generated glob/URN inputs |
| `tests/unit/test_lease_table_invariants.py` | M | Run claim/release sequences and assert table invariants |

### Smoke

🟢 **State.** Five files, 14 tests. Sub-second when broker isn't
built (broker tests skip cleanly); ~1s with broker built. The
fail-fast layer CI wants first.

**Done rows:**

| File | Tests | Pins |
|---|---|---|
| `tests/smoke/test_sdk_imports.py` | 3 | `__all__` consistency, no leaked privates, EH-01 (every Error descends from MgfHrbError) |
| `tests/smoke/test_broker_boots.py` | 4 | Spawn, socket bind, handshake-with-entropy, one list op, the connect-then-close-without-reading SIGPIPE regression |
| `tests/smoke/test_cli_runs.py` | 4 | `--help` works + names every subcommand, `doctor` against missing broker exits 1 with remediation, `explain doctor` renders, unknown subcommand → argparse error (no traceback leak) |
| `tests/smoke/test_install_points_to_main.py` | 2 | `mgf.hrb.__file__` resolves under `<repo>/src/` (no worktree-shadowed install). Catches the 2026-05-25 trap where the venv pointed at `.claude/worktrees/doc-staleness-pass/` |
| `tests/smoke/test_broker_freshness.py` | 1 | Broker binary mtime is ≥ newest `hrb-code/src/**/*.zig` mtime. Fails (not skips) with the rebuild command when stale — the early-detection counterpart of `tests/_broker.broker_is_stale` |
| `tests/smoke/test_mtest_self_check.py` | 1 | `mgf-test-supervisor --self-check` exits 0 + prints PASS. Catches a broken supervisor before any other claim it makes is trusted (MGF-TS-23 contract) |

### Use-case

🟢 **State.** All five planned scenarios shipped. Five files, 22
tests. Shared broker fixture lives at `tests/conftest.py` (the
2026-05-25 reliability pass consolidated the per-suite fixtures
into one top-level source).

**Done rows:**

| File | Tests | Cites |
|---|---|---|
| `tests/use_cases/test_gui_lease_pane.py` | 3 | FEEDBACK SDK-03 — multi-operator lease pane (the "central reason GUI exists" scenario) |
| `tests/use_cases/test_gui_doctor_pane.py` | 6 | FEEDBACK TOOLS-02 — programmatic doctor surface (the "one of two reasons GUI exists" scenario) |
| `tests/use_cases/test_gui_event_monitor.py` | 4 | FEEDBACK SDK-01 — three-layer event API (sync iterator + async iterator + fd). All three shapes deliver identical event streams in identical order |
| `tests/use_cases/test_vfio_passthrough.py` | 4 | `recipes/claim-a-device-for-vfio-passthrough.md` — the vm-vmanager-app find/claim/raw/release pattern; two-device concurrent passthrough; real-VFIO path skipped when /dev/vfio absent |
| `tests/use_cases/test_usb_device_replug.py` | 5 | `recipes/track-a-device-across-a-mode-change.md` — iFig Recovery↔DFU as LIKELY_SAME, fast replug as SAME, different-port-same-VID as DIFFERENT, PID-only-match-cross-bus as DIFFERENT, `tolerate_port_change` knob actually changes outcome |

### Fuzz

🟢 **State.** Five Zig-side fuzz tests exercise the trust gate (S1
adversarial input), the ring's asymmetric-trust contract, and the
synth's pending cap.

**Open rows:**

| Row | Effort | Notes |
|---|---|---|
| `tests/fuzz/test_client_wire_fuzz.py` | M | Bombard Client with random bytes-as-response; assert it always raises typed `MgfHrbError`, never crashes |
| Long-seed nightly fuzz job | M | Extended seed list for the existing Zig fuzzers (CI-only, ≥30 min) |

### Stress

🟢 **State.** Six Zig-side stress tests (concurrent claims, synth
flood) plus seven Python-side wire stress tests in `tests/stress/`,
gated behind the `stress` marker (nightly-only; multi-second under
contention, brittle under CI-host load).

**Done rows:**

| File | Tests | Pins |
|---|---|---|
| `tests/stress/test_many_clients.py` | 3 | 50 concurrent `Client.connect` succeed (Part 7.1 thread-per-client under load); a held lease is visible across 20 concurrent observers; broker survives a 50-cycle connect/close storm + a fresh client still works after |
| `tests/stress/test_claim_churn.py` | 2 | 1000 claim/release iterations across 8 clients on two shared URNs end with an empty lease table (G3 no ghost leases under churn); a separate observer thread never sees two leases on one URN under contention |
| `tests/stress/test_monitor_fd_under_event_storm.py` | 2 | 10000 synthesised events (via monkey-patched drain_events on the dedicated monitor Client) all reach the pipe consumer, in order, without loss; 10 concurrent monitor_fds stay alive through a 4-thread claim/release storm and tear down cleanly |

### Performance

🟢 **State.** Zig has one in-process benchmark (ring throughput, 50M
items/sec). Python side now has five wire-level latency budgets in
`tests/perf/test_hot_path_latency.py`, gated behind the `perf`
marker (nightly-only; hardware variance would make PR-gating flaky).

**Methodology:** each test warms up 50× then measures 500× with
`time.perf_counter_ns`; asserts p99 against a documented budget.

**Done rows:**

| Op | p99 budget | Observed (laptop) | Rationale |
|---|---|---|---|
| `Client.list("hw:/test/**")` | 2000µs | ~263µs | One wire roundtrip; 20× headroom over the ~100µs typical |
| `Client.capabilities()` | 3000µs | ~2052µs | Wire roundtrip + typed projection across 3 backends |
| `claim + release` round-trip | 4000µs | ~637µs | Two wire roundtrips; catches O(N) lease-table regressions |
| `Client.inspect()` | 2500µs | ~394µs | Wire roundtrip + `ResourceDetail.from_wire` projection |
| `Client.doctor()` full sweep | 100ms | ~2.5ms | 7 checks; GUI doctor pane runs this on every refresh |

**Future rows** (when broker features land):

| Row | Effort | Budget candidate |
|---|---|---|
| `tests/perf/test_event_drain_throughput.py` | M | `drain_events()` under N-events-per-second burst, p99 < 10ms |
| `tests/perf/test_many_clients_fan_in.py` | M | 16 clients all calling `list()` concurrently, aggregate p99 < 5ms |

### Conformance

🟢 **State.** Both sides covered. `conformance.zig` (Zig side) pins
S1–S5 + G1–G4 in-process; `tests/conformance/` (Python side) pins the
same promises AT THE WIRE — three files, 14 tests.

**Done rows:**

| File | Tests | Pins |
|---|---|---|
| `tests/conformance/test_g2_honest_emptiness.py` | 6 | G2 "the model never lies": empty scopes → `[]`, unknown URN → typed `NotFoundError`, quiet bus → no fake events, no holders → empty lease list, mode reported honestly, DEFERRED stays DEFERRED |
| `tests/conformance/test_g3_no_ghost_leases.py` | 5 | G3 "ghost ownership impossible": release → immediately reclaimable, double-release no-op, unknown-lease-release no-op, lease count returns to zero, client close doesn't leak (broker stays alive) |
| `tests/conformance/test_s2_deadline_typed_outcome.py` | 3 | S2 deadline-bounded ops: broker survives client disconnect mid-op, deadline_ms flows to wire, deadline=0 means none |

**Future rows** (waiting on broker features):

| Row | Effort | Pins | Blocked on |
|---|---|---|---|
| `test_G4_use_path_broker_absent.py` | M | Acquire fd, kill broker, fd still works | Needs a deterministic "kill broker after fd handed off" harness |
| `test_S4_worker_crash_contained.py` | L | One worker dying doesn't take the others | Needs worker-supervisor wiring (Part 7) |
| `test_S2_quarantine_on_timeout_wire_side.py` | M | Slow op + small deadline → broker quarantines → next claim returns `UnavailableError` | Needs broker-side support for forcing slow ops over the wire |

### Regression

🟡 **State.** Several regression tests exist (e.g.
`test_e2e_subprocess_stdout_pipe_closed_on_exit` would cite commit
`e083bf9`) but aren't tagged as such.

**Open rows:**

| Row | Effort | Notes |
|---|---|---|
| Convention: a regression test docstring MUST cite the fixing commit | XS | Add to RECIPES.md (already done) + sweep existing tests for retro-tagging |
| Add `regression` marker | XS | Optional; today regressions live with their topical test file |

---

## Test-count trajectory

A loose count over the last 30 commits (when SDK-01–SDK-05 + TOOLS-02
shipped):

```
e2e checks:  9 →  32  →  36
Python:     58 →  74  →  97 → 104
Zig:       249 →  252 →  252
```

The 32 → 36 e2e bump came with the multi-client broker work; the
big Python jump (74 → 97) was the typed-projection round (SDK-02
typed half + SDK-01 three-layer events); the 97 → 104 bump closed
the six prioritised gap rows; the 104 → 181 bump came with the
2026-05-25 reliability pass (3 use-case rows + 2 install/freshness
smoke tests + the consolidated infrastructure).

## Coverage policy

* **Gate 3 enforces ≥ 80% branch coverage** on `mgf.hrb` (per
  `[tool.coverage.run]` + `[tool.coverage.report]` in
  `pyproject.toml`). The source pin (`source = ["mgf.hrb"]`)
  makes `pytest --cov` (no args) measure the right scope — without
  it, the gate was silently bypassable.
* The number drifts UPWARD over time — we're at 81.33% (branch)
  as of the last measure. A PR that drops it below 80 fails gate 3.
* New code without tests is unusual but not forbidden; the next PR
  that adds the test pulls coverage back up.

## Priority order for closing gaps

The six prioritised gap rows have all shipped:

1. ✅ **Smoke suite** — commit `eb54e7b` (3 → 11 tests) + 2026-05-25 (11 → 14).
2. ✅ **Use-case suite** — commit `ec017b3` (partial 2/5) + 2026-05-25 (full 5/5, 22 tests).
3. ✅ **Contract tests in their own dir** — commit `29e8444`.
4. ✅ **Python conformance suite** — commit `f86cf11` (14 tests
   pinning G2 / G3 / S2 at the wire).
5. ✅ **Python perf budgets** — commit `b1b4c2b` (5 hot-path
   latency budgets, `perf` marker).
6. ✅ **Python stress** — commit `35163b9` (7 tests, `stress` marker).

The 2026-05-25 reliability pass also shipped (see `TESTING.md`
+ [`DIAGNOSING_HANGS.md`](DIAGNOSING_HANGS.md)):

* Consolidated broker fixture at `tests/conftest.py` + `tests/_broker.py`
  (replaced four near-identical per-suite copies).
* Broker-binary freshness check (fails fast with the rebuild command).
* `pytest-timeout` default 30 s + per-marker 60–120 s overrides
  + faulthandler 60 s.
* `[tool.coverage.run].source = ["mgf.hrb"]` pin — coverage gate
  is no longer silently bypassable.
* `scripts/gates` unified runner (`--fast` + `--keep-going`).
* `hooks/pre-push` opt-in fast-gate hook.

The federation extraction (2026-05-26) replaced the originally-
in-tree supervisor + adoption-message docs with a published sibling
dependency:

* `mgf-test-supervisor` dev dep (v0.1.1) — extracted from this
  repo's `scripts/_mtest/` after all 7 framework siblings copy-
  pasted the pre-extraction code; mgf-hrb is sibling-#8. v0.1.1
  carries the `MGF_TEST_SUPERVISOR_DAEMON_PROCESS_NAME` env-var
  override so the daemon-state probe for `mgf-hrb-server` stays
  in failure bundles. Standard spec lives upstream:
  [`mgf-standard/docs/standards/TESTING.md`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/TESTING.md)
  §13.11 + §19 (TS-22..TS-27).
* `.woodpecker.yml` — Codeberg-native CI (per WF-01..WF-12 in
  `mgf-standard/docs/standards/WORKFLOWS.md`). Replaced
  `.forgejo/workflows/gates.yml`.
* `hooks/pre-push` — invokes `python -m mgf.test_supervisor
  --tier 0-1 --skip-quarantined` with the daemon-name env var
  wired.
* `tests/smoke/test_mtest_self_check.py` — supervisor verifies
  itself in the default suite (now invokes the published library
  instead of the in-tree copy).
* Removed: `scripts/_mtest/`, `scripts/mtest`,
  `docs/testing/{FOUNDATION_TESTING_STANDARD,
  MGF_COMMON_ADOPTION_MESSAGE, SUPERVISOR_DESIGN}.md` — superseded
  upstream.

## Operations (mtest + CI shape)

The runner architecture matches MGF-TS-22..27:

| Layer | Tool | Trigger | Tier scope |
|---|---|---|---|
| Pre-push hook | `mgf-test-supervisor --tier 0-1 --skip-quarantined` | `git push` (opt-in) | tier 0-1 |
| PR job | `mgf-test-supervisor --tier 0-2 --skip-quarantined` | `pull_request` | tier 0-2 |
| Main job | `scripts/gates` + `mgf-test-supervisor --tier 0-3 --quarantine-attempt` | `push: main` | tier 0-3 |
| Nightly | `mgf-test-supervisor --tier 0-3 --quarantine-attempt` | cron 03:00 UTC | tier 0-3 + artifact upload |
| Self-check | `mgf-test-supervisor --self-check` | every CI job, before tests | (supervisor only) |

`scripts/gates` (4-gate runner: ruff + mypy + pytest + zig)
remains the canonical 4-gate runner; `mtest` is the
tier/budget/diagnostic layer on top.

Long-tail items now next:

* `pytest-xdist` parallel runs (the consolidated fixture's
  `default_sock_path` is one knob away — pass an argv override
  through to the broker).
* `pytest-randomly` for order-independence verification.
* Hypothesis property testing (the open Property rows above).
* Long-seed nightly Zig fuzz job (the open Fuzz row).
* Mutation testing.
