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",
]