fix(agent): reconcile terminal incident read models
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m20s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
AI 技術雷達監控 / ai-technology-watch (push) Successful in 41s

This commit is contained in:
ogt
2026-07-11 19:56:24 +08:00
parent 8a0c916d82
commit 155ea47958
4 changed files with 261 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import inspect
import json
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from unittest.mock import AsyncMock
import pytest
@@ -42,6 +43,17 @@ class _SequenceDB:
return self._results.pop(0) if self._results else _MappingResult()
class _RowsResult:
def __init__(self, rows: list[dict]) -> None:
self._rows = rows
def mappings(self) -> _RowsResult:
return self
def all(self) -> list[dict]:
return self._rows
def _claim() -> service.AnsibleCheckModeClaim:
return service.AnsibleCheckModeClaim(
op_id="00000000-0000-0000-0000-000000000101",
@@ -224,6 +236,7 @@ async def test_closure_resolves_only_after_durable_readback(
"incident_resolved": True,
"closure_receipt_written": True,
"resolved_lifecycle_written": True,
"working_memory_projection_ready": True,
}
)
monkeypatch.setattr(
@@ -255,6 +268,95 @@ async def test_closure_resolves_only_after_durable_readback(
assert atomic_writer.await_args.kwargs["closure_prerequisite_count"] == 1
@pytest.mark.asyncio
async def test_closure_waits_for_working_memory_projection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
service,
"_read_verified_apply_closure_prerequisites",
AsyncMock(return_value={"ready": True, "receipts": {"all": True}}),
)
monkeypatch.setattr(
service,
"_commit_verified_incident_closure",
AsyncMock(return_value={"working_memory_projection_ready": False}),
)
readback = AsyncMock()
monkeypatch.setattr(service, "_read_incident_closure_readback", readback)
result = await service._finalize_verified_apply_closure(
_claim(),
apply_op_id="00000000-0000-0000-0000-000000000104",
project_id="awoooi",
)
assert result == {
"status": "working_memory_projection_pending",
"closed": False,
"missing": ["working_memory_terminal_projection"],
}
readback.assert_not_awaited()
@pytest.mark.asyncio
async def test_terminal_working_memory_reconciler_projects_durable_rows(
monkeypatch: pytest.MonkeyPatch,
) -> None:
updated_at = datetime(2026, 7, 11, 11, 45, tzinfo=UTC)
db = _SequenceDB(
_RowsResult(
[
{
"incident_id": "INC-20260711-C50677",
"incident_status": "RESOLVED",
"updated_at": updated_at,
"resolved_at": updated_at,
},
{
"incident_id": "INC-20260711-8A28BC",
"incident_status": "RESOLVED",
"updated_at": updated_at,
"resolved_at": updated_at,
},
]
)
)
@asynccontextmanager
async def fake_db_context(_project_id: str):
yield db
project = AsyncMock(side_effect=["updated", "current"])
monkeypatch.setattr(service, "get_db_context", fake_db_context)
monkeypatch.setattr(
service,
"_project_incident_terminal_to_working_memory",
project,
)
result = await service.reconcile_verified_incident_working_memory_once(
project_id="awoooi",
window_hours=24,
limit=6,
)
assert result == {
"scanned": 2,
"updated": 1,
"current": 1,
"missing": 0,
"failed": 0,
"error": None,
}
assert project.await_count == 2
assert db.parameters[0] == {
"project_id": "awoooi",
"window_hours": 24,
"limit": 6,
}
@pytest.mark.asyncio
async def test_atomic_closure_rolls_back_when_bundle_readback_is_missing(
monkeypatch: pytest.MonkeyPatch,