aps-agent/tests/golden/test_mps_planning.py

1253 lines
41 KiB
Python
Raw Normal View History

# ============================================================
# R71.4 plan-layer MPS golden tests
# MPS draft generation, finite rough-cut capacity assessment,
# plan -> schedule -> publish -> execution feedback trace,
# and plan-layer KPI vs detailed scheduling gap summary.
# ============================================================
from __future__ import annotations
import copy
import json
from types import SimpleNamespace
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from server.agent_core.plan_runtime import PlanStore
from server.aps_domain.mps_planning import (
MpsTraceStore,
_draft_hash,
_draft_hash_for_version,
bind_mps_draft,
build_mps_draft,
compare_mps_schedule_gap,
get_mps_trace,
link_mps_trace_local_fact,
list_mps_schedule_versions,
persist_mps_draft,
replay_mps_trace,
schedule_fingerprint,
validate_mps_draft_binding,
)
from server.gateway import mps_api
from server.state.seed import seed_world
TENANT = "tenant-a"
PROJECT = "project-a"
WORLD_KEY = "project-a"
def _bound_draft(
world: dict,
draft: dict,
*,
tenant_uuid: str = TENANT,
project_id: str = PROJECT,
world_key: str = WORLD_KEY,
) -> dict:
return bind_mps_draft(
world,
draft,
tenant_uuid=tenant_uuid,
project_id=project_id,
world_key=world_key,
)
def _trace_store(
path,
*,
tenant_uuid: str = TENANT,
project_id: str = PROJECT,
world_key: str = WORLD_KEY,
) -> MpsTraceStore:
return MpsTraceStore(
str(path),
tenant_uuid=tenant_uuid,
project_id=project_id,
world_key=world_key,
)
def test_mps_draft_buckets_forecast_and_long_cycle():
world = seed_world()
world["salesOrders"][0]["isLongCycle"] = True
draft = build_mps_draft(world, mode="WEEK", horizon_days=90)
assert draft["kind"] == "MPS_DRAFT"
assert draft["summary"]["firmQty"] > 0
assert draft["summary"]["forecastQty"] > 0
assert draft["calendar"]["source"] in ("flexCalendar", "shiftCalendar", "default")
assert draft["calendar"]["shiftMinutes"] >= 60
assert sum(bucket["workdays"] for bucket in draft["buckets"]) > 0
assert all(
"mpsLines" in bucket and "byProduct" in bucket for bucket in draft["buckets"]
)
sources = {
line["source"] for bucket in draft["buckets"] for line in bucket["mpsLines"]
}
assert "FORECAST" in sources
assert "LONG_CYCLE" in sources
def test_mps_draft_hash_survives_browser_number_roundtrip():
world = seed_world()
world["salesOrders"][0]["items"][0]["quantity"] = 1.0
draft = _bound_draft(world, build_mps_draft(world, horizon_days=30))
def browser_numbers(value):
if isinstance(value, dict):
return {key: browser_numbers(item) for key, item in value.items()}
if isinstance(value, list):
return [browser_numbers(item) for item in value]
if isinstance(value, float) and value.is_integer():
return int(value)
return value
browser_payload = json.loads(json.dumps(browser_numbers(draft), ensure_ascii=False))
assert browser_payload["draftHashVersion"] == "mps-draft-hash.v2"
validated = validate_mps_draft_binding(
world,
browser_payload,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
assert validated["draftHash"] == draft["draftHash"]
browser_payload["summary"]["weightedDemand"] += 1
with pytest.raises(ValueError, match="content hash mismatch"):
validate_mps_draft_binding(
world,
browser_payload,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
def test_mps_v2_hash_has_no_number_string_or_business_object_collision():
world = seed_world()
draft = _bound_draft(world, build_mps_draft(world, horizon_days=30))
original = draft["summary"]["weightedDemand"]
for replacement in (
str(original),
{"__apsJsonNumber__": str(original)},
["number", str(original)],
):
tampered = copy.deepcopy(draft)
tampered["summary"]["weightedDemand"] = replacement
with pytest.raises(ValueError, match="content hash mismatch"):
validate_mps_draft_binding(
world,
tampered,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
def test_mps_legacy_v1_draft_remains_replayable_server_side(tmp_path):
world = seed_world()
legacy = build_mps_draft(world, horizon_days=30)
legacy["scope"] = {
"tenantUuid": TENANT,
"projectId": PROJECT,
"worldKey": WORLD_KEY,
}
legacy["worldFingerprint"] = _bound_draft(
world, build_mps_draft(world, horizon_days=30)
)["worldFingerprint"]
legacy["draftHash"] = _draft_hash(legacy)
validated = validate_mps_draft_binding(
world,
legacy,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
allow_legacy=True,
)
assert "draftHashVersion" not in validated
assert validated["draftHash"] == legacy["draftHash"]
trace_store = _trace_store(tmp_path / "legacy-trace.json")
envelope = trace_store.create(
plan_id="legacy-plan",
plan_version=1,
draft_id=legacy["draftId"],
draft_snapshot=legacy,
input_summary={
"tenantUuid": TENANT,
"projectId": PROJECT,
"worldKey": WORLD_KEY,
"draftId": legacy["draftId"],
"draftHash": legacy["draftHash"],
"worldFingerprint": legacy["worldFingerprint"],
"parameters": {},
},
world_fingerprint=legacy["worldFingerprint"],
)
assert envelope["draftHashVersion"] == "mps-draft-hash.v1"
replayed = replay_mps_trace(envelope["traceId"], world=world, trace_store=trace_store)
assert replayed["draft"] == legacy
def test_mps_interim_unversioned_numeric_hash_trace_remains_replayable(tmp_path):
world = seed_world()
interim = build_mps_draft(world, horizon_days=30)
interim["scope"] = {
"tenantUuid": TENANT,
"projectId": PROJECT,
"worldKey": WORLD_KEY,
}
interim["worldFingerprint"] = _bound_draft(
world, build_mps_draft(world, horizon_days=30)
)["worldFingerprint"]
interim["draftHash"] = _draft_hash_for_version(
interim, "mps-draft-hash.v1-json-numbers"
)
assert "draftHashVersion" not in interim
validated = validate_mps_draft_binding(
world,
interim,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
allow_legacy=True,
)
trace_store = _trace_store(tmp_path / "interim-trace.json")
envelope = trace_store.create(
plan_id="interim-plan",
plan_version=1,
draft_id=validated["draftId"],
draft_snapshot=validated,
input_summary={
"tenantUuid": TENANT,
"projectId": PROJECT,
"worldKey": WORLD_KEY,
"draftId": validated["draftId"],
"draftHash": validated["draftHash"],
"worldFingerprint": validated["worldFingerprint"],
"parameters": {},
},
world_fingerprint=validated["worldFingerprint"],
)
assert envelope["draftHashVersion"] == "mps-draft-hash.v1-json-numbers"
assert replay_mps_trace(
envelope["traceId"], world=world, trace_store=trace_store
)["draft"] == validated
def test_mps_interim_hash_algorithm_matches_frozen_fixture():
fixture = {
"kind": "MPS_DRAFT",
"summary": {
"weightedDemand": 1.0,
"marker": {"__apsJsonNumber__": "1"},
},
}
assert _draft_hash_for_version(
fixture, "mps-draft-hash.v1-json-numbers"
) == "5d42e32ede0efc008ebb122a69a18c2c87cc68b18cf71ada47d266598d81bac2"
def test_mps_api_rejects_legacy_hash_downgrade_and_marker_attack(mps_api_client):
client, _state, _stores, _ensure_scope = mps_api_client
draft = client.post("/api/mps/draft", json={"horizonDays": 30}).json()["draft"]
downgraded = copy.deepcopy(draft)
downgraded.pop("draftHashVersion", None)
downgraded["summary"]["weightedDemand"] = {
"__apsJsonNumber__": str(draft["summary"]["weightedDemand"])
}
downgraded["draftHash"] = _draft_hash_for_version(
downgraded, "mps-draft-hash.v1-json-numbers"
)
response = client.post("/api/mps/persist", json={"draft": downgraded})
assert response.status_code == 409
assert "requires draftHashVersion v2" in response.json()["detail"]
def test_mps_draft_long_cycle_only_filter():
world = seed_world()
world["salesOrders"][0]["isLongCycle"] = True
draft = build_mps_draft(
world,
mode="WEEK",
horizon_days=90,
long_cycle_only=True,
include_forecast=False,
)
sources = {
line["source"] for bucket in draft["buckets"] for line in bucket["mpsLines"]
}
assert sources == {"LONG_CYCLE"}
def test_mps_finite_capacity_assessment_reports_feasibility_and_bottlenecks():
world = seed_world()
for line in world["lines"]:
line["capacityPerDay"] = 50
draft = build_mps_draft(world, mode="WEEK", horizon_days=60, capacity_mode="FINITE")
assert draft["capacity"]["capacityMode"] == "FINITE"
assert draft["capacity"]["feasibleRate"] < 1.0
assert draft["capacity"]["bottleneckBucketCount"] >= 1
first = draft["capacity"]["bottlenecks"][0]
assert first["loadRatio"] >= 1.0
assert first["drivers"]
over_lines = [
line
for bucket in draft["buckets"]
for line in bucket["mpsLines"]
if bucket["status"] == "OVER"
]
assert over_lines
assert all(line["commitment"] == "INFEASIBLE" for line in over_lines)
infinite = build_mps_draft(
world, mode="WEEK", horizon_days=60, capacity_mode="INFINITE"
)
assert infinite["capacityMode"] == "INFINITE"
assert infinite["summary"]["overCount"] == 0
assert all(bucket["capacity"] is None for bucket in infinite["buckets"])
def test_mps_trace_envelope_chain_is_persistent(tmp_path):
plan_store = PlanStore(str(tmp_path / "plans.json"))
trace_store = _trace_store(tmp_path / "mps_trace.json")
world = seed_world()
draft = _bound_draft(world, build_mps_draft(world, mode="WEEK", horizon_days=30))
persisted = persist_mps_draft(
draft,
world=world,
plan_store=plan_store,
trace_store=trace_store,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
node = persisted["planNode"]
envelope = persisted["envelope"]
assert node.layer == "L2"
assert node.parentId == "strategy-mps"
assert node.payload["kind"] == "mps-draft"
assert envelope["planId"] == node.planId
assert envelope["scheduleId"] is None
assert envelope["publishId"] is None
assert envelope["executionFeedback"] == []
assert persisted["draft"]["planId"] == node.planId
assert envelope["tenantUuid"] == TENANT
assert envelope["projectId"] == PROJECT
assert envelope["worldKey"] == WORLD_KEY
assert envelope["draftSnapshot"] == persisted["draft"]
assert envelope["inputSummary"]["draftHash"] == draft["draftHash"]
assert envelope["draftHashVersion"] == "mps-draft-hash.v2"
assert envelope["inputSummary"]["draftHashVersion"] == "mps-draft-hash.v2"
assert node.payload["draftSnapshot"] == draft
assert node.payload["worldFingerprint"] == draft["worldFingerprint"]
trace_store.link_schedule(
envelope["traceId"],
42,
schedule_version_no="V1",
track="fixed",
)
trace_store.link_publish(
envelope["traceId"],
"PUB-1",
published_at="2026-08-12T00:00:00Z",
)
trace_store.link_feedback(
envelope["traceId"],
feedback_id="FB-1",
work_order_id=7,
progress_pct=50,
status="RUNNING",
qty_done=250,
)
got = get_mps_trace(envelope["traceId"], trace_store=trace_store)
assert got["scheduleId"] == 42
assert got["scheduleVersionNo"] == "V1"
assert got["scheduleTrack"] == "fixed"
assert got["publishId"] == "PUB-1"
assert len(got["executionFeedback"]) == 1
assert [event["event"] for event in got["events"]] == [
"OPEN",
"SCHEDULE",
"PUBLISH",
"FEEDBACK",
]
reloaded = _trace_store(tmp_path / "mps_trace.json")
assert reloaded.get(envelope["traceId"])["scheduleId"] == 42
replayed = replay_mps_trace(envelope["traceId"], world=world, trace_store=reloaded)
assert replayed["draft"] == persisted["draft"]
assert replayed["inputSummary"] == envelope["inputSummary"]
assert plan_store.latest("intent-mps").layer == "L0"
assert plan_store.latest("strategy-mps").layer == "L1"
def test_compare_mps_schedule_gap_summary():
world = seed_world()
draft = _bound_draft(world, build_mps_draft(world, mode="WEEK", horizon_days=30))
world["scheduleVersions"].append(
{
"id": 1,
"versionNo": "V20260812-001",
"status": "PUBLISHED",
"orderCount": 9,
"poCount": 7,
"woCount": 21,
"conflictCount": 1,
"totalTardiness": 5.5,
"avgUtilization": 0.82,
}
)
world["productionOrders"] = [
{"id": index, "schedulingVersionId": 1, "quantity": qty}
for index, qty in enumerate([500, 300, 800, 400, 600, 200, 500], start=1)
]
draft = _bound_draft(
world,
{
key: value
for key, value in draft.items()
if key not in {"draftHash", "scope", "worldFingerprint"}
},
)
report = compare_mps_schedule_gap(
world,
draft,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
schedule_id=1,
track="fixed",
)
assert report["schedule"]["versionNo"] == "V20260812-001"
assert report["gaps"]["demandGapQty"] == round(
draft["summary"]["weightedDemand"] - 3300,
1,
)
assert {row["key"] for row in report["rows"]} == {
"demandQty",
"loadRatio",
"feasibleRate",
"riskCount",
}
assert all(
"plan" in row and "schedule" in row and "delta" in row for row in report["rows"]
)
assert report["trace"]["scheduleId"] == 1
def test_compare_mps_schedule_gap_uses_trace_refs(tmp_path):
world = seed_world()
draft = build_mps_draft(world, mode="WEEK", horizon_days=30)
world["scheduleVersions"].append(
{
"id": 2,
"versionNo": "V2",
"status": "PUBLISHED",
"orderCount": 9,
"poCount": 7,
"woCount": 21,
"conflictCount": 0,
"totalTardiness": 0.0,
"avgUtilization": 0.78,
}
)
world["productionOrders"] = [
{"id": index, "schedulingVersionId": 2, "quantity": qty}
for index, qty in enumerate([500, 300, 800, 400, 600, 200, 500], start=1)
]
draft = _bound_draft(world, draft)
plan_store = PlanStore(str(tmp_path / "plans.json"))
trace_store = _trace_store(tmp_path / "mps_trace.json")
persisted = persist_mps_draft(
draft,
world=world,
plan_store=plan_store,
trace_store=trace_store,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
trace_id = persisted["envelope"]["traceId"]
trace_store.link_schedule(
trace_id,
2,
schedule_version_no="V2",
track="fixed",
schedule_fingerprint=schedule_fingerprint(
world, "fixed", world["scheduleVersions"][-1]
),
)
trace_store.link_publish(trace_id, "PUB-2")
trace_store.link_feedback(
trace_id,
feedback_id="FB-1",
work_order_id=7,
progress_pct=100,
status="COMPLETED",
qty_done=300,
)
trace = get_mps_trace(trace_id, trace_store=trace_store)
report = compare_mps_schedule_gap(
world,
draft,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
track="fixed",
trace=trace,
)
assert report["schedule"]["versionNo"] == "V2"
assert report["trace"]["planId"] == persisted["envelope"]["planId"]
assert report["trace"]["publishId"] == "PUB-2"
assert report["trace"]["feedbackCount"] == 1
def test_compare_mps_schedule_gap_without_schedule_returns_null():
world = seed_world()
draft = _bound_draft(world, build_mps_draft(world, mode="WEEK", horizon_days=30))
report = compare_mps_schedule_gap(
world,
draft,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
assert report["schedule"] is None
assert report["gaps"] is None
assert report["rows"] == []
class _MpsApiMemStore:
def __init__(self, world: dict, world_key: str) -> None:
self.data = world
self.world_key = world_key
self.path = "memory://mps-api-test"
def next_id(self, _kind: str) -> int:
return 1
def save(self) -> None:
pass
@pytest.fixture
def mps_api_client(tmp_path, monkeypatch):
def make_world() -> dict:
world = seed_world()
world["scheduleVersions"] = [
{
"id": 1,
"versionNo": "V20260812-001",
"status": "PUBLISHED",
"orderCount": 9,
"poCount": 7,
"woCount": 21,
"conflictCount": 1,
"totalTardiness": 5.5,
"avgUtilization": 0.82,
}
]
world["productionOrders"] = [
{"id": index, "schedulingVersionId": 1, "quantity": qty}
for index, qty in enumerate([500, 300, 800, 400, 600, 200, 500], start=1)
]
return world
state = {
"tenantUuid": TENANT,
"projectId": PROJECT,
"worldKey": WORLD_KEY,
"userId": 101,
"username": "alice",
}
stores: dict[tuple[str, str], _MpsApiMemStore] = {}
plan_stores: dict[tuple[str, str], PlanStore] = {}
def ensure_scope(tenant_uuid: str, project_id: str, world_key: str) -> None:
stores.setdefault(
(tenant_uuid, world_key),
_MpsApiMemStore(make_world(), world_key),
)
plan_stores.setdefault(
(tenant_uuid, world_key),
PlanStore(
str(tmp_path / f"plans-{tenant_uuid}-{project_id}-{world_key}.json")
),
)
ensure_scope(TENANT, PROJECT, WORLD_KEY)
def identity(*, required: bool = False):
del required
return SimpleNamespace(
tenant_uuid=state["tenantUuid"],
user_id=state["userId"],
username=state["username"],
auth_kind="user",
)
monkeypatch.setattr(mps_api, "get_identity", identity)
monkeypatch.setattr(
mps_api,
"get_project_store",
lambda: SimpleNamespace(active_world_key=lambda: state["projectId"]),
)
monkeypatch.setattr(
mps_api,
"get_store",
lambda: stores[(state["tenantUuid"], state["worldKey"])],
)
monkeypatch.setattr(
mps_api,
"get_plan_store",
lambda: plan_stores[(state["tenantUuid"], state["worldKey"])],
)
monkeypatch.setattr(mps_api, "aps_home", lambda: tmp_path)
app = FastAPI(title="mps-router-test")
app.include_router(mps_api.router)
return TestClient(app), state, stores, ensure_scope
def test_mps_api_persist_trace_and_gap_contract(mps_api_client):
client, _state, _stores, _ensure_scope = mps_api_client
draft_resp = client.post("/api/mps/draft", json={"mode": "WEEK", "horizonDays": 30})
assert draft_resp.status_code == 200
draft = draft_resp.json()["draft"]
persist_resp = client.post(
"/api/mps/persist",
json={"draft": draft, "createdBy": "SYSTEM"},
)
assert persist_resp.status_code == 200
persisted = persist_resp.json()
trace_id = persisted["envelope"]["traceId"]
assert persisted["planNode"]["layer"] == "L2"
assert persisted["planNode"]["createdBy"] == "USER"
assert persisted["draft"]["planId"] == persisted["planNode"]["planId"]
assert persisted["envelope"]["events"][0]["by"] == "alice"
assert persisted["envelope"]["draftSnapshot"] == persisted["draft"]
assert persisted["envelope"]["inputSummary"]["draftHash"] == draft["draftHash"]
traces_resp = client.get("/api/mps/traces")
assert traces_resp.status_code == 200
assert any(row["traceId"] == trace_id for row in traces_resp.json())
trace_resp = client.get(f"/api/mps/trace/{trace_id}")
assert trace_resp.status_code == 200
assert trace_resp.json()["events"][0]["event"] == "OPEN"
gap_resp = client.post(
"/api/mps/gap", json={"draft": draft, "traceId": trace_id, "track": "fixed"}
)
assert gap_resp.status_code == 200
report = gap_resp.json()
assert report["schedule"] is not None
assert report["gaps"] is not None
assert {row["key"] for row in report["rows"]} == {
"demandQty",
"loadRatio",
"feasibleRate",
"riskCount",
}
def test_mps_api_trace_404_and_gap_without_schedule(mps_api_client):
client, state, stores, _ensure_scope = mps_api_client
store = stores[(state["tenantUuid"], state["worldKey"])]
assert client.get("/api/mps/trace/TR-NOPE").status_code == 404
store.data["scheduleVersions"] = []
store.data["productionOrders"] = []
draft_resp = client.post("/api/mps/draft", json={"mode": "WEEK", "horizonDays": 30})
draft = draft_resp.json()["draft"]
gap_resp = client.post("/api/mps/gap", json={"draft": draft, "track": "fixed"})
assert gap_resp.status_code == 200
assert gap_resp.json()["schedule"] is None
assert gap_resp.json()["rows"] == []
def test_mps_trace_store_rejects_cross_scope_reads(tmp_path):
world = seed_world()
draft = _bound_draft(world, build_mps_draft(world, horizon_days=30))
owner_store = _trace_store(tmp_path / "shared.json")
persisted = persist_mps_draft(
draft,
world=world,
plan_store=PlanStore(str(tmp_path / "plans.json")),
trace_store=owner_store,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
trace_id = persisted["envelope"]["traceId"]
other_tenant = _trace_store(
tmp_path / "shared.json",
tenant_uuid="tenant-b",
project_id=PROJECT,
world_key=WORLD_KEY,
)
assert other_tenant.list() == []
with pytest.raises(ValueError, match="does not exist"):
other_tenant.get(trace_id)
with pytest.raises(ValueError, match="store scope"):
persist_mps_draft(
draft,
world=world,
plan_store=PlanStore(str(tmp_path / "other-plans.json")),
trace_store=other_tenant,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
other_project = _trace_store(
tmp_path / "shared.json",
project_id="project-b",
world_key="project-b",
)
assert other_project.list() == []
with pytest.raises(ValueError, match="does not exist"):
other_project.get(trace_id)
def test_mps_api_trace_isolated_by_authenticated_tenant_and_project(mps_api_client):
client, state, _stores, ensure_scope = mps_api_client
draft = client.post("/api/mps/draft", json={"horizonDays": 30}).json()["draft"]
persisted = client.post("/api/mps/persist", json={"draft": draft}).json()
trace_id = persisted["envelope"]["traceId"]
state.update(
{"tenantUuid": "tenant-b", "projectId": PROJECT, "worldKey": WORLD_KEY}
)
ensure_scope("tenant-b", PROJECT, WORLD_KEY)
assert client.get("/api/mps/traces").json() == []
assert client.get(f"/api/mps/trace/{trace_id}").status_code == 404
assert client.post("/api/mps/persist", json={"draft": draft}).status_code == 409
state.update(
{"tenantUuid": TENANT, "projectId": "project-b", "worldKey": "project-b"}
)
ensure_scope(TENANT, "project-b", "project-b")
assert client.get("/api/mps/traces").json() == []
assert client.get(f"/api/mps/trace/{trace_id}").status_code == 404
assert client.post("/api/mps/persist", json={"draft": draft}).status_code == 409
state.update({"tenantUuid": TENANT, "projectId": PROJECT, "worldKey": WORLD_KEY})
assert client.get(f"/api/mps/trace/{trace_id}").status_code == 200
def test_mps_api_gap_rejects_draft_and_world_version_drift(mps_api_client):
client, state, stores, _ensure_scope = mps_api_client
draft = client.post("/api/mps/draft", json={"horizonDays": 30}).json()["draft"]
persisted = client.post("/api/mps/persist", json={"draft": draft}).json()
trace_id = persisted["envelope"]["traceId"]
tampered = copy.deepcopy(draft)
tampered["summary"]["weightedDemand"] += 1
tampered_resp = client.post(
"/api/mps/gap",
json={"draft": tampered, "traceId": trace_id, "track": "fixed"},
)
assert tampered_resp.status_code == 409
assert "hash" in str(tampered_resp.json()["detail"]).lower()
world = stores[(state["tenantUuid"], state["worldKey"])].data
world["lines"][0]["capacityPerDay"] += 1
stale_resp = client.post(
"/api/mps/gap",
json={"traceId": trace_id, "track": "fixed"},
)
assert stale_resp.status_code == 409
assert "fingerprint" in str(stale_resp.json()["detail"]).lower()
def test_mps_persist_does_not_leave_plan_when_trace_write_fails(monkeypatch, tmp_path):
world = seed_world()
draft = _bound_draft(world, build_mps_draft(world, horizon_days=30))
plan_store = PlanStore(str(tmp_path / "plans.json"))
trace_store = _trace_store(tmp_path / "traces.json")
def fail_trace_write():
raise OSError("injected trace write failure")
monkeypatch.setattr(trace_store, "_write", fail_trace_write)
with pytest.raises(OSError, match="injected trace write failure"):
persist_mps_draft(
draft,
world=world,
plan_store=plan_store,
trace_store=trace_store,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
assert plan_store._plans == {}
assert trace_store.list() == []
def test_mps_persist_discards_trace_when_l2_plan_write_fails(monkeypatch, tmp_path):
world = seed_world()
draft = _bound_draft(world, build_mps_draft(world, horizon_days=30))
plan_store = PlanStore(str(tmp_path / "plans.json"))
trace_store = _trace_store(tmp_path / "traces.json")
original_create = plan_store.create
def fail_l2_create(**kwargs):
if kwargs.get("layer") == "L2":
raise OSError("injected L2 write failure")
return original_create(**kwargs)
monkeypatch.setattr(plan_store, "create", fail_l2_create)
with pytest.raises(OSError, match="injected L2 write failure"):
persist_mps_draft(
draft,
world=world,
plan_store=plan_store,
trace_store=trace_store,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
assert set(plan_store._plans) == {"intent-mps", "strategy-mps"}
assert trace_store.list() == []
def _append_fixed_schedule(
world: dict,
*,
version_id: int = 42,
version_no: str = "V-LOCAL-42",
status: str = "DRAFT",
) -> dict:
version = {
"id": version_id,
"versionNo": version_no,
"status": status,
"engineType": "RULE",
"note": "strategy=DELIVERY_FIRST",
"orderCount": 1,
"poCount": 1,
"woCount": 1,
"conflictCount": 0,
"totalTardiness": 0.0,
"avgUtilization": 0.75,
"createdAt": "2026-08-17 10:00:00",
"publishedAt": None,
}
world.setdefault("scheduleVersions", []).append(version)
production_order_id = version_id * 10
world.setdefault("productionOrders", []).append(
{
"id": production_order_id,
"schedulingVersionId": version_id,
"quantity": 100,
"status": "DRAFT",
"plannedStartDate": "2026-08-18 08:00:00",
"plannedEndDate": "2026-08-18 12:00:00",
}
)
world.setdefault("workOrders", []).append(
{
"id": version_id * 100,
"productionOrderId": production_order_id,
"quantity": 100,
"plannedStartTime": "2026-08-18 08:00:00",
"plannedEndTime": "2026-08-18 12:00:00",
"status": "PENDING",
}
)
return version
def _append_flex_schedule(
world: dict,
*,
version_id: int = 42,
version_no: str = "FV-LOCAL-42",
) -> dict:
version = {
"id": version_id,
"versionNo": version_no,
"status": "DRAFT",
"engineType": "FLEX",
"sortMode": "SKILL_FIRST",
"orderCount": 1,
"woCount": 1,
"conflictCount": 0,
"totalTardiness": 0.0,
"avgUtilization": 0.7,
"createdAt": "2026-08-17 10:01:00",
}
world.setdefault("flexScheduleVersions", []).append(version)
world.setdefault("flexVirtualLines", []).append(
{
"id": version_id * 10,
"versionId": version_id,
"quantity": 100,
"plannedStart": "2026-08-18 08:00:00",
"plannedEnd": "2026-08-18 12:00:00",
}
)
world.setdefault("flexWorkOrders", []).append(
{
"id": version_id * 100,
"versionId": version_id,
"quantity": 100,
"plannedStartTime": "2026-08-18 08:00:00",
"plannedEndTime": "2026-08-18 12:00:00",
"status": "PENDING",
}
)
return version
def _persist_trace_before_schedule(tmp_path, world: dict):
draft = _bound_draft(world, build_mps_draft(world, horizon_days=30))
trace_store = _trace_store(tmp_path / "local-link-traces.json")
persisted = persist_mps_draft(
draft,
world=world,
plan_store=PlanStore(str(tmp_path / "local-link-plans.json")),
trace_store=trace_store,
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
)
return persisted, trace_store
def test_mps_local_adopt_link_is_exact_atomic_idempotent_and_gap_replays(
tmp_path,
monkeypatch,
):
world = seed_world()
persisted, trace_store = _persist_trace_before_schedule(tmp_path, world)
version = _append_fixed_schedule(world)
summary = next(
row
for row in list_mps_schedule_versions(world)
if row["track"] == "fixed" and row["scheduleId"] == version["id"]
)
mes_called = False
from server.aps_domain import mes
def reject_mes_dispatch(*_args, **_kwargs):
nonlocal mes_called
mes_called = True
raise AssertionError("MPS local link must not call MES dispatch")
monkeypatch.setattr(mes, "stage_dispatch", reject_mes_dispatch)
envelope = persisted["envelope"]
linked = link_mps_trace_local_fact(
world,
trace_store=trace_store,
trace_id=envelope["traceId"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
draft_hash=envelope["draftHash"],
world_fingerprint=envelope["worldFingerprint"],
schedule_id=summary["scheduleId"],
schedule_version_no=summary["scheduleVersionNo"],
schedule_fingerprint=summary["scheduleFingerprint"],
track=summary["track"],
fact_type="ADOPT",
actor="planner",
)
assert linked["idempotent"] is False
assert linked["trace"]["scheduleId"] == version["id"]
assert linked["trace"]["scheduleVersionNo"] == version["versionNo"]
assert linked["trace"]["scheduleTrack"] == "fixed"
assert linked["trace"]["scheduleFingerprint"] == summary["scheduleFingerprint"]
assert linked["trace"]["adoptId"] == "ADOPT:fixed:V-LOCAL-42"
assert [event["event"] for event in linked["trace"]["events"]] == [
"OPEN",
"SCHEDULE",
"ADOPT",
]
assert mes_called is False
retried = link_mps_trace_local_fact(
world,
trace_store=trace_store,
trace_id=envelope["traceId"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
draft_hash=envelope["draftHash"],
world_fingerprint=envelope["worldFingerprint"],
schedule_id=summary["scheduleId"],
schedule_version_no=summary["scheduleVersionNo"],
schedule_fingerprint=summary["scheduleFingerprint"],
track=summary["track"],
fact_type="ADOPT",
actor="planner",
)
assert retried["idempotent"] is True
assert len(retried["trace"]["events"]) == 3
report = compare_mps_schedule_gap(
world,
persisted["draft"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
track="auto",
trace=retried["trace"],
)
assert report["schedule"]["versionNo"] == version["versionNo"]
assert report["trace"]["scheduleFingerprint"] == summary["scheduleFingerprint"]
assert report["trace"]["adoptId"] == "ADOPT:fixed:V-LOCAL-42"
def test_mps_local_link_rejects_conflict_track_schedule_and_world_drift(tmp_path):
world = seed_world()
persisted, trace_store = _persist_trace_before_schedule(tmp_path, world)
fixed = _append_fixed_schedule(world)
fixed_summary = next(
row for row in list_mps_schedule_versions(world) if row["track"] == "fixed"
)
envelope = persisted["envelope"]
linked = link_mps_trace_local_fact(
world,
trace_store=trace_store,
trace_id=envelope["traceId"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
draft_hash=envelope["draftHash"],
world_fingerprint=envelope["worldFingerprint"],
schedule_id=fixed_summary["scheduleId"],
schedule_version_no=fixed_summary["scheduleVersionNo"],
schedule_fingerprint=fixed_summary["scheduleFingerprint"],
track="fixed",
fact_type="ADOPT",
)
before_conflict = copy.deepcopy(linked["trace"])
flex = _append_flex_schedule(world, version_id=fixed["id"])
flex_summary = next(
row
for row in list_mps_schedule_versions(world)
if row["track"] == "flex" and row["scheduleId"] == flex["id"]
)
with pytest.raises(ValueError, match="different schedule"):
link_mps_trace_local_fact(
world,
trace_store=trace_store,
trace_id=envelope["traceId"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
draft_hash=envelope["draftHash"],
world_fingerprint=envelope["worldFingerprint"],
schedule_id=flex_summary["scheduleId"],
schedule_version_no=flex_summary["scheduleVersionNo"],
schedule_fingerprint=flex_summary["scheduleFingerprint"],
track="flex",
fact_type="ADOPT",
)
assert trace_store.get(envelope["traceId"]) == before_conflict
with pytest.raises(ValueError, match="track"):
compare_mps_schedule_gap(
world,
persisted["draft"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
track="flex",
trace=before_conflict,
)
world["workOrders"][-1]["plannedEndTime"] = "2026-08-18 13:00:00"
with pytest.raises(ValueError, match="schedule fingerprint"):
compare_mps_schedule_gap(
world,
persisted["draft"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
track="auto",
trace=before_conflict,
)
world["workOrders"][-1]["plannedEndTime"] = "2026-08-18 12:00:00"
world["lines"][0]["capacityPerDay"] += 1
with pytest.raises(ValueError, match="world fingerprint"):
compare_mps_schedule_gap(
world,
persisted["draft"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
track="auto",
trace=before_conflict,
)
def test_mps_local_publish_fact_requires_published_version_and_keeps_adopt(tmp_path):
world = seed_world()
persisted, trace_store = _persist_trace_before_schedule(tmp_path, world)
version = _append_fixed_schedule(world)
summary = next(
row for row in list_mps_schedule_versions(world) if row["track"] == "fixed"
)
envelope = persisted["envelope"]
request = {
"world": world,
"trace_store": trace_store,
"trace_id": envelope["traceId"],
"tenant_uuid": TENANT,
"project_id": PROJECT,
"world_key": WORLD_KEY,
"draft_hash": envelope["draftHash"],
"world_fingerprint": envelope["worldFingerprint"],
"schedule_id": summary["scheduleId"],
"schedule_version_no": summary["scheduleVersionNo"],
"schedule_fingerprint": summary["scheduleFingerprint"],
"track": "fixed",
}
with pytest.raises(ValueError, match="published local version"):
link_mps_trace_local_fact(**request, fact_type="PUBLISH")
assert trace_store.get(envelope["traceId"])["scheduleId"] is None
adopted = link_mps_trace_local_fact(**request, fact_type="ADOPT")
version["status"] = "PUBLISHED"
version["publishedAt"] = "2026-08-17 11:00:00"
published = link_mps_trace_local_fact(**request, fact_type="PUBLISH")
assert published["trace"]["adoptId"] == adopted["trace"]["adoptId"]
assert published["trace"]["publishId"] == "PUBLISH:fixed:V-LOCAL-42"
assert published["trace"]["publishedAt"] == version["publishedAt"]
assert [event["event"] for event in published["trace"]["events"]] == [
"OPEN",
"SCHEDULE",
"ADOPT",
"PUBLISH",
]
retried = link_mps_trace_local_fact(**request, fact_type="PUBLISH")
assert retried["idempotent"] is True
assert len(retried["trace"]["events"]) == 4
def test_mps_local_link_write_failure_rolls_back_all_fields(tmp_path, monkeypatch):
world = seed_world()
persisted, trace_store = _persist_trace_before_schedule(tmp_path, world)
_append_fixed_schedule(world)
summary = next(
row for row in list_mps_schedule_versions(world) if row["track"] == "fixed"
)
envelope = persisted["envelope"]
before = trace_store.get(envelope["traceId"])
def fail_write():
raise OSError("injected local-link write failure")
monkeypatch.setattr(trace_store, "_write", fail_write)
with pytest.raises(OSError, match="injected local-link write failure"):
link_mps_trace_local_fact(
world,
trace_store=trace_store,
trace_id=envelope["traceId"],
tenant_uuid=TENANT,
project_id=PROJECT,
world_key=WORLD_KEY,
draft_hash=envelope["draftHash"],
world_fingerprint=envelope["worldFingerprint"],
schedule_id=summary["scheduleId"],
schedule_version_no=summary["scheduleVersionNo"],
schedule_fingerprint=summary["scheduleFingerprint"],
track="fixed",
fact_type="ADOPT",
)
assert trace_store.get(envelope["traceId"]) == before
def test_mps_api_lists_and_links_exact_local_version_with_scope_isolation(
mps_api_client,
):
client, state, _stores, ensure_scope = mps_api_client
draft = client.post("/api/mps/draft", json={"horizonDays": 30}).json()["draft"]
persisted = client.post("/api/mps/persist", json={"draft": draft}).json()
trace = persisted["envelope"]
versions_resp = client.get("/api/mps/schedule-versions")
assert versions_resp.status_code == 200
fixed = next(row for row in versions_resp.json() if row["track"] == "fixed")
payload = {
"draftHash": trace["draftHash"],
"worldFingerprint": trace["worldFingerprint"],
"scheduleId": fixed["scheduleId"],
"scheduleVersionNo": fixed["scheduleVersionNo"],
"scheduleFingerprint": fixed["scheduleFingerprint"],
"track": "fixed",
"factType": "ADOPT",
}
linked_resp = client.post(
f"/api/mps/trace/{trace['traceId']}/link",
json=payload,
)
assert linked_resp.status_code == 200
linked = linked_resp.json()
assert linked["idempotent"] is False
assert linked["trace"]["scheduleId"] == fixed["scheduleId"]
assert linked["trace"]["adoptId"].startswith("ADOPT:fixed:")
retry_resp = client.post(
f"/api/mps/trace/{trace['traceId']}/link",
json=payload,
)
assert retry_resp.status_code == 200
assert retry_resp.json()["idempotent"] is True
assert len(retry_resp.json()["trace"]["events"]) == 3
conflict_resp = client.post(
f"/api/mps/trace/{trace['traceId']}/link",
json={**payload, "factId": "ADOPT:fixed:conflicting-choice"},
)
assert conflict_resp.status_code == 409
assert len(client.get(f"/api/mps/trace/{trace['traceId']}").json()["events"]) == 3
gap_resp = client.post(
"/api/mps/gap",
json={"traceId": trace["traceId"], "track": "auto"},
)
assert gap_resp.status_code == 200
assert gap_resp.json()["schedule"]["versionNo"] == fixed["scheduleVersionNo"]
assert gap_resp.json()["trace"]["scheduleFingerprint"] == fixed[
"scheduleFingerprint"
]
state.update(
{"tenantUuid": "tenant-b", "projectId": PROJECT, "worldKey": WORLD_KEY}
)
ensure_scope("tenant-b", PROJECT, WORLD_KEY)
cross_scope = client.post(
f"/api/mps/trace/{trace['traceId']}/link",
json=payload,
)
assert cross_scope.status_code == 404