from datetime import datetime from types import SimpleNamespace from src.services.platform_operator_service import ( _collect_run_incident_ids, _list_filter_context_limit, _outbound_timeline_title, _run_remediation_list_summary, _remediation_summary_matches_incident_id, _remediation_summary_matches_status, _remediation_timeline_summary, _timeline_sort_key, ) def test_outbound_timeline_title_labels_runbook_review() -> None: title = _outbound_timeline_title( "telegram", "approval_request", "📄 RUNBOOK REVIEW|待審核\nIncident:INC-1", ) assert title == "TELEGRAM:Runbook 待人工審核" def test_outbound_timeline_title_labels_governance_alert() -> None: title = _outbound_timeline_title( "telegram", "final", "⚠️ *AI 治理警報|知識庫劣化*", ) assert title == "TELEGRAM:AI 治理警報" def test_outbound_timeline_title_labels_cicd_status() -> None: title = _outbound_timeline_title( "telegram", "final", "✅ [AWOOOI CI/CD] | code-review\n📦 Code Review 完成・LOW", ) assert title == "TELEGRAM:CI/CD 狀態通知" def test_outbound_timeline_title_labels_auto_repair_handoff() -> None: title = _outbound_timeline_title( "telegram", "error", "🤖❌ HANDOFF REQUIRED|AI 自動修復失敗,已轉人工", ) assert title == "TELEGRAM:AI 自動修復失敗,已轉人工" def test_outbound_timeline_title_falls_back_to_human_label() -> None: title = _outbound_timeline_title("telegram", "interim", "正在調用 MCP 工具") assert title == "TELEGRAM:漸進式狀態回饋" def test_collect_run_incident_ids_reads_source_refs_and_legacy_text() -> None: run = SimpleNamespace( trigger_ref="not-an-incident", error_detail=None, ) inbound_events = [ SimpleNamespace( source_envelope={ "source_refs": { "incident_ids": ["INC-20260514-F85F21", "INC-20260514-F85F21"], } }, content_preview="Alertmanager inbound converged", content_redacted=None, ) ] outbound_messages = [ SimpleNamespace( content_preview="詳情:INC-20260513-79ED5E", send_error=None, ) ] incident_ids = _collect_run_incident_ids( run=run, inbound_events=inbound_events, outbound_messages=outbound_messages, ) assert incident_ids == ["INC-20260514-F85F21", "INC-20260513-79ED5E"] def test_remediation_timeline_summary_surfaces_route_and_write_flags() -> None: summary = _remediation_timeline_summary({ "incident_id": "INC-20260514-F85F21", "mode": "replay", "verification_result_preview": "degraded", "agent_id": "auto_repair_executor", "tool_name": "ssh_diagnose", "required_scope": "read", "writes_incident_state": False, "writes_auto_repair_result": False, }) assert "incident=INC-20260514-F85F21" in summary assert "route=auto_repair_executor/ssh_diagnose/read" in summary assert "writes_incident=False" in summary assert "writes_auto_repair=False" in summary def test_run_remediation_list_summary_marks_read_only_dry_run() -> None: run = SimpleNamespace(state="waiting_approval") summary = _run_remediation_list_summary( run=run, incident_ids=["INC-20260514-F85F21"], items=[ { "created_at": "2026-05-14T23:04:00+00:00", "incident_id": "INC-20260514-F85F21", "mode": "replay", "verification_result_preview": "degraded", "agent_id": "auto_repair_executor", "tool_name": "ssh_diagnose", "required_scope": "read", "writes_incident_state": False, "writes_auto_repair_result": False, } ], ) assert summary["status"] == "read_only_dry_run" assert summary["has_dry_run"] is True assert summary["is_read_only"] is True assert summary["human_gate_open"] is True assert summary["latest_route"] == "auto_repair_executor/ssh_diagnose/read" def test_run_remediation_list_summary_flags_write_observed() -> None: run = SimpleNamespace(state="completed") summary = _run_remediation_list_summary( run=run, incident_ids=["INC-20260514-F85F21"], items=[ { "created_at": "2026-05-14T23:05:00+00:00", "incident_id": "INC-20260514-F85F21", "agent_id": "auto_repair_executor", "tool_name": "state_update", "required_scope": "write", "writes_incident_state": True, "writes_auto_repair_result": False, } ], ) assert summary["status"] == "write_observed" assert summary["is_read_only"] is False assert summary["writes_incident_state"] is True def test_remediation_summary_matches_status_filter() -> None: assert _remediation_summary_matches_status( {"status": "read_only_dry_run"}, "read_only_dry_run", ) assert not _remediation_summary_matches_status( {"status": "write_observed"}, "read_only_dry_run", ) assert _remediation_summary_matches_status(None, "no_evidence") def test_remediation_summary_matches_incident_id_filter() -> None: assert _remediation_summary_matches_incident_id( {"incident_ids": ["INC-20260514-F85F21"]}, "INC-20260514-F85F21", ) assert not _remediation_summary_matches_incident_id( {"incident_ids": ["INC-20260514-F85F21"]}, "INC-20260513-79ED5E", ) assert _remediation_summary_matches_incident_id(None, None) def test_list_filter_context_limit_scales_with_candidate_rows() -> None: assert _list_filter_context_limit(2) == 500 assert _list_filter_context_limit(4176) == 16704 assert _list_filter_context_limit(10000) == 20000 def test_timeline_sort_key_normalizes_datetime_and_iso_string() -> None: fallback = datetime(2026, 5, 14, 10, 0, 0) keys = [ _timeline_sort_key({"ts": datetime(2026, 5, 14, 10, 0, 1)}, fallback), _timeline_sort_key({"ts": "2026-05-14T10:00:02+00:00"}, fallback), _timeline_sort_key({"ts": None}, fallback), ] assert keys == [ "2026-05-14T10:00:01", "2026-05-14T10:00:02+00:00", "2026-05-14T10:00:00", ] assert sorted(keys) == [ "2026-05-14T10:00:00", "2026-05-14T10:00:01", "2026-05-14T10:00:02+00:00", ]