aps-agent/tests/golden/test_cp_marginal.py

765 lines
28 KiB
Python
Raw Normal View History

from __future__ import annotations
import copy
from datetime import date
import pytest
from fastapi.testclient import TestClient
from server.agent_core.async_jobs import JobCancelled, JobQueue
from server.aps_domain.cp_marginal import run_cp_marginal_resolve
from server.engines import get_engine
from server.engines.base import EngineParams
from server.engines.cp_engine import CpSatEngine
from server.engines.solver_process import (
PROTOCOL_VERSION,
SolverProcessError,
_validate_result_semantics,
run_cp_constraint_diagnostic,
)
from server.engines.solver_worker import _request_digest, _validate_request
from server.state.seed import build_demo_world, empty_world
from tests.auth_provider import install_test_auth
def _align_shift_calendar(world: dict, start_date: str) -> None:
rows = world.get("shiftCalendar") or []
if not rows:
return
source_start = min(date.fromisoformat(str(row["date"])) for row in rows)
offset = date.fromisoformat(start_date) - source_start
for row in rows:
row["date"] = (date.fromisoformat(str(row["date"])) + offset).isoformat()
def _tight_two_order_world() -> dict:
world = build_demo_world()
_align_shift_calendar(world, "2026-08-03")
orders = [row for row in world["salesOrders"] if row["items"][0]["productId"] == 1][:2]
for order in orders:
order["deliveryDate"] = "2026-08-03"
keep = {row["id"] for row in orders}
world["salesOrders"] = [row for row in world["salesOrders"] if row["id"] in keep]
world["constraintProfile"] = {
"profileId": "test-isolate-c2",
"constraints": {"C7_capacity": {"enabled": False}},
}
return world
def _tight_c2_exact_world() -> dict:
world = _tight_two_order_world()
routing_id = next(
row["id"] for row in world["routings"]
if row["productId"] == 1 and row["isDefault"]
)
world["routingSteps"] = [
row for row in world["routingSteps"]
if row["routingId"] == routing_id and row["sequenceNo"] == 1
]
return world
def _counter():
values: dict[str, int] = {}
def next_id(kind: str) -> int:
values[kind] = values.get(kind, 0) + 1
return values[kind]
return next_id
def _entries_and_params(world: dict, **overrides):
values = {
"orderIds": [],
"engineType": "CP",
"strategyTemplate": "COMPREHENSIVE",
"planningHorizonDays": 14,
"startDate": "2026-08-03",
"timeLimitSeconds": 4,
"constraints": {"capacity": False},
}
values.update(overrides)
params = EngineParams(**values)
entries, _, _ = CpSatEngine().collect_and_order(world, params)
return entries, params
def test_real_c2_removal_improves_optimal_cp_objective_without_mutating_world():
world = _tight_c2_exact_world()
before = copy.deepcopy(world)
report = run_cp_marginal_resolve(
world,
start_date="2026-08-03",
planning_horizon_days=3,
constraint_ids=["C2_no_overlap"],
time_limit_seconds=8,
)
assert world == before
assert report["status"] == "completed"
assert report["evaluations"] == 2
assert report["baseline"]["status"] == "OPTIMAL"
assert report["baseline"]["operation"] == "diagnose_constraint_baseline"
assert report["baseline"]["numSearchWorkers"] == 1
assert report["baseline"]["randomSeed"] == 0
row = report["rows"][0]
assert row["status"] == "available"
assert row["comparisonQuality"] == "exact-optimal"
assert row["objectiveImprovement"] == 2100.0
assert row["relaxed"]["operation"] == "diagnose_constraint_removal"
assert row["relaxed"]["relaxedConstraintIds"] == ["C2_no_overlap"]
assert row["relaxed"]["invocationId"] != report["baseline"]["invocationId"]
assert report["objectiveSpecDigest"]
assert report["worldDigest"] and report["entriesDigest"] and report["paramsDigest"]
def test_inactive_assumption_has_no_re_solve_row():
world = _tight_two_order_world()
for workstation in world["workstations"]:
workstation.pop("teamId", None)
world["teams"] = []
report = run_cp_marginal_resolve(
world,
start_date="2026-08-03",
constraint_ids=["C12_team"],
time_limit_seconds=4,
)
assert report["evaluations"] == 1
assert report["rows"][0]["status"] == "inactive"
assert report["baseline"]["constraintInstanceCounts"]["C12_team"] == 0
def test_diagnostic_invocation_id_prevents_same_snapshot_response_replay():
world = _tight_two_order_world()
entries, params = _entries_and_params(world)
_, first = run_cp_constraint_diagnostic(
world, entries, params, pipeline_label="REPLAY-BASELINE",
)
_, second = run_cp_constraint_diagnostic(
world, entries, params, pipeline_label="REPLAY-BASELINE",
)
assert first["objective"] == second["objective"]
assert first["solverProcess"]["invocationId"] != second["solverProcess"]["invocationId"]
assert first["solverProcess"]["requestId"] != second["solverProcess"]["requestId"]
assert first["solverProcess"]["protocolVersion"] == "aps.solver-process.v2"
def test_c11_removal_disables_start_gate_and_frozen_obstacle_presence():
world = build_demo_world()
rule_params = EngineParams(
orderIds=[], engineType="RULE", strategyTemplate="COMPREHENSIVE",
planningHorizonDays=14, startDate="2026-08-03",
)
get_engine("RULE").solve(world, rule_params, _counter())
for work_order in world["workOrders"][:3]:
work_order["isFrozen"] = True
entries, params = _entries_and_params(world, freezeWindowHours=24.0)
_, baseline = run_cp_constraint_diagnostic(
world, entries, params, pipeline_label="C11-BASELINE",
)
_, relaxed = run_cp_constraint_diagnostic(
world, entries, params, pipeline_label="C11-REMOVAL",
relaxed_constraint_id="C11_freeze",
)
assert baseline["constraintInstanceCounts"]["C11_freeze"] > baseline["frozenCount"]
baseline_new = [slot for slot in baseline["operationSlots"] if not slot["isFrozen"]]
relaxed_new = [slot for slot in relaxed["operationSlots"] if not slot["isFrozen"]]
assert baseline_new and all(slot["startMin"] >= 24 * 60 for slot in baseline_new)
assert relaxed_new and any(slot["startMin"] < 24 * 60 for slot in relaxed_new)
assert relaxed["relaxedConstraintIds"] == ["C11_freeze"]
assert relaxed["frozenCount"] == 0
assert not any(slot["isFrozen"] for slot in relaxed["operationSlots"])
def test_c2_removal_also_removes_frozen_obstacle_no_overlap():
world = build_demo_world()
world["workOrders"] = [
{
"id": index,
"orderNo": f"FROZEN-{index}",
"workstationId": 1,
"lineId": 1,
"plannedStartTime": "2026-08-03 08:00",
"plannedEndTime": "2026-08-03 10:00",
"isFrozen": True,
}
for index in (1, 2)
]
entries, params = _entries_and_params(world, freezeWindowHours=24.0)
_, baseline = run_cp_constraint_diagnostic(
world, entries, params, pipeline_label="C2-FROZEN-BASELINE",
)
_, relaxed = run_cp_constraint_diagnostic(
world, entries, params, pipeline_label="C2-FROZEN-REMOVAL",
relaxed_constraint_id="C2_no_overlap",
)
assert baseline["status"] == "INFEASIBLE"
assert relaxed["status"] in {"OPTIMAL", "FEASIBLE"}
assert relaxed["frozenCount"] == 0
assert not any(slot["isFrozen"] for slot in relaxed["operationSlots"])
def test_no_demand_is_explicit_and_does_not_spawn_solver():
world = empty_world()
result = run_cp_marginal_resolve(
world,
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
)
assert result["status"] == "no_demand"
assert result["evaluations"] == 0
assert result["rows"] == []
def test_explicit_empty_constraint_list_is_rejected():
with pytest.raises(ValueError, match="不能为空"):
run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=[],
)
def test_cancellation_contract_exposes_parent_supervision_latency():
report = run_cp_marginal_resolve(
empty_world(),
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
time_limit_seconds=4,
)
assert report["cancellationGranularity"] == "between-solves"
assert report["cancellationDoesNotInterruptActiveSolve"] is True
assert report["activeSolveSupervisionTimeoutSeconds"] == 16.0
assert report["cancellationCleanupTimeoutSeconds"] == 11.0
assert report["cancellationLatencyUpperBoundSeconds"] == 27.0
def test_cancellation_is_checked_after_baseline_before_partial_rows_escape():
world = _tight_two_order_world()
checks = 0
def cancel_after_baseline():
nonlocal checks
checks += 1
if checks == 2:
raise JobCancelled()
with pytest.raises(JobCancelled):
run_cp_marginal_resolve(
world,
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
time_limit_seconds=4,
cancel_check=cancel_after_baseline,
)
assert checks == 2
def _fake_meta(
*,
status: str,
objective: float | None,
bound: float | None,
relaxed: list[str],
) -> dict:
return {
"status": status,
"objective": objective,
"bestBound": bound,
"gap": None if objective is None else round(abs(objective - bound) / abs(objective), 6),
"assumptionConstraints": ["C2_no_overlap"],
"activeAssumptionConstraints": ["C2_no_overlap"],
"enforcedAssumptionConstraints": [] if relaxed else ["C2_no_overlap"],
"relaxedConstraintIds": relaxed,
"constraintInstanceCounts": {"C2_no_overlap": 1},
"diagnosticMode": True,
"numSearchWorkers": 1,
"randomSeed": 0,
"solverProcess": {
"requestId": "req",
"invocationId": "inv",
"operation": (
"diagnose_constraint_removal" if relaxed else "diagnose_constraint_baseline"
),
"runtimeIdentity": {"safe": True},
},
}
def _fake_c3_calendar(*, relaxed: bool, digest: str = "0" * 64) -> dict:
return {
"schemaVersion": "cp-calendar-segmented.v1",
"modelMode": "assumption-gated-dual-mode",
"pausePolicy": "calendar-boundary-only",
"anchor": "2026-08-03 08:00",
"horizonMinutes": 40320,
"coverageStart": "2026-08-03",
"coverageEnd": "2026-08-31",
"coverageComplete": True,
"normalizedCalendarDigest": digest,
"calendarBucketCount": 29,
"lineWindowCounts": {"1": 80},
"segmentIntervalCount": 160,
"segmentIntervalLimit": 50000,
"maxSegmentsPerOperation": 80,
"selectedMode": "continuous" if relaxed else "calendar-boundary-only",
"active": not relaxed,
}
def test_c3_calendar_topology_drift_is_solver_error(monkeypatch):
from server.aps_domain import cp_marginal
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
relaxed = relaxed_constraint_id is not None
meta = _fake_meta(
status="OPTIMAL", objective=100.0, bound=100.0,
relaxed=["C3_calendar"] if relaxed else [],
)
meta["assumptionConstraints"] = ["C3_calendar"]
meta["activeAssumptionConstraints"] = ["C3_calendar"]
meta["enforcedAssumptionConstraints"] = [] if relaxed else ["C3_calendar"]
meta["constraintInstanceCounts"] = {"C3_calendar": 1}
meta["c3Calendar"] = _fake_c3_calendar(
relaxed=relaxed, digest=("1" * 64 if relaxed else "0" * 64),
)
return [], meta
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_c2_exact_world(), start_date="2026-08-03",
constraint_ids=["C3_calendar"],
)
assert report["rows"][0]["status"] == "solver_error"
assert "模型实例清单不一致" in report["rows"][0]["reason"]
def test_feasible_comparison_reports_bounds_not_incumbent_improvement(monkeypatch):
from server.aps_domain import cp_marginal
calls = 0
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
nonlocal calls
calls += 1
if relaxed_constraint_id is None:
return [], _fake_meta(status="FEASIBLE", objective=100.0, bound=80.0, relaxed=[])
return [], _fake_meta(
status="FEASIBLE", objective=70.0, bound=50.0, relaxed=[relaxed_constraint_id],
)
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
)
row = report["rows"][0]
assert calls == 2
assert row["status"] == "bounded"
assert row["objectiveImprovement"] is None
assert row["incumbentDifference"] == 30.0
assert row["improvementLowerBound"] == 10.0
assert row["improvementUpperBound"] == 50.0
def test_feasible_to_infeasible_is_monotonicity_violation(monkeypatch):
from server.aps_domain import cp_marginal
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
if relaxed_constraint_id is None:
return [], _fake_meta(status="OPTIMAL", objective=100.0, bound=100.0, relaxed=[])
return [], _fake_meta(
status="INFEASIBLE", objective=None, bound=None, relaxed=[relaxed_constraint_id],
)
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
)
assert report["status"] == "monotonicity_violation"
assert report["rows"][0]["objectiveImprovement"] is None
def test_optimal_negative_improvement_is_monotonicity_violation(monkeypatch):
from server.aps_domain import cp_marginal
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
if relaxed_constraint_id is None:
return [], _fake_meta(status="OPTIMAL", objective=100.0, bound=100.0, relaxed=[])
return [], _fake_meta(
status="OPTIMAL", objective=110.0, bound=110.0, relaxed=[relaxed_constraint_id],
)
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
)
assert report["status"] == "monotonicity_violation"
assert report["rows"][0]["status"] == "monotonicity_violation"
assert report["rows"][0]["objectiveImprovement"] is None
def test_monotonicity_violation_suppresses_other_available_improvements(monkeypatch):
from server.aps_domain import cp_marginal
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
if relaxed_constraint_id is None:
meta = _fake_meta(status="OPTIMAL", objective=100.0, bound=100.0, relaxed=[])
meta["assumptionConstraints"] = ["C1_precedence", "C2_no_overlap"]
meta["activeAssumptionConstraints"] = ["C1_precedence", "C2_no_overlap"]
meta["enforcedAssumptionConstraints"] = ["C1_precedence", "C2_no_overlap"]
meta["constraintInstanceCounts"] = {"C1_precedence": 1, "C2_no_overlap": 1}
return [], meta
objective = 80.0 if relaxed_constraint_id == "C1_precedence" else 110.0
meta = _fake_meta(
status="OPTIMAL", objective=objective, bound=objective,
relaxed=[relaxed_constraint_id],
)
meta["assumptionConstraints"] = ["C1_precedence", "C2_no_overlap"]
meta["activeAssumptionConstraints"] = ["C1_precedence", "C2_no_overlap"]
meta["enforcedAssumptionConstraints"] = [
value for value in ("C1_precedence", "C2_no_overlap")
if value != relaxed_constraint_id
]
meta["constraintInstanceCounts"] = {"C1_precedence": 1, "C2_no_overlap": 1}
return [], meta
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=["C1_precedence", "C2_no_overlap"],
)
assert report["status"] == "monotonicity_violation"
by_id = {row["constraintId"]: row for row in report["rows"]}
assert by_id["C1_precedence"]["status"] == "suppressed"
assert by_id["C1_precedence"]["objectiveImprovement"] is None
assert by_id["C2_no_overlap"]["status"] == "monotonicity_violation"
@pytest.mark.parametrize(
("relaxed_status", "row_status"),
[("UNKNOWN", "unavailable"), ("MODEL_INVALID", "solver_error")],
)
def test_infeasible_baseline_does_not_overclaim_unknown_or_invalid_variant(
monkeypatch, relaxed_status, row_status,
):
from server.aps_domain import cp_marginal
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
if relaxed_constraint_id is None:
return [], _fake_meta(status="INFEASIBLE", objective=None, bound=None, relaxed=[])
return [], _fake_meta(
status=relaxed_status, objective=None, bound=None, relaxed=[relaxed_constraint_id],
)
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
)
assert report["status"] == "partial"
assert report["rows"][0]["status"] == row_status
def test_model_instance_signature_mismatch_is_partial_solver_error(monkeypatch):
from server.aps_domain import cp_marginal
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
meta = _fake_meta(
status="OPTIMAL", objective=100.0, bound=100.0,
relaxed=[] if relaxed_constraint_id is None else [relaxed_constraint_id],
)
if relaxed_constraint_id is not None:
meta["constraintInstanceCounts"] = {"C2_no_overlap": 2}
return [], meta
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
)
assert report["status"] == "partial"
assert report["rows"][0]["status"] == "solver_error"
assert "实例清单" in report["rows"][0]["reason"]
def test_feasible_baseline_to_model_invalid_is_partial_solver_error(monkeypatch):
from server.aps_domain import cp_marginal
def fake_run(*_args, relaxed_constraint_id=None, **_kwargs):
if relaxed_constraint_id is None:
return [], _fake_meta(status="OPTIMAL", objective=100.0, bound=100.0, relaxed=[])
return [], _fake_meta(
status="MODEL_INVALID", objective=None, bound=None, relaxed=[relaxed_constraint_id],
)
monkeypatch.setattr(cp_marginal, "run_cp_constraint_diagnostic", fake_run)
report = run_cp_marginal_resolve(
_tight_two_order_world(),
start_date="2026-08-03",
constraint_ids=["C2_no_overlap"],
)
assert report["status"] == "partial"
assert report["rows"][0]["status"] == "solver_error"
def _request(operation: str, relaxed: list[str] | None = None) -> dict:
body = {
"protocolVersion": PROTOCOL_VERSION,
"operation": operation,
"world": {},
"entries": [],
"params": EngineParams(engineType="CP").model_dump(mode="json"),
"warmStart": None,
"pipelineLabel": "TEST",
}
if operation.startswith("diagnose_"):
body["relaxedConstraintIds"] = relaxed or []
body["invocationId"] = "a" * 32
return {**body, "requestId": _request_digest(body)}
@pytest.mark.parametrize(
"payload",
[
{**_request("optimize_line_assignment"), "unknown": True},
_request("diagnose_constraint_removal", []),
_request("diagnose_constraint_removal", ["C1_precedence", "C2_no_overlap"]),
_request("diagnose_constraint_removal", ["UNKNOWN"]),
],
)
def test_worker_strict_schema_and_single_relaxation_reject_invalid_requests(payload):
payload["requestId"] = _request_digest({k: v for k, v in payload.items() if k != "requestId"})
with pytest.raises((TypeError, ValueError)):
_validate_request(payload)
def test_worker_rejects_v1_protocol():
request = _request("diagnose_constraint_baseline")
request["protocolVersion"] = "aps.solver-process.v1"
request["requestId"] = _request_digest({k: v for k, v in request.items() if k != "requestId"})
with pytest.raises(RuntimeError):
_validate_request(request)
@pytest.mark.parametrize("status", ["INFEASIBLE", "UNKNOWN", "MODEL_INVALID"])
def test_relaxed_response_echo_is_checked_before_nonfeasible_return(status):
meta = {
"pipeline": "TEST",
"status": status,
"relaxedConstraintIds": [],
"diagnosticMode": True,
"assumptionConstraints": ["C2_no_overlap"],
"activeAssumptionConstraints": ["C2_no_overlap"],
"enforcedAssumptionConstraints": ["C2_no_overlap"],
"constraintInstanceCounts": {"C2_no_overlap": 1},
}
with pytest.raises(SolverProcessError, match="松弛约束"):
_validate_result_semantics(
[{"orderNo": "SO-1"}],
meta,
expected_entries=[{"orderNo": "SO-1"}],
pipeline_label="TEST",
expected_relaxed_constraint_ids=["C2_no_overlap"],
expected_diagnostic_mode=True,
)
def test_feasible_response_rejects_invalid_bound_and_gap():
meta = {
"pipeline": "TEST",
"status": "OPTIMAL",
"objective": 10.0,
"bestBound": 11.0,
"gap": 0.0,
"relaxedConstraintIds": [],
"diagnosticMode": False,
"operationSlots": [{"orderIndex": 0, "isFrozen": False}],
}
with pytest.raises(SolverProcessError, match="bestBound"):
_validate_result_semantics(
[{"orderNo": "SO-1"}],
meta,
expected_entries=[{"orderNo": "SO-1"}],
pipeline_label="TEST",
expected_relaxed_constraint_ids=[],
expected_diagnostic_mode=False,
)
def test_optimal_response_requires_zero_gap_and_equal_bound():
meta = {
"pipeline": "TEST",
"status": "OPTIMAL",
"objective": 10.0,
"bestBound": 9.0,
"gap": 0.1,
"relaxedConstraintIds": [],
"diagnosticMode": False,
"operationSlots": [{"orderIndex": 0, "isFrozen": False}],
}
with pytest.raises(SolverProcessError, match="OPTIMAL"):
_validate_result_semantics(
[{"orderNo": "SO-1"}],
meta,
expected_entries=[{"orderNo": "SO-1"}],
pipeline_label="TEST",
expected_relaxed_constraint_ids=[],
expected_diagnostic_mode=False,
)
def test_diagnostic_active_assumptions_must_match_positive_instance_counts():
meta = {
"pipeline": "TEST",
"status": "INFEASIBLE",
"relaxedConstraintIds": [],
"diagnosticMode": True,
"assumptionConstraints": ["C2_no_overlap"],
"activeAssumptionConstraints": [],
"enforcedAssumptionConstraints": ["C2_no_overlap"],
"constraintInstanceCounts": {"C2_no_overlap": 1},
}
with pytest.raises(SolverProcessError, match="实例数"):
_validate_result_semantics(
[{"orderNo": "SO-1"}],
meta,
expected_entries=[{"orderNo": "SO-1"}],
pipeline_label="TEST",
expected_relaxed_constraint_ids=[],
expected_diagnostic_mode=True,
)
def test_parent_rejects_invalid_c3_calendar_topology():
meta = {
"pipeline": "TEST",
"status": "INFEASIBLE",
"relaxedConstraintIds": [],
"diagnosticMode": True,
"assumptionConstraints": ["C3_calendar"],
"activeAssumptionConstraints": ["C3_calendar"],
"enforcedAssumptionConstraints": ["C3_calendar"],
"constraintInstanceCounts": {"C3_calendar": 1},
"c3Calendar": _fake_c3_calendar(relaxed=False, digest="not-a-digest"),
}
with pytest.raises(SolverProcessError, match="C3"):
_validate_result_semantics(
[{"orderNo": "SO-1"}],
meta,
expected_entries=[{"orderNo": "SO-1"}],
pipeline_label="TEST",
expected_relaxed_constraint_ids=[],
expected_diagnostic_mode=True,
)
class _Store:
def __init__(self) -> None:
self.data = _tight_c2_exact_world()
class _Projects:
def active_world_key(self) -> str:
return "project-cp-marginal"
def require_active_write(self) -> None:
return None
@pytest.fixture
def cp_marginal_client(monkeypatch):
import server.gateway.app as gateway
from server.agent_core import async_jobs
from server.state import projects
install_test_auth(monkeypatch, "tenant-cp-marginal")
monkeypatch.setattr(gateway, "get_store", lambda: _Store())
monkeypatch.setattr(projects, "get_project_store", lambda: _Projects())
queue = JobQueue()
monkeypatch.setattr(async_jobs, "_queue", queue)
client = TestClient(gateway.create_app())
assert client.post(
"/api/auth/login", json={"username": "planner", "password": "test"},
).status_code == 200
return client, queue
@pytest.mark.parametrize(
"params",
[
{"startDate": "2026-08-03", "track": "flex"},
{"startDate": "2026-08-03", "planningHorizonDays": 0},
{"startDate": "2026-08-03", "timeLimitSeconds": "4"},
{"startDate": "2026-08-03", "constraintIds": []},
{"startDate": "2026-08-03", "constraintIds": ["UNKNOWN"]},
{"startDate": "20260803"},
],
)
def test_gateway_rejects_invalid_cp_marginal_before_submit(cp_marginal_client, params):
client, queue = cp_marginal_client
response = client.post(
"/api/jobs", json={"kind": "cp-marginal.recompute", "params": params},
)
assert response.status_code == 422, response.text
assert queue.stats()["total"] == 0
def test_gateway_submit_poll_is_tenant_project_bound(monkeypatch, cp_marginal_client):
from server.aps_domain import cp_marginal
client, queue = cp_marginal_client
def fast_report(_world, **options):
options["cancel_check"]()
return {"method": "cp-one-constraint-at-a-time-resolve.v1", "rows": []}
monkeypatch.setattr(cp_marginal, "run_cp_marginal_resolve", fast_report)
response = client.post("/api/jobs", json={
"kind": "cp-marginal.recompute",
"params": {
"track": "fixed",
"startDate": "2026-08-03",
"constraintIds": ["C2_no_overlap"],
"timeLimitSeconds": 1,
},
})
assert response.status_code == 200, response.text
job_id = response.json()["jobId"]
record = queue.wait(job_id, timeout=10)
assert record["status"] == "done"
assert record["tenant_uuid"] == "tenant-cp-marginal"
assert record["project_id"] == "project-cp-marginal"
assert client.get(f"/api/jobs/{job_id}").json()["job"]["result"]["method"].startswith("cp-")
def test_gateway_real_cp_marginal_submit_and_poll(cp_marginal_client):
client, queue = cp_marginal_client
response = client.post("/api/jobs", json={
"kind": "cp-marginal.recompute",
"params": {
"track": "fixed",
"startDate": "2026-08-03",
"planningHorizonDays": 3,
"constraintIds": ["C2_no_overlap"],
"timeLimitSeconds": 8,
},
})
assert response.status_code == 200, response.text
job_id = response.json()["jobId"]
record = queue.wait(job_id, timeout=20)
assert record["status"] == "done"
result = record["result"]
assert result["status"] == "completed"
assert result["evaluations"] == 2
assert result["rows"][0]["objectiveImprovement"] == 2100.0