Files
next-tjwater-server/tests/unit/test_scada_replay_processing.py
T
jiang 26fa5d1909
Generic Container CI/CD / test-build-publish (push) Successful in 3m39s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 3m39s
feat(scada): add project monitoring replay
2026-11-04 14:27:21 +08:00

67 lines
3.1 KiB
Python

from datetime import datetime, timedelta, timezone
from uuid import uuid4
from app.domain.schemas.scada_replay import ScadaReplayCreate
from app.services.scada_replay_processing import prepare_cleaning, detect_frame
START = datetime(2026, 10, 8, tzinfo=timezone.utc)
def config(**changes):
return ScadaReplayCreate.model_validate({
"request_id": str(uuid4()), "start_time": START, "end_time": START + timedelta(hours=2),
"device_ids": ["P1"], "rules": [{"rule_id": "pressure", "device_ids": ["P1"],
"kind": "relative_change", "direction": "decrease", "threshold": 30, "unit": "m"}],
**changes,
})
def rows(value, minute=0):
return [{"time": START + timedelta(minutes=minute), "device_id": "P1", "monitored_value": value, "source_hash": "h"}]
def test_quality_does_not_repair_a_real_pressure_drop_or_reverse_flow():
for value in (0, -5, 1):
result = prepare_cleaning(rows(value), config())
assert result[0]["quality"] == "valid"
assert result[0]["cleaned_value"] == value
invalid = prepare_cleaning(rows(-5), config(quality_ranges={"P1": {"lower": 0}}))
assert invalid[0]["quality"] == "invalid"
assert invalid[0]["monitored_value"] == -5
assert invalid[0]["cleaned_value"] is None
def test_persistent_drop_is_one_episode_and_recovery_then_drop_is_new_episode():
cfg = config()
doc = {"replay_id": "run", "histories": {"P1": [10, 10, 10, 10]}}
all_events = []
for minute, value in enumerate((1, 1, None, 10, 10, 1)):
doc, events = detect_frame(doc, prepare_cleaning(rows(value, minute), cfg), cfg, START + timedelta(minutes=minute))
all_events.extend(events)
business = [event for event in all_events if event.get("category") != "data_quality"]
quality = [event for event in all_events if event.get("category") == "data_quality"]
assert [event["change"] for event in business] == ["raised", "resolved", "raised"]
assert [event["change"] for event in quality] == ["raised", "resolved"]
assert business[0]["event_id"] != business[2]["event_id"]
assert doc["anomaly_count"] == 3
assert doc["processed_count"] == 6
def test_water_quality_zero_baseline_uses_explicit_bounds():
cfg = config(rules=[{"rule_id": "tracer", "device_ids": ["P1"], "kind": "bounds", "upper": 0.1, "unit": "m"}])
doc = {"replay_id": "run"}
doc, events = detect_frame(doc, prepare_cleaning(rows(0), cfg), cfg, START)
assert events == []
_, events = detect_frame(doc, prepare_cleaning(rows(0.4, 15), cfg), cfg, START + timedelta(minutes=15))
assert events[0]["change"] == "raised"
def test_empty_clock_ticks_do_not_break_consecutive_observations():
cfg = config(rules=[{"rule_id": "p", "device_ids": ["P1"], "kind": "bounds", "upper": 10,
"unit": "m", "consecutive_samples": 2}])
doc, events = detect_frame({"replay_id": "run"}, prepare_cleaning(rows(20), cfg), cfg, START)
assert events == []
doc, _ = detect_frame(doc, [], cfg, START + timedelta(minutes=1))
_, events = detect_frame(doc, prepare_cleaning(rows(20, 15), cfg), cfg, START + timedelta(minutes=15))
assert events[0]["change"] == "raised"