fix(api): tolerate legacy incident decision chains
This commit is contained in:
@@ -502,6 +502,32 @@ def normalize_severity(value: str | Severity) -> str:
|
||||
return legacy_map.get(normalized, raw)
|
||||
|
||||
|
||||
def parse_decision_chain(value: Any, incident_id: str | None = None):
|
||||
"""Best-effort restore of legacy decision_chain payloads from PostgreSQL."""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
from src.models.incident import AIDecisionChain
|
||||
|
||||
if not isinstance(value, dict):
|
||||
logger.warning(
|
||||
"legacy_decision_chain_skipped",
|
||||
incident_id=incident_id,
|
||||
value_type=type(value).__name__,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
return AIDecisionChain(**value)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"decision_chain_parse_failed",
|
||||
incident_id=incident_id,
|
||||
error=str(exc),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
@@ -790,7 +816,7 @@ class IncidentService:
|
||||
|
||||
方案 C: 解析時正規化舊格式 Enum 值
|
||||
"""
|
||||
from src.models.incident import AIDecisionChain, IncidentOutcome
|
||||
from src.models.incident import IncidentOutcome
|
||||
|
||||
# 方案 C: 正規化 signals 內的舊格式 severity
|
||||
signals = []
|
||||
@@ -800,10 +826,9 @@ class IncidentService:
|
||||
signal_data["severity"] = normalize_severity(signal_data["severity"])
|
||||
signals.append(Signal(**signal_data))
|
||||
|
||||
decision_chain = (
|
||||
AIDecisionChain(**record.decision_chain)
|
||||
if record.decision_chain
|
||||
else None
|
||||
decision_chain = parse_decision_chain(
|
||||
record.decision_chain,
|
||||
incident_id=record.incident_id,
|
||||
)
|
||||
outcome = (
|
||||
IncidentOutcome(**record.outcome)
|
||||
|
||||
@@ -12,13 +12,18 @@ test_incident_service_resolve_idempotency
|
||||
重新放大「resolve_incident 重複觸發 postmortem 洗版」的舊風險。
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.models.incident import IncidentStatus
|
||||
from src.services.incident_service import IncidentService, normalize_status
|
||||
from src.services.incident_service import (
|
||||
IncidentService,
|
||||
normalize_status,
|
||||
parse_decision_chain,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -95,3 +100,37 @@ def test_normalize_status_accepts_db_enum_name() -> None:
|
||||
"""PostgreSQL SQLEnum 會存 Enum name;讀回時必須正規化成 Pydantic value。"""
|
||||
assert normalize_status("INVESTIGATING") == "investigating"
|
||||
assert normalize_status(IncidentStatus.CLOSED) == "closed"
|
||||
|
||||
|
||||
def test_parse_decision_chain_skips_legacy_list_payload() -> None:
|
||||
"""舊資料 decision_chain 可能是 list;不應阻斷 incident hydrate / resolve。"""
|
||||
assert parse_decision_chain([{"stage": "router"}], "INC-LEGACY-CHAIN") is None
|
||||
|
||||
|
||||
def test_record_to_incident_tolerates_legacy_decision_chain_list() -> None:
|
||||
"""DB fallback 必須能讀回舊 incident,即使 decision_chain 不是新 schema。"""
|
||||
now = datetime.now(UTC)
|
||||
record = SimpleNamespace(
|
||||
incident_id="INC-LEGACY-CHAIN",
|
||||
status="INVESTIGATING",
|
||||
severity="P2",
|
||||
signals=[],
|
||||
affected_services=[],
|
||||
decision_chain=[{"stage": "router"}],
|
||||
proposal_ids=[],
|
||||
outcome=None,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
resolved_at=None,
|
||||
closed_at=None,
|
||||
ttl_days=7,
|
||||
vectorized=False,
|
||||
notification_type="TYPE-3",
|
||||
alert_category="host_resource",
|
||||
)
|
||||
|
||||
incident = IncidentService()._record_to_incident(record)
|
||||
|
||||
assert incident.incident_id == "INC-LEGACY-CHAIN"
|
||||
assert incident.status == IncidentStatus.INVESTIGATING
|
||||
assert incident.decision_chain is None
|
||||
|
||||
Reference in New Issue
Block a user