# ============================================================ # EX-04 甘特拖拽调程黄金测试 # ============================================================ from __future__ import annotations from server.aps_domain.adjust import commit_flex_adjust, preview_flex_adjust, snap_dt from server.aps_domain.flex import run_flex_schedule from server.timeutil import add_minutes, parse_dt 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, 3000) + 1 return self.data[key] def save(self): pass def test_snap_dt_15min(): dt = parse_dt("2026-07-23 08:07") assert snap_dt(dt).minute in (0, 15) assert snap_dt(parse_dt("2026-07-23 08:08")).minute == 15 def test_preview_ok_and_overlap_fail(): store = _MemStore(seed_world()) run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test") wos = [w for w in store.data["flexWorkOrders"] if w["versionId"] == store.data["flexScheduleVersions"][-1]["id"]] assert wos from server.timeutil import fmt_dt # 找一条能后移 15 分钟的工单 ok = None for wo in wos: later = fmt_dt(add_minutes(parse_dt(wo["plannedStartTime"]), 15)) cand = preview_flex_adjust(store.data, wo["id"], later) if cand["ok"] and cand["moves"]: ok = cand break assert ok is not None, "种子排产后应至少有一条可平移 15 分钟的工单" # 故意拖到同设备另一工单起点 → 应报不可行(若存在同伴) wo0 = next(w for w in wos if w["id"] == ok["woId"]) same_eq = [w for w in wos if w["equipmentId"] == wo0["equipmentId"] and w["id"] != wo0["id"]] if same_eq: bad = preview_flex_adjust(store.data, wo0["id"], same_eq[0]["plannedStartTime"]) assert bad["ok"] is False or bad["reasons"] is not None def test_commit_creates_new_version(): store = _MemStore(seed_world()) run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test") before = len(store.data["flexScheduleVersions"]) from server.timeutil import fmt_dt wos = [w for w in store.data["flexWorkOrders"] if w["versionId"] == store.data["flexScheduleVersions"][-1]["id"]] target = None later = None for wo in wos: cand_start = fmt_dt(add_minutes(parse_dt(wo["plannedStartTime"]), 15)) prev = preview_flex_adjust(store.data, wo["id"], cand_start) if prev["ok"] and prev["moves"]: target, later = wo, cand_start break assert target is not None r = commit_flex_adjust(store, target["id"], later, actor="test") assert len(store.data["flexScheduleVersions"]) == before + 1 assert r["versionNo"] latest = store.data["flexScheduleVersions"][-1]["id"] assert any(w.get("adjustedFrom") for w in store.data["flexWorkOrders"] if w["versionId"] == latest) def test_preview_does_not_mutate(): store = _MemStore(seed_world()) run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test") wo = next(w for w in store.data["flexWorkOrders"] if w["versionId"] == store.data["flexScheduleVersions"][-1]["id"]) old = wo["plannedStartTime"] from server.timeutil import fmt_dt preview_flex_adjust(store.data, wo["id"], fmt_dt(add_minutes(parse_dt(old), 60))) assert wo["plannedStartTime"] == old assert len(store.data["flexScheduleVersions"]) == 1 def test_fixed_preview_and_commit(): from server.aps_domain.adjust import commit_fixed_adjust, preview_fixed_adjust from server.engines import get_engine from server.engines.base import EngineParams from server.timeutil import fmt_date, fmt_dt, today0 store = _MemStore(seed_world()) ep = EngineParams( orderIds=[], engineType="RULE", strategyTemplate="COMPREHENSIVE", planningHorizonDays=14, startDate=fmt_date(add_minutes(today0(), 24 * 60)), ) get_engine("RULE").solve(store.data, ep, store.next_id) before = len(store.data["scheduleVersions"]) latest = store.data["scheduleVersions"][-1] po_ids = {p["id"] for p in store.data["productionOrders"] if p.get("schedulingVersionId") == latest["id"]} wos = [w for w in store.data["workOrders"] if w["productionOrderId"] in po_ids] assert wos target = None later = None for wo in wos: cand = fmt_dt(add_minutes(parse_dt(wo["plannedStartTime"]), 15)) prev = preview_fixed_adjust(store.data, wo["id"], cand) if prev["ok"] and prev["moves"]: target, later = wo, cand break assert target is not None, "应至少有一条可平移 15 分钟的固定轨工单" r = commit_fixed_adjust(store, target["id"], later, actor="test") assert len(store.data["scheduleVersions"]) == before + 1 assert r["versionNo"].startswith("V") new_vid = store.data["scheduleVersions"][-1]["id"] new_po_ids = {p["id"] for p in store.data["productionOrders"] if p.get("schedulingVersionId") == new_vid} assert any(w.get("adjustedFrom") for w in store.data["workOrders"] if w["productionOrderId"] in new_po_ids)