# ============================================================ # 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