214 lines
7.9 KiB
Python
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
|