Ctrl K
ComunidadGuías…

docs/testing-and-coverage.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Testing & coverage

How the Crux Daemon is tested, how coverage is measured and gated, and an honest
account of why the gated number sits where it does.

> **Snapshot (2026-07-30, `main`):** **6,671** test functions; **90.11%** gated
> region coverage (ungated, whole tree: **88.81%**).
> CI is the source of truth — the live numbers are printed in
> the [Coverage job](../.github/workflows/ci.yml) of every run and attested by
> [`coverage-attestation.yml`](../.github/workflows/coverage-attestation.yml).

## Running the tests

```bash
cargo test --workspace            # the full suite (6,671 test functions)
cargo test -p corecruxd           # one crate (corecruxd is a binary crate — no lib target)
cargo fmt --check                 # formatting gate
cargo clippy --workspace -- -D warnings   # lint gate (lib + bins; not #[cfg(test)] code)
```

Coverage locally (matches CI exactly — same ignore regex):

```bash
RX='(.*/corecruxd/src/pool\.rs|.*/corecruxd/src/dataplane_store\.rs|.*/corecruxd/src/http/dataplane\.rs|.*/corecruxd/src/main\.rs|.*/corecruxctl/src/main\.rs|.*/crux-claude-hooks/src/main\.rs|.*/crux-claude-hooks/src/bin/crux_llm_shim\.rs|.*/crux-config-wizard/src/main\.rs|.*/crux-config-wizard/src/interactive\.rs)$'
cargo llvm-cov --workspace --ignore-filename-regex "$RX" --summary-only
```

(Needs `cargo-llvm-cov` + the `llvm-tools-preview` component, and a C toolchain
for linking — `apt install build-essential` on a bare box.)

## What the gate measures

The CI gate uses **region coverage** — column 4 of the `cargo llvm-cov` `TOTAL`
line — not line coverage. Region coverage is the stricter of the two (it counts
distinct branch regions, so a half-executed line doesn't read as fully covered).

The [`Coverage` job in `ci.yml`](../.github/workflows/ci.yml) enforces:

| Scope | Floor | Actual (snapshot) | Notes |
|---|---|---|---|
| **Workspace total** | **89%** | 90.11% | the headline gate; ratchet target **90** |
| `corecrux-memory` | 93% | 93.5% | ratchet target **95** (see below) |
| `crux-sync` | 98% | 98.4% | |
| `crux-contrib` | 99% | 100% | |
| `corecrux-receipts` | 88% | 91.2% | trust core (CROWN receipts); ratchet available |
| `corecrux-segment` | 85% | 93.8% | trust core (sealed `.ccxseg`); ratchet available |
| `corecrux-storage` | 79% | 89.9% | trust core (append-only store); ratchet available |

Floors are set **at current-rounded-down** ("ratchet from reality"): they prevent
regression today and are raised as coverage improves. The job also prints the
**ungated** total (full tree, no exclusions) next to the gated one, so the
exclusion list below can never quietly hide low-coverage code from review.

> **Note on the memory floor.** The per-crate floor check was historically a
> no-op (the awk summed the file-path column → always "100.0"), so the
> long-stated `corecrux-memory` ≥95% target was never actually enforced. The
> check is now correct; `corecrux-memory` is 93.5% today, so the enforced floor
> is 93 with **95 as the ratchet target**.

### What is excluded from the gate — and why

The ignore list is deliberately narrow: only code that is **not meaningfully
unit-testable**.

- **Binary entry points** — `main.rs` (`#[tokio::main]` bootstrap / clap
  dispatch) for `corecruxd`, `corecruxctl`, `crux-claude-hooks`,
  `crux-config-wizard`, plus the `crux_llm_shim` bin.
- **The interactive config wizard** — `crux-config-wizard/src/interactive.rs`
  (stdin-driven prompts).
- **The dataplane layer**, which is an **unconstructable typecheck stub** in the
  CPU-only CE build: [`pool.rs`](../crates/corecruxd/src/pool.rs)
  (`DataPlanePool { _private: () }`, every method `unreachable!()`, never
  constructed), `dataplane_store.rs`, and
  [`http/dataplane.rs`](../crates/corecruxd/src/http/dataplane.rs)
  (`PoolBackedHttpDataplane` whose `pool` is always `None`). The real dataplane —
  and its append→read→verify integration coverage — lives in the
  dataplane-enabled (CoreCrux) distribution, not this repo. The CE handlers'
  contract is covered against the `FakeHttpDataplane` test double in
  [`http/tests.rs`](../crates/corecruxd/src/http/tests.rs).

Everything else — including the critical append, query, receipt, projection, and
admin HTTP surfaces — is gated.

## Tests per crate (top)

| Crate | Tests | | Crate | Tests |
|---|---:|---|---|---:|
| `corecruxd` | 2671 | | `corecrux-receipts` | 275 |
| `corecruxctl` | 1174 | | `corecrux-storage` | 223 |
| `crux-mcp` | 798 | | `crux-claude-hooks` | 222 |
| `corecrux-memory` | 288 | | `crux-session` | 89 |
| `corecrux-projections` | 276 | | `corecrux-segment` | 62 |

## Why the gated number is ~90%, not higher

This is the honest part. 90.11% is what an *accurate* gate over the
*meaningfully-testable* tree reports — it is not a target someone padded up to.

1. **The trust-core crates used to pull the average down, and no longer do.**
   `corecrux-storage` (89.9%), `corecrux-segment` (93.8%) and
   `corecrux-receipts` (91.2%) are large and full of deep I/O and error
   branches; they sat at 80/86/89% as recently as 2026-06-18. The
   **security-critical** paths in them *are* covered — tamper-rejection in
   [`corecrux-segment/tests/corruption_matrix.rs`](../crates/corecrux-segment/tests/corruption_matrix.rs)
   (magic / version / CRC / record-hash / TOC corruption all rejected) and
   fail-closed signature verification in
   [`corecrux-receipts/src/verify_v1.rs`](../crates/corecrux-receipts/src/verify_v1.rs)
   (`assert!(!report.signature_valid)`). What's *un*covered is mostly
   exhaustive error/IO-branch fan-out, not the invariants (see
   [`docs/agent/INVARIANTS.md`](agent/INVARIANTS.md)).

2. **`corecruxd` is an ~87k-LOC surface.** Most handlers are covered. The three
   that this doc previously recorded at 0% — `http/events.rs`, `http/infra.rs`,
   `http/policy.rs` — were closed on 2026-07-30 and now sit at **95.4%**,
   **98.8%** and **98.7%** region coverage respectively. They were *gated* (not
   excluded) throughout, which is why they showed up as debt rather than
   staying hidden. Remaining `corecruxd` debt is tracked per-file in the
   Coverage job log.

   A second sweep on 2026-07-30 took the nine largest remaining concentrations
   across `corecruxd` and `corecruxctl` — `http/replay.rs` (1032 missed regions
   → 66), `corecruxctl/audit_pack.rs` (1164 → 228), `corecruxctl/login.rs`
   (1035 → 159), `repo_watch.rs` (456 → 125), `storybook.rs` (411 → 35),
   `context_graph.rs` (296 → 19) and `http/incidents.rs` (379 → 211).

   **What is left is mostly not testable without a production change.** The
   clearest example is `integrations_github_sync.rs` (532 → 421) and
   `integrations_github.rs` (253 → 215): every fetch builds a `ureq::Agent`
   inline against a hard-coded `https://api.github.com/...` URL, with no
   base-URL or transport injection seam, so the pagination and status-code
   branches cannot be reached without hitting the real network. Adding a seam
   is the prerequisite for covering them — not more test effort.

   A related trap worth knowing: **no CI job runs `cargo test -- --ignored`**, so
   an `#[ignore]`d test contributes nothing to the gate. Two exist today —
   `sse_session_survives_30s_idle` (>35s wall clock) and `witness_submit`'s live
   Rekor probe. Both are ignored for good reasons, but the SSE endpoint's
   *only* test was one of them, which is how a whole handler sat at 0% while
   looking tested. Prefer a fast handler-level test alongside any long-running
   or network-dependent one.

3. **The denominator includes the test code itself.** `#[cfg(test)]` regions
   count toward the total, so each new test batch raises coverage by less than
   its raw covered-region count.

4. **Coverage measures execution, not assertion strength.** Region coverage
   proves a line ran; it does not prove a test would catch a regression.
   Mutation testing fills that gap on the trust-core crates
   (`corecrux-receipts`, `corecrux-segment`, `corecrux-storage`):

   - Nightly [`mutants.yml`](../.github/workflows/mutants.yml) runs
     `cargo-mutants` sharded 8 ways, merges the shard reports into a
     per-crate mutation-score table in the job summary, and **ratchets**
     against [`mutants-baseline.txt`](../.github/mutants-baseline.txt):
     a *new* survivor (a mutant no test kills that isn't in the baseline)
     turns the nightly red; survivors that become caught are listed so the
     baseline can be shrunk. Neither is a required PR check.
   - PR-time [`mutants-diff.yml`](../.github/workflows/mutants-diff.yml)
     runs `cargo mutants --in-diff` over the PR's changes to the trust-core
     crates, so assertion-free new code is caught at review time in minutes
     instead of overnight.
   - To burn a survivor down: write a test that kills it, run
     `cargo mutants --file <file> -p <crate> --timeout 120` to confirm, and
     delete its line from the baseline. Only genuinely inert mutations
     (logging, metrics) belong in the baseline long-term — prefer
     `#[mutants::skip]` with a comment for those so the baseline shrinks.

## Maintaining the gate

- **To raise a floor:** add tests for the crate, confirm the new per-crate number
  in the Coverage job log, then bump the `pair` floor in
  [`ci.yml`](../.github/workflows/ci.yml) (keep it ≤ actual). Three per-crate
  floors are currently well below reality and are the standing opportunities:
  `corecrux-storage` (79 → 89), `corecrux-segment` (85 → 93) and
  `corecrux-receipts` (88 → 91). They were left alone on 2026-07-30 because
  part of that headroom comes from `corecruxctl` tests exercising those crates
  *transitively*, so the numbers should be confirmed stable across a couple of
  main runs before being locked in. `corecrux-memory` remains at 93.5% against
  its 95 target.
- **Leave headroom when ratcheting the workspace floor.** It is set one point
  below the measured total, not at it: a floor equal to reality turns main red
  on the first feature PR that adds uncovered code.
- **Keep the regex in lock-step:** the `COVERAGE_IGNORE_REGEX` is duplicated in
  `ci.yml` and `coverage-attestation.yml` — change both together.
- **Adding an exclusion is a reviewed decision:** justify it inline (the only
  acceptable classes are entry points, interactive surfaces, and
  platform-inert/unconstructable code). Prefer testing over excluding.

## See also

- [`AGENTS.md`](../AGENTS.md) — the 60-second trust-core tour + "verify the
  claims yourself".
- [`docs/agent/CLAIMS.md`](agent/CLAIMS.md) — each product claim → code → the
  test that proves it.
- [`docs/code-health-harvest.md`](code-health-harvest.md) — the code-health
  (dead-code / stub / unused-dep) harvester.