aps-agent/tests/golden/test_kangni_flex.py

121 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 康尼柔性场景黄金测试:交期三区间 / 三模式 / 故障 / 模具寿命
# ============================================================
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-")