"""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]