Files
next-tjwater-server/tests/unit/test_replay_flushing.py
T
jiang b45c5c6f46
Generic Container CI/CD / test-build-publish (push) Successful in 4m31s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 4m31s
feat(investigations): refine report authoring and replay flushing
2026-11-14 23:00:15 +08:00

92 lines
4.6 KiB
Python

import asyncio
from contextlib import asynccontextmanager
from types import SimpleNamespace
from unittest.mock import AsyncMock
from uuid import uuid4
import pytest
from fastapi import HTTPException
from app.mcp import analysis_service, query_service
from app.mcp.analysis_models import FlushingInput
from app.mcp.calculation_jobs import should_queue
from app.mcp.catalog import TOOLS
def parameters(**changes):
return FlushingInput.model_validate({"context_handle": "a" * 43,
"start_time": "2026-07-16T08:00:00+08:00", "duration": 1800,
"output_node_ids": ["J1"], "drainage_node_id": "J2", "flushing_flow_m3h": 12,
"opened_valves": ["V-in"], "closed_valves": ["V-user"], **changes})
def context(monkeypatch):
monkeypatch.setattr(query_service, "permissions_for_context", lambda _: {"simulation.run", "scada.view"})
@asynccontextmanager
async def routing():
yield
scope = SimpleNamespace(connection=SimpleNamespace(commit=AsyncMock()),
project=SimpleNamespace(project_code="owned", project_role="member"), user=SimpleNamespace(username="actor"),
simulation_routing=routing)
binding = SimpleNamespace(tool_cap=["analyze_flushing"], permission_cap=["simulation.run", "scada.view"],
purpose="task_execution", task_id=uuid4(), task_scope_cap=[],
replay_context={"knowledge_cutoff": "2026-07-16T08:00:00+08:00"})
return scope, binding
def test_replay_flushing_uses_the_model_only_adapter_and_retains_prediction_boundary(monkeypatch):
scope, binding = context(monkeypatch)
worker = AsyncMock(return_value={"flushing_plan": {"water_quality_clearance_verified": False}})
monkeypatch.setattr(analysis_service, "calculate_replay_flushing", worker)
result = asyncio.run(query_service.execute_query(scope, binding, TOOLS["analyze_flushing"], parameters()))
assert result.origin == "synthetic" and "预测" in result.data_gaps[0]
assert result.data["result"]["flushing_plan"]["water_quality_clearance_verified"] is False
assert result.data["parameters"]["opened_valves"] == ["V-in"]
scope.connection.commit.assert_awaited_once()
worker.assert_awaited_once()
assert worker.await_args.kwargs == {"network": "owned", "username": "actor"}
assert should_queue(TOOLS["analyze_flushing"], parameters())
@pytest.mark.parametrize("problem", ["future_start", "reader", "permission", "tool_cap"])
def test_replay_flushing_preserves_time_permissions_and_reader_boundaries(monkeypatch, problem):
scope, binding = context(monkeypatch)
args = parameters()
if problem == "future_start": args = parameters(start_time="2026-07-16T08:15:00+08:00")
if problem == "reader": binding.purpose = "task_reader"
if problem == "permission": binding.permission_cap.remove("scada.view")
if problem == "tool_cap": binding.tool_cap.clear()
with pytest.raises(HTTPException) as caught:
asyncio.run(query_service.authorize_query(scope, binding, TOOLS["analyze_flushing"], args))
assert caught.value.status_code == (422 if problem == "future_start" else 403)
def test_model_only_flushing_and_native_runner_both_receive_empty_scada_mappings(monkeypatch):
from app.services import simulation_scenarios as scenarios
captures = []
native = scenarios.flushing_analysis.__wrapped__
monkeypatch.setattr("app.native.wndb.model.options.get_option", lambda _: {"UNITS": "LPS"})
def isolated(**kwargs):
captures.append(kwargs)
return native(**kwargs, _temporary_project="temporary")
monkeypatch.setattr(scenarios, "flushing_analysis", isolated)
monkeypatch.setattr(scenarios, "is_junction", lambda *_: True)
monkeypatch.setattr(scenarios, "get_option", lambda _: {"UNITS": "LPS"})
monkeypatch.setattr(scenarios, "get_time", lambda _: {"HYDRAULIC TIMESTEP": "00:15:00"})
monkeypatch.setattr(scenarios, "get_demand", lambda *_: {"demands": []})
for name in ("add_pattern", "set_demand", "set_option"):
monkeypatch.setattr(scenarios, name, lambda *_: None)
monkeypatch.setattr(scenarios.simulation, "run_simulation", lambda **kw: captures.append(kw) or {
"node_results": [], "link_results": []})
output = asyncio.run(analysis_service.calculate_replay_flushing(parameters(), network="owned", username="actor"))
assert len(captures) == 2
assert captures[0]["scada_mappings"] is captures[1]["scada_mappings"]
assert all(not values for values in vars(captures[0]["scada_mappings"]).values())
assert captures[1]["name"] == "temporary" and captures[1]["persist_results"] is False
assert output["flushing_plan"]["boundary_source"] == "model_only_no_live_scada"