test_satellite_system.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. #!/usr/bin/env python3
  2. """
  3. Comprehensive Test for Satellite Management System
  4. This test script demonstrates all the features of the Trixy satellite management
  5. system as specified in CLAUDE.md, including advanced access patterns, bulk
  6. operations, registration management, and blacklist functionality.
  7. """
  8. import sys
  9. import os
  10. import tempfile
  11. import shutil
  12. from pathlib import Path
  13. from datetime import datetime, timezone
  14. # Add the source directory to the Python path - run this from project root
  15. sys.path.insert(0, '.')
  16. from trixy_core.satellites import (
  17. SatelliteManager,
  18. Satellite,
  19. SatelliteInfo,
  20. SatelliteCapability,
  21. AudioPortInfo,
  22. RegistrationManager,
  23. BlacklistManager,
  24. QueryParser,
  25. normalize_mac_address
  26. )
  27. from trixy_core.satellites.satellite import create_satellite_info
  28. def pprint_test(message: str) -> None:
  29. """Test logging function."""
  30. print(f"[TEST] {message}")
  31. class MockApplication:
  32. """Mock application container for testing."""
  33. def __init__(self):
  34. self._event_handler = None
  35. self._network_manager = None
  36. def get_event_handler(self):
  37. return self._event_handler
  38. def get_network_manager(self):
  39. return self._network_manager
  40. def set_event_handler(self, handler):
  41. self._event_handler = handler
  42. def set_network_manager(self, manager):
  43. self._network_manager = manager
  44. def create_test_satellite_info(
  45. mac_address: str,
  46. room_id: str,
  47. alias_name: str,
  48. capabilities: list = None
  49. ) -> SatelliteInfo:
  50. """Create test satellite info."""
  51. return create_satellite_info(
  52. satellite_id=f"sat_{mac_address.replace(':', '_')}",
  53. mac_address=normalize_mac_address(mac_address),
  54. room_id=room_id,
  55. alias_name=alias_name,
  56. ip_address=f"192.168.1.{hash(mac_address) % 200 + 50}",
  57. version="1.0.0",
  58. capabilities=capabilities or [
  59. SatelliteCapability.WAKEWORD_DETECTION,
  60. SatelliteCapability.AUDIO_INPUT,
  61. SatelliteCapability.AUDIO_OUTPUT,
  62. SatelliteCapability.TEXT_TO_SPEECH
  63. ]
  64. )
  65. def test_basic_satellite_manager():
  66. """Test basic satellite manager functionality."""
  67. pprint_test("=== Testing Basic Satellite Manager ===")
  68. # Create temporary directories for testing
  69. temp_dir = tempfile.mkdtemp()
  70. try:
  71. app = MockApplication()
  72. manager = SatelliteManager(
  73. app,
  74. registration_dir=os.path.join(temp_dir, "registrations"),
  75. blacklist_file=os.path.join(temp_dir, "blacklist.json"),
  76. max_satellites=10,
  77. enable_auto_registration=True
  78. )
  79. pprint_test(f"✓ Created satellite manager: {manager}")
  80. # Test adding satellites
  81. sat1_info = create_test_satellite_info(
  82. "aa:bb:cc:dd:ee:01",
  83. "kitchen",
  84. "Kitchen Assistant"
  85. )
  86. sat2_info = create_test_satellite_info(
  87. "aa:bb:cc:dd:ee:02",
  88. "living_room",
  89. "Living Room Speaker"
  90. )
  91. sat3_info = create_test_satellite_info(
  92. "aa:bb:cc:dd:ee:03",
  93. "bedroom",
  94. "Bedroom Helper"
  95. )
  96. # Add satellites
  97. sat1 = manager.add_satellite(sat1_info)
  98. sat2 = manager.add_satellite(sat2_info)
  99. sat3 = manager.add_satellite(sat3_info)
  100. pprint_test(f"✓ Added 3 satellites, total: {len(manager)}")
  101. # Test basic properties
  102. assert len(manager) == 3
  103. assert sat1_info.mac_address in manager
  104. assert sat1 in manager
  105. pprint_test("✓ Basic satellite manager tests passed")
  106. return manager, [sat1, sat2, sat3]
  107. finally:
  108. shutil.rmtree(temp_dir, ignore_errors=True)
  109. def test_advanced_access_patterns(manager, satellites):
  110. """Test advanced access patterns as specified in CLAUDE.md."""
  111. pprint_test("=== Testing Advanced Access Patterns ===")
  112. sat1, sat2, sat3 = satellites
  113. # Test direct index access: satellite_manager[0]
  114. pprint_test("Testing direct index access...")
  115. first_satellite = manager[0]
  116. assert isinstance(first_satellite, Satellite)
  117. pprint_test(f"✓ manager[0] = {first_satellite.alias_name}")
  118. # Test index access for all satellites
  119. for i in range(len(manager)):
  120. satellite = manager[i]
  121. pprint_test(f"✓ manager[{i}] = {satellite.alias_name}")
  122. # Simulate connections for query testing
  123. sat1.set_connected("192.168.1.101", AudioPortInfo(2102, 2103, 2104, 2101))
  124. sat2.set_connected("192.168.1.102", AudioPortInfo(2102, 2103, 2104, 2101))
  125. # sat3 remains disconnected (registered status)
  126. # Test query-based access: satellite_manager["status=connected"]
  127. pprint_test("Testing query-based access...")
  128. # Test status queries
  129. connected_satellites = manager["status=connected"]
  130. assert isinstance(connected_satellites, list)
  131. assert len(connected_satellites) == 2
  132. pprint_test(f"✓ Found {len(connected_satellites)} connected satellites")
  133. not_connected_satellites = manager["status=registered"]
  134. assert len(not_connected_satellites) == 1
  135. pprint_test(f"✓ Found {len(not_connected_satellites)} registered (not connected) satellites")
  136. # Test room queries
  137. kitchen_satellites = manager["room=kitchen"]
  138. assert len(kitchen_satellites) == 1
  139. assert kitchen_satellites[0].alias_name == "Kitchen Assistant"
  140. pprint_test(f"✓ Found {len(kitchen_satellites)} satellites in kitchen")
  141. # Test combined queries: satellite_manager["status=connected,room=kitchen"]
  142. connected_kitchen = manager["status=connected,room=kitchen"]
  143. assert len(connected_kitchen) == 1
  144. pprint_test(f"✓ Found {len(connected_kitchen)} connected satellites in kitchen")
  145. # Test capability queries
  146. wakeword_satellites = manager["capability=wakeword_detection"]
  147. assert len(wakeword_satellites) == 3 # All have wakeword detection
  148. pprint_test(f"✓ Found {len(wakeword_satellites)} satellites with wakeword detection")
  149. # Test alias queries
  150. kitchen_by_alias = manager["alias contains Kitchen"]
  151. assert len(kitchen_by_alias) == 1
  152. pprint_test(f"✓ Found {len(kitchen_by_alias)} satellites with 'Kitchen' in alias")
  153. # Test IP address queries
  154. local_satellites = manager["ip starts_with 192.168"]
  155. assert len(local_satellites) >= 2
  156. pprint_test(f"✓ Found {len(local_satellites)} satellites on local network")
  157. pprint_test("✓ Advanced access pattern tests passed")
  158. def test_bulk_operations(manager, satellites):
  159. """Test bulk operations functionality."""
  160. pprint_test("=== Testing Bulk Operations ===")
  161. sat1, sat2, sat3 = satellites
  162. # Test bulk TTS: say_all()
  163. pprint_test("Testing bulk TTS operations...")
  164. sent_count = manager.say_all("Hello everyone!")
  165. pprint_test(f"✓ Sent TTS to {sent_count} satellites (all connected)")
  166. # Test filtered bulk TTS
  167. sent_count = manager.say_all("Kitchen announcement", "room=kitchen")
  168. pprint_test(f"✓ Sent TTS to {sent_count} satellites in kitchen")
  169. # Test bulk capability management
  170. pprint_test("Testing bulk capability operations...")
  171. updated_count = manager.update_all_capabilities(
  172. SatelliteCapability.LED_CONTROL,
  173. add=True,
  174. query="status=connected"
  175. )
  176. pprint_test(f"✓ Added LED control to {updated_count} connected satellites")
  177. # Verify capability was added
  178. led_satellites = manager["capability=led_control"]
  179. assert len(led_satellites) == 2 # Only connected satellites got the capability
  180. pprint_test(f"✓ Verified {len(led_satellites)} satellites now have LED control")
  181. # Test bulk disconnect: disconnect_all()
  182. pprint_test("Testing bulk disconnect operations...")
  183. disconnected_count = manager.disconnect_all("room=living_room", "bulk_test")
  184. pprint_test(f"✓ Disconnected {disconnected_count} satellites in living room")
  185. # Test bulk reconnect: reconnect_all()
  186. pprint_test("Testing bulk reconnect operations...")
  187. # First simulate that satellites can reconnect
  188. for satellite in manager["room=living_room"]:
  189. satellite._status = satellite._status # Keep current status for reconnect attempt
  190. reconnected_count = manager.reconnect_all("room=living_room")
  191. pprint_test(f"✓ Attempted to reconnect {reconnected_count} satellites in living room")
  192. pprint_test("✓ Bulk operations tests passed")
  193. def test_registration_system():
  194. """Test satellite registration and blacklist management."""
  195. pprint_test("=== Testing Registration System ===")
  196. # Create temporary directory
  197. temp_dir = tempfile.mkdtemp()
  198. try:
  199. reg_manager = RegistrationManager(
  200. registration_dir=os.path.join(temp_dir, "registrations"),
  201. backup_dir=os.path.join(temp_dir, "backups")
  202. )
  203. blacklist_manager = BlacklistManager(
  204. blacklist_file=os.path.join(temp_dir, "blacklist.json")
  205. )
  206. # Test registration creation
  207. pprint_test("Testing satellite registration...")
  208. sat_info = create_test_satellite_info(
  209. "bb:cc:dd:ee:ff:01",
  210. "office",
  211. "Office Assistant"
  212. )
  213. registration = reg_manager.create_registration(
  214. sat_info,
  215. registered_by="test_user",
  216. notes="Test registration",
  217. auto_approve=True
  218. )
  219. assert registration.status.value == "approved"
  220. pprint_test("✓ Created and approved registration")
  221. # Test registration retrieval
  222. retrieved = reg_manager.get_registration(sat_info.mac_address)
  223. assert retrieved is not None
  224. assert retrieved.satellite_info.alias_name == "Office Assistant"
  225. pprint_test("✓ Retrieved registration successfully")
  226. # Test blacklist management
  227. pprint_test("Testing blacklist management...")
  228. # Add to blacklist
  229. success = blacklist_manager.add_to_blacklist(
  230. "cc:dd:ee:ff:aa:01",
  231. reason="test_blacklist",
  232. blacklisted_by="test_user",
  233. notes="Testing blacklist functionality"
  234. )
  235. assert success
  236. pprint_test("✓ Added MAC to blacklist")
  237. # Check if blacklisted
  238. is_blacklisted = blacklist_manager.is_blacklisted("cc:dd:ee:ff:aa:01")
  239. assert is_blacklisted
  240. pprint_test("✓ Confirmed MAC is blacklisted")
  241. # Get blacklist stats
  242. stats = blacklist_manager.get_blacklist_stats()
  243. assert stats["total_entries"] == 1
  244. assert stats["active_entries"] == 1
  245. pprint_test(f"✓ Blacklist stats: {stats['total_entries']} total, {stats['active_entries']} active")
  246. # Remove from blacklist
  247. success = blacklist_manager.remove_from_blacklist("cc:dd:ee:ff:aa:01")
  248. assert success
  249. is_blacklisted = blacklist_manager.is_blacklisted("cc:dd:ee:ff:aa:01")
  250. assert not is_blacklisted
  251. pprint_test("✓ Removed MAC from blacklist")
  252. pprint_test("✓ Registration system tests passed")
  253. finally:
  254. shutil.rmtree(temp_dir, ignore_errors=True)
  255. def test_connection_management(manager, satellites):
  256. """Test satellite connection management."""
  257. pprint_test("=== Testing Connection Management ===")
  258. # Test registration mode
  259. pprint_test("Testing registration mode...")
  260. assert not manager.is_registration_mode_active()
  261. success = manager.enter_registration_mode(timeout=5.0)
  262. assert success
  263. assert manager.is_registration_mode_active()
  264. pprint_test("✓ Entered registration mode")
  265. success = manager.exit_registration_mode()
  266. assert success
  267. assert not manager.is_registration_mode_active()
  268. pprint_test("✓ Exited registration mode")
  269. # Test satellite connection handling
  270. pprint_test("Testing satellite connection handling...")
  271. success, message, satellite = manager.handle_satellite_connection(
  272. mac_address="dd:ee:ff:aa:bb:01",
  273. ip_address="192.168.1.200",
  274. room_id="garage",
  275. alias_name="Garage Speaker",
  276. capabilities=["wakeword_detection", "audio_output"]
  277. )
  278. # This might fail if not in registration mode, which is expected
  279. pprint_test(f"✓ Connection attempt result: {success} - {message}")
  280. # Test with registration mode
  281. manager.enter_registration_mode(10.0)
  282. success, message, satellite = manager.handle_satellite_connection(
  283. mac_address="dd:ee:ff:aa:bb:02",
  284. ip_address="192.168.1.201",
  285. room_id="garage",
  286. alias_name="Garage Light Controller",
  287. capabilities=["led_control", "button_input"]
  288. )
  289. if success:
  290. assert satellite is not None
  291. assert satellite.room_id == "garage"
  292. pprint_test("✓ Successfully connected new satellite")
  293. manager.exit_registration_mode()
  294. pprint_test("✓ Connection management tests passed")
  295. def test_statistics_and_monitoring(manager, satellites):
  296. """Test statistics and monitoring functionality."""
  297. pprint_test("=== Testing Statistics and Monitoring ===")
  298. # Get comprehensive stats
  299. stats = manager.get_stats()
  300. pprint_test(f"✓ Total satellites: {stats.total_satellites}")
  301. pprint_test(f"✓ Connected satellites: {stats.connected_satellites}")
  302. pprint_test(f"✓ Registered satellites: {stats.registered_satellites}")
  303. pprint_test(f"✓ Uptime: {stats.uptime_seconds:.1f} seconds")
  304. # Get full status
  305. status = manager.get_status()
  306. pprint_test(f"✓ Statistics: {len(status['statistics'])} fields")
  307. pprint_test(f"✓ Configuration: {len(status['configuration'])} fields")
  308. pprint_test(f"✓ Satellites by room: {status['satellites_by_room']}")
  309. pprint_test(f"✓ Capabilities: {status['capabilities']}")
  310. # Test individual satellite status
  311. for i, satellite in enumerate(satellites[:2]): # Test first 2
  312. sat_status = satellite.get_status()
  313. pprint_test(f"✓ Satellite {i+1} status: {len(sat_status)} fields")
  314. pprint_test(f" - Connection status: {sat_status['connection']['status']}")
  315. pprint_test(f" - Capabilities: {len(sat_status['capabilities'])}")
  316. pprint_test(f" - Uptime: {sat_status['connection']['uptime_seconds']:.1f}s")
  317. pprint_test("✓ Statistics and monitoring tests passed")
  318. def test_query_parser():
  319. """Test the query parser functionality."""
  320. pprint_test("=== Testing Query Parser ===")
  321. parser = QueryParser(case_sensitive=False)
  322. # Test query parsing
  323. test_queries = [
  324. "status=connected",
  325. "room=kitchen",
  326. "status=connected,room=kitchen",
  327. "capability=wakeword_detection",
  328. "uptime>3600",
  329. "error_count=0",
  330. "alias contains kitchen",
  331. "ip starts_with 192.168",
  332. "status in [connected,connecting]",
  333. "capability exists",
  334. ]
  335. for query in test_queries:
  336. try:
  337. conditions = parser.parse_query(query)
  338. pprint_test(f"✓ Parsed '{query}' -> {len(conditions)} conditions")
  339. except Exception as e:
  340. pprint_test(f"✗ Failed to parse '{query}': {e}")
  341. # Test field validation
  342. supported_fields = parser.get_supported_fields()
  343. pprint_test(f"✓ Supports {len(supported_fields)} query fields")
  344. field_aliases = parser.get_field_aliases()
  345. pprint_test(f"✓ Supports {len(field_aliases)} field aliases")
  346. pprint_test("✓ Query parser tests passed")
  347. def test_file_persistence():
  348. """Test file persistence and recovery."""
  349. pprint_test("=== Testing File Persistence ===")
  350. temp_dir = tempfile.mkdtemp()
  351. try:
  352. # Create manager with file persistence
  353. app = MockApplication()
  354. manager = SatelliteManager(
  355. app,
  356. registration_dir=os.path.join(temp_dir, "registrations"),
  357. blacklist_file=os.path.join(temp_dir, "blacklist.json")
  358. )
  359. # Add some satellites
  360. for i in range(3):
  361. sat_info = create_test_satellite_info(
  362. f"ff:ee:dd:cc:bb:0{i+1}",
  363. f"room_{i+1}",
  364. f"Satellite {i+1}"
  365. )
  366. manager.add_satellite(sat_info)
  367. # Add to blacklist
  368. manager.add_to_blacklist("aa:aa:aa:aa:aa:aa", "test_persistence")
  369. pprint_test(f"✓ Created manager with {len(manager)} satellites")
  370. # Create new manager to test loading
  371. manager2 = SatelliteManager(
  372. app,
  373. registration_dir=os.path.join(temp_dir, "registrations"),
  374. blacklist_file=os.path.join(temp_dir, "blacklist.json")
  375. )
  376. pprint_test(f"✓ Loaded manager with {len(manager2)} satellites")
  377. assert len(manager2) == len(manager)
  378. # Check blacklist was loaded
  379. assert manager2.is_blacklisted("aa:aa:aa:aa:aa:aa")
  380. pprint_test("✓ Blacklist loaded successfully")
  381. pprint_test("✓ File persistence tests passed")
  382. finally:
  383. shutil.rmtree(temp_dir, ignore_errors=True)
  384. def run_comprehensive_test():
  385. """Run all tests in sequence."""
  386. pprint_test("=" * 60)
  387. pprint_test("COMPREHENSIVE SATELLITE MANAGEMENT SYSTEM TEST")
  388. pprint_test("=" * 60)
  389. try:
  390. # Basic functionality
  391. manager, satellites = test_basic_satellite_manager()
  392. # Advanced patterns as specified in CLAUDE.md
  393. test_advanced_access_patterns(manager, satellites)
  394. # Bulk operations
  395. test_bulk_operations(manager, satellites)
  396. # Connection management
  397. test_connection_management(manager, satellites)
  398. # Statistics and monitoring
  399. test_statistics_and_monitoring(manager, satellites)
  400. # Registration system
  401. test_registration_system()
  402. # Query parser
  403. test_query_parser()
  404. # File persistence
  405. test_file_persistence()
  406. pprint_test("=" * 60)
  407. pprint_test("✅ ALL TESTS PASSED - SATELLITE SYSTEM READY FOR PRODUCTION")
  408. pprint_test("=" * 60)
  409. except Exception as e:
  410. pprint_test("=" * 60)
  411. pprint_test(f"❌ TEST FAILED: {e}")
  412. pprint_test("=" * 60)
  413. import traceback
  414. traceback.print_exc()
  415. return False
  416. return True
  417. if __name__ == "__main__":
  418. success = run_comprehensive_test()
  419. sys.exit(0 if success else 1)