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
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user