feat(scada): add calibrated monitoring and incident recovery
This commit is contained in:
@@ -12,7 +12,7 @@ from app.mcp.analysis_models import BurstLocationInput
|
||||
from app.mcp.catalog import TOOLS
|
||||
from app.mcp.models import BoundContext
|
||||
from app.mcp import query_service
|
||||
from app.services.scada_replay_queries import replay_burst_location_inputs
|
||||
from app.services.scada_replay_queries import replay_burst_location_inputs, replay_snapshot_rows
|
||||
|
||||
|
||||
START = datetime(2026, 10, 8, tzinfo=timezone.utc)
|
||||
@@ -85,6 +85,60 @@ def test_replay_location_builds_series_only_from_frozen_outputs():
|
||||
assert connection.statements[1][1][2] == 9
|
||||
|
||||
|
||||
def test_replay_queries_frozen_history_with_project_replay_and_batch_bounds():
|
||||
project_id, replay_id = uuid4(), str(uuid4())
|
||||
at = START - timedelta(days=7)
|
||||
run = {"start_time": START.isoformat(), "config": {"device_ids": ["P1"]},
|
||||
"baseline": {"start_time": (START - timedelta(days=15)).isoformat()}}
|
||||
reading = {"time": at.isoformat(), "device_id": "P1", "monitored_value": 23.,
|
||||
"cleaned_value": 23., "quality": "valid", "reason": "冻结历史基线"}
|
||||
connection = Connection([[{"document": run}], [{"reading": reading}]])
|
||||
context = {"replay_id": replay_id, "knowledge_cutoff": START.isoformat(), "batch_sequence": 9}
|
||||
result = asyncio.run(replay_snapshot_rows(connection, project_id, context, ["P1"], at, at, 10))
|
||||
assert result == [reading]
|
||||
statement, params = connection.statements[1]
|
||||
assert "b.stage='detected'" in statement and "b.sequence<=%s" in statement
|
||||
assert params[:4] == (str(project_id), replay_id, 9, ["P1"])
|
||||
assert params[4:6] == (at, at)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("defect,status", [("old_replay", 422), ("too_early", 422), ("future", 422), ("foreign_device", 403)])
|
||||
def test_replay_history_rejects_missing_freeze_or_scope_escape(defect, status):
|
||||
at = START - timedelta(days=7)
|
||||
run = {"start_time": START.isoformat(), "config": {"device_ids": ["P1"]},
|
||||
"baseline": {"start_time": (START - timedelta(days=15)).isoformat()}}
|
||||
if defect == "old_replay": run.pop("baseline")
|
||||
if defect == "too_early": at = START - timedelta(days=16)
|
||||
if defect == "future": at = START + timedelta(minutes=1)
|
||||
connection = Connection([[{"document": run}]])
|
||||
context = {"replay_id": str(uuid4()), "knowledge_cutoff": START.isoformat(), "batch_sequence": 9}
|
||||
with pytest.raises(HTTPException) as caught:
|
||||
asyncio.run(replay_snapshot_rows(connection, uuid4(), context,
|
||||
["P2" if defect == "foreign_device" else "P1"], at, at, 10))
|
||||
assert caught.value.status_code == status
|
||||
assert len(connection.statements) <= 1
|
||||
|
||||
|
||||
def test_location_accepts_frozen_pre_replay_normal_window():
|
||||
replay_id = str(uuid4())
|
||||
normal = START - timedelta(days=7)
|
||||
run = {"start_time": START.isoformat(),
|
||||
"baseline": {"start_time": (START - timedelta(days=15)).isoformat()},
|
||||
"devices": [{"device_id": "P1", "node_id": "J1"}, {"device_id": "F1", "link_id": "L1"}],
|
||||
"monitoring_config": {"pressure_device_ids": ["P1"], "outlet_flow_device_ids": ["F1"]}}
|
||||
readings = [{"reading": {"time": at.isoformat(), "device_id": device,
|
||||
"cleaned_value": value, "quality": "valid"}}
|
||||
for at, device, value in [(normal, "P1", 100), (normal, "F1", 50),
|
||||
(START + timedelta(minutes=15), "P1", 70),
|
||||
(START + timedelta(minutes=15), "F1", 80)]]
|
||||
connection = Connection([[{"document": run}], readings])
|
||||
context = {"replay_id": replay_id, "knowledge_cutoff": (START + timedelta(hours=1)).isoformat(), "batch_sequence": 9}
|
||||
result = asyncio.run(replay_burst_location_inputs(connection, uuid4(), context,
|
||||
_args(scada_normal_start=normal, scada_normal_end=normal + timedelta(minutes=14))))
|
||||
assert result["normal_pressure"] == {"J1": 100}
|
||||
assert result["burst_flow"] == {"L1": 80}
|
||||
|
||||
|
||||
def test_replay_location_authorization_requires_explicit_bounded_normal_window(monkeypatch):
|
||||
binding = BoundContext(
|
||||
context_handle="a" * 43, project_id=uuid4(), subject="u", client_id="host",
|
||||
|
||||
Reference in New Issue
Block a user