9740bc64c9
Continuous Deployment / Deploy to Production (push) Blocked by required conditions
Continuous Deployment / Rollback Deployment (push) Blocked by required conditions
Continuous Deployment / Post-deployment Monitoring (push) Blocked by required conditions
Continuous Deployment / Notify Deployment Status (push) Blocked by required conditions
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier1) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (full-adr060) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (tdm-3node) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / Swarm Test (ADR-062) (push) Has been skipped
npm packages / tools/ruview-mcp (node 22) (push) Failing after 1s
nvsim-server → ghcr.io / build-and-publish (push) Failing after 1s
ruview-swarm CI guard / tests (full+train) (push) Failing after 2s
Bench Regression Guard / bench compile-verify (--no-run) (push) Failing after 0s
Bench Regression Guard / bench fast-run (informational, non-gating) (push) Has been skipped
Firmware CI / Verify version.txt matches release tag (push) Has been skipped
Dashboard a11y + cross-browser / a11y (push) Failing after 0s
nvsim Dashboard → GitHub Pages / build-and-deploy (push) Failing after 2s
Firmware CI / Build firmware (esp32s3 / 4mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / Build Espressif QEMU (push) Failing after 1s
Firmware QEMU Tests (ADR-061) / Fuzz Testing (ADR-061 Layer 6) (push) Failing after 1s
Continuous Deployment / Pre-deployment Checks (push) Has been skipped
Continuous Deployment / Deploy to Staging (push) Waiting to run
Firmware CI / Build firmware (esp32c6 / c6-4mb) (push) Failing after 15s
Firmware CI / Build firmware (esp32s3 / 8mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-max) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-min) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (default) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier0) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / NVS Matrix Generation (push) Failing after 1s
Security Scanning / Security Policy Compliance (push) Failing after 0s
Security Scanning / Security Report (push) Waiting to run
Security Scanning / Dependency Vulnerability Scan (push) Failing after 0s
Security Scanning / Static Application Security Testing (push) Failing after 1s
Security Scanning / Infrastructure Security Scan (push) Failing after 1s
Security Scanning / Secret Scanning (push) Failing after 1s
npm packages / harness/ruview (node 22) (push) Failing after 17s
Security Scanning / License Compliance Scan (push) Failing after 1s
Security Scanning / Container Security Scan (push) Failing after 4s
three.js demos → GitHub Pages / build-and-deploy (push) Failing after 1s
Verify Pipeline Determinism / Verify Pipeline Determinism (3.11) (push) Failing after 1s
Fix-Marker Regression Guard / Verify fix markers (push) Failing after 1s
ADR-115 MQTT integration tests / mqtt-integration (push) Failing after 1s
npm packages / harness/ruview (node 20) (push) Failing after 1s
npm packages / tools/ruview-mcp (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 22) (push) Failing after 1s
BFLD MQTT Integration / cargo test --features mqtt (live mosquitto) (push) Failing after 29s
ruview-swarm CI guard / build train_marl bin (push) Failing after 2s
ruview-swarm CI guard / clippy (-D warnings, --no-deps) (push) Failing after 3s
ruview-swarm CI guard / tests (ruflo) (push) Failing after 1s
ruview-swarm CI guard / tests (train) (push) Failing after 2s
ruview-swarm CI guard / tests (default) (push) Failing after 2s
Point Cloud Viewer → GitHub Pages / build-and-deploy (push) Failing after 8s
ruview-swarm CI guard / ITAR / publish guard (push) Failing after 0s
wifi-densepose sensing-server → Docker Hub + ghcr.io / build · push · smoke-test (push) Failing after 1s
181 lines
5.9 KiB
Python
181 lines
5.9 KiB
Python
"""ADR-117 P4 — Tests for the HA-MIND semantic primitive listener.
|
||
|
||
Pure routing tests — no MQTT broker needed.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from wifi_densepose.client import (
|
||
SemanticPrimitive,
|
||
SemanticPrimitiveEvent,
|
||
SemanticPrimitiveListener,
|
||
)
|
||
|
||
|
||
# ─── SemanticPrimitive enum ──────────────────────────────────────────
|
||
|
||
|
||
def test_enum_covers_all_10_v1_primitives() -> None:
|
||
expected = {
|
||
"someone_sleeping",
|
||
"possible_distress",
|
||
"room_active",
|
||
"elderly_inactivity",
|
||
"meeting_in_progress",
|
||
"bathroom_occupied",
|
||
"fall_risk_elevated",
|
||
"bed_exit",
|
||
"no_movement_safety",
|
||
"multi_room_transition",
|
||
}
|
||
actual = {p.value for p in SemanticPrimitive}
|
||
assert actual == expected
|
||
|
||
|
||
def test_enum_from_object_id_round_trips() -> None:
|
||
for p in SemanticPrimitive:
|
||
assert SemanticPrimitive.from_object_id(p.value) is p
|
||
|
||
|
||
def test_enum_from_object_id_returns_none_for_unknown() -> None:
|
||
assert SemanticPrimitive.from_object_id("garbage") is None
|
||
|
||
|
||
# ─── Listener routing ────────────────────────────────────────────────
|
||
|
||
|
||
def test_listener_dispatches_to_specific_handler() -> None:
|
||
listener = SemanticPrimitiveListener()
|
||
received: list[SemanticPrimitiveEvent] = []
|
||
listener.on(SemanticPrimitive.SomeoneSleeping, received.append)
|
||
|
||
evt = listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/someone_sleeping/state",
|
||
json.dumps({"state": "ON", "confidence": 0.92, "explanation": ["motion<5%"]}),
|
||
)
|
||
assert evt is not None
|
||
assert evt.kind is SemanticPrimitive.SomeoneSleeping
|
||
assert evt.node_id == "aabb"
|
||
assert evt.state == "ON"
|
||
assert evt.confidence == 0.92
|
||
assert evt.explanation == ("motion<5%",)
|
||
assert len(received) == 1
|
||
assert received[0] is evt
|
||
|
||
|
||
def test_listener_on_any_fires_for_every_primitive() -> None:
|
||
listener = SemanticPrimitiveListener()
|
||
seen: list[SemanticPrimitiveEvent] = []
|
||
listener.on_any(seen.append)
|
||
|
||
listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/room_active/state",
|
||
json.dumps({"state": "ON"}),
|
||
)
|
||
listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/bathroom_occupied/state",
|
||
json.dumps({"state": "OFF"}),
|
||
)
|
||
assert len(seen) == 2
|
||
assert seen[0].kind is SemanticPrimitive.RoomActive
|
||
assert seen[1].kind is SemanticPrimitive.BathroomOccupied
|
||
|
||
|
||
def test_listener_specific_handler_does_not_fire_for_other_primitives() -> None:
|
||
listener = SemanticPrimitiveListener()
|
||
received: list[SemanticPrimitiveEvent] = []
|
||
listener.on(SemanticPrimitive.PossibleDistress, received.append)
|
||
|
||
listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/someone_sleeping/state",
|
||
json.dumps({"state": "ON"}),
|
||
)
|
||
assert received == []
|
||
|
||
|
||
def test_listener_decodes_plain_state_string() -> None:
|
||
"""HA convention: binary_sensors that don't carry attributes emit
|
||
plain strings ('ON' / 'OFF'). We must accept that too."""
|
||
listener = SemanticPrimitiveListener()
|
||
evt = listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/room_active/state",
|
||
"ON",
|
||
)
|
||
assert evt is not None
|
||
assert evt.state == "ON"
|
||
assert evt.confidence == 0.0 # not provided in plain string
|
||
assert evt.explanation == ()
|
||
|
||
|
||
def test_listener_decodes_numeric_sensor_state() -> None:
|
||
"""fall_risk_elevated is a 0–100 sensor — verify numeric string."""
|
||
listener = SemanticPrimitiveListener()
|
||
evt = listener.handle_mqtt_message(
|
||
"homeassistant/sensor/wifi_densepose_aabb/fall_risk_elevated/state",
|
||
"73",
|
||
)
|
||
assert evt is not None
|
||
assert evt.kind is SemanticPrimitive.FallRiskElevated
|
||
assert evt.state == "73"
|
||
|
||
|
||
def test_listener_decodes_bytes_payload() -> None:
|
||
listener = SemanticPrimitiveListener()
|
||
evt = listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/room_active/state",
|
||
b"ON",
|
||
)
|
||
assert evt is not None
|
||
assert evt.state == "ON"
|
||
|
||
|
||
def test_listener_ignores_non_state_topics() -> None:
|
||
listener = SemanticPrimitiveListener()
|
||
assert listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/room_active/config",
|
||
json.dumps({"name": "Room Active"}),
|
||
) is None
|
||
|
||
|
||
def test_listener_ignores_unknown_slug() -> None:
|
||
listener = SemanticPrimitiveListener()
|
||
assert listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/unknown_primitive/state",
|
||
"ON",
|
||
) is None
|
||
|
||
|
||
def test_listener_ignores_non_wifi_densepose_node() -> None:
|
||
listener = SemanticPrimitiveListener()
|
||
# third segment doesn't start with wifi_densepose_
|
||
assert listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/aqara_fp2/room_active/state",
|
||
"ON",
|
||
) is None
|
||
|
||
|
||
def test_listener_explanation_string_is_normalised_to_tuple() -> None:
|
||
"""Producers may send `explanation` as a single string by mistake;
|
||
accept that and wrap in a 1-tuple so downstream code can iterate
|
||
uniformly."""
|
||
listener = SemanticPrimitiveListener()
|
||
evt = listener.handle_mqtt_message(
|
||
"homeassistant/binary_sensor/wifi_densepose_aabb/possible_distress/state",
|
||
json.dumps({"state": "ON", "explanation": "HR=120 baseline=80"}),
|
||
)
|
||
assert evt is not None
|
||
assert evt.explanation == ("HR=120 baseline=80",)
|
||
|
||
|
||
def test_event_is_frozen() -> None:
|
||
evt = SemanticPrimitiveEvent(
|
||
kind=SemanticPrimitive.SomeoneSleeping,
|
||
node_id="aabb",
|
||
state="ON",
|
||
)
|
||
import pytest
|
||
with pytest.raises((AttributeError, Exception)): # FrozenInstanceError subclass
|
||
evt.state = "OFF" # type: ignore[misc]
|