Files
awoooi/apps/api/tests/test_awooop_operator_timeline_labels.py
Your Name 6868a9a93d
Some checks failed
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Failing after 1m58s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
feat(awooop): link telegram alerts to incident runs
2026-05-17 22:17:21 +08:00

212 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
"📄 <b>RUNBOOK REVIEW待審核</b>\nIncidentINC-1",
)
assert title == "TELEGRAMRunbook 待人工審核"
def test_outbound_timeline_title_labels_governance_alert() -> None:
title = _outbound_timeline_title(
"telegram",
"final",
"⚠️ *AI 治理警報|知識庫劣化*",
)
assert title == "TELEGRAMAI 治理警報"
def test_outbound_timeline_title_labels_cicd_status() -> None:
title = _outbound_timeline_title(
"telegram",
"final",
"✅ <b>[AWOOOI CI/CD]</b> | code-review\n📦 Code Review 完成・LOW",
)
assert title == "TELEGRAMCI/CD 狀態通知"
def test_outbound_timeline_title_labels_auto_repair_handoff() -> None:
title = _outbound_timeline_title(
"telegram",
"error",
"🤖❌ HANDOFF REQUIREDAI 自動修復失敗,已轉人工",
)
assert title == "TELEGRAMAI 自動修復失敗,已轉人工"
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",
]