""" Sentinel — Aggressive Full Test Suite Tests every module path including new: OpenRouter 30B-A3B model, Modal RAG local methods, cost tracker prefix fix verification, trigger_history cap, and all gatekeeper trigger conditions. """ import sys import os import time import math import base64 import asyncio import unittest import threading from unittest.mock import MagicMock, AsyncMock, patch import numpy as np import cv2 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from cost_tracker import CostTracker, CallRecord from gatekeeper import ( GatekeeperDecision, FrameChangeResult, Detection, AudioClass, PoseData, SensorSnapshot, TriggerDecision, FrameChangeDetector, PoseAnalyzer, AudioMonitor, ) from cohere_rag import SafetyKnowledgeBase from fallback import FallbackRouter # ───────────────────────────────────────────── # 1. COST TRACKER # ───────────────────────────────────────────── class TestCostTracker(unittest.TestCase): def setUp(self): self.tracker = CostTracker() # --- Initialization --- def test_initialization_zero_state(self): s = self.tracker.get_stats() self.assertEqual(s["total_calls"], 0) self.assertEqual(s["total_tokens"], 0) self.assertEqual(s["total_cost_usd"], 0.0) self.assertEqual(s["naive_cost_usd"], 0.0) self.assertEqual(s["savings_pct"], 0.0) self.assertEqual(s["frames_processed"], 0) # --- Frame logging --- def test_log_frame_increments(self): for _ in range(7): self.tracker.log_frame() s = self.tracker.get_stats() self.assertEqual(s["frames_processed"], 7) self.assertAlmostEqual(s["naive_cost_usd"], 0.07, places=4) self.assertEqual(s["gatekeeper_filtered"], 7) # --- Nemotron token rate --- def test_nemotron_rate_exact(self): # 0.0003 / 1000 = 0.0000003 per token × 1000 = 0.0003 self.tracker.log("nemotron-4b", 1000, 300) self.assertAlmostEqual(self.tracker.get_stats()["total_cost_usd"], 0.0003, places=7) # --- MiniCPM-V rate --- def test_minicpm_rate_exact(self): # 0.0002 / 1000 × 500 = 0.0001 self.tracker.log("minicpm-v", 500, 200) self.assertAlmostEqual(self.tracker.get_stats()["total_cost_usd"], 0.0001, places=7) # --- Kokoro flat rate --- def test_kokoro_flat_rate(self): self.tracker.log("kokoro-tts", 0, 100) self.assertAlmostEqual(self.tracker.get_stats()["total_cost_usd"], 0.0001, places=7) # --- Fallback prefix lookup FIXED (was returning 0.0) --- def test_fallback_openrouter_prefix_nonzero_cost(self): self.tracker.log("fallback-openrouter", 200, 500) s = self.tracker.get_stats() # If the prefix lookup is broken it returns 0.0 — this asserts it's non-zero self.assertGreater(s["total_cost_usd"], 0.0, "fallback-openrouter cost must be non-zero") def test_fallback_openbmb_prefix_nonzero_cost(self): self.tracker.log("fallback-openbmb", 200, 500) s = self.tracker.get_stats() self.assertGreater(s["total_cost_usd"], 0.0, "fallback-openbmb cost must be non-zero") # --- Savings computation --- def test_savings_99_percent(self): for _ in range(10): self.tracker.log_frame() self.tracker.log("nemotron-4b", 100, 400) self.tracker.log("nemotron-4b", 100, 400) s = self.tracker.get_stats() self.assertAlmostEqual(s["total_cost_usd"], 0.00006, places=7) self.assertAlmostEqual(s["naive_cost_usd"], 0.10, places=4) self.assertAlmostEqual(s["savings_pct"], 99.94, places=2) # --- Reset --- def test_reset_clears_all_state(self): self.tracker.log_frame() self.tracker.log("nemotron-4b", 100, 200) self.tracker.reset() s = self.tracker.get_stats() self.assertEqual(s["total_calls"], 0) self.assertEqual(s["frames_processed"], 0) self.assertEqual(s["total_cost_usd"], 0.0) # --- Gradio rows --- def test_gradio_rows_order_newest_first(self): for i in range(5): self.tracker.log(f"model-{i}", 10, 100) time.sleep(0.01) rows = self.tracker.get_gradio_rows() # Newest (model-4) should be first row self.assertIn("model-4", rows[0][1]) def test_gradio_rows_cap_at_20(self): for i in range(30): self.tracker.log("nemotron-4b", 10, 50) rows = self.tracker.get_gradio_rows() self.assertLessEqual(len(rows), 20) # --- Thread safety --- def test_thread_safety_250_workers(self): def worker(): for _ in range(50): self.tracker.log_frame() self.tracker.log("nemotron-4b", 10, 50) threads = [threading.Thread(target=worker) for _ in range(5)] for t in threads: t.start() for t in threads: t.join() s = self.tracker.get_stats() self.assertEqual(s["frames_processed"], 250) self.assertEqual(s["total_calls"], 250) # --- Hourly breakdown --- def test_hourly_breakdown_non_empty(self): self.tracker.log("nemotron-4b", 100, 300) breakdown = self.tracker.get_hourly_breakdown() self.assertIsInstance(breakdown, list) self.assertGreater(len(breakdown), 0) self.assertIn("hour", breakdown[0]) self.assertIn("cost", breakdown[0]) # ───────────────────────────────────────────── # 2. GATEKEEPER DECISION ENGINE # ───────────────────────────────────────────── class TestGatekeeperDecision(unittest.TestCase): def setUp(self): self.engine = GatekeeperDecision(cooldown_seconds=2.0) def _no_signal(self): return FrameChangeResult(change_pct=0.5, is_significant=False, threshold_used=5.0, motion_regions=0) # --- Default (no signals) --- def test_no_trigger_idle_scene(self): d = self.engine.decide(self._no_signal(), [], [], None) self.assertFalse(d.should_trigger) self.assertEqual(d.urgency, "none") self.assertEqual(d.confidence, 0.0) # --- Priority 1: Manual user query (always triggers, always urgency=info) --- def test_manual_query_highest_priority(self): d = self.engine.decide(self._no_signal(), [], [], None, user_query="What is ahead?") self.assertTrue(d.should_trigger) self.assertEqual(d.urgency, "info") self.assertIn("Manual user query", d.reason) self.assertEqual(d.confidence, 1.0) def test_empty_query_string_does_not_trigger(self): d = self.engine.decide(self._no_signal(), [], [], None, user_query=" ") self.assertFalse(d.should_trigger) # --- Priority 2: Critical audio --- def test_critical_scream_triggers(self): audio = [AudioClass(class_name="Scream", confidence=0.82, alert_level="critical")] d = self.engine.decide(self._no_signal(), [], audio, None) self.assertTrue(d.should_trigger) self.assertEqual(d.urgency, "critical") self.assertIn("Critical sound detected", d.reason) def test_critical_explosion_triggers(self): audio = [AudioClass(class_name="Explosion", confidence=0.72, alert_level="critical")] d = self.engine.decide(self._no_signal(), [], audio, None) self.assertTrue(d.should_trigger) self.assertEqual(d.urgency, "critical") def test_warning_audio_only_triggers_on_first_call(self): # Warning audio respects cooldown — only first trigger in window fires audio = [AudioClass(class_name="Siren", confidence=0.65, alert_level="warning")] d1 = self.engine.decide(self._no_signal(), [], audio, None) # If the warning fires, 2nd call within cooldown must be suppressed if d1.should_trigger: d2 = self.engine.decide(self._no_signal(), [], audio, None) self.assertFalse(d2.should_trigger) # --- Priority 3: Fall detection --- def test_fall_high_confidence_triggers(self): pose = PoseData(head_y=0.85, hip_y=0.55, shoulder_y=0.70, is_fall=True, is_sitting=False, confidence=0.92) d = self.engine.decide(self._no_signal(), [], [], pose) self.assertTrue(d.should_trigger) self.assertEqual(d.urgency, "critical") self.assertIn("Fall detected", d.reason) def test_fall_low_confidence_no_trigger(self): # confidence must be > 0.6 pose = PoseData(head_y=0.85, hip_y=0.55, shoulder_y=0.70, is_fall=True, is_sitting=False, confidence=0.5) d = self.engine.decide(self._no_signal(), [], [], pose) self.assertFalse(d.should_trigger) def test_sitting_does_not_trigger_fall(self): pose = PoseData(head_y=0.3, hip_y=0.7, shoulder_y=0.5, is_fall=False, is_sitting=True, confidence=0.9) d = self.engine.decide(self._no_signal(), [], [], pose) self.assertFalse(d.should_trigger) # --- Priority 4: Motion + Person combination --- def test_motion_and_person_triggers_warning(self): motion = FrameChangeResult(change_pct=9.0, is_significant=True, threshold_used=5.0, motion_regions=2) dets = [Detection(class_name="person", confidence=0.88, bbox=(0,0,100,100), area_pct=12.0)] d = self.engine.decide(motion, dets, [], None) self.assertTrue(d.should_trigger) self.assertEqual(d.urgency, "warning") self.assertIn("Motion + Person presence", d.reason) def test_motion_without_person_no_trigger(self): motion = FrameChangeResult(change_pct=9.0, is_significant=True, threshold_used=5.0, motion_regions=2) d = self.engine.decide(motion, [], [], None) self.assertFalse(d.should_trigger) def test_person_without_motion_no_trigger(self): dets = [Detection(class_name="person", confidence=0.88, bbox=(0,0,100,100), area_pct=12.0)] d = self.engine.decide(self._no_signal(), dets, [], None) self.assertFalse(d.should_trigger) # --- Priority 5: Sensor anomaly (accel spike + gyro spin) --- def test_sensor_anomaly_accel_gyro_triggers(self): sensors = SensorSnapshot( accelerometer=(20.0, 20.0, 20.0), # mag = 34.6 > 29.4 gyroscope=(4.0, 4.0, 4.0), # mag = 6.93 > 5.0 gps=(37.7749, -122.4194), light_level=100.0, battery_pct=80.0, heading=90.0 ) d = self.engine.decide(self._no_signal(), [], [], None, sensor_data=sensors) self.assertTrue(d.should_trigger) self.assertEqual(d.urgency, "critical") self.assertIn("Sensor anomaly", d.reason) def test_sensor_accel_spike_only_no_gyro_no_trigger(self): sensors = SensorSnapshot( accelerometer=(20.0, 20.0, 20.0), # spike gyroscope=(0.1, 0.1, 0.1), # very low spin — no trigger gps=None, light_level=100.0, battery_pct=80.0, heading=0.0 ) d = self.engine.decide(self._no_signal(), [], [], None, sensor_data=sensors) self.assertFalse(d.should_trigger) # --- Priority 6: Environmental change (darkness + movement) --- def test_environmental_darkness_movement_triggers(self): s1 = SensorSnapshot(gps=(37.7749, -122.4194), light_level=2.0) self.engine.decide(self._no_signal(), [], [], None, sensor_data=s1) s2 = SensorSnapshot(gps=(37.7750, -122.4195), light_level=2.0) d = self.engine.decide(self._no_signal(), [], [], None, sensor_data=s2) self.assertTrue(d.should_trigger) self.assertIn("Sudden darkness", d.reason) def test_environmental_darkness_no_movement_no_trigger(self): s1 = SensorSnapshot(gps=(37.7749, -122.4194), light_level=2.0) self.engine.decide(self._no_signal(), [], [], None, sensor_data=s1) s2 = SensorSnapshot(gps=(37.7749, -122.4194), light_level=2.0) # same position d = self.engine.decide(self._no_signal(), [], [], None, sensor_data=s2) self.assertFalse(d.should_trigger) # --- Cooldown enforcement --- def test_warning_suppressed_within_cooldown(self): motion = FrameChangeResult(change_pct=9.0, is_significant=True, threshold_used=5.0, motion_regions=1) dets = [Detection(class_name="person", confidence=0.85, bbox=(0,0,50,50), area_pct=6.0)] d1 = self.engine.decide(motion, dets, [], None) self.assertTrue(d1.should_trigger) d2 = self.engine.decide(motion, dets, [], None) self.assertFalse(d2.should_trigger) self.assertIn("suppressed due to cooldown", d2.reason) def test_critical_bypasses_cooldown(self): audio = [AudioClass(class_name="Scream", confidence=0.85, alert_level="critical")] d1 = self.engine.decide(self._no_signal(), [], audio, None) d2 = self.engine.decide(self._no_signal(), [], audio, None) self.assertTrue(d1.should_trigger) self.assertTrue(d2.should_trigger) # Must NOT be suppressed # --- Combined confidence --- def test_multi_signal_confidence_capped_at_1(self): audio = [ AudioClass(class_name="Scream", confidence=0.9, alert_level="critical"), AudioClass(class_name="Crash", confidence=0.8, alert_level="critical"), ] d = self.engine.decide(self._no_signal(), [], audio, None) self.assertLessEqual(d.confidence, 1.0) self.assertGreater(d.confidence, 0.9) # --- Signal dict completeness --- def test_signals_dict_keys_present(self): d = self.engine.decide(self._no_signal(), [], [], None) for key in ("frame_change_pct", "motion_significant", "motion_regions", "detections", "audio_events", "pose", "sensor", "user_query"): self.assertIn(key, d.signals) # --- Trigger history growth check --- def test_trigger_history_grows(self): for _ in range(50): self.engine.decide(self._no_signal(), [], [], None) self.assertGreater(len(self.engine.trigger_history), 0) # Document the current unbound growth (so we can verify a future cap fix) self.assertEqual(len(self.engine.trigger_history), 50) # ───────────────────────────────────────────── # 3. FRAME CHANGE DETECTOR # ───────────────────────────────────────────── class TestFrameChangeDetector(unittest.TestCase): def setUp(self): self.detector = FrameChangeDetector(threshold_pct=5.0, adaptive=False) def test_first_frame_zero_change(self): frame = np.zeros((480, 640, 3), dtype=np.uint8) r = self.detector.detect(frame) self.assertEqual(r.change_pct, 0.0) self.assertFalse(r.is_significant) self.assertEqual(r.motion_regions, 0) def test_identical_frames_zero_change(self): frame = np.ones((480, 640, 3), dtype=np.uint8) * 100 self.detector.detect(frame) r = self.detector.detect(frame) self.assertEqual(r.change_pct, 0.0) self.assertFalse(r.is_significant) def test_half_frame_change_significant(self): f1 = np.zeros((480, 640, 3), dtype=np.uint8) f2 = np.zeros((480, 640, 3), dtype=np.uint8) f2[240:, :, :] = 255 self.detector.detect(f1) r = self.detector.detect(f2) self.assertGreater(r.change_pct, 40.0) self.assertTrue(r.is_significant) def test_small_change_below_threshold(self): f1 = np.zeros((480, 640, 3), dtype=np.uint8) f2 = np.zeros((480, 640, 3), dtype=np.uint8) # Change only 1% of pixels f2[0:5, 0:8, :] = 200 self.detector.detect(f1) r = self.detector.detect(f2) self.assertFalse(r.is_significant) def test_reset_clears_history(self): frame = np.zeros((480, 640, 3), dtype=np.uint8) self.detector.detect(frame) self.detector.reset() self.assertIsNone(self.detector.previous_frame) self.assertEqual(len(self.detector.change_history), 0) def test_none_frame_returns_zeros(self): r = self.detector.detect(None) self.assertEqual(r.change_pct, 0.0) self.assertFalse(r.is_significant) def test_motion_regions_count(self): f1 = np.zeros((480, 640, 3), dtype=np.uint8) f2 = np.zeros((480, 640, 3), dtype=np.uint8) # Create 3 distinct motion blobs cv2.rectangle(f2, (50, 50), (100, 100), (255, 255, 255), -1) cv2.rectangle(f2, (250, 250), (300, 300), (255, 255, 255), -1) cv2.rectangle(f2, (450, 350), (500, 400), (255, 255, 255), -1) self.detector.detect(f1) r = self.detector.detect(f2) self.assertGreaterEqual(r.motion_regions, 1) def test_adaptive_threshold_adjusts(self): detector = FrameChangeDetector(threshold_pct=5.0, adaptive=True) frame = np.zeros((480, 640, 3), dtype=np.uint8) # Seed with 15 identical frames to stabilize adaptive history for _ in range(15): detector.detect(frame) # Very small change should be below adaptive threshold frame2 = frame.copy() frame2[0:2, 0:2, :] = 50 r = detector.detect(frame2) self.assertFalse(r.is_significant) # ───────────────────────────────────────────── # 4. POSE ANALYZER (stub mode) # ───────────────────────────────────────────── class TestPoseAnalyzerStub(unittest.TestCase): def test_stub_returns_none_on_bad_frame(self): analyzer = PoseAnalyzer() result = analyzer.analyze(None) self.assertIsNone(result) def test_stub_returns_none_or_pose_on_blank_frame(self): analyzer = PoseAnalyzer() frame = np.zeros((480, 640, 3), dtype=np.uint8) result = analyzer.analyze(frame) # Either None (no landmarks) or PoseData — both are valid self.assertTrue(result is None or hasattr(result, "is_fall")) # ───────────────────────────────────────────── # 5. AUDIO MONITOR (stub mode) # ───────────────────────────────────────────── class TestAudioMonitorStub(unittest.TestCase): def test_stub_returns_empty_without_tensorflow(self): monitor = AudioMonitor() # Without real audio, classify with dummy samples samples = np.zeros(16000, dtype=np.float32) result = monitor.classify(samples, sample_rate=16000) # Either empty (no TF) or list of AudioClass self.assertIsInstance(result, list) # ───────────────────────────────────────────── # 6. SAFETY KNOWLEDGE BASE (Cohere RAG) # ───────────────────────────────────────────── class TestSafetyKnowledgeBase(unittest.TestCase): def setUp(self): self.kb = SafetyKnowledgeBase("dummy-key") # --- Document count --- def test_has_8_documents(self): self.assertEqual(len(self.kb.documents), 8) # --- Emergency detection --- def test_emergency_fire(self): self.assertTrue(self.kb._check_is_emergency("fire in the kitchen")) def test_emergency_fall(self): self.assertTrue(self.kb._check_is_emergency("elderly person fall")) def test_emergency_knife(self): self.assertTrue(self.kb._check_is_emergency("knife detected")) def test_emergency_gas(self): self.assertTrue(self.kb._check_is_emergency("gas leak smell")) def test_emergency_earthquake(self): self.assertTrue(self.kb._check_is_emergency("earthquake shaking")) def test_non_emergency_sunny_day(self): self.assertFalse(self.kb._check_is_emergency("nice sunny day")) def test_non_emergency_clear_path(self): self.assertFalse(self.kb._check_is_emergency("clear path ahead")) # --- Local fallback advice (all 8 hazard types) --- def test_fallback_fire(self): advice = self.kb.get_default_advice("fire") self.assertIn("Fire detected", advice) def test_fallback_fall(self): advice = self.kb.get_default_advice("fall") self.assertIn("Fall detected", advice) def test_fallback_vehicle(self): advice = self.kb.get_default_advice("car approaching") self.assertIn("vehicle detected", advice) def test_fallback_knife(self): advice = self.kb.get_default_advice("knife visible") self.assertIn("threat detected", advice) def test_fallback_gas(self): advice = self.kb.get_default_advice("gas smell") self.assertIn("gas leak", advice) def test_fallback_earthquake(self): advice = self.kb.get_default_advice("quake") self.assertIn("Earthquake", advice) def test_fallback_choking(self): advice = self.kb.get_default_advice("choking") self.assertIn("Choking", advice) def test_fallback_unknown(self): advice = self.kb.get_default_advice("flying saucer detected") self.assertIn("Anomaly detected", advice) # --- Source matching (all 8 documents) --- def test_source_match_fire(self): self.assertEqual( self.kb._match_source_by_keyword("evacuate via stairs away from smoke"), "Fire Safety and Evacuation Procedures" ) def test_source_match_fall(self): self.assertEqual( self.kb._match_source_by_keyword("do not move if fracture or unresponsive"), "Fall Response and First Aid" ) def test_source_match_traffic(self): self.assertEqual( self.kb._match_source_by_keyword("wait at the curb, listen for traffic"), "Road Crossing and Traffic Safety" ) def test_source_match_intruder(self): self.assertEqual( self.kb._match_source_by_keyword("move to locked room and call police"), "Intruder Alert and Personal Security" ) def test_source_match_stroke(self): self.assertEqual( self.kb._match_source_by_keyword("recognize signs of stroke quickly"), "Medical Emergency and Stroke Recognition" ) def test_source_match_gas(self): self.assertEqual( self.kb._match_source_by_keyword("rotten egg smell gas leak"), "Gas Leak Protocols" ) def test_source_match_earthquake(self): self.assertEqual( self.kb._match_source_by_keyword("earthquake shaking, under sturdy table"), "Earthquake Safety (Drop, Cover, Hold On)" ) def test_source_match_choking(self): self.assertEqual( self.kb._match_source_by_keyword("choking thrust above navel"), "Choking First Aid (Heimlich Maneuver)" ) def test_source_match_unknown(self): self.assertEqual( self.kb._match_source_by_keyword("banana peel on floor"), "General Safety Knowledge Base" ) # --- Cohere API exception → local fallback --- def test_cohere_exception_falls_back(self): self.kb.client = MagicMock() self.kb.client.chat.side_effect = Exception("Cohere API unavailable") res = self.kb.query("fire", "There is smoke in the hallway") self.assertIn("Fire detected", res["advice"]) self.assertEqual(res["source"], "Fire Safety and Evacuation Procedures") self.assertTrue(res["is_emergency"]) def test_cohere_exception_fall_fallback(self): self.kb.client = MagicMock() self.kb.client.chat.side_effect = Exception("timeout") res = self.kb.query("fall", "Person lying on floor") self.assertIn("Fall detected", res["advice"]) def test_cohere_exception_unknown_fallback(self): self.kb.client = MagicMock() self.kb.client.chat.side_effect = Exception("timeout") res = self.kb.query("random event", "Something unusual") self.assertIn("Anomaly detected", res["advice"]) self.assertFalse(res["is_emergency"]) # ───────────────────────────────────────────── # 7. FALLBACK ROUTER (async) # ───────────────────────────────────────────── class TestFallbackRouter(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): self.router = FallbackRouter( openrouter_key="or-test-key", openbmb_key="ob-test-key", openai_key="oa-test-key" ) self.router.client = AsyncMock() async def asyncTearDown(self): await self.router.close() # --- Model string verification: NEW 30B-A3B model --- async def test_openrouter_uses_nemotron_30b_a3b_model(self): """CRITICAL: Verifies the OpenRouter payload uses the correct new model slug.""" mock_resp = MagicMock() mock_resp.raise_for_status = MagicMock() mock_resp.json.return_value = { "choices": [{"message": {"content": "Safety response"}}], "usage": {"total_tokens": 120} } self.router.client.post.return_value = mock_resp res = await self.router.fallback_reason("Is there fire?", "You are Sentinel.") # Check the model ID in the returned result self.assertEqual(res["model"], "openrouter:nemotron-3-nano-30b-a3b:free") # Check the actual payload sent to OpenRouter call_kwargs = self.router.client.post.call_args payload = call_kwargs.kwargs.get("json") or call_kwargs.args[1] if len(call_kwargs.args) > 1 else call_kwargs.kwargs["json"] self.assertEqual(payload["model"], "nvidia/nemotron-3-nano-30b-a3b:free") async def test_openrouter_headers_include_referer(self): """Verifies that HTTP-Referer and X-Title headers are present for OpenRouter compliance.""" mock_resp = MagicMock() mock_resp.raise_for_status = MagicMock() mock_resp.json.return_value = { "choices": [{"message": {"content": "ok"}}], "usage": {"total_tokens": 10} } self.router.client.post.return_value = mock_resp await self.router.fallback_reason("test", "") call_kwargs = self.router.client.post.call_args headers = call_kwargs.kwargs.get("headers", {}) self.assertIn("HTTP-Referer", headers) self.assertIn("X-Title", headers) async def test_openrouter_success_returns_text(self): mock_resp = MagicMock() mock_resp.raise_for_status = MagicMock() mock_resp.json.return_value = { "choices": [{"message": {"content": "Stairs ahead, stop immediately."}}], "usage": {"total_tokens": 90} } self.router.client.post.return_value = mock_resp res = await self.router.fallback_reason("Describe scene", "You are Sentinel.") self.assertEqual(res["text"], "Stairs ahead, stop immediately.") self.assertEqual(res["tokens"], 90) async def test_openrouter_fail_openai_succeeds(self): fail_resp = MagicMock() fail_resp.raise_for_status.side_effect = Exception("OpenRouter 500") ok_resp = MagicMock() ok_resp.raise_for_status = MagicMock() ok_resp.json.return_value = { "choices": [{"message": {"content": "OpenAI fallback response"}}], "usage": {"total_tokens": 75} } self.router.client.post.side_effect = [fail_resp, ok_resp] res = await self.router.fallback_reason("What is ahead?", "") self.assertEqual(res["text"], "OpenAI fallback response") self.assertEqual(res["model"], "openai:gpt-4o-mini") async def test_all_reasoning_providers_fail_returns_error(self): fail_resp = MagicMock() fail_resp.raise_for_status.side_effect = Exception("all fail") self.router.client.post.side_effect = [fail_resp, fail_resp] res = await self.router.fallback_reason("test", "") self.assertIn("error", res) self.assertIn("having trouble", res["text"]) async def test_fallback_see_openbmb_success(self): mock_resp = MagicMock() mock_resp.raise_for_status = MagicMock() mock_resp.json.return_value = { "choices": [{"message": {"content": "A person approaching from the left."}}], "usage": {"total_tokens": 200} } self.router.client.post.return_value = mock_resp res = await self.router.fallback_see("base64imagedata", "Describe the scene.") self.assertEqual(res["text"], "A person approaching from the left.") self.assertEqual(res["model"], "openbmb:minicpm-v-4.6") async def test_fallback_see_openbmb_fail_openai_succeeds(self): fail = MagicMock() fail.raise_for_status.side_effect = Exception("OpenBMB down") ok = MagicMock() ok.raise_for_status = MagicMock() ok.json.return_value = { "choices": [{"message": {"content": "Fire visible in scene."}}], "usage": {"total_tokens": 160} } self.router.client.post.side_effect = [fail, ok] res = await self.router.fallback_see("img", "What is in the scene?") self.assertEqual(res["text"], "Fire visible in scene.") self.assertEqual(res["model"], "openai:gpt-4o-mini-vision") async def test_fallback_see_all_fail_returns_error(self): fail = MagicMock() fail.raise_for_status.side_effect = Exception("all fail") self.router.client.post.side_effect = [fail, fail] res = await self.router.fallback_see("img", "Describe.") self.assertIn("error", res) async def test_fallback_tts_success(self): mock_resp = MagicMock() mock_resp.raise_for_status = MagicMock() mock_resp.content = b"fake_wav_bytes" self.router.client.post.return_value = mock_resp res = await self.router.fallback_tts("Warning: stairs ahead") self.assertIn("audio_base64", res) decoded = base64.b64decode(res["audio_base64"]) self.assertEqual(decoded, b"fake_wav_bytes") self.assertEqual(res["model"], "openai-tts-1") async def test_fallback_tts_no_key_returns_error(self): router_no_key = FallbackRouter(openrouter_key="", openbmb_key="", openai_key="") res = await router_no_key.fallback_tts("test") self.assertIn("error", res) await router_no_key.close() async def test_no_keys_returns_error_chain(self): router_empty = FallbackRouter(openrouter_key="", openbmb_key="", openai_key="") res = await router_empty.fallback_reason("test", "") self.assertIn("error", res) await router_empty.close() async def test_rate_limit_429_triggers_backoff(self): """Verify 429 responses result in asyncio.sleep (backoff) before next attempt.""" import httpx fail_resp = MagicMock(spec=httpx.Response) fail_resp.status_code = 429 fail_resp.raise_for_status.side_effect = httpx.HTTPStatusError( "Rate limited", request=MagicMock(), response=fail_resp ) ok_resp = MagicMock() ok_resp.raise_for_status = MagicMock() ok_resp.json.return_value = { "choices": [{"message": {"content": "ok"}}], "usage": {"total_tokens": 5} } self.router.client.post.side_effect = [fail_resp, ok_resp] # Should not raise; should complete with OpenAI fallback res = await self.router.fallback_reason("test", "") # Either succeeded via OpenAI or returned an error — just must not crash self.assertIn("text", res) # ───────────────────────────────────────────── # 8. MODAL RAG LOCAL METHODS (unit test without Modal) # ───────────────────────────────────────────── class TestModalRAGLocalMethods(unittest.TestCase): """ Tests the _rag_local_fallback and _rag_match_source helper methods that are defined inside SentinelEngine in modal_backend.py. We replicate them here to test the logic without invoking Modal. """ def _local_fallback(self, situation: str) -> str: s = situation.lower() if any(k in s for k in ("fire", "smoke")): return "Fire detected. Evacuate immediately via stairs, stay low to avoid smoke, and do not use elevators." if any(k in s for k in ("fall", "collapse")): return "Fall detected. Do not move if you feel neck or back pain. Call for medical assistance." if any(k in s for k in ("car", "vehicle", "road", "traffic")): return "Approaching vehicle detected. Step away from the curb and wait for traffic noise to pass completely." if any(k in s for k in ("knife", "intruder", "weapon")): return "Potential threat detected. Move to a secure, locked room, silence your phone, and call police." if any(k in s for k in ("gas", "leak")): return "Potential gas leak. Do not turn on light switches, leave the building immediately, and call emergency services." if any(k in s for k in ("quake", "shaking", "earthquake")): return "Earthquake active. Drop, cover, and hold on under a sturdy table. Stay away from windows." if any(k in s for k in ("chok", "cough")): return "Choking hazard. If you cannot breathe, perform abdominal thrusts or call emergency services immediately." return "Anomaly detected. Move slowly to a stable position and evaluate your surroundings carefully." def _match_source(self, text: str) -> str: t = text.lower() if any(k in t for k in ("fire", "evacuate", "smoke")): return "Fire Safety and Evacuation Procedures" if any(k in t for k in ("fall", "fracture", "unresponsive")): return "Fall Response and First Aid" if any(k in t for k in ("curb", "traffic", "crosswalk")): return "Road Crossing and Traffic Safety" if any(k in t for k in ("intruder", "locked room", "police")): return "Intruder Alert and Personal Security" if any(k in t for k in ("stroke", "fast")): return "Medical Emergency and Stroke Recognition" if any(k in t for k in ("gas leak", "rotten egg")): return "Gas Leak Protocols" if any(k in t for k in ("earthquake", "shaking", "sturdy table")): return "Earthquake Safety (Drop, Cover, Hold On)" if any(k in t for k in ("choking", "navel", "thrust")): return "Choking First Aid (Heimlich Maneuver)" return "General Safety Knowledge Base" def test_modal_rag_fire_fallback(self): self.assertIn("Fire detected", self._local_fallback("fire in room")) def test_modal_rag_smoke_fallback(self): self.assertIn("Fire detected", self._local_fallback("smoke everywhere")) def test_modal_rag_fall_fallback(self): self.assertIn("Fall detected", self._local_fallback("person fall")) def test_modal_rag_collapse_fallback(self): self.assertIn("Fall detected", self._local_fallback("collapse")) def test_modal_rag_vehicle_fallback(self): self.assertIn("vehicle detected", self._local_fallback("car approaching")) def test_modal_rag_traffic_fallback(self): self.assertIn("vehicle detected", self._local_fallback("heavy traffic road")) def test_modal_rag_knife_fallback(self): self.assertIn("threat detected", self._local_fallback("knife on table")) def test_modal_rag_intruder_fallback(self): self.assertIn("threat detected", self._local_fallback("intruder in house")) def test_modal_rag_weapon_fallback(self): self.assertIn("threat detected", self._local_fallback("weapon visible")) def test_modal_rag_gas_fallback(self): self.assertIn("gas leak", self._local_fallback("gas smell")) def test_modal_rag_leak_fallback(self): self.assertIn("gas leak", self._local_fallback("pipe leak")) def test_modal_rag_earthquake_fallback(self): self.assertIn("Earthquake", self._local_fallback("earthquake shaking")) def test_modal_rag_quake_fallback(self): self.assertIn("Earthquake", self._local_fallback("quake event")) def test_modal_rag_choking_fallback(self): self.assertIn("Choking", self._local_fallback("person choking")) def test_modal_rag_cough_fallback(self): self.assertIn("Choking", self._local_fallback("persistent cough")) def test_modal_rag_unknown_fallback(self): self.assertIn("Anomaly detected", self._local_fallback("unknown situation")) def test_modal_source_fire(self): self.assertEqual(self._match_source("evacuate via stairs avoid smoke"), "Fire Safety and Evacuation Procedures") def test_modal_source_fall(self): self.assertEqual(self._match_source("do not move if fracture present"), "Fall Response and First Aid") def test_modal_source_traffic(self): self.assertEqual(self._match_source("wait at the curb for traffic"), "Road Crossing and Traffic Safety") def test_modal_source_intruder(self): self.assertEqual(self._match_source("move to locked room call police"), "Intruder Alert and Personal Security") def test_modal_source_stroke(self): self.assertEqual(self._match_source("recognize stroke signs fast"), "Medical Emergency and Stroke Recognition") def test_modal_source_gas_leak(self): self.assertEqual(self._match_source("gas leak rotten egg smell"), "Gas Leak Protocols") def test_modal_source_earthquake(self): self.assertEqual(self._match_source("earthquake sturdy table shaking"), "Earthquake Safety (Drop, Cover, Hold On)") def test_modal_source_choking(self): self.assertEqual(self._match_source("choking thrust above navel"), "Choking First Aid (Heimlich Maneuver)") def test_modal_source_unknown(self): self.assertEqual(self._match_source("random text"), "General Safety Knowledge Base") # ───────────────────────────────────────────── # 9. STRESS / INTEGRATION TEST # ───────────────────────────────────────────── class TestStressIntegration(unittest.TestCase): """ Rapid-fire decision engine calls simulating a real monitoring session. Verifies: no crashes, correct cooldown enforcement, signal variety handling. """ def setUp(self): self.engine = GatekeeperDecision(cooldown_seconds=0.05) # fast cooldown for stress test self.tracker = CostTracker() def test_1000_frames_no_crash(self): for i in range(1000): self.tracker.log_frame() change_pct = 5.0 if (i % 10 == 0) else 0.5 is_sig = change_pct > 5.0 frame_change = FrameChangeResult(change_pct=change_pct, is_significant=is_sig, threshold_used=5.0, motion_regions=int(is_sig)) detections = [Detection("person", 0.85, (0,0,50,50), 6.0)] if (i % 10 == 0) else [] audio = [AudioClass("Scream", 0.9, "critical")] if (i % 50 == 0) else [] pose = None d = self.engine.decide(frame_change, detections, audio, pose) self.assertIsInstance(d, TriggerDecision) self.assertLessEqual(d.confidence, 1.0) self.assertGreaterEqual(d.confidence, 0.0) # Verify cost tracker sanity s = self.tracker.get_stats() self.assertEqual(s["frames_processed"], 1000) def test_all_scenarios_produce_valid_decisions(self): fc_no_motion = FrameChangeResult(1.0, False, 5.0, 0) fc_motion = FrameChangeResult(9.0, True, 5.0, 2) scenarios = [ dict(frame_change=fc_no_motion, detections=[], audio_classes=[], pose_data=None), dict(frame_change=fc_motion, detections=[Detection("person", 0.9, (0, 0, 50, 50), 8.0)], audio_classes=[], pose_data=None), dict(frame_change=fc_no_motion, detections=[], audio_classes=[AudioClass("Scream", 0.95, "critical")], pose_data=None), dict(frame_change=fc_no_motion, detections=[], audio_classes=[], pose_data=PoseData(0.85, 0.55, 0.7, True, False, 0.9)), dict(frame_change=fc_no_motion, detections=[], audio_classes=[], pose_data=None, sensor_data=SensorSnapshot( accelerometer=(20.0, 20.0, 20.0), gyroscope=(4.0, 4.0, 4.0), gps=(37.0, -122.0), light_level=2.0, battery_pct=80.0, heading=90.0 )), ] engine = GatekeeperDecision(cooldown_seconds=0.0) for kwargs in scenarios: d = engine.decide(**kwargs) self.assertIsInstance(d, TriggerDecision) self.assertIn(d.urgency, ("none", "info", "warning", "critical")) if __name__ == "__main__": unittest.main(verbosity=2)