feat(investigations): refine report authoring and replay flushing
Generic Container CI/CD / test-build-publish (push) Successful in 4m31s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 4m31s

This commit is contained in:
2026-11-14 23:00:15 +08:00
parent 2e4f6cb93b
commit b45c5c6f46
27 changed files with 1125 additions and 94 deletions
+78
View File
@@ -0,0 +1,78 @@
from datetime import datetime, timezone
from uuid import uuid4
import pytest
from pydantic import ValidationError
from app.mcp.analysis_models import FlushingInput
from app.mcp.extended_models import SavedScenarioInput
from app.mcp.mutation_service import saved_scenario
from app.mcp.scenario_service import calculate_scenario, flushing_valve_controls
def parameters(**changes):
return {"context_handle": "a" * 43, "start_time": datetime(2026, 7, 16, tzinfo=timezone.utc),
"duration": 1800, "output_node_ids": ["J1"], "drainage_node_id": "J2",
"flushing_flow_m3h": 12, **changes}
@pytest.mark.parametrize("changes", [
{"closed_valves": ["V1"], "opened_valves": ["V1"]},
{"closed_valves": ["V1", "V1"]},
{"opened_valves": ["V1", "V1"]},
])
def test_conflicting_or_duplicate_valves_are_rejected_for_preview_and_saved_scenarios(changes):
values = parameters(**changes)
with pytest.raises(ValidationError):
FlushingInput.model_validate(values)
with pytest.raises(ValidationError):
SavedScenarioInput.model_validate({"context_handle": values.pop("context_handle"),
"request_id": uuid4(), "name": "冲洗", "scenario": "flushing", "parameters": values})
def test_preview_and_saved_flushing_apply_identical_open_and_closed_states(monkeypatch):
from app.services import simulation_scenarios
calls = []
monkeypatch.setattr(simulation_scenarios, "flushing_analysis", lambda **kw: calls.append(kw) or {
"node_results": [{"node": "J1", "result": []}], "link_results": []})
monkeypatch.setattr("app.native.wndb.model.options.get_option", lambda _: {"UNITS": "LPS"})
args = FlushingInput.model_validate(parameters(opened_valves=["V-in"], closed_valves=["V-user"]))
output = calculate_scenario("analyze_flushing", args.model_dump(exclude={"context_handle", "timeout_seconds"}),
network="owned", username="actor")
values = SavedScenarioInput.model_validate({"context_handle": args.context_handle,
"request_id": uuid4(), "name": "分区冲洗", "scenario": "flushing",
"parameters": args.model_dump(exclude={"context_handle", "timeout_seconds"})})
saved_scenario("owned", "actor", values)
for call in calls:
assert call["valve_control"] == {"V-in": {"status": "OPEN"}, "V-user": {"status": "CLOSED"}}
assert "modify_valve_opening" not in call
assert call["drainage_node_ID"] == "J2" and call["flushing_flow"] == 12
assert calls[0]["persist_results"] is False and calls[1]["persist_results"] is True
assert output["flushing_plan"] == {
"opened_valves": ["V-in"], "closed_valves": ["V-user"], "drainage_node_id": "J2",
"flushing_flow_m3h": 12, "estimated_discharge_volume_m3": 6,
"state_basis": "independent_current_model_scenario", "result_scope": "hydraulic_only",
"boundary_source": "model_with_start_time_scada",
"field_execution_verified": False, "water_quality_clearance_verified": False,
}
def test_legacy_closed_only_inputs_remain_valid():
args = FlushingInput.model_validate(parameters(closed_valves=["V1"]))
assert args.opened_valves == []
assert flushing_valve_controls(args.model_dump()) == {"V1": {"status": "CLOSED"}}
def test_explicit_opening_reopens_a_closed_valve_without_overwriting_its_setting(monkeypatch):
from app.services import simulation
model = {"V-in": {"link": "V-in", "status": "CLOSED", "setting": 15},
"V-user": {"link": "V-user", "status": "ACTIVE", "setting": 23}}
monkeypatch.setattr(simulation, "get_status", lambda project, valve: dict(model[valve]))
monkeypatch.setattr(simulation, "set_status", lambda project, changes: model.update({
row["link"]: row for row in changes.operations}))
simulation._apply_valve_control("temporary", flushing_valve_controls({
"opened_valves": ["V-in"], "closed_valves": ["V-user"]}))
assert model["V-in"] == {"link": "V-in", "status": "OPEN", "setting": 15}
assert model["V-user"] == {"link": "V-user", "status": "CLOSED", "setting": 23}