fix(scada): verify recovery after replay work-order completion
This commit is contained in:
@@ -97,8 +97,13 @@ def test_off_grid_feedback_uses_first_later_sample_even_without_monitoring_inter
|
||||
assert result["verified_through"] == (RETURNED + timedelta(minutes=15)).isoformat()
|
||||
|
||||
|
||||
def test_dispatch_waits_for_all_orders_and_accepts_corrected_field_completion_time():
|
||||
@pytest.mark.parametrize("replay", [False, True])
|
||||
def test_dispatch_waits_for_all_orders_and_accepts_corrected_field_completion_time(replay):
|
||||
task = {"workflow": "burst_report"}
|
||||
if replay:
|
||||
task["source"] = {"replay_context": {
|
||||
"replay_id": str(uuid4()), "knowledge_cutoff": INCIDENT.isoformat(), "batch_sequence": 3,
|
||||
}}
|
||||
for order_id in ("one", "two"):
|
||||
track_work_order(task, {"kind": "create_work_order", "action_id": str(uuid4()), "execution_status": "succeeded",
|
||||
"receipt": {"adapter": "peripheral_sample", "receipt_id": order_id}})
|
||||
@@ -120,6 +125,9 @@ def test_dispatch_waits_for_all_orders_and_accepts_corrected_field_completion_ti
|
||||
assert task["handling_context"]["returned_at"] == corrected.isoformat()
|
||||
assert task["handling_context"]["phase"] == "waiting_scada"
|
||||
assert "verification_evidence_id" not in task["handling_context"] and "last_check" not in task["handling_context"]
|
||||
if replay:
|
||||
assert task["source"]["replay_context"]["knowledge_cutoff"] == INCIDENT.isoformat()
|
||||
assert task["source"]["replay_context"]["batch_sequence"] == 3
|
||||
|
||||
|
||||
def test_field_feedback_time_requires_a_real_note_and_explicit_timezone():
|
||||
|
||||
@@ -1128,21 +1128,39 @@ def _seed_concluded_report(repository, now):
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_burst_waits_for_completion_then_scada_evidence_then_agent_report(monkeypatch):
|
||||
@pytest.mark.parametrize("replay", [False, True])
|
||||
async def test_burst_waits_for_completion_then_scada_evidence_then_agent_report(monkeypatch, replay):
|
||||
from copy import deepcopy
|
||||
from app.domain.schemas.project_settings import ProjectSettings
|
||||
from app.services.operational_recovery import reconcile_recoveries, track_work_order
|
||||
|
||||
now = datetime(2026, 7, 16, 4, tzinfo=timezone.utc)
|
||||
incident = now - timedelta(hours=4)
|
||||
returned = incident + timedelta(hours=1)
|
||||
now = datetime(2026, 7, 16, 6, tzinfo=timezone.utc)
|
||||
incident = now - timedelta(hours=6)
|
||||
returned = datetime.fromisoformat("2026-07-16T13:00:00+08:00")
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
task.update(workflow="burst_report", trigger={"kind": "event", "occurred_at": incident.isoformat(), "detail": "爆管"})
|
||||
if replay:
|
||||
task["workflow"] = "scada_anomaly"
|
||||
task["source"]["replay_context"] = {
|
||||
"replay_id": str(uuid4()), "knowledge_cutoff": incident.isoformat(), "batch_sequence": 3,
|
||||
}
|
||||
repository = RunTaskRepository(task, _active_run(task, now))
|
||||
repository.connection = object()
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
monkeypatch.setattr(service, "_resolve_investigation_alerts", AsyncMock())
|
||||
incident_evidence_id = None
|
||||
if replay:
|
||||
frozen = task["source"]["replay_context"]
|
||||
evidence, _ = await service.record_data_snapshot(task["task_id"], TaskDataSnapshot(
|
||||
source="query_scada_readings", source_ref="mcp:frozen-incident", queried_at=now,
|
||||
origin="synthetic", truncated=False,
|
||||
data={"data": {"devices": [{"device_id": "P1"}], "readings": [], "coverage": {
|
||||
"mode": "all_scada", "query_complete": True,
|
||||
"replay_id": frozen["replay_id"], "batch_sequence": 3,
|
||||
}}, "window": {"start": (incident - timedelta(hours=3)).isoformat(), "end": incident.isoformat()}},
|
||||
))
|
||||
incident_evidence_id = evidence.evidence_id
|
||||
_seed_concluded_report(repository, now)
|
||||
action = await service.propose_action(task["task_id"], ActionProposalCommand(
|
||||
request_id=uuid4(), kind="create_work_order", reason="隔离抢修与恢复供水",
|
||||
@@ -1178,6 +1196,11 @@ async def test_burst_waits_for_completion_then_scada_evidence_then_agent_report(
|
||||
reader.return_value = rows
|
||||
assert await reconcile_recoveries(service, timescale, settings) == 1
|
||||
assert await reconcile_recoveries(service, timescale, settings) == 0
|
||||
reader.assert_awaited_with(timescale, ["P1"], incident, returned, now, baseline_limit=100_001)
|
||||
if replay:
|
||||
assert repository.task["source"]["replay_context"]["replay_id"] == task["source"]["replay_context"]["replay_id"]
|
||||
assert datetime.fromisoformat(repository.task["source"]["replay_context"]["knowledge_cutoff"]) == incident
|
||||
assert repository.task["source"]["replay_context"]["batch_sequence"] == 3
|
||||
recovery = repository.task["handling_context"]
|
||||
evidence_id = recovery["verification_evidence_id"]
|
||||
assert repository.evidence[evidence_id]["data_snapshot"]["data"]["status"] == "passed"
|
||||
@@ -1208,7 +1231,14 @@ async def test_burst_waits_for_completion_then_scada_evidence_then_agent_report(
|
||||
await service.complete_run(task["task_id"], repository.run["run_id"], AgentRunCommand(
|
||||
request_id=uuid4(), expected_revision=repository.run["revision"], source_session_id="session-1"))
|
||||
assert rejected.value.code == "RECOVERY_EVIDENCE_REQUIRED"
|
||||
repository.reports[-1]["report"]["evidence_refs"] = [evidence_id]
|
||||
if replay:
|
||||
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
|
||||
request_id=uuid4(), source_session_id="session-1",
|
||||
report={**_current_report(), "evidence_refs": [incident_evidence_id, evidence_id]},
|
||||
))
|
||||
assert repository.reports[-1]["report"]["evidence_refs"] == [incident_evidence_id, evidence_id]
|
||||
else:
|
||||
repository.reports[-1]["report"]["evidence_refs"] = [evidence_id]
|
||||
result = await service.complete_run(task["task_id"], repository.run["run_id"], AgentRunCommand(
|
||||
request_id=uuid4(), expected_revision=repository.run["revision"], source_session_id="session-1"))
|
||||
assert result.status == "completed" and result.handling_context["phase"] == "completed"
|
||||
|
||||
Reference in New Issue
Block a user