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