"""Smoke tests — verify imports, paths, basic instantiation, and isolation. Run with: PYTHONPATH=/path/to/yslootahtech python3 -m unittest \ Project.Sanad.tests.test_smoke -v These tests do NOT require any third-party dependency. They prove that the project loads cleanly and that subsystem failures stay isolated. """ from __future__ import annotations import os import sys import unittest from pathlib import Path class TestConfig(unittest.TestCase): """Path resolution and config loading.""" def test_base_dir_auto_detect(self): from Project.Sanad.config import BASE_DIR self.assertTrue(BASE_DIR.exists(), f"BASE_DIR missing: {BASE_DIR}") self.assertEqual(BASE_DIR.name, "Sanad") def test_data_dirs_exist(self): from Project.Sanad.config import ( DATA_DIR, MOTIONS_DIR, AUDIO_RECORDINGS_DIR, MOTION_RECORDINGS_DIR, LOGS_DIR, SCRIPTS_DIR, ) for d in (DATA_DIR, MOTIONS_DIR, AUDIO_RECORDINGS_DIR, MOTION_RECORDINGS_DIR, LOGS_DIR, SCRIPTS_DIR): self.assertTrue(d.exists(), f"Missing: {d}") def test_skills_file_resolves(self): from Project.Sanad.config import SKILLS_FILE, MOTIONS_DIR self.assertEqual(SKILLS_FILE.parent, MOTIONS_DIR) def test_dds_interface_default(self): from Project.Sanad.config import DDS_NETWORK_INTERFACE self.assertIsInstance(DDS_NETWORK_INTERFACE, str) def test_env_override_dds_interface(self): os.environ["SANAD_DDS_INTERFACE"] = "test_iface" try: import importlib import Project.Sanad.config as cfg importlib.reload(cfg) self.assertEqual(cfg.DDS_NETWORK_INTERFACE, "test_iface") finally: del os.environ["SANAD_DDS_INTERFACE"] import importlib import Project.Sanad.config as cfg importlib.reload(cfg) def test_load_config_handles_missing(self): from Project.Sanad.config import load_config result = load_config() self.assertIsInstance(result, dict) def test_local_tts_paths(self): from Project.Sanad.config import ( LOCAL_TTS_MODEL_PATH, LOCAL_TTS_HIFIGAN_PATH, LOCAL_TTS_XVECTOR_PATH, ) self.assertIn("speecht5_tts_clartts_ar", LOCAL_TTS_MODEL_PATH) self.assertIn("speecht5_hifigan", LOCAL_TTS_HIFIGAN_PATH) class TestSkillRegistry(unittest.TestCase): """SkillRegistry CRUD + atomic writes + validation.""" def setUp(self): from Project.Sanad.core.skill_registry import SkillRegistry self.sr = SkillRegistry() def test_load(self): skills = self.sr.list_skills() self.assertIsInstance(skills, list) def test_invalid_sync_mode_rejected(self): from Project.Sanad.core.skill_registry import Skill bad = Skill(id="test_invalid", sync_mode="garbage") with self.assertRaises(ValueError): self.sr.add(bad) def test_update_missing_returns_none(self): result = self.sr.update("nonexistent_id_12345", {"description": "x"}) self.assertIsNone(result) def test_delete_missing_returns_none(self): result = self.sr.delete("nonexistent_id_12345") self.assertIsNone(result) class TestEventBus(unittest.TestCase): """EventBus emit_sync handles missing event loop and async handlers.""" def test_emit_sync_no_handlers(self): from Project.Sanad.core.event_bus import EventBus bus = EventBus() # Should not raise bus.emit_sync("nonexistent.event", value=1) def test_emit_sync_sync_handler(self): from Project.Sanad.core.event_bus import EventBus bus = EventBus() captured = [] bus.on("test.event", lambda **kw: captured.append(kw)) bus.emit_sync("test.event", value=42) self.assertEqual(captured, [{"value": 42}]) def test_emit_sync_async_handler_no_loop(self): from Project.Sanad.core.event_bus import EventBus bus = EventBus() async def handler(**kw): pass bus.on("test.async", handler) # No running loop — should warn but not crash bus.emit_sync("test.async", value=1) def test_handler_exception_isolated(self): from Project.Sanad.core.event_bus import EventBus bus = EventBus() results = [] def good(**kw): results.append("ok") def bad(**kw): raise RuntimeError("intentional") bus.on("test.iso", bad) bus.on("test.iso", good) bus.emit_sync("test.iso", x=1) # Good handler still ran self.assertEqual(results, ["ok"]) class TestBrainCallbackWhitelist(unittest.TestCase): """Brain._resolve_callback rejects non-whitelisted modules (RCE block).""" def setUp(self): from Project.Sanad.core.brain import Brain self.brain = Brain() def test_rce_blocked_os(self): cb = self.brain._resolve_callback("os:system") self.assertIsNone(cb, "os:system must be rejected") def test_rce_blocked_subprocess(self): cb = self.brain._resolve_callback("subprocess:run") self.assertIsNone(cb, "subprocess:run must be rejected") def test_rce_blocked_eval(self): cb = self.brain._resolve_callback("builtins:eval") self.assertIsNone(cb) def test_empty_callback_returns_none(self): self.assertIsNone(self.brain._resolve_callback("")) self.assertIsNone(self.brain._resolve_callback(None)) def test_invalid_format_returns_none(self): self.assertIsNone(self.brain._resolve_callback("no_colon")) def test_whitelisted_prefix_attempted(self): # Module doesn't exist but the prefix is allowed — must NOT be rejected # by the whitelist (it'll fail at import_module instead) cb = self.brain._resolve_callback("Project.Sanad.motion.nonexistent_module:fn") self.assertIsNone(cb) # fails at import, not at whitelist class TestWakePhraseMatching(unittest.TestCase): """Token-based phrase matching — no false positives on substrings.""" def test_no_false_positive_substring(self): from Project.Sanad.voice.text_utils import match_phrase # 'this' contains 'hi' as substring — must NOT match result = match_phrase("this is a test", {"hi_action": {"hi"}}) self.assertIsNone(result) def test_exact_word_match(self): from Project.Sanad.voice.text_utils import match_phrase result = match_phrase("hi there friend", {"greet": {"hi"}}) self.assertEqual(result, "greet") def test_multi_word_phrase_all_required(self): from Project.Sanad.voice.text_utils import match_phrase # All words must appear result = match_phrase("please shake hands", {"act": {"shake hands"}}) self.assertEqual(result, "act") result = match_phrase("just shake", {"act": {"shake hands"}}) self.assertIsNone(result) def test_longest_phrase_wins(self): from Project.Sanad.voice.text_utils import match_phrase sets = { "short": {"hi"}, "long": {"hi five"}, } # When both match, prefer the more-specific (longer) phrase result = match_phrase("hi five there", sets) self.assertEqual(result, "long") def test_normalize_arabic(self): from Project.Sanad.voice.text_utils import normalize_arabic out = normalize_arabic("مَرْحَبًا") self.assertNotIn("\u064b", out) # tashkeel removed class TestArmController(unittest.TestCase): """Arm controller in simulation mode.""" def test_constructs(self): from Project.Sanad.motion.arm_controller import ArmController arm = ArmController() self.assertFalse(arm.is_busy) def test_action_registry(self): from Project.Sanad.motion.arm_controller import ACTIONS, ACTION_BY_NAME self.assertGreater(len(ACTIONS), 0) # SDK actions should be in the registry sdk_names = ["shake_hand", "high_five", "clap"] for name in sdk_names: self.assertIn(name, ACTION_BY_NAME) def test_motion_files_cache(self): from Project.Sanad.motion.arm_controller import ArmController arm = ArmController() first = arm.list_motion_files() second = arm.list_motion_files() # Same call twice — should hit cache self.assertEqual(len(first), len(second)) def test_unknown_action_raises(self): from Project.Sanad.motion.arm_controller import ArmController arm = ArmController() with self.assertRaises(KeyError): arm.trigger_by_name("definitely_not_a_real_action_xyz") def test_arm_adapter_works(self): """The MacroPlayer arm adapter should work over a real ArmController.""" from Project.Sanad.motion.arm_controller import ArmController from Project.Sanad.motion.macro_player import _ArmAdapter arm = ArmController() adapter = _ArmAdapter(arm) # All adapter methods should be callable without raising q = adapter.get_current_q() self.assertEqual(len(q), 29) self.assertIsInstance(adapter.state_age(), float) # wait_for_state with no real DDS subscriber should just return True # (since the adapter falls back when wait_for_state is missing) OR # block until timeout (if the method exists and waits). Either way # it should NOT raise. result = adapter.wait_for_state(timeout=0.05) self.assertIsInstance(result, bool) class TestSafeIO(unittest.TestCase): """Path traversal protection + atomic writes.""" def test_safe_filename_strips_traversal(self): from Project.Sanad.dashboard.routes._safe_io import safe_filename with self.assertRaises(Exception): safe_filename("..") with self.assertRaises(Exception): safe_filename("") with self.assertRaises(Exception): safe_filename(None) # Embedded path components should be stripped to basename self.assertEqual(safe_filename("../../etc/passwd"), "passwd") self.assertEqual(safe_filename("foo.wav"), "foo.wav") def test_safe_path_under_blocks_escape(self): from Project.Sanad.dashboard.routes._safe_io import safe_path_under from Project.Sanad.config import MOTIONS_DIR # Normal name → ok p = safe_path_under(MOTIONS_DIR, "foo.jsonl") self.assertTrue(str(p).startswith(str(MOTIONS_DIR.resolve()))) # Traversal attempt → rejected (basename strip means it's just "passwd" # under MOTIONS_DIR, which is safe) p2 = safe_path_under(MOTIONS_DIR, "../../etc/passwd") self.assertTrue(str(p2).startswith(str(MOTIONS_DIR.resolve()))) def test_atomic_write_text(self): import tempfile from Project.Sanad.dashboard.routes._safe_io import atomic_write_text with tempfile.TemporaryDirectory() as td: target = Path(td) / "test.txt" atomic_write_text(target, "hello\nworld") self.assertEqual(target.read_text(), "hello\nworld") def test_atomic_write_json(self): import tempfile import json from Project.Sanad.dashboard.routes._safe_io import atomic_write_json with tempfile.TemporaryDirectory() as td: target = Path(td) / "test.json" atomic_write_json(target, {"a": 1, "b": [1, 2, 3]}) self.assertEqual(json.loads(target.read_text()), {"a": 1, "b": [1, 2, 3]}) class TestGeminiClientStructure(unittest.TestCase): """GeminiVoiceClient structural tests — no actual websocket.""" def setUp(self): try: from Project.Sanad.gemini.client import GeminiVoiceClient self.client = GeminiVoiceClient() except ImportError: self.skipTest("websockets not installed") def test_initial_state(self): self.assertFalse(self.client.connected) self.assertIsNone(self.client.session_owner) def test_send_audio_chunk_when_disconnected(self): """Should return False, not raise — the no-op-forever bug is fixed.""" import asyncio result = asyncio.run(self.client.send_audio_chunk("dGVzdA==")) self.assertFalse(result) def test_acquire_session_returns_guard(self): """acquire_session should be sync and return a context manager.""" guard = self.client.acquire_session("test") # Has __aenter__/__aexit__ self.assertTrue(hasattr(guard, "__aenter__")) self.assertTrue(hasattr(guard, "__aexit__")) def test_session_lock_exclusive(self): """Two consecutive acquires should serialize, not deadlock.""" import asyncio events = [] async def consumer(name): async with self.client.acquire_session(name): events.append(f"{name}:enter") await asyncio.sleep(0.05) events.append(f"{name}:exit") async def runner(): await asyncio.gather(consumer("A"), consumer("B")) asyncio.run(runner()) # Either A fully runs then B, or B fully runs then A — never interleaved self.assertIn("A:enter", events) self.assertIn("B:enter", events) # The exit of one comes before the enter of the other a_exit = events.index("A:exit") b_enter = events.index("B:enter") a_enter = events.index("A:enter") b_exit = events.index("B:exit") ok1 = (a_enter < a_exit < b_enter < b_exit) ok2 = (b_enter < b_exit < a_enter < a_exit) self.assertTrue(ok1 or ok2, f"Lock not exclusive: {events}") class TestAudioDevices(unittest.TestCase): """audio_devices module — pure helpers tested without pactl.""" def test_profiles_defined(self): from Project.Sanad.voice import audio_devices as ad self.assertGreater(len(ad.PROFILES), 0) ids = {p.id for p in ad.PROFILES} self.assertIn("anker_powerconf", ids) self.assertIn("hollyland_builtin", ids) self.assertIn("builtin", ids) def test_pactl_available_no_crash(self): from Project.Sanad.voice import audio_devices as ad # Should not raise, just return False if pactl missing result = ad.pactl_available() self.assertIsInstance(result, bool) def test_find_first_match(self): from Project.Sanad.voice import audio_devices as ad items = [ {"name": "alsa_output.usb-Anker_PowerConf_A3321-DEV-SN1-01.analog-stereo", "description": "", "index": "0"}, {"name": "alsa_output.platform-sound.analog-stereo", "description": "", "index": "1"}, {"name": "alsa_input.usb-Shenzhen_Hollyland_Technology_Co._Ltd_Wireless_microphone_C63X223T6MX-01.analog-stereo", "description": "", "index": "5"}, ] # Anker pattern matches (multi-pattern: "powerconf,anker") m = ad.find_first_match(items, "powerconf,anker") self.assertIsNotNone(m) self.assertIn("Anker_PowerConf", m["name"]) # Built-in pattern matches m2 = ad.find_first_match(items, "platform-sound") self.assertIsNotNone(m2) # Hollyland — matches by "hollyland" OR "wireless_microphone" m3 = ad.find_first_match(items, "hollyland,wireless_microphone") self.assertIsNotNone(m3) self.assertIn("Hollyland", m3["name"]) # Case-insensitive m4 = ad.find_first_match(items, "HOLLYLAND") self.assertIsNotNone(m4) # No match returns None m5 = ad.find_first_match(items, "nonexistent") self.assertIsNone(m5) # Empty pattern returns None m6 = ad.find_first_match(items, "") self.assertIsNone(m6) # Different USB port — Anker on SN2-03 instead of SN1-01 items_port2 = [ {"name": "alsa_output.usb-Anker_PowerConf_A3321-DEV-SN2-03.analog-stereo", "description": "", "index": "0"}, ] m7 = ad.find_first_match(items_port2, "powerconf,anker") self.assertIsNotNone(m7, "Must match Anker regardless of USB port suffix") def test_profile_detection_with_jetson_devices(self): """Simulate the exact PulseAudio names from the G1 Jetson and verify all three profiles match correctly.""" from Project.Sanad.voice import audio_devices as ad fake_sinks = [ {"name": "alsa_output.platform-sound.analog-stereo", "description": "", "index": "0"}, {"name": "alsa_output.usb-Anker_PowerConf_A3321-DEV-SN1-01.analog-stereo", "description": "", "index": "1"}, ] fake_sources = [ {"name": "alsa_output.platform-sound.analog-stereo.monitor", "description": "", "index": "0"}, {"name": "alsa_input.platform-sound.analog-stereo", "description": "", "index": "1"}, {"name": "alsa_output.usb-Anker_PowerConf_A3321-DEV-SN1-01.analog-stereo.monitor", "description": "", "index": "3"}, {"name": "alsa_input.usb-Anker_PowerConf_A3321-DEV-SN1-01.mono-fallback", "description": "", "index": "4"}, {"name": "alsa_input.usb-Shenzhen_Hollyland_Technology_Co._Ltd_Wireless_microphone_C63X223T6MX-01.analog-stereo", "description": "", "index": "5"}, ] # Patch list_sinks/list_sources orig_sinks = ad.list_sinks orig_sources = ad.list_sources ad.list_sinks = lambda: fake_sinks ad.list_sources = lambda: fake_sources try: detected = ad.detect_plugged_profiles() detected_ids = [d["profile"]["id"] for d in detected] self.assertIn("hollyland_builtin", detected_ids, "Hollyland + built-in must be detected") self.assertIn("anker_powerconf", detected_ids, "Anker PowerConf must be detected") self.assertIn("builtin", detected_ids, "Built-in must be detected") # Verify Hollyland gets the correct sink and source holly = next(d for d in detected if d["profile"]["id"] == "hollyland_builtin") self.assertIn("platform-sound", holly["sink"]["name"]) self.assertIn("Hollyland", holly["source"]["name"]) # Verify Anker gets Anker sink AND Anker source (not built-in) anker = next(d for d in detected if d["profile"]["id"] == "anker_powerconf") self.assertIn("PowerConf", anker["sink"]["name"]) self.assertIn("PowerConf", anker["source"]["name"]) finally: ad.list_sinks = orig_sinks ad.list_sources = orig_sources def test_profile_detection_different_usb_port(self): """Verify that Anker is detected even when plugged into a different port (serial suffix changes from SN1-01 to SN2-03).""" from Project.Sanad.voice import audio_devices as ad fake_sinks = [ {"name": "alsa_output.usb-Anker_PowerConf_A3321-DEV-SN2-03.analog-stereo", "description": "", "index": "1"}, ] fake_sources = [ {"name": "alsa_input.usb-Anker_PowerConf_A3321-DEV-SN2-03.mono-fallback", "description": "", "index": "4"}, ] orig_sinks = ad.list_sinks orig_sources = ad.list_sources ad.list_sinks = lambda: fake_sinks ad.list_sources = lambda: fake_sources try: detected = ad.detect_plugged_profiles() detected_ids = [d["profile"]["id"] for d in detected] self.assertIn("anker_powerconf", detected_ids, "Anker must be detected regardless of USB port") finally: ad.list_sinks = orig_sinks ad.list_sources = orig_sources def test_status_no_crash_without_pactl(self): from Project.Sanad.voice import audio_devices as ad # Should return a dict even without pactl s = ad.status() self.assertIsInstance(s, dict) self.assertIn("pactl_available", s) self.assertIn("current", s) self.assertIn("profiles", s) # current always has these keys cur = s["current"] self.assertIn("sink", cur) self.assertIn("source", cur) self.assertIn("source_kind", cur) def test_load_save_state_atomic(self): from Project.Sanad.voice import audio_devices as ad # Round-trip original = ad.load_state() try: ad.save_state({"profile_id": "_test_unit", "sink": "x", "source": "y"}) self.assertEqual(ad.load_state()["profile_id"], "_test_unit") finally: ad.save_state(original) def test_select_unknown_profile_rejected(self): from Project.Sanad.voice import audio_devices as ad result = ad.select_profile("definitely_not_a_real_profile") self.assertFalse(result["ok"]) class TestIsolation(unittest.TestCase): """Failure isolation: one missing dep doesn't take down others.""" def test_main_module_imports_with_missing_deps(self): """main.py must import even when third-party deps are missing.""" import importlib if "Project.Sanad.main" in sys.modules: del sys.modules["Project.Sanad.main"] m = importlib.import_module("Project.Sanad.main") # Critical subsystems must be present even with missing deps self.assertIsNotNone(m.brain, "brain must always be available") def test_subsystem_status_reported(self): import Project.Sanad.main as m self.assertTrue(hasattr(m, "SUBSYSTEMS")) self.assertIn("brain", m.SUBSYSTEMS) self.assertIn("arm", m.SUBSYSTEMS) if __name__ == "__main__": unittest.main(verbosity=2)