from __future__ import annotations from fastapi.testclient import TestClient from server.aps_domain.mes import validate_dispatchable_version from tests.golden.test_closed_loop_runtime import _ready_world class _MemStore: def __init__(self, data: dict): self.data = data self.world_key = "closed-loop-gateway" def next_id(self, kind: str) -> int: key = f"_gateway_{kind}" self.data[key] = int(self.data.get(key) or 9000) + 1 return self.data[key] def save(self) -> None: return None def test_closed_loop_api_decompose_schedule_publish_without_mes(monkeypatch): import server.gateway.app as gateway_module from tests.auth_provider import install_test_auth world = _ready_world() world["businessDate"] = "2026-08-03" world["scheduleVersions"] = [] store = _MemStore(world) monkeypatch.setattr(gateway_module, "get_store", lambda: store) provider = install_test_auth(monkeypatch, "tenant-closed-loop-gateway") client = TestClient(gateway_module.create_app()) assert client.post("/api/auth/login", json={"username": "planner"}).status_code == 200 decomposed = client.post( "/api/mrp/decompose", json={"sessionId": "closed-loop-api", "orderNo": "SO-READY"}, ) assert decomposed.status_code == 200 payload = decomposed.json()["result"] closed_loop = payload["closedLoop"] assert closed_loop["businessDate"] == "2026-08-03" assert closed_loop["summary"]["makeCount"] == 1 assert closed_loop["projection"]["readyDemandCount"] == 1 assert len(closed_loop["manufacturingDemands"]) == 1 assert world.get("productionOrders") in (None, []) assert world.get("workOrders") in (None, []) scheduled = client.post( "/api/flex/schedule", json={"sessionId": "closed-loop-api", "sortMode": "ASC"}, ) assert scheduled.status_code == 200 result = scheduled.json()["result"] assert result["executionMode"] == "CLOSED_LOOP_V1" assert result["solveStatus"] == "FEASIBLE" assert result["woCount"] == 1 version_id = int(result["versionId"]) version = next(row for row in world["flexScheduleVersions"] if row["id"] == version_id) assert version["status"] == "DRAFT" assert validate_dispatchable_version(world, "flex", version_id)["publishReady"] is True staged = client.post( "/api/schedule/publish/stage", json={ "sessionId": "closed-loop-api", "track": "flex", "versionId": version_id, }, ) assert staged.status_code == 200 assert staged.json()["staged"] is True confirm_id = staged.json()["confirmId"] assert confirm_id assert version["status"] == "DRAFT" assert world.get("mesLinks") in (None, []) from server.aps_domain.workflow import execute_confirmed from server.auth.context import bind_identity, reset_identity from server.agent_core import harness monkeypatch.setattr(harness, "resolve_confirmation_store", lambda _confirm_id, fallback: fallback) identity_token = bind_identity(provider._identity("planner")) try: confirmation_message = execute_confirmed( store, confirm_id, approve=True, actor="planner", ) finally: reset_identity(identity_token) assert "\u5df2\u53d1\u5e03" in confirmation_message assert version["status"] == "PUBLISHED" assert world.get("mesLinks") in (None, []) assert validate_dispatchable_version(world, "flex", version_id)["dispatchReady"] is True def test_orders_api_restores_persisted_closed_loop_and_real_gate_state(monkeypatch): import server.gateway.app as gateway_module from tests.auth_provider import install_test_auth world = _ready_world() world["businessDate"] = "2026-08-03" world["scheduleVersions"] = [] for sales_order in world.get("salesOrders") or []: sales_order.setdefault("customerName", "闭环测试客户") sales_order.setdefault("customerLevel", "A") sales_order.setdefault("orderDate", "2026-08-03") sales_order.setdefault("deliveryDate", sales_order.get("dueDate") or "2026-08-03") store = _MemStore(world) monkeypatch.setattr(gateway_module, "get_store", lambda: store) install_test_auth(monkeypatch, "tenant-closed-loop-orders-status") client = TestClient(gateway_module.create_app()) assert client.post("/api/auth/login", json={"username": "planner"}).status_code == 200 assert client.post( "/api/mrp/decompose", json={"sessionId": "orders-status", "orderNo": "SO-READY"}, ).status_code == 200 scheduled = client.post( "/api/flex/schedule", json={"sessionId": "orders-status", "sortMode": "ASC"}, ).json()["result"] version_id = int(scheduled["versionId"]) snapshot = client.get("/api/orders").json() assert snapshot["closedLoop"]["problemId"] == scheduled["planningProblemId"] assert snapshot["closedLoop"]["projection"]["readyDemandCount"] == 1 status = snapshot["closedLoopStatus"] assert status["latestVersion"]["id"] == version_id assert status["latestVersion"]["solveStatus"] == "FEASIBLE" assert status["gate"]["publishReady"] is True assert status["gate"]["dispatchReady"] is False assert status["mesReceipt"] is None staged = client.post( "/api/schedule/publish/stage", json={"sessionId": "orders-status", "track": "flex", "versionId": version_id}, ).json() from server.aps_domain.workflow import execute_confirmed from server.auth.context import bind_identity, reset_identity from server.agent_core import harness monkeypatch.setattr(harness, "resolve_confirmation_store", lambda _confirm_id, fallback: fallback) provider = install_test_auth(monkeypatch, "tenant-closed-loop-orders-status") identity_token = bind_identity(provider._identity("planner")) try: execute_confirmed(store, staged["confirmId"], approve=True, actor="planner") finally: reset_identity(identity_token) published = client.get("/api/orders").json()["closedLoopStatus"] assert published["latestVersion"]["status"] == "PUBLISHED" assert published["gate"]["publishReady"] is False assert published["gate"]["dispatchReady"] is True def test_orders_get_does_not_save_when_flex_projection_is_unchanged(monkeypatch): import copy import server.gateway.app as gateway_module from server.aps_domain.orders import sync_flex_orders_to_sales from server.state.seed import empty_world from tests.auth_provider import install_test_auth world = empty_world() world["businessDate"] = "2026-08-03" world["flexOrders"] = [{ "id": 1, "orderNo": "SO-READ-IDEMPOTENT", "productCode": "P-READ-IDEMPOTENT", "productName": "Read Idempotent Product", "quantity": 2, "dueDate": "2026-08-10", "status": "RELEASED", }] world["materials"] = [{ "id": 1, "code": "P-READ-IDEMPOTENT", "name": "Read Idempotent Product", "type": "FINISHED_PRODUCT", "unit": "PCS", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0, }] assert sync_flex_orders_to_sales(world) == 1 before = copy.deepcopy(world) class CountingStore(_MemStore): def __init__(self, data: dict): super().__init__(data) self.save_count = 0 def save(self) -> None: self.save_count += 1 store = CountingStore(world) monkeypatch.setattr(gateway_module, "get_store", lambda: store) install_test_auth(monkeypatch, "tenant-orders-read-idempotent") client = TestClient(gateway_module.create_app()) assert client.post("/api/auth/login", json={"username": "planner"}).status_code == 200 response = client.get("/api/orders") assert response.status_code == 200 assert response.json()["orders"][0]["orderNo"] == "SO-READ-IDEMPOTENT" assert store.save_count == 0 assert world == before