Add project-bound OAuth MCP queries and controlled analysis with scoped evidence, limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs. Default nearby searches to 200 meters and five results. Integrate investigation messaging/presentation, health/readiness and typed history; remove duplicate web search and fix online simulation and compressed baseline writes. Include project migrations 023-025; keep local credentials and demo seeding excluded. Validation: 319 focused pytest checks; real PostGIS temporary-table regression; read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
66 lines
3.0 KiB
Python
66 lines
3.0 KiB
Python
from datetime import datetime, timedelta
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from app.services import simulation
|
|
|
|
|
|
START = datetime.fromisoformat("2026-10-08T12:00:00+08:00")
|
|
|
|
|
|
def test_online_simulation_uses_realtime_steps_and_returns_requested_samples(monkeypatch):
|
|
calls = []
|
|
mappings = object()
|
|
monkeypatch.setattr(simulation, "get_time", lambda _: {"HYDRAULIC TIMESTEP": "0:15:00"})
|
|
monkeypatch.setattr(simulation, "query_corresponding_element_id_and_query_id", lambda _: mappings)
|
|
|
|
def run(**kwargs):
|
|
calls.append(kwargs)
|
|
return {
|
|
"units": {"pressure": "MTR", "flow": "LPS"},
|
|
"node_results": [{"node": "123422", "result": [{"pressure": len(calls)}]}, {"node": "other", "result": [{}]}],
|
|
"link_results": [{"link": "540672", "result": [{"flow": len(calls)}]}],
|
|
}
|
|
|
|
monkeypatch.setattr(simulation, "run_simulation", run)
|
|
result = simulation.run_online_simulation("online", START, 3600, output_node_ids=["123422"], output_link_ids=["540672"])
|
|
assert calls == [{
|
|
"name": "online", "simulation_type": "realtime",
|
|
"modify_pattern_start_time": (START + timedelta(minutes=15 * index)).isoformat(timespec="seconds"),
|
|
"scada_mappings": mappings,
|
|
} for index in range(4)]
|
|
assert result["node_results"] == [{"node": "123422", "result": [{"pressure": index} for index in range(1, 5)]}]
|
|
assert result["link_results"] == [{"link": "540672", "result": [{"flow": index} for index in range(1, 5)]}]
|
|
assert result["times"]["num_periods"] == 4
|
|
assert result["times"]["report_step"] == 900
|
|
assert result["times"]["sample_times"][-1] == "2026-10-08T12:45:00+08:00"
|
|
|
|
|
|
def test_online_simulation_rejects_invalid_step_before_calculating(monkeypatch):
|
|
monkeypatch.setattr(simulation, "get_time", lambda _: {"HYDRAULIC TIMESTEP": "0:00:00"})
|
|
run = Mock()
|
|
monkeypatch.setattr(simulation, "run_simulation", run)
|
|
with pytest.raises(ValueError, match="HYDRAULIC TIMESTEP"):
|
|
simulation.run_online_simulation("online", START, 3600)
|
|
run.assert_not_called()
|
|
|
|
|
|
def test_mcp_online_simulation_does_not_copy_a_database(monkeypatch):
|
|
from app.mcp.scenario_service import calculate_scenario
|
|
|
|
def reject_copy(*_):
|
|
pytest.fail("Online simulation must not clone the project database")
|
|
|
|
run = Mock(return_value={"node_results": [], "link_results": [], "times": {}, "units": {}})
|
|
monkeypatch.setattr("app.native.wndb.core.projects.temporary_project_database", reject_copy)
|
|
monkeypatch.setattr(simulation, "run_online_simulation", run, raising=False)
|
|
result = calculate_scenario("run_simulation", {
|
|
"start_time": START, "duration": 3600,
|
|
"output_node_ids": ["123422", "123425"], "output_link_ids": ["540672"],
|
|
}, network="online", username="actor")
|
|
run.assert_called_once_with("online", START, 3600, output_node_ids=["123422", "123425"], output_link_ids=["540672"])
|
|
assert result["input_source"] == "online_model_simulation"
|
|
assert result["persisted_baseline"] is True
|
|
assert result["persisted_scheme"] is False
|