aps-agent/tests/golden/test_closed_loop_gateway.py

214 lines
7.9 KiB
Python

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