fix(scada): verify recovery after replay work-order completion
Generic Container CI/CD / test-build-publish (push) Successful in 3m58s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m58s

This commit is contained in:
2026-11-07 23:12:42 +08:00
parent 06d8539aa8
commit 2a7b96f813
7 changed files with 61 additions and 10 deletions
+9 -1
View File
@@ -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():
+35 -5
View File
@@ -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"