# Test Recipes

> **What this is.** Copy-pasteable patterns for writing one of each
> test type in this repo. After [`TAXONOMY.md`](TAXONOMY.md) (what)
> and [`INFRASTRUCTURE.md`](INFRASTRUCTURE.md) (what to compose
> with), this is the HOW.
>
> Reusable: every recipe is generic except where annotated
> `[hrb-specific]`. Sibling projects swap the imports and the
> fixture names; the shape stays.

## Conventions every recipe follows

1. **Test name = the property it pins**, not the function it calls.
   * Good: `test_socket_missing_returns_fail_with_remediation`
   * Bad: `test_check_socket`
2. **Docstring states the WHY**, not the WHAT. The test code itself
   states what.
3. **One logical assertion per test.** Multiple `assert` calls are
   fine if they pin one property.
4. **Fixture scope is the narrowest that works.** Function-scoped
   by default; session-scoped only with a comment explaining why.

---

## Recipe 1 — Unit test (Python)

**When.** One function, no I/O, sub-millisecond.

```python
# tests/unit/mgf/hrb/test_domain.py

from mgf.hrb.domain import ResourceDetail, FieldFidelity, Kind, Family


def test_resource_detail_unknown_kind_falls_back_to_default() -> None:
    """G2 defensive coercion: an unknown wire enum value (wire-protocol
    skew, future broker) falls back to the per-field default rather
    than raising. The consumer keeps rendering with honest defaults."""
    d = ResourceDetail.from_wire({
        "urn": "hw:/future/widget0",
        "kind": "some_new_kind",  # not yet in the enum
        "family": "claimable",
        "fidelity": "live",
    })
    assert d.kind is Kind.PCIE_FUNCTION   # documented default
    assert d.family is Family.CLAIMABLE
    assert d.fidelity is FieldFidelity.LIVE
```

Notes:

* No mocks, no fixtures, no fakes — pure data in, typed record out.
* The `# documented default` comment points at the design (here, the
  `_enum_or_default` helper's docstring).

## Recipe 2 — Unit test (Zig)

**When.** Same as Python, but on the Zig side.

```zig
// hrb-code/src/core_test.zig

test "S2: a generous deadline lets a slow op succeed" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    var core = Core.init(testing.allocator, try seeded(&fb));
    defer core.deinit();
    const urn = Urn.from("hw:/pci/0000:01:00.0/fn0");

    fb.slow_acquire_ms = 50;  // backend declares 50 ms of work
    const ok = try core.claim(&urn, .exclusive, 1, D.fromNow(5000)); // 5 s budget
    core.release(ok, D.none);
}
```

Notes:

* Every Zig test follows `var fb = FakeBackend.init(testing.allocator); defer fb.deinit(); var core = Core.init(testing.allocator, try seeded(&fb)); defer core.deinit();` — copy-paste this 4-line preamble.
* `try testing.expect…` for assertions; `try testing.expectError(Err.X, ...)` for error-path tests.

## Recipe 3 — Functional test (Python)

**When.** One feature exercised through the public surface.

```python
def test_claim_default_does_not_send_detach_true() -> None:
    """Without detach=, the wire MUST carry detach=false. The broker
    treats that as "never silently steal devices" — a plain claim
    against a kernel-driver-bound interface returns ConflictError,
    not a silent detach. Pinned because flipping the default would
    be a security regression."""
    client, t = _make_client([{"ok": True, "lease": 1}])
    client.claim("hw:/test/dev0", ClaimMode.EXCLUSIVE)
    assert t.requests[0]["detach"] is False
```

Notes:

* Uses `_make_client` to get a `Client` + a scripted fake `_Transport`.
* Asserts on `t.requests` (what we SENT), not just the return value.
* Docstring explains WHY this is pinned, not what the test does.

## Recipe 4 — Integration test

**When.** Two or more components composed in one process.

```zig
test "constellation SDK-03: listLeases merges across workers (Part 7.2)" {
    var c: Constellation = undefined;
    try buildConstellation(&c, testing.allocator, null);
    defer c.deinit();

    const pci = Urn.from("hw:/pci/0000:01:00.0/fn0");
    const usb = Urn.from("hw:/usb/bus1/dev4/if0");
    _ = try c.facade.claim(0, &pci, .exclusive, 1, kabi.Deadline.none);
    _ = try c.facade.claim(0, &usb, .observe, 2, kabi.Deadline.none);

    var col = FacadeRecordCollector{ .alloc = testing.allocator };
    defer col.deinit();
    const s = col.sink();
    c.facade.listLeases("hw:/**", &s);

    // Both holders are present (cross-worker visibility).
    try testing.expectEqual(@as(usize, 2), col.records.items.len);
}
```

Notes:

* Lives in the test file of the OUTER component (`facade_test.zig`,
  not `core_test.zig`).
* The `Constellation` builder wires up 2 workers, 2 cores, 2 fakes,
  and a facade — sets the scene once, asserts the interaction.

## Recipe 5 — Contract test

**When.** Pinning a wire-format field or an SDK return shape.

```python
def test_capabilities_returns_wire_payload() -> None:
    """`Client.capabilities()` returns the broker's `backends` array
    verbatim. Pinned shape so a wire-protocol change can't silently
    break the consumer (mgf-hrb doctor) without a test catching it.

    Includes the rich §6.2.1 fields (per-op support, mode, version)
    so a backend that drops one of those is caught here, not in
    production."""
    client, t = _make_client([{"ok": True, "backends": [
        {
            "scope_prefix": "hw:/pci/",
            "backend_name": "mgf-hrb-linux",
            "backend_version": "0.1.0",
            "resource_count": 36,
            "mode": "full",
            "ops": {"enumerate": True, "claim": True},
        },
    ]}])
    backends = client.capabilities_raw()  # raw wire shape
    assert backends[0]["scope_prefix"] == "hw:/pci/"
    assert backends[0]["mode"] == "full"
    assert backends[0]["ops"]["enumerate"] is True
    assert t.requests == [{"op": "capabilities"}]
```

Notes:

* Asserts the field NAMES (literal strings), not just the values.
* Uses `capabilities_raw()` so we test the WIRE shape — `capabilities()`
  is the typed projection (tested separately).
* When a field name changes, this test changes in the SAME commit.

## Recipe 6 — Property test

**When.** Asserting an invariant holds for all valid inputs.

```zig
test "cap U3: minting 100 caps yields 100 distinct non-zero ids" {
    var ct = cap.CapTable.init(testing.allocator);
    defer ct.deinit();
    var ids: [100]u64 = undefined;
    for (&ids) |*id_slot| {
        id_slot.* = try ct.mint(.{
            .scope = Urn.from("hw:/**"),
            .rights = RightSet.of(&.{.observe}),
        });
        try testing.expect(id_slot.* != 0);    // property 1
    }
    // property 2: every pair distinct
    for (ids, 0..) |a, i| {
        for (ids[i + 1 ..]) |b| {
            try testing.expect(a != b);
        }
    }
}
```

Notes:

* The properties are TRUE invariants (uniqueness, non-zero), not
  happy-path values.
* 100 is enough that a 1-in-2^32 collision rate would trip
  occasionally; 1000 would be unnecessarily slow.

For Python with `hypothesis` (not yet a dep — add when needed):

```python
from hypothesis import given, strategies as st

@given(st.text(min_size=0, max_size=512))
def test_urn_glob_match_never_panics(text: str) -> None:
    """Property: glob matching against arbitrary text never raises;
    returns False on nonsense."""
    from mgf.hrb._urn import urn_matches_glob
    result = urn_matches_glob(text, "hw:/**")
    assert isinstance(result, bool)
```

## Recipe 7 — Concurrency test

**When.** Pinning that a lock / atomic / serialization invariant
holds under real parallel execution.

```zig
test "concurrency: N threads claiming the SAME URN — exactly one wins" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    try fb.add("hw:/pci/0000:01:00.0/fn0", .claimable, .pcie_function);
    var core = Core.init(testing.allocator, fb.backend());
    defer core.deinit();

    const N: usize = 8;
    var ctxs: [N]ThreadCtx = undefined;
    var threads: [N]std.Thread = undefined;
    // ... spawn N threads ...
    for (ctxs) |ctx| {
        if (ctx.result == null) winners += 1;
        else if (ctx.result.? == model.Err.Conflict) conflicts += 1;
    }
    try testing.expectEqual(@as(usize, 1), winners);
    try testing.expectEqual(N - 1, conflicts);
    try testing.expectEqual(@as(usize, 1), core.liveLeaseCount());
}
```

Python version uses `threading.Thread` + `socket.socketpair`:

```python
def test_transport_call_holds_lock_across_send_recv() -> None:
    """Pin: the transport lock serializes wire access — two threads
    calling Client.list concurrently must not interleave their
    requests on the wire."""
    # ... 40 concurrent calls from 2 threads, assert each received
    # line is a complete valid JSON request (no garbled interleave)
```

Notes:

* Use ENOUGH threads / iterations to make a race trip if one
  exists (N=8 minimum, ITERS=200 stress).
* Assert on the END STATE; intermediate observations race with
  the test itself.

## Recipe 8 — End-to-end test

**When.** Wire-format pin + real subprocess broker.

```python
# tests/e2e/test_e2e.py — pytestmark = pytest.mark.e2e at top

def test_e2e() -> None:
    """Pytest entry point. Skips if the broker binary isn't built."""
    if not SERVER.exists():
        pytest.skip(f"Broker binary not built at {SERVER}")
    exit_code = run_e2e()
    if exit_code != 0:
        raise AssertionError(f"E2E checks failed (exit {exit_code})")
```

The actual checks live in `run_e2e()` which:

1. Spawns the broker via `subprocess.Popen`
2. Waits for the socket file
3. Runs ~36 assertions (every SDK verb exercised)
4. SIGTERMs the broker, closes stdout, removes socket

See [`INFRASTRUCTURE.md`](INFRASTRUCTURE.md) §5 for the broker harness pattern.

## Recipe 9 — Use-case test

**When.** Walking a specific consumer scenario from a paper or recipe doc.

```python
# tests/use_cases/test_doctor_pane.py  [planned — illustrative]

import pytest
pytestmark = pytest.mark.use_case

def test_gui_doctor_pane_renders_findings(broker_subprocess) -> None:
    """Reproduces hrb-app's Phase 1.1 chunk D — the doctor-lite pane.
    The GUI calls `Client.doctor()` and renders each finding as a row.
    This test walks the same scenario programmatically. Cites:
    FEEDBACK TOOLS-02 / hrb-app docs/inprogress/PRESENTATION_LAYER_RESEARCH.md §1."""
    with Client.connect(SOCK) as c:
        findings = c.doctor()
        # Render order matches the engine's stable order; a GUI iterating
        # `for f in findings: row(f)` produces a predictable display.
        assert [f.code for f in findings] == [
            "SOCKET_REACHABLE",
            "PCI_SYSFS_READABLE",
            "USB_SYSFS_READABLE",
            "VFIO_DEVICE_PRESENT",
            "IOMMU_PRESENT",
            "UDEV_RULE_INSTALLED",
            "BACKEND_CAPABILITIES",
        ]
        # Severity-as-color logic the GUI implements:
        for f in findings:
            assert f.severity in (DoctorSeverity.OK, DoctorSeverity.WARN,
                                  DoctorSeverity.FAIL, DoctorSeverity.DEFERRED)
```

Notes:

* Cite the paper / recipe by name in the docstring — a future
  maintainer can find the source.
* Walk the scenario in the order the consumer would.
* Asserts at points where the consumer would be observing.

## Recipe 10 — Fuzz test

**When.** Asserting "never panics on hostile input."

```zig
test "S1 fuzz: 4 000 random byte sequences never panic, never escape BadRequest" {
    try fuzzOnce(0xDEADBEEF, 4000);
}

fn fuzzOnce(seed: u64, iterations: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();
    var buf: [256]u8 = undefined;
    var i: usize = 0;
    while (i < iterations) : (i += 1) {
        const len = rand.intRangeAtMost(usize, 0, buf.len);
        rand.bytes(buf[0..len]);

        const result = request.parse(buf[0..len], testing.allocator);
        if (result) |req| {
            // Whatever shape it took, the discriminant must be a known op
            switch (req) {
                .list, .claim, .release, ... => {},
            }
        } else |err| {
            // Only BadRequest is allowed at the gate
            try testing.expectEqual(Err.BadRequest, err);
        }
    }
}
```

Notes:

* Tests the **trust gate** (the input validation layer), not deeper.
* The PROPERTY is "never panics" + "never escapes BadRequest" — no
  functional assertion.
* Deterministic seed so a flake is reproducible.

## Recipe 11 — Stress test

**When.** Sustained load, asserting the end state.

```zig
test "concurrency: stress — interleaved claim+release across threads stays consistent" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    // ... seed 3 URNs ...
    var core = Core.init(testing.allocator, fb.backend());
    defer core.deinit();

    const ITERS: usize = 200;
    const t1 = try std.Thread.spawn(.{}, Worker.run, .{ &core, urn_a, 1, ITERS });
    const t2 = try std.Thread.spawn(.{}, Worker.run, .{ &core, urn_a, 2, ITERS });  // same URN
    const t3 = try std.Thread.spawn(.{}, Worker.run, .{ &core, urn_b, 3, ITERS });
    const t4 = try std.Thread.spawn(.{}, Worker.run, .{ &core, urn_c, 4, ITERS });
    t1.join(); t2.join(); t3.join(); t4.join();

    // After all churn settles, the table should be empty
    try testing.expectEqual(@as(usize, 0), core.liveLeaseCount());
}
```

Notes:

* Asserts END STATE matches START STATE (here, empty lease table).
* No timing assertion — that's a performance test.
* `ITERS` is tunable; could go to 10_000 in nightly.

## Recipe 12 — Performance test

**When.** Pinning a latency / throughput budget.

```zig
test "ring BENCHMARK: single-thread throughput (the 'blazing' claim, priority #2)" {
    var slots: [1024]Slot = undefined;
    var r = makeRing(&slots);

    const N: usize = 50_000_000;
    const start = std.time.nanoTimestamp();
    var pushed: usize = 0;
    while (pushed < N) {
        if (r.push(@intCast(pushed), .drop_newest)) pushed += 1;
        _ = r.pop();
    }
    const elapsed_ns: u64 = @intCast(std.time.nanoTimestamp() - start);
    const items_per_sec = (N * std.time.ns_per_s) / elapsed_ns;
    std.debug.print(
        "[ring] {d} items in {d:.1}ms = {d:.1}M items/sec\n",
        .{ N, @as(f64, @floatFromInt(elapsed_ns)) / 1e6,
           @as(f64, @floatFromInt(items_per_sec)) / 1e6 },
    );
    // Budget: ≥ 50M items/sec on a modern desktop.
    try testing.expect(items_per_sec > 50_000_000);
}
```

Notes:

* Has an EXPLICIT NUMERIC BUDGET in the test + a comment on
  rationale.
* Reports the actual number so a near-miss is visible.
* Don't loosen the budget without filing a paper.

## Recipe 13 — Conformance test

**When.** Pinning a design-level promise (G1–G4 / S1–S5).

```zig
test "core G3: client death reclaims ALL its leases — ghost ownership impossible" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    var core = Core.init(testing.allocator, try seeded(&fb));
    defer core.deinit();
    const pci = Urn.from("hw:/pci/0000:01:00.0/fn0");
    const usb = Urn.from("hw:/usb/bus1/dev4/if0");

    // client 7 holds two devices, then "crashes"
    _ = try core.claim(&pci, .exclusive, 7, D.none);
    _ = try core.claim(&usb, .exclusive, 7, D.none);
    try testing.expectEqual(@as(usize, 2), core.liveLeaseCount());

    const reclaimed = core.holderDied(7);
    try testing.expectEqual(@as(usize, 2), reclaimed);
    try testing.expectEqual(@as(usize, 0), core.liveLeaseCount());

    // Both resources are safe-reset and re-claimable by someone else.
    try testing.expect(fb.isSafe("hw:/pci/0000:01:00.0/fn0"));
    try testing.expect(fb.isSafe("hw:/usb/bus1/dev4/if0"));
}
```

Notes:

* Test name cites G3 (the design promise).
* Asserts the promise CLASSWIDE (not just one example): "for any
  client, death reclaims its leases."
* If a refactor changes the death-detection mechanism, this test
  catches that the promise still holds.

## Recipe 14 — Regression test

**When.** Pinning a specific bug fix so it doesn't return.

```python
def test_e2e_subprocess_stdout_pipe_closed_on_exit() -> None:
    """REGRESSION (commit e083bf9). The e2e test's broker subprocess
    leaves an open `subprocess.PIPE` stdout that survives until GC.
    Under Python 3.13's stricter unraisable-exception flow, this
    fails the test with PytestUnraisableExceptionWarning even though
    every e2e check passed. Pin: the finally block must close
    proc.stdout explicitly."""
    # ... reproduce the original failure shape ...
```

Notes:

* Cite the commit / bug-tracker entry that fixed it.
* Reproduce the original failure shape (the test would FAIL on
  pre-fix code).
* Tag with a comment so a future "cleanup" doesn't delete it
  thinking it's redundant.

---

## Anti-patterns (avoid these)

* **Mocks deep inside the system.** A test that mocks `Core._leases` is
  testing the implementation, not the behaviour. Use the in-memory
  `FakeBackend` and exercise through the public surface.
* **Sleep-based synchronization.** `time.sleep(0.5)` to wait for a
  thread is flake-prone. Use `threading.Event` or
  `assertEventually(predicate, timeout)`.
* **Wide assertions (`assert result`)**. Always assert what you
  meant: `assert result is True`, `assert len(result) == 2`. A
  truthy result that's not what you expected slips through.
* **One test asserting five things.** Split into five tests with
  five names. The CI failure message will tell you which property
  broke.
* **Tests that take the same input as the function and assert the
  same output.** That's a copy. The test should EXERCISE an
  invariant the function PROMISES.
