2026-09-03 19:18:42 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import copy
|
|
|
|
|
|
import json
|
2026-09-16 17:32:29 +08:00
|
|
|
|
from datetime import date
|
2026-09-03 19:18:42 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from server.aps_domain.closed_loop_runtime import run_closed_loop_candidate
|
2026-09-16 17:32:29 +08:00
|
|
|
|
from server.aps_domain.views import due_view
|
2026-09-03 19:18:42 +08:00
|
|
|
|
from server.state.packs import load_pack, list_packs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PACK_PATH = Path(__file__).resolve().parents[2] / "server" / "data" / "packs" / "optimize-simulation-v1.json"
|
|
|
|
|
|
RULES = ("EDD", "SPT", "PRIORITY", "FIFO", "LPT", "CR", "ATC")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _next_id_factory():
|
|
|
|
|
|
counters: dict[str, int] = {}
|
|
|
|
|
|
|
|
|
|
|
|
def next_id(kind: str) -> int:
|
|
|
|
|
|
counters[kind] = counters.get(kind, 0) + 1
|
|
|
|
|
|
return counters[kind]
|
|
|
|
|
|
|
|
|
|
|
|
return next_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_simulation_world_pack_is_a_generic_aps_fixture():
|
|
|
|
|
|
pack = json.loads(PACK_PATH.read_text(encoding="utf-8"))
|
|
|
|
|
|
world = load_pack(str(PACK_PATH))
|
|
|
|
|
|
|
|
|
|
|
|
assert pack["packVersion"] == 1
|
|
|
|
|
|
assert pack["name"] == "Optimize synthetic debug V1"
|
|
|
|
|
|
assert world["flexParams"]["dataGrade"] == "synthetic"
|
|
|
|
|
|
assert len(world["flexOrders"]) == 10
|
|
|
|
|
|
assert len(world["salesOrders"]) == 10
|
|
|
|
|
|
assert len(world["flexRoutings"]) == 72
|
|
|
|
|
|
assert len(world["flexEquipment"]) == 6
|
|
|
|
|
|
assert all(row["source"] == "SYNTHETIC" for row in world["flexOrders"])
|
|
|
|
|
|
assert {row["productCode"] for row in world["flexRoutings"]} == {
|
|
|
|
|
|
row["productCode"] for row in world["flexOrders"]
|
|
|
|
|
|
}
|
|
|
|
|
|
assert any(item["file"] == PACK_PATH.name for item in list_packs())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_simulation_world_pack_runs_all_optimize_dispatch_rules():
|
|
|
|
|
|
source_world = load_pack(str(PACK_PATH))
|
|
|
|
|
|
|
|
|
|
|
|
for rule in RULES:
|
|
|
|
|
|
world = copy.deepcopy(source_world)
|
|
|
|
|
|
result = run_closed_loop_candidate(
|
|
|
|
|
|
world,
|
|
|
|
|
|
_next_id_factory(),
|
|
|
|
|
|
business_date="2026-08-02",
|
|
|
|
|
|
engine_type="OPTIMIZE",
|
|
|
|
|
|
sort_mode=rule,
|
|
|
|
|
|
strict=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert result["solveStatus"] == "FEASIBLE", rule
|
|
|
|
|
|
assert result["engineType"] == "OPTIMIZE", rule
|
|
|
|
|
|
assert result["algorithmId"] == f"optimize.{rule.lower()}", rule
|
|
|
|
|
|
assert result["orderCount"] == 10, rule
|
|
|
|
|
|
assert result["vlCount"] == 10, rule
|
|
|
|
|
|
assert result["woCount"] == 72, rule
|
|
|
|
|
|
assert result["validation"]["valid"] is True, rule
|
|
|
|
|
|
assert result["validation"]["hardViolations"] == [], rule
|
2026-09-16 17:32:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_simulation_world_pack_keeps_sales_order_fields_the_due_board_reads():
|
|
|
|
|
|
"""交期承诺看板直取订单客户字段,数据包必须按订单契约补齐(回归:/api/world/due 500)。"""
|
|
|
|
|
|
world = load_pack(str(PACK_PATH))
|
|
|
|
|
|
|
|
|
|
|
|
rows = due_view(world)
|
|
|
|
|
|
|
|
|
|
|
|
assert len(rows) == len(world["salesOrders"]) == 10
|
|
|
|
|
|
assert all(row["customerName"] and row["customerLevel"] for row in rows)
|
|
|
|
|
|
assert all(isinstance(row["isRush"], bool) for row in rows)
|
|
|
|
|
|
assert any(row["isRush"] for row in rows) # 至少一张插单,加急标记在 UI 有数据可验
|
|
|
|
|
|
assert {row["customerLevel"] for row in rows} >= {"VIP", "A", "B", "C"} # 等级过滤有数据可筛
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_simulation_world_pack_timeline_follows_its_case_business_day():
|
|
|
|
|
|
"""回归:baseDate 必须是案例业务日,否则世界日期随运行日漂移、黄金测试输入每天变。
|
|
|
|
|
|
|
|
|
|
|
|
源案例(kangni-simulation-package-v1)的业务日交期结构固定为「9 张逾期 + 最晚交期在业务日 +6 天」,
|
|
|
|
|
|
世界日期整体贴着 baseDate 平移后,这个结构在任意运行日都应原样落在"今天"上。
|
|
|
|
|
|
"""
|
|
|
|
|
|
world = load_pack(str(PACK_PATH))
|
|
|
|
|
|
due_dates = sorted(date.fromisoformat(order["dueDate"]) for order in world["flexOrders"])
|
|
|
|
|
|
today = date.today()
|
|
|
|
|
|
|
|
|
|
|
|
assert (due_dates[-1] - today).days == 6 # 最晚交期 = 案例业务日 +6 天
|
|
|
|
|
|
assert sum(1 for due in due_dates if due < today) == 9 # 案例本身是 9 张逾期单
|