fix(recovery): scope incident verification to relevant devices
Full-network browsing expanded persisted recovery gates to unrelated devices, while full fifteen-day reference reads exhausted the row budget. Rebuild legacy cached scopes from report devices and project monitoring roles, and query only the nearest post-feedback frame and its historical phase. Regression tests fail before the fix for both new and cached scopes and verify automatic Agent requeue after recovery.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.domain.schemas.project_settings import ProjectSettings
|
||||
from app.services.operational_recovery import reconcile_recoveries
|
||||
from app.services.operational_tasks import OperationalTaskService
|
||||
from tests.support.operational_task_repository import TaskRepository, _active_run, _context, _task_document
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("cached", [False, True])
|
||||
async def test_recovery_uses_report_devices_instead_of_all_browsed_network_devices(monkeypatch, cached):
|
||||
now = datetime(2026, 7, 16, 6, tzinfo=timezone.utc)
|
||||
incident = now - timedelta(hours=6)
|
||||
returned = now - timedelta(minutes=20)
|
||||
task = _task_document(_context(), now, lease=now + timedelta(minutes=1))
|
||||
task.update(workflow="scada_anomaly", status="waiting_human", lifecycle_status="waiting_human",
|
||||
current_report_revision=1, trigger={"kind": "event", "occurred_at": incident.isoformat(), "detail": "异常"},
|
||||
handling_context={"phase": "waiting_scada", "returned_at": returned.isoformat()})
|
||||
if cached:
|
||||
task["handling_context"].update(device_ids=["P1", "unrelated"], interval_seconds=900)
|
||||
repo = TaskRepository(task, _active_run(task, now))
|
||||
repo.get_latest_report = AsyncMock(return_value={"revision": 1, "report": {"scada_device_ids": ["P1"]}})
|
||||
repo.list_evidence = AsyncMock(return_value=[{"data_snapshot": {"source": "query_scada_readings",
|
||||
"data": {"data": [{"device_id": "P1"}, {"device_id": "unrelated"}]}}}])
|
||||
repo.recovery_candidates = AsyncMock(side_effect=lambda *_: [deepcopy(repo.task)]
|
||||
if repo.task["handling_context"]["phase"] == "waiting_scada" else [])
|
||||
service = OperationalTaskService(repo, _context(), set(), clock=lambda: now)
|
||||
at = returned + timedelta(minutes=5)
|
||||
rows = [{"time": at - timedelta(days=day), "device_id": "P1", "monitored_value": 100.}
|
||||
for day in range(1, 16)] + [{"time": at, "device_id": "P1", "monitored_value": 100.}]
|
||||
reader = AsyncMock(side_effect=lambda _, devices, *args, **kwargs: rows if devices == ["P1"] else [{}] * 100_001)
|
||||
monkeypatch.setattr("app.infra.db.timescaledb.repositories.scada.ScadaRepository.get_recovery_scada_readings", reader)
|
||||
settings = ProjectSettings(scada_monitoring_interval_seconds=900, scada_monitoring={"pressure_device_ids": ["P1"]})
|
||||
assert await reconcile_recoveries(service, SimpleNamespace(transaction=repo.transaction), settings) == 1
|
||||
assert repo.task["handling_context"]["device_ids"] == ["P1"]
|
||||
assert repo.task["handling_context"]["phase"] == "verification_ready"
|
||||
assert repo.run["status"] == "queued"
|
||||
assert await reconcile_recoveries(service, SimpleNamespace(transaction=repo.transaction), settings) == 0
|
||||
Reference in New Issue
Block a user