docs(adr-114): ADR for replay regression suite

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
arsen 2026-05-17 17:00:16 +07:00
parent 96225e27cf
commit c827cde69e
1 changed files with 162 additions and 0 deletions

View File

@ -0,0 +1,162 @@
# ADR-114 — 2000-Packet Replay Regression Suite
**Status**: Accepted
**Date**: 2026-05-17
**Scope**: `v2/crates/wifi-densepose-sensing-server/src/main.rs`
(`replay_tests` module under `#[cfg(test)]`),
`v2/crates/wifi-densepose-sensing-server/tests/fixtures/replay_*.jsonl`,
`scripts/generate-replay-fixtures.py`. Closes the "2 000-packet fixed-
replay test suite" item in CHECKLIST.
## Context
Up to now the amplitude classifier has been protected by per-function
unit tests (cv calculation, NBVI selection, baseline drop trigger) but
not by an end-to-end regression test that feeds a known-good stream
through the full `amp_presence_override` pipeline and checks that the
labels still look right.
Without that, a refactor of NBVI selection or a threshold tweak could
silently regress classifier behaviour on real deployments — the unit
tests would all pass while the production output flipped.
Pace's ESPectre has a similar pattern: 1000 idle + 1000 motion frames,
checked into the repo, replayed in CI on every PR.
## Decisions
### D1 — Fixture format: line-delimited JSON, `{node_id, amplitude[]}`
```jsonl
{"node_id":1,"amplitude":[28.842, 19.333, ...]}
{"node_id":2,"amplitude":[15.601, 17.220, ...]}
...
```
Minimal: just the two fields the classifier reads. Round-robined across
nodes (500 per node × 2 nodes = 1000 frames per fixture file). 1000
frames per file × 2 files = 2000 packets total.
### D2 — Fixtures live in-repo under `tests/fixtures/`
```
v2/crates/wifi-densepose-sensing-server/tests/fixtures/
replay_idle.jsonl (1000 lines)
replay_motion.jsonl (1000 lines)
```
Co-located with the test that consumes them. `cargo test` picks them up
via `env!("CARGO_MANIFEST_DIR")`. The fixture files are ~1.5 MB total
(text JSON) — small enough for the repo, not so small that the test
loses statistical power.
### D3 — Synthetic but parameter-matched to live data
The fixtures are generated by `scripts/generate-replay-fixtures.py` with
two deterministic seeds (42 and 43). Parameters chosen to mirror the
live deployment:
* Baseline mean amplitudes per node taken from `data/baseline.json`
(node 1: 27.04, node 2: 14.72).
* Idle: per-frame Gaussian noise σ = 1.8 % of the per-subcarrier mean.
* Motion: ±40 % slow envelope (0.15 Hz sinusoid, 6.7 s cycle, longer
than the classifier's 4.5 s `AMP_SHORT_WIN`) + 5 % per-frame noise.
Mimics a body slowly modulating the channel during walking.
This is deliberately *synthetic*. Capturing 1000 real frames of
"empty room" requires the operator to step out and stay out for ~50 s,
and capturing "motion" requires walking through the room — neither is
something this session could do without manual operator labour. The
synthetic-but-realistic alternative gives deterministic regression
coverage today, with the option to swap in live captures (same JSONL
schema, same filenames) when time allows.
### D4 — Test lives inside `main.rs` under `#[cfg(test)] mod replay_tests`
`amp_presence_override` is private to the binary crate, so the test
can't sit in `tests/` (which is for integration tests against
`lib.rs`). Putting it under `#[cfg(test)]` in `main.rs` keeps the
helper visibility minimal and exercises the exact function path
production uses.
### D5 — Test resets per-node history before each fixture run
`amp_presence_override` accumulates per-node state in
`OnceLock<Mutex<HashMap<…>>>` statics. The test clears those between
the idle and motion runs so each fixture starts with a fresh classifier
(no cross-contamination from the previous fixture's frames sitting in
the rolling window).
It also clears the per-subcarrier baseline (`amp_baseline_per_sub`)
because the synthetic fixtures don't share a per-subcarrier profile
with whatever real recording lives in `data/baseline.json` — leaving
the live per-sub baseline in place would make the drift channel
saturate and obscure the CV-threshold path we're actually testing.
### D6 — F1 threshold: 0.85
Convention from Pace's ESPectre CI gate. Current value on the synthetic
fixtures with this deployment's baseline is `F1 = 1.000` (tp=822,
fp=0, tn=822, fn=0; 178 warmup frames excluded per fixture). The 0.15
headroom gives room for legitimate classifier evolution without
forcing a fixture re-record on every tuning change.
### D7 — Test loads the deployment baseline at startup
Without `data/baseline.json` loaded, the classifier compares raw CV
against thresholds of 3.0 (300 %) and 6.0 — values no realistic signal
reaches. The test discovers the baseline via a couple of canonical
relative paths (`../../data/baseline.json` from the crate dir, etc.)
and exits early with a clear `eprintln!` hint if none are found.
## Trade-offs
* **Synthetic fixtures don't catch sensor-specific bugs.** A
Kconfig-level FW regression that produced subtly different amplitude
scaling would not be caught — the synthetic fixtures encode the
*expected* scaling, not whatever the FW currently emits. The witness
bundle (ADR-028) still covers that end of the pipeline.
* **`replay_2000` runs only when explicitly named or via the full
suite.** No filtering hides it from CI. It runs in well under a
second so cost is negligible.
* **F1 currently 1.0 — too clean to detect subtle regressions.** A
followup with live captures may bring the natural F1 to ~0.9, at
which point the 0.85 threshold becomes a real gate. For now it's
primarily a contract test: "the classifier still emits something
reasonable on a known input".
## Files Touched
```
scripts/generate-replay-fixtures.py (new)
v2/crates/wifi-densepose-sensing-server/tests/fixtures/
replay_idle.jsonl (new)
replay_motion.jsonl (new)
v2/crates/wifi-densepose-sensing-server/src/main.rs
- replay_tests module (D4, D5, D7)
docs/adr/ADR-114-replay-regression-suite.md (this)
```
## Verified Acceptance
```
$ cargo test --release -p wifi-densepose-sensing-server \
--no-default-features --bin sensing-server replay_2000 -- --nocapture
replay_2000 F1=1.000 tp=822 fp=0 tn=822 fn=0
test replay_tests::replay_2000_packets_f1_above_threshold ... ok
test result: ok. 1 passed; 0 failed; 0 ignored;
```
Full workspace suite: 327 tests pass (was 326 + this one).
## References
* ADR-101 — raw-amplitude classifier this test exercises.
* ADR-102 — NBVI subcarrier selection that feeds CV calculation.
* ADR-103 — persistent baseline that drives the universal-threshold
normalization the test relies on.
* ADR-028 — witness bundle (the other end-to-end regression
mechanism; ADR-114 covers classifier code paths, ADR-028 covers
the deterministic-CSI proof pipeline).
* Francesco Pace, *How I Turned My Wi-Fi Into a Motion Sensor —
Part 2*, "Replay regression test" — the upstream pattern.