---
name: spt-test-conventions
description: How to write + run unit and integration tests for the spt binary crate (bin-only, isolated_home, dummy-harness e2e, scoped reaping)
metadata:
  type: project
---

The `spt` crate (crates/spt) is BIN-ONLY (no `[lib]` target). Consequences:
- Unit tests run via `cargo test -p spt --bin spt <name>` — `--lib` FAILS.
- Integration tests in `crates/spt/tests/` CANNOT reach `spt::` internals (no lib),
  only the workspace lib crates (`spt_store`, `spt_runtime`, `spt_daemon`, `spt_net`,
  `spt_proto`, `spt_live`, `spt_msg`). To assert picker/CLI logic from an int test,
  re-derive the equivalent over the public `spt_store` path and unit-test the pure
  rule in-crate. Example: picker project membership = `info.cwd` →
  `spt_store::project::project_id_for_dir` → origin-union; the pure union rule is
  unit-tested in `crates/spt/src/picker/data.rs::merge_origin_project`.

**Unit tests** live in `#[cfg(test)] mod tests` at the bottom of each module.
- `crate::testutil::isolated_home()` returns an RAII guard: serializes all
  env-mutating tests on one process-wide lock, points `SPT_HOME` at a fresh tempdir,
  clears `SPT_API_TOKEN`/`OWL_SESSION_ID`, restores on drop. Hold it for the whole test.
- Bind/startup unit tests exercise the PUBLIC `cmd_bind` / `bind_from_seed`
  (`establish_perch` is private); they stand up an in-process seed daemon via a local
  `start_seed_daemon()` helper (spawns `spt_daemon::seedmap::serve_seed_control` on the
  isolated home's socket; `put_seed`/`reg.put(Seed{...})` to seed it). Use
  `std::process::id()` as a live anchor pid.

**Integration tests** model on `crates/spt/tests/dummy_harness_e2e.rs`:
- REAL detached `spt daemon run` (broker) + REAL `endpoint run --start` bringup; the
  dummy harness = `mock-session --mode dummy` (kind=harness adapter, long-lived,
  heartbeats, binds its own perch). REQUIRES the sibling bin built first:
  `cargo build -p mock-adapter --bin mock-session`. Run with `--test-threads=1`.
- `mod common; use common::CommandNoWindowExt;` → `.no_window()` on every spawned
  Command (Windows: no console pop). Reuse the verbatim helpers: `kill_pid`
  (`#[cfg(windows)]` taskkill /F /T, `#[cfg(unix)]` kill -9), `output_bounded`
  (deadline'd Command::output via a thread+channel), `wait_for_ready_pid`,
  `sibling_bin`, `ready_pid`.
- Reap ALL spawned pids SCOPED, never machine-wide (shared CI runner): the harness pid
  (parsed from `ENDPOINT_RUN:… pid=Some(NNN)` on stderr), any reconcile-hosted
  `{id}-psyche` perch pid, then `daemon stop`, then the brain pid, then `broker.kill()`.
- Bringup timing that worked: brain.ready within 30s, `endpoint run` bounded 45s, perch
  goes ONLINE within ~20s of poll. Whole test ~3s once bins are built.
- The W1 `endpoint run --start` shortcut launches the harness with cwd=None → it
  INHERITS the broker's cwd, and the broker inherits the test process's cwd. So a
  bind's recorded `info.cwd` == the test process's current_dir (the spt crate dir
  during cargo test) — deterministic enough to assert non-empty + derive a project id.

**Evidence tags** (traceable-reqs): `// [unit->REQ-X]` / `// [int->REQ-X]` ON or
immediately above the test fn — never at file tops.

**Mutation-checking a test is worthwhile**: temporarily break the impl two ways
(e.g. `rec.cwd = None` vs dropping `.or_else(prior.cwd)`) to prove each test guards a
DISTINCT branch, then revert. Confirmed the SET test, seed-thread test, and
carry-forward test each fail independently for the right reason.
