refactor(backend)!: separate algorithm and data layers

Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories.

Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage.

BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
This commit is contained in:
2026-09-04 17:30:55 +08:00
parent 9b095c7439
commit 5966d039de
91 changed files with 1418 additions and 4020 deletions
+67 -94
View File
@@ -25,15 +25,6 @@ def _load_simulation_module(monkeypatch):
seconds = parts[2] if len(parts) == 3 else 0
return hours * 3600 + minutes * 60 + seconds
install_stub(
monkeypatch,
"app.services.time_api",
{
"parse_aware_time": parse_aware_time,
"parse_clock_duration_seconds": parse_clock_duration_seconds,
"parse_utc_time": parse_utc_time,
},
)
install_stub(
monkeypatch,
"app.services.simulation",
@@ -62,42 +53,22 @@ def _load_simulation_module(monkeypatch):
"dump_output": lambda output: f"dump::{output}",
},
)
install_stub(monkeypatch, "app.algorithms", package=True)
install_stub(monkeypatch, "app.algorithms.simulation", package=True)
install_stub(
monkeypatch,
"app.algorithms.simulation.scenarios",
"app.services.simulation_scenarios",
{
"burst_analysis": lambda *args, **kwargs: "burst",
"valve_close_analysis": lambda *args, **kwargs: "valve",
"flushing_analysis": lambda *args, **kwargs: "flush",
"contaminant_simulation": lambda *args, **kwargs: "contaminant",
"age_analysis": lambda *args, **kwargs: "age",
"pressure_regulation": lambda *args, **kwargs: "pressure",
},
)
install_stub(
monkeypatch,
"app.algorithms.sensor",
{
"pressure_sensor_placement_sensitivity": lambda *args, **kwargs: [],
"pressure_sensor_placement_kmeans": lambda *args, **kwargs: [],
},
)
install_stub(
monkeypatch,
"app.services.network_import",
{"network_update": lambda *args, **kwargs: "updated"},
)
install_stub(
monkeypatch,
"app.services.simulation_ops",
{
"project_management": lambda *args, **kwargs: "managed",
"scheduling_simulation": lambda *args, **kwargs: "scheduled",
"daily_scheduling_simulation": lambda *args, **kwargs: "daily",
},
)
install_stub(
monkeypatch,
"app.services.valve_isolation",
@@ -126,78 +97,52 @@ def test_run_project_endpoint_returns_plain_text(monkeypatch):
assert response.text == "report::demo"
def test_scheduling_analysis_maps_request_body(monkeypatch):
def test_removed_legacy_simulation_workflows_are_not_exposed(monkeypatch):
module = _load_simulation_module(monkeypatch)
captured = {}
def fake_schedule(network, start_time, pump_control, tank_id, water_plant_output_id, time_delta):
captured["args"] = (
network,
start_time,
pump_control,
tank_id,
water_plant_output_id,
time_delta,
)
return "scheduled"
monkeypatch.setattr(module, "scheduling_simulation", fake_schedule)
client = TestClient(build_test_app(module.router, "/api/v1"))
response = client.post(
"/api/v1/scheduling-analyses",
json={
"network": "demo",
"start_time": "2025-01-01T08:00:00+08:00",
"pump_control": {"P1": [1, 0, 1]},
"tank_id": "T1",
"water_plant_output_id": "R1",
},
)
assert response.status_code == 200
assert response.json() == "scheduled"
assert captured["args"] == (
"demo",
"2025-01-01T08:00:00+08:00",
{"P1": [1, 0, 1]},
"T1",
"R1",
300,
)
def test_project_management_maps_named_arguments(monkeypatch):
module = _load_simulation_module(monkeypatch)
captured = {}
def fake_project_management(**kwargs):
captured.update(kwargs)
return "managed"
monkeypatch.setattr(module, "project_management", fake_project_management)
client = TestClient(build_test_app(module.router, "/api/v1"))
response = client.post(
for path in (
"/api/v1/project-managements",
json={
"/api/v1/scheduling-analyses",
"/api/v1/daily-scheduling-analyses",
"/api/v1/water-age-analyses",
"/api/v1/inp-runs",
"/api/v1/outputs",
"/api/v1/pump-failure-events",
):
assert client.post(path).status_code == 404
def test_removed_basic_pressure_regulation_is_not_exposed(monkeypatch):
module = _load_simulation_module(monkeypatch)
client = TestClient(build_test_app(module.router, "/api/v1"))
response = client.post(
"/api/v1/pressure-regulation-calculations",
params={
"network": "demo",
"start_time": "2025-01-01T08:00:00+08:00",
"pump_control": {"P1": [1]},
"tank_init_level": {"T1": 10.0},
"region_demand": {"R1": 20.0},
"target_node": "J1",
"target_pressure": 50,
},
)
assert response.status_code == 200
assert response.json() == "managed"
assert captured == {
"prj_name": "demo",
"start_datetime": "2025-01-01T08:00:00+08:00",
"pump_control": {"P1": [1]},
"tank_initial_level_control": {"T1": 10.0},
"region_demand_control": {"R1": 20.0},
}
assert response.status_code == 404
def test_pressure_regulation_requires_scheme_name(monkeypatch):
module = _load_simulation_module(monkeypatch)
client = TestClient(build_test_app(module.router, "/api/v1"))
response = client.post(
"/api/v1/pressure-regulation-analyses",
json={
"network": "demo",
"start_time": "2025-01-02T03:04:05+08:00",
"pump_control": {},
},
)
assert response.status_code == 422
def test_run_simulation_manually_by_date_uses_utc_aware_timestamps(monkeypatch):
@@ -353,6 +298,34 @@ def test_burst_endpoint_passes_current_username(monkeypatch):
assert captured["username"] == "alice"
def test_burst_endpoint_rejects_mismatched_pipe_and_size_counts(monkeypatch):
module = _load_simulation_module(monkeypatch)
called = False
def fake_burst_analysis(**kwargs):
nonlocal called
called = True
monkeypatch.setattr(module, "burst_analysis", fake_burst_analysis)
client = _build_authenticated_client(module)
response = client.post(
"/api/v1/burst-analyses",
params=[
("network", "demo"),
("modify_pattern_start_time", "2025-01-02T03:04:05+08:00"),
("burst_ID", "P1"),
("burst_ID", "P2"),
("burst_size", "10.0"),
("modify_total_duration", "900"),
("scheme_name", "burst_case_01"),
],
)
assert response.status_code == 422
assert called is False
def test_flushing_endpoint_passes_required_scheme_name(monkeypatch):
module = _load_simulation_module(monkeypatch)
captured = {}