feat(governance): persist remediation dry run history
All checks were successful
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 3m44s
CD Pipeline / post-deploy-checks (push) Successful in 1m24s

This commit is contained in:
Your Name
2026-05-14 22:38:42 +08:00
parent 36cb9d6aeb
commit 6aaaf87ade
6 changed files with 253 additions and 24 deletions

View File

@@ -51,6 +51,24 @@ class _FakeVerifier:
return self.state
class _FakeAlertOperationLogRepository:
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
async def append(self, event_type: str, **kwargs: Any):
self.calls.append({"event_type": event_type, **kwargs})
return type("AlertOperationRecord", (), {"id": "aol-1"})()
class _FakeTimelineService:
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
async def add_event(self, **kwargs: Any) -> dict[str, Any]:
self.calls.append(kwargs)
return {"id": "timeline-1"}
class _NoopPlaybookService:
async def get_recommendations(self, *_args, **_kwargs): # noqa: ANN002, ANN003
return []
@@ -111,6 +129,9 @@ def _service(
item: dict[str, Any],
incident: Incident | None = None,
state: dict[str, Any] | None = None,
timeline_service: Any | None = None,
alert_operation_log_repository: Any | None = None,
record_history: bool = False,
) -> Adr100RemediationService:
return Adr100RemediationService(
slo_service=_FakeSloService([item]),
@@ -120,6 +141,9 @@ def _service(
cooldown_checker=_no_cooldown,
),
verifier=_FakeVerifier(state or {"k8s_get_pod_status": {"phase": "Running"}}),
timeline_service=timeline_service,
alert_operation_log_repository=alert_operation_log_repository,
record_history=record_history,
)
@@ -156,6 +180,7 @@ async def test_dry_run_reverify_collects_state_without_writes():
assert result["post_state_summary"]["tool_count"] == 1
assert result["mcp_route"]["agent_id"] == "post_execution_verifier"
assert result["mcp_route"]["required_scope"] == "read"
assert result["history"]["recorded"] is False
@pytest.mark.asyncio
@@ -187,6 +212,36 @@ async def test_dry_run_blocks_when_incident_missing():
assert any(check["name"] == "incident_loaded" and not check["passed"] for check in result["checks"])
@pytest.mark.asyncio
async def test_dry_run_records_alert_operation_and_timeline_history():
alert_repo = _FakeAlertOperationLogRepository()
timeline = _FakeTimelineService()
svc = _service(
item=_queue_item(),
timeline_service=timeline,
alert_operation_log_repository=alert_repo,
record_history=True,
)
result = await svc.dry_run("verification:INC-20260514-TEST01:are-1")
assert result["history"] == {
"recorded": True,
"alert_operation_id": "aol-1",
"timeline_event_id": "timeline-1",
}
assert alert_repo.calls[0]["event_type"] == "PRE_FLIGHT_PASSED"
assert alert_repo.calls[0]["incident_id"] == "INC-20260514-TEST01"
assert alert_repo.calls[0]["success"] is True
assert alert_repo.calls[0]["context"]["schema_version"] == (
"adr100_remediation_dry_run_history_v1"
)
assert alert_repo.calls[0]["context"]["writes_incident_state"] is False
assert timeline.calls[0]["event_type"] == "verifier"
assert timeline.calls[0]["status"] == "success"
assert timeline.calls[0]["actor_role"] == "replay"
@pytest.mark.asyncio
async def test_missing_work_item_raises_not_found():
svc = _service(item=_queue_item())