from datetime import datetime, timedelta, timezone from uuid import uuid4 import pytest from app.services.operational_recovery import accept_completion, evaluate_recovery, track_work_order from app.services.operational_tasks import OperationalTaskService INCIDENT = datetime(2026, 7, 16, 0, tzinfo=timezone.utc) RETURNED = INCIDENT + timedelta(hours=1) def observations(): rows = [] for sample in (1, 2, 3): at = RETURNED + timedelta(minutes=15 * sample) for device in ("P1", "F1"): for day in range(1, 16): rows.append({"time": at - timedelta(days=day), "device_id": device, "monitored_value": 100.}) rows.append({"time": at, "device_id": device, "monitored_value": 100.}) return rows def evaluate(rows): return evaluate_recovery(rows, device_ids=["P1", "F1"], returned_at=RETURNED, incident_at=INCIDENT, interval_seconds=900) def test_recovery_uses_nearest_post_receipt_sample_and_pre_incident_reference(): result = evaluate(observations()) assert result["status"] == "passed" assert result["required_samples"] == 1 assert [item["at"] for item in result["samples"]] == [(RETURNED + timedelta(minutes=15)).isoformat()] assert all(check["normal_day_count"] == 15 for sample in result["samples"] for check in sample["devices"]) @pytest.mark.parametrize("problem", ["missing_device", "low_pressure", "high_flow", "nan", "bad_quality", "missing_baseline", "gap"]) def test_incomplete_or_abnormal_data_never_proves_recovery(problem): rows = observations() current = [row for row in rows if row["time"] > RETURNED] for row in current: if problem == "missing_device" and row["device_id"] == "P1": rows.remove(row) elif problem in {"low_pressure", "high_flow", "nan"}: row["monitored_value"] = {"low_pressure": 60., "high_flow": 130., "nan": float("nan")}[problem] elif problem == "bad_quality": row["quality"] = "invalid" elif problem == "gap": row["time"] += timedelta(minutes=15 * current.index(row)) if problem == "missing_baseline": rows = [row for row in rows if row["time"] >= INCIDENT - timedelta(days=14)] assert evaluate(rows)["status"] == "waiting" def test_samples_at_or_before_return_time_cannot_close_incident(): rows = [row for row in observations() if row["time"] < INCIDENT] rows.extend({"time": RETURNED, "device_id": device, "monitored_value": 100.} for device in ("P1", "F1")) assert evaluate(rows)["status"] == "waiting" def test_nearest_sample_does_not_wait_for_monitoring_interval_or_use_later_data(): rows = [] for minute in range(5, 36, 5): at = RETURNED + timedelta(minutes=minute) for device in ("P1", "F1"): rows.extend({"time": at - timedelta(days=day), "device_id": device, "monitored_value": 100.} for day in range(1, 16)) rows.append({"time": at, "device_id": device, "monitored_value": 100.}) first = evaluate(rows) assert first["status"] == "passed" assert first["verified_through"] == (RETURNED + timedelta(minutes=5)).isoformat() next(row for row in rows if row["time"] == RETURNED + timedelta(minutes=25) and row["device_id"] == "P1")["monitored_value"] = 50. assert evaluate(rows) == first @pytest.mark.parametrize("problem", ["abnormal", "invalid", "null", "missing_device"]) def test_nearest_bad_sample_is_not_replaced_by_later_normal_samples(problem): rows = observations() nearest = RETURNED + timedelta(minutes=15) row = next(row for row in rows if row["time"] == nearest and row["device_id"] == "P1") if problem == "missing_device": rows.remove(row) elif problem == "invalid": row["quality"] = "invalid" else: row["monitored_value"] = 50. if problem == "abnormal" else None result = evaluate(rows) assert result["status"] == "waiting" assert result["sample"]["at"] == nearest.isoformat() def test_off_grid_feedback_uses_first_later_sample_even_without_monitoring_interval(): result = evaluate_recovery(observations(), device_ids=["P1", "F1"], returned_at=RETURNED + timedelta(minutes=7), incident_at=INCIDENT, interval_seconds=0) assert result["status"] == "passed" assert result["verified_through"] == (RETURNED + timedelta(minutes=15)).isoformat() @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}}) assert OperationalTaskService._investigation_status([], task) == "waiting_human" assert not accept_completion(task, {"source_event_id": "peripheral_sample:unrelated"}) feedback = {"source_event_id": "peripheral_sample:one", "status": "completed", "source_revision": 2, "occurred_at": RETURNED.isoformat()} assert accept_completion(task, feedback) assert task["handling_context"]["phase"] == "waiting_work_order" accept_completion(task, {**feedback, "source_event_id": "peripheral_sample:two"}) assert task["handling_context"]["phase"] == "waiting_scada" task["handling_context"]["phase"] = "verification_ready" task["handling_context"].update(verification_evidence_id="previous-proof", last_check={"status": "passed"}) accept_completion(task, {**feedback, "source_revision": 3}) assert task["handling_context"]["phase"] == "verification_ready" assert OperationalTaskService._investigation_status([], task) == "completed" corrected = RETURNED + timedelta(hours=1) accept_completion(task, {**feedback, "source_revision": 4, "occurred_at": corrected.isoformat()}) 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(): from pydantic import ValidationError from samples.peripheral.schemas import UpdateWorkOrder body = {"request_id": uuid4(), "expected_revision": 1, "status": "completed", "progress_note": "演示抢修完成并恢复供水", "occurred_at": RETURNED.isoformat()} assert UpdateWorkOrder.model_validate(body).occurred_at == RETURNED with pytest.raises(ValidationError): UpdateWorkOrder.model_validate({**body, "progress_note": None}) with pytest.raises(ValidationError): UpdateWorkOrder.model_validate({**body, "occurred_at": "2026-07-16T09:00:00"})