"""Tests for the AI Receptionist decision logic (services/receptionist.py).""" from types import SimpleNamespace import pytest from config import ReceptionistSettings, Settings from models.routing import ( RoutingAction, RoutingActionType, RoutingDecision, ) from services.receptionist import ReceptionistService def _make_gateway(): settings = Settings() settings.receptionist = ReceptionistSettings() return SimpleNamespace(settings=settings, devices={}) class TestReceptionistDecide: def test_rule_wins_over_llm_when_rule_is_actionable(self): gw = _make_gateway() svc = ReceptionistService(gw) rule_action = RoutingAction(type=RoutingActionType.REJECT, message="nope") decision = RoutingDecision(action=rule_action, reason="rule said so") chosen = svc._decide(decision, {"recommended_action": "ring"}) assert chosen.type == RoutingActionType.REJECT def test_falls_back_to_llm_when_rule_is_default_take_message(self): gw = _make_gateway() svc = ReceptionistService(gw) decision = RoutingDecision( action=RoutingAction(type=RoutingActionType.TAKE_MESSAGE), reason="default", ) chosen = svc._decide(decision, {"recommended_action": "ring"}) assert chosen.type == RoutingActionType.RING_CHAIN def test_llm_reject_recommendation_is_honored(self): gw = _make_gateway() svc = ReceptionistService(gw) chosen = svc._decide(None, {"recommended_action": "reject"}) assert chosen.type == RoutingActionType.REJECT assert chosen.message # should carry a polite decline message class TestReceptionistDeviceList: def test_ring_device_returns_explicit_device(self): gw = _make_gateway() svc = ReceptionistService(gw) action = RoutingAction(type=RoutingActionType.RING_DEVICE, device_id="dev_a") assert svc._resolve_device_list(action, {}) == ["dev_a"] def test_ring_chain_uses_action_list_when_present(self): gw = _make_gateway() svc = ReceptionistService(gw) action = RoutingAction( type=RoutingActionType.RING_CHAIN, device_ids=["dev_a", "dev_b"], ) assert svc._resolve_device_list(action, {}) == ["dev_a", "dev_b"] class TestFrameSilenceHeuristic: def test_zeroes_are_silent(self): # 16-bit PCM zeros → silent assert ReceptionistService._frame_is_silent(b"\x00\x00" * 80) is True def test_loud_pattern_not_silent(self): # Loud values → not silent loud = b"\xff\x7f" * 80 # max int16 every sample assert ReceptionistService._frame_is_silent(loud) is False