---
name: psyche-health-int-observation-seam
description: How to write an spt-daemon INTEGRATION test against the psyche health budgets — the counters are crate-private, so the perch `psyche_host_error` stamp plus the ASYMMETRIC soft/hard budget depths are the only external discriminator.
metadata:
  type: project
---

The BAROMETER W1 psyche health budgets can be driven honestly from a `crates/spt-daemon/tests/`
integration test even though their counters are unreadable from outside.

**The constraint.** `PsycheBudgets`' fields, `note_outcome`, `note_success` and
`budgets_snapshot` (which is `#[cfg(test)]`) are all crate-private, so an int test cannot assert
"soft counter == N". The in-crate unit rows in `lifecycle.rs` can, and do — which is exactly why
they all feed the budget a *synthesized* `PsycheOutcome` and none of them exercise a real kill.

**The seam that works.** Only `run_pulse_loop` feeds outcomes to the budget (`pulse_tick` merely
returns a public `TickReport`), and the budget's only external effect is the perch's
`psyche_host_error` field, recomposed from the latch set on every note. So an int test drives
`BrainLifecycle::run_pulse_loop` directly and reads the stamp off disk via
`spt_store::info::read_info`.

**Why that is still discriminating.** The two budgets have different depths — hard 3, soft 10 —
so the depth substitutes for the missing accessor: at exactly 3 consecutive kills a correct build
is UNSTAMPED while a build that classified the kill as a defect has already latched, and at 10 a
correct build stamps with the timeout latch's own sentence. Asserting both is the
soft-incremented-AND-hard-unchanged pair. Verify the two depths are still what ship before
relying on this — they are private consts, and both are env-shrinkable
(`SPT_PSYCHE_TURN_STRIKE_BUDGET` / `SPT_PSYCHE_TIMEOUT_STRIKE_BUDGET`), which the source comments
explicitly offer to int gates.

**Rig mechanics worth not rediscovering:** the echo gate is read-and-clear, so a multi-fire loop
must re-arm it every tick or the loop spins forever (give the driving closure a wall-clock escape
hatch); the `psyche_resume` role must SUCCEED or its failure takes precedence over the echo leg's
and you measure the wrong thing; a role's `invocation_budget_secs` makes a real bound kill cheap
to stage (1s), and the killed spawn's reason renders `timed out after 1s (killed)` so the reason
text proves the ROLE's bound was applied rather than the 90s default. Point the `echo_commune`
role at a script FILE and rewrite its body to flip slow→fast mid-test — that is how a real
success positive-control runs on the same `BrainLifecycle` instance (a second instance would have
fresh budgets). Use `spt_test_support::TestHome` for `SPT_HOME`: it holds the process-global env
lock for the whole body and binds no broker socket.

Related: [[mutation-proof-is-the-house-standard]], [[brain-resume-test-rig]]
