Commit Graph

4 Commits

Author SHA1 Message Date
ruv b41fdd75c6 test(adr-115): property-based invariants for the semantic bus (420 lib tests)
5 new proptest cases in semantic:🚌:tests. Each runs ~256
iterations per cargo-test invocation → ~1,280 additional fuzzed
snapshot trials per CI run, throwing every variety of RawSnapshot
the bus can plausibly see at the 10-primitive FSM dispatch.

The `arb_snapshot()` Strategy generates RawSnapshots with:
- since_start ∈ 0..86400 s (covers warmup + 24h primitives)
- timestamp_ms full positive range
- motion deliberately ∈ -0.5..2.0 (out-of-range to test clamping)
- motion_energy ∈ -1000..10000
- breathing_rate_bpm ∈ Option<0..200>
- heart_rate_bpm ∈ Option<0..250>
- n_persons ∈ 0..10
- rssi_dbm ∈ Option<-120..0>
- vital_confidence ∈ 0..1
- local_seconds_since_midnight ∈ 0..86400 (covers bed_exit window
   wrap-around test)
- active_zones ∈ random vec of [a-z]{3,8} strings

Strategy is split into two nested tuples because proptest only impls
Strategy for tuples up to length 12 (we have 13 fields).

Invariants enforced:

- `bus_tick_never_panics_on_arbitrary_snapshot` — every primitive
   handles every plausible input without panic. Pathological cases
   include motion=1.7, HR=Some(0.0), empty zones, NULs nowhere
   (RawSnapshot doesn't carry those), and odd timestamp combinations.
- `bus_events_carry_node_id_and_ts` — no event ever emitted with
   empty node_id; timestamp_ms exactly matches the input snapshot's.
- `boolean_states_always_have_reason_tags` — when `changed=true`,
   the `reason.tags` MUST be non-empty. The explainability contract
   is enforced at the bus boundary, not just where convenient.
- `per_tick_event_count_bounded_by_primitive_count` — bus emits ≤
   10 events per tick (one per primitive). Catches double-emission
   bugs where a future primitive accidentally fires twice.
- `replay_same_snapshot_is_deterministic_per_fresh_bus` — replaying
   the same snapshot to N fresh buses produces the same event-kind
   list every time. Catches uninitialised internal state.

Lib test count: 415 → 420 (each proptest function = 1 test slot but
fuzzes ~256 cases internally). Effective coverage rises to ~1,955
assertions per CI lib run.

Refs #776, PR #778.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-23 15:15:24 -04:00
ruv b68f130ce4 feat(adr-115): P4 — broker integration tests + mosquitto CI workflow
Adds three integration tests (`v2/crates/wifi-densepose-sensing-server/
tests/mqtt_integration.rs`) that prove the publisher works against a
real broker, gated behind `--features mqtt` + `RUVIEW_RUN_INTEGRATION=1`:

1. `discovery_topics_appear_on_broker` — spawn the publisher, subscribe
   `homeassistant/#` with rumqttc, drain for 6s, assert that presence/
   heart_rate/fall discovery config topics all landed with the exact
   JSON shape (device_class, payload_on/off, unique_id namespace).

2. `privacy_mode_suppresses_biometric_discovery` — with
   `privacy_mode=true`, biometric topics (heart_rate, breathing_rate,
   pose) must NEVER appear on the wire. Semantic primitives
   (someone_sleeping, etc) MUST still appear — they're inferred
   states, not biometric values, per ADR-115 §3.12.3.

3. `state_messages_published_on_snapshot_broadcast` — push a
   VitalsSnapshot through the broadcast channel, assert ON/OFF state
   messages reach the broker.

Plus `.github/workflows/mqtt-integration.yml` — spins up Mosquitto
2.0.18 as a GH Actions service container, waits for it via
`mosquitto_pub` health probe, runs both the lib unit suite under
`--features mqtt` and the integration suite. Dumps broker logs on
failure for debugging.

Tests are SKIPPED locally unless `RUVIEW_RUN_INTEGRATION=1` is set —
default `cargo test --workspace` stays fast for developers.

Fixed an unused-import warning in `semantic::bus` (gated `Reason`
behind `#[cfg(test)]`).

Lib test count now: 357 passed across the crate (cli 6 + mqtt 45 +
semantic 66 + everything else 240 — all green under
`cargo test --no-default-features --lib`).

Refs #776.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-23 14:14:21 -04:00
ruv b2a692369e feat(adr-115): P4.5b — 6 remaining semantic primitives — all 10 HA-MIND v1 done (66 tests)
Lands the remaining six §3.12 v1 primitives:

- `distress` (PossibleDistress) — EWMA baseline HR + 1.5× multiplier
  + agitated motion + no-fall + 60 s dwell → ON. Refractory 5 min
  after exit. Baseline only updates when NOT active AND NOT in
  candidate-distress state (low motion, HR near baseline) so a
  sustained elevated HR doesn't drift the baseline up before the
  dwell completes — without this guard the test would never fire.
- `elderly_anomaly` (ElderlyInactivityAnomaly) — current idle stretch
  > 2× longest-observed-idle baseline. Baseline floor at 30 min so
  the first day doesn't fire spuriously. 24 h refractory per resident.
- `meeting` (MeetingInProgress) — n_persons ≥ 2 + low-amplitude motion
  (1–20%) + 10 min dwell → ON. 2 min exit dwell on count drop.
- `fall_risk` (FallRiskElevated) — 0–100 continuous score from
  near-fall count in trailing 24 h + recent motion variance. Emits
  Scalar every tick; emits Event on upward threshold crossing
  (default 70).
- `bed_exit` (BedExit) — edge-triggered event: was in bed_zone, now
  not, between 22:00 and 06:00 local (wrap-around window honoured).
- `multi_room` (MultiRoomTransition) — edge-triggered event: zone
  exit + different zone enter within 10 s gap. Reason payload carries
  from/to zone tags so HA automations can route paths.

Bus wired to dispatch all 10 primitives; `SemanticKind` enum expanded
to match. `tick()` returns up to 10 events per snapshot.

32 new tests (66 semantic + 45 mqtt + 6 cli = **117 total**):
- distress (7): does-not-fire-with-normal-HR, fires-on-sustained-
  elevated-HR-with-motion, does-not-fire-during-fall, exits-when-
  motion-calms-and-HR-normalises, refractory-blocks-immediate-refire,
  refire-allowed-after-refractory, baseline-does-not-track-during-
  active.
- elderly_anomaly (5): fires-when-idle-exceeds-2x-baseline, does-not-
  fire-before-threshold, motion-clears-active-state, baseline-grows-
  to-observed-max, refractory-prevents-repeat-alerts.
- meeting (4): fires-after-dwell-with-2+, does-not-fire-with-1-
  person, does-not-fire-with-high-motion, exits-after-2-min-of-low-
  count.
- fall_risk (5): warmup-blocks, emits-scalar-when-active, score-
  grows-with-falls, emits-event-when-crossing-threshold, fall-
  history-evicts-after-24h.
- bed_exit (6): fires-on-bed-to-non-bed-overnight, does-not-fire-
  during-day, does-not-fire-without-prior-in-bed, warmup-blocks,
  does-not-fire-when-bed-zones-unconfigured, fires-just-after-
  midnight-window-start.
- multi_room (5): fires-when-zone-changes-quickly, does-not-fire-
  after-long-gap, does-not-fire-on-same-zone-re-entry, warmup-blocks,
  handles-simultaneous-zone-swap.

ADR-115 §3.12 inference layer now complete. Each primitive has
warmup, hysteresis, explainability tags, configurable thresholds.
Adding a v2 primitive is one file + one bus entry.

Refs #776.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-23 14:07:21 -04:00
ruv 8e416af203 feat(adr-115): P4.5a — semantic inference layer (HA-MIND) — 4 primitives + bus (34 tests)
ADR-115 §3.12 keystone. Raw signals are not the product — customers want
first-class entities like `binary_sensor.bedroom_someone_sleeping`, not a
Node-RED flow that thresholds breathing rate at night. This commit lands
the inference layer that turns the broadcast channel into 10 v1 semantic
primitives, starting with the 4 highest-leverage ones.

Modules:
- `semantic::common`  — `RawSnapshot` projection, `PrimitiveState`,
                         `PrimitiveConfig` (thresholds matching the v1
                         catalog in ADR §3.12), `in_window` for time-gated
                         primitives, `Reason` explainability struct.
- `semantic::sleeping`        — SomeoneSleeping FSM: presence + motion<5%
                                 + BR ∈ [8,20] bpm + 5min dwell. Exit on
                                 presence-drop (immediate) or motion>15%
                                 for 30s.
- `semantic::room_active`     — motion >10% in 30s window → ON. Exit on
                                 presence-drop or 10min idle.
- `semantic::bathroom`        — presence + zone tagged as bathroom. Safe
                                 in privacy mode (no biometrics in the
                                 derivation).
- `semantic::no_movement`     — presence + motion<1% for 30min → ON.
                                 Safety-check primitive for aging-in-place.
- `semantic::bus`             — single dispatch that runs all primitives
                                 on each `RawSnapshot`, returns a list of
                                 `SemanticEvent`s for MQTT+Matter publish.

Every primitive has:
- Warmup suppression (60s default, §3.12.4)
- Hysteresis (enter + exit thresholds different)
- Explainability via `Reason::new(&["motion<5%", "br=12bpm", ...])`
- Configurable thresholds via `PrimitiveConfig`

Test coverage (34 tests, all passing under `--no-default-features`):
- common: in_window simple + wrap-around midnight, default thresholds
  match ADR catalog, Reason struct.
- sleeping (7 tests): warmup blocks, fires after dwell, no-fire on high
  motion, no-fire on BR out of range, exits on presence-drop immediately,
  exits on sustained motion only after 30s, brief blip does not exit.
- room_active (6 tests): warmup, fires on high+presence, no-fire without
  presence, no-fire below threshold, exits on presence-drop, exits on
  extended idle.
- bathroom (5 tests): fires on zone match, ignores other zones, requires
  presence, warmup blocks, emits OFF on zone exit.
- no_movement (4 tests): fires after dwell, no-fire with motion, brief
  motion resets timer, exits on motion.
- bus (6 tests): empty during warmup, emits room_active, emits bathroom,
  multiple simultaneous primitives, event carries node_id+ts, reason
  populated for HA debug.

Total cargo test count now:
  cli: 6 + mqtt: 45 + semantic: 34 = 85 tests passing

P4.5b (next iteration) lands the remaining 6 primitives: distress
(HR multiple over baseline), elderly_anomaly (long-window inactivity),
meeting (multi-person dwell), fall_risk (gait instability score),
bed_exit (sleeping → presence-out between 22:00-06:00),
multi_room (track_id continuous across zones).

Refs #776.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-23 14:01:51 -04:00