import io import json import unittest from h8536.protocol_capture import analyze_capture_text, format_text_report, main, parse_capture_text class ProtocolCaptureTest(unittest.TestCase): def test_parses_timestamped_capture_chunks(self): chunks = parse_capture_text("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n") self.assertEqual(len(chunks), 1) self.assertEqual(chunks[0].timestamp_ms, 57975502) self.assertEqual(chunks[0].analyzer_direction, "rx") self.assertEqual(chunks[0].device_direction, "tx") self.assertEqual(chunks[0].bytes, (0x00, 0x00, 0x15, 0x80, 0x00, 0xCF)) def test_recombines_user_split_rx_chunks_into_valid_call_frame(self): analysis = analyze_capture_text( "16:06:15.502 RX 003 bytes 00 00 15\n" "16:06:15.506 RX 003 bytes 80 00 CF\n" ) self.assertEqual(analysis["frame_count"], 1) frame = analysis["frames"][0] self.assertEqual(frame["source_chunk_indexes"], [0, 1]) self.assertEqual(frame["analyzer_direction"], "rx") self.assertEqual(frame["device_direction"], "tx") self.assertEqual(frame["report_candidate"]["index"], 0x15) self.assertEqual(frame["report_candidate"]["value"], 0x8000) self.assertEqual( frame["report_candidate"]["observed_candidate"]["name_candidate"], "call_button_candidate", ) self.assertEqual(frame["report_candidate"]["observed_candidate"]["state_candidate"], "active") def test_recombines_split_chunks_with_multiple_frames_and_labels_known_reports(self): analysis = analyze_capture_text( "16:06:15.500 RX 004 bytes 00 00 00 00\n" "16:06:15.502 RX 005 bytes 80 DA 00 00 07\n" "16:06:15.504 RX 003 bytes 80 00 DD\n" "16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n" ) self.assertEqual(analysis["frame_count"], 3) names = [ frame["report_candidate"]["observed_candidate"]["name_candidate"] for frame in analysis["frames"] ] self.assertEqual( names, [ "heartbeat_alive_candidate", "cam_power_button_candidate", "heartbeat_alive_candidate", ], ) self.assertEqual(analysis["repeated_group_count"], 1) group = analysis["repeated_groups"][0] self.assertEqual(group["count"], 2) self.assertEqual(group["cadence_ms"]["samples"], [100]) def test_text_report_mentions_split_label_and_cadence(self): report = format_text_report( analyze_capture_text( "16:06:15.502 RX 003 bytes 00 00 15\n" "16:06:15.506 RX 003 bytes 80 00 CF\n" "16:06:15.602 RX 006 bytes 00 00 15 80 00 CF\n" ) ) self.assertIn("call_button_candidate state=active", report) self.assertIn("checksum=ok split", report) self.assertIn("cadence=avg=100.0ms", report) def test_gate_session_hints_summarize_reports_transitions_and_heartbeat_interruptions(self): analysis = analyze_capture_text( "16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n" "16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n" "16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n" "16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n" "16:06:15.700 RX 006 bytes 00 00 00 00 80 DA\n" ) hints = analysis["gate_session_hints"] self.assertEqual( hints["observed_autonomous_report_names"], ["call_button_candidate", "heartbeat_alive_candidate"], ) reports = {item["name_candidate"]: item for item in hints["observed_reports"]} self.assertEqual(reports["heartbeat_alive_candidate"]["count"], 3) self.assertEqual(reports["heartbeat_alive_candidate"]["first_timestamp"], "16:06:15.500") self.assertEqual(reports["heartbeat_alive_candidate"]["last_timestamp"], "16:06:15.700") self.assertEqual(reports["call_button_candidate"]["states"], ["active", "inactive"]) self.assertEqual(hints["heartbeat_cadence_ms"]["samples"], [100, 100]) self.assertEqual(hints["heartbeat_cadence_ms"]["average"], 100.0) self.assertEqual(len(hints["heartbeat_interruptions"]), 2) self.assertEqual( hints["heartbeat_interruptions"][0]["interrupted_by"][0]["name_candidate"], "call_button_candidate", ) self.assertEqual(len(hints["active_inactive_transitions"]), 1) transition = hints["active_inactive_transitions"][0] self.assertEqual(transition["index_hex"], "0x0015") self.assertEqual(transition["from_state"], "active") self.assertEqual(transition["to_state"], "inactive") self.assertEqual(hints["evidence_scope"], "capture_side_observation_only") self.assertIn("host/session gating", hints["caveat"]) def test_text_report_mentions_gate_session_hints_and_caveat(self): report = format_text_report( analyze_capture_text( "16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n" "16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n" "16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n" "16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n" ) ) self.assertIn( "observed autonomous report candidates: call_button_candidate, heartbeat_alive_candidate", report, ) self.assertIn("heartbeat cadence count=2 cadence=avg=100.0ms", report) self.assertIn("transition index=0x0015 active->inactive", report) self.assertIn("heartbeat gap 16:06:15.500..16:06:15.600", report) self.assertIn("caveat: Missing autonomous reports", report) def test_cli_json_output(self): output = io.StringIO() rc = main( ["--json", "-"], stdin=io.StringIO("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n"), stdout=output, ) self.assertEqual(rc, 0) payload = json.loads(output.getvalue()) self.assertEqual(payload["frames"][0]["report_candidate"]["index"], 0x15) if __name__ == "__main__": unittest.main()