feat(scada): add calibrated monitoring and incident recovery

This commit is contained in:
2026-11-07 19:44:58 +08:00
parent 26fa5d1909
commit ff8aba4964
109 changed files with 9165 additions and 629 deletions
+44 -1
View File
@@ -15,9 +15,10 @@ from app.mcp.work_order_skills import read_work_order_skill
@pytest.mark.parametrize("workflow,name", [
("burst_report", "burst-report"),
("burst_report", "burst-event-handling"),
("water_quality_complaint", "water-quality-complaint"),
("general", "appointment-follow-up"),
("scada_anomaly", "scada-anomaly"),
])
def test_skill_descriptor_and_body_have_same_version_and_digest(workflow, name):
descriptor = read_work_order_skill(workflow)
@@ -36,6 +37,18 @@ def test_unknown_workflow_never_falls_back_or_reads_an_arbitrary_path(workflow):
assert read_work_order_skill(workflow)["available"] is False
def test_work_order_handling_and_legacy_scada_replay_receive_separate_skills():
handling = read_work_order_skill("burst_report", include_content=True)
replay = read_work_order_skill("burst_report", replay_context={"replay_id": "r"}, include_content=True)
assert handling["name"] == "burst-event-handling"
assert replay["name"] == "scada-anomaly"
assert replay["workflow"] == "burst_report"
assert "影响说明" in handling["instructions"]
assert "supply_affected_nodes" in handling["instructions"]
assert "run_burst_location" not in handling["instructions"]
assert "run_burst_location" in replay["instructions"]
def test_task_skill_uses_persisted_workflow_and_preserves_task_scope(monkeypatch):
task_id, project_id = uuid4(), uuid4()
reader = AsyncMock(return_value={"task": {"workflow": "burst_report"}})
@@ -71,3 +84,33 @@ def test_task_skill_uses_persisted_workflow_and_preserves_task_scope(monkeypatch
def test_skill_input_cannot_select_another_workflow_or_partial_instructions(extra):
with pytest.raises(ValueError):
TaskContextInput(context_handle="a" * 43, section="skill", **extra)
def test_replay_context_advertises_demo_location_without_revealing_truth(monkeypatch):
from app.services import burst_location_demo as demo
monkeypatch.setattr(query_service.reads, "task_context_summary", AsyncMock(
return_value={"task": {"workflow": "scada_anomaly"}}))
monkeypatch.setattr(query_service, "permissions_for_context", lambda _: {"operational_task.view"})
scope = SimpleNamespace(connection=object(), project=SimpleNamespace(project_code="tjwater2_v2"))
binding = BoundContext(
context_handle="a" * 43, project_id=uuid4(), subject="u", client_id="host",
native_session_id="s", purpose="task_execution", task_id=uuid4(),
tool_cap=["get_task_context", "run_burst_location"], permission_cap=["operational_task.view"],
expires_at=datetime.now(timezone.utc),
replay_context={"replay_id": "r", "knowledge_cutoff": demo.EVENT_START.isoformat(), "batch_sequence": 3},
)
for section in ("summary", "skill"):
result = asyncio.run(query_service.execute_query(scope, binding, TOOLS["get_task_context"],
TaskContextInput(context_handle=binding.context_handle, section=section)))
capability = result.data["location_demo"]
assert capability["available"] is True
assert capability["tool"] == "run_burst_location"
assert capability["burst_leakage_required"] is False
assert datetime.fromisoformat(capability["scada_burst_end"]) <= demo.EVENT_START
assert all(key not in json.dumps(capability) for key in (demo.REFERENCE_PIPE, demo.REFERENCE_NODE, demo.CANDIDATE_PIPE))
binding.replay_context["knowledge_cutoff"] = "2026-07-15T23:45:00Z"
result = asyncio.run(query_service.execute_query(scope, binding, TOOLS["get_task_context"],
TaskContextInput(context_handle=binding.context_handle)))
assert result.data["location_demo"]["available"] is False