121 lines
4.7 KiB
Python
121 lines
4.7 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 康尼柔性场景黄金测试:交期三区间 / 三模式 / 故障 / 模具寿命
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from server.agent_core.intent import parse_fast
|
|||
|
|
from server.aps_domain.flex import (
|
|||
|
|
apply_equipment_fault, compare_sort_modes, insert_rush_order, simulate_due,
|
|||
|
|
)
|
|||
|
|
from server.engines import PoolEngine
|
|||
|
|
from server.state.seed import seed_world
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _MemStore:
|
|||
|
|
def __init__(self, data):
|
|||
|
|
self.data = data
|
|||
|
|
|
|||
|
|
def next_id(self, kind: str) -> int:
|
|||
|
|
key = f"_c_{kind}"
|
|||
|
|
self.data[key] = self.data.get(key, 100) + 1
|
|||
|
|
return self.data[key]
|
|||
|
|
|
|||
|
|
def save(self):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_simulate_due_has_three_bands_and_gaps():
|
|||
|
|
store = _MemStore(seed_world())
|
|||
|
|
r = simulate_due(store, "PDU-UNIT", 500)
|
|||
|
|
assert r["optimisticFinish"]
|
|||
|
|
assert r["expectedFinish"]
|
|||
|
|
assert r["pessimisticFinish"]
|
|||
|
|
assert r["gaps"]
|
|||
|
|
assert any(g.get("kind") for g in r["gaps"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_compare_modes_three_rows():
|
|||
|
|
store = _MemStore(seed_world())
|
|||
|
|
cmp = compare_sort_modes(store, compress_due=True)
|
|||
|
|
assert len(cmp["rows"]) == 3
|
|||
|
|
modes = {r["sortMode"] for r in cmp["rows"]}
|
|||
|
|
assert modes == {"ASC", "DESC", "BOTTLENECK"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_capacity_forecast_horizons():
|
|||
|
|
from server.aps_domain.flex import capacity_analysis, run_flex_schedule
|
|||
|
|
store = _MemStore(seed_world())
|
|||
|
|
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test")
|
|||
|
|
cap = capacity_analysis(store.data)
|
|||
|
|
assert cap["horizons"]
|
|||
|
|
assert {h["key"] for h in cap["horizons"]} == {"h4", "d1", "w1"}
|
|||
|
|
assert cap["pools"]
|
|||
|
|
p0 = cap["pools"][0]
|
|||
|
|
assert "forecast" in p0
|
|||
|
|
assert set(p0["forecast"].keys()) >= {"h4", "d1", "w1"}
|
|||
|
|
assert 0 <= p0["forecast"]["d1"]["utilization"]
|
|||
|
|
assert p0["forecast"]["d1"]["alert"] in ("ok", "warn", "crit")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_mold_life_consumed_and_can_lock():
|
|||
|
|
world = seed_world()
|
|||
|
|
mold = next(m for m in world["flexMolds"] if m["code"] == "MOLD-HV-01")
|
|||
|
|
before = mold["lifeUsed"]
|
|||
|
|
mold["lifeTotal"] = before + 50 # 排一单就会到限
|
|||
|
|
counters: dict[str, int] = {}
|
|||
|
|
|
|||
|
|
def nid(k: str) -> int:
|
|||
|
|
counters[k] = counters.get(k, 0) + 1
|
|||
|
|
return counters[k]
|
|||
|
|
|
|||
|
|
PoolEngine().solve(world, nid, sort_mode="BOTTLENECK")
|
|||
|
|
assert mold["lifeUsed"] > before
|
|||
|
|
assert mold["status"] == "LOCKED" or mold["lifeUsed"] >= mold["lifeTotal"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_fault_weld_can_raise_no_capability():
|
|||
|
|
store = _MemStore(seed_world())
|
|||
|
|
# 先正常排一轮,再焊机故障
|
|||
|
|
from server.aps_domain.flex import run_flex_schedule
|
|||
|
|
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test")
|
|||
|
|
r = apply_equipment_fault(store, "WELD-01", status="MAINTENANCE", reschedule=True, actor="test")
|
|||
|
|
assert r["equipment"]["afterStatus"] == "MAINTENANCE"
|
|||
|
|
# 焊接单点:无备机 → L4 缩池重排
|
|||
|
|
assert r.get("responseLevel") == "L4"
|
|||
|
|
types = {c.get("conflictType") for c in r.get("conflicts") or []}
|
|||
|
|
assert "NO_CAPABILITY" in types or r["schedule"]["conflictCount"] >= 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_fault_press_prefers_l1_swap():
|
|||
|
|
store = _MemStore(seed_world())
|
|||
|
|
from server.aps_domain.flex import run_flex_schedule
|
|||
|
|
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test")
|
|||
|
|
before_ver = len(store.data["flexScheduleVersions"])
|
|||
|
|
r = apply_equipment_fault(store, "PRESS-01", status="MAINTENANCE", reschedule=True, actor="test")
|
|||
|
|
assert r["equipment"]["afterStatus"] == "MAINTENANCE"
|
|||
|
|
# 压接池有冗余:优先 L1,不新建版本
|
|||
|
|
if r.get("responseLevel") == "L1":
|
|||
|
|
assert r.get("swap", {}).get("swapped")
|
|||
|
|
assert r.get("schedule") is None
|
|||
|
|
assert len(store.data["flexScheduleVersions"]) == before_ver
|
|||
|
|
eq = next(e for e in store.data["flexEquipment"] if e["code"] == "PRESS-01")
|
|||
|
|
assert eq["status"] == "MAINTENANCE"
|
|||
|
|
else:
|
|||
|
|
# 极端拥挤时退化为 L4 亦可接受
|
|||
|
|
assert r.get("responseLevel") == "L4"
|
|||
|
|
assert r.get("schedule")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_rush_insert_intent_and_order():
|
|||
|
|
world = seed_world()
|
|||
|
|
assert parse_fast("三模式对比", world).intent == "flex.compare"
|
|||
|
|
assert parse_fast("设备故障 WELD-01", world).intent == "flex.fault"
|
|||
|
|
assert parse_fast("柔性插单 PDU 80套", world).intent == "flex.rush"
|
|||
|
|
assert parse_fast("局部换机 PRESS-01", world).intent == "flex.swap"
|
|||
|
|
assert parse_fast("局部换机 PRESS-01", world).params.get("equipmentCode") == "PRESS-01"
|
|||
|
|
store = _MemStore(seed_world())
|
|||
|
|
before = len(store.data["flexOrders"])
|
|||
|
|
r = insert_rush_order(store, "PDU-UNIT", 80, actor="test")
|
|||
|
|
assert len(store.data["flexOrders"]) == before + 1
|
|||
|
|
assert r["order"]["orderNo"].startswith("RUSH-")
|