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.
49 lines
2.9 KiB
Python
49 lines
2.9 KiB
Python
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
|