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:
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user