aps-agent/tests/golden/test_rolling_reschedule.py

132 lines
5.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.

# ============================================================
# SC-12 滚动窗 / DY-01 分级重排 / SC-11 班组约束
# ============================================================
from __future__ import annotations
from server.aps_domain.flex import reschedule_by_level, run_flex_schedule
from server.engines.pool_engine import PoolEngine, parse_duration, resolve_window_minutes
from server.state.seed import seed_world
from server.timeutil import add_minutes, fmt_date, today0
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, 1000) + 1
return self.data[key]
def save(self):
pass
def test_parse_duration_and_window():
assert parse_duration("2h") == 120
assert parse_duration("2d") == 2 * 24 * 60
assert parse_duration("60m") == 60
assert resolve_window_minutes({"rollingWindows": {"short": "2h"}}, "short") == 120
assert resolve_window_minutes({}, "full") is None
assert resolve_window_minutes({}, "realtime") == 60
from server.engines.pool_engine import window_step_minutes
assert window_step_minutes("realtime") == 1
assert window_step_minutes("short") == 15
def test_short_window_defers_or_truncates():
world = seed_world()
start = fmt_date(add_minutes(today0(), 24 * 60))
r = PoolEngine().solve(world, _MemStore(world).next_id, sort_mode="BOTTLENECK",
start_date=start, window="short")
# 短窗 2h 不可能排完全部 10 单全工序 → 应有延期或截断冲突,或 wo 明显少于全量
full = seed_world()
r_full = PoolEngine().solve(full, _MemStore(full).next_id, sort_mode="BOTTLENECK",
start_date=start, window="full")
assert r["woCount"] <= r_full["woCount"]
assert r.get("window") == "short"
assert r["stepMinutes"] == 15
def test_team_constraint_assigns_team_code():
world = seed_world()
start = fmt_date(add_minutes(today0(), 24 * 60))
PoolEngine().solve(world, _MemStore(world).next_id, sort_mode="BOTTLENECK",
start_date=start, enforce_teams=True)
wos = world["flexWorkOrders"]
assert wos
assert any(w.get("teamCode") for w in wos)
def test_l2_reschedule_freezes_outside():
store = _MemStore(seed_world())
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test", window="full")
before_wo = len([w for w in store.data["flexWorkOrders"]
if w["versionId"] == store.data["flexScheduleVersions"][-1]["id"]])
assert before_wo > 0
r = reschedule_by_level(store, level="L2", actor="test")
assert r["level"] == "L2"
assert r["schedule"]["versionNo"]
# 新版本含冻结标记工单(若有窗外)或至少产出版本
latest = store.data["flexScheduleVersions"][-1]["id"]
frozen = [w for w in store.data["flexWorkOrders"]
if w["versionId"] == latest and w.get("frozen")]
assert r.get("frozenWo", 0) == len(frozen) or r["schedule"]["woCount"] >= 0
def test_realtime_window_step_one_minute():
world = seed_world()
start = fmt_date(add_minutes(today0(), 24 * 60))
r = PoolEngine().solve(world, _MemStore(world).next_id, sort_mode="BOTTLENECK",
start_date=start, window="realtime")
assert r.get("window") == "realtime"
assert r["stepMinutes"] == 1
assert r.get("windowMinutes") == 60
def test_l1_local_swap_keeps_others():
"""L1:PRESS-01 工单换备机;他单设备与时段不变;不新建版本。"""
from server.aps_domain.flex import local_swap_equipment
store = _MemStore(seed_world())
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test", window="full")
latest = store.data["flexScheduleVersions"][-1]["id"]
before_ver_n = len(store.data["flexScheduleVersions"])
wos = [w for w in store.data["flexWorkOrders"] if w["versionId"] == latest]
press_wos = [w for w in wos if w.get("equipmentCode") == "PRESS-01"]
assert press_wos, "种子排产后应有 PRESS-01 工单"
snapshot = {
w["id"]: (w["equipmentCode"], w["plannedStartTime"], w["plannedEndTime"])
for w in wos if w["id"] not in {x["id"] for x in press_wos}
}
r = local_swap_equipment(store, equipment_code="PRESS-01", mark_fault=True, actor="test")
assert r["swapped"]
assert all(s["from"] == "PRESS-01" and s["to"] != "PRESS-01" for s in r["swapped"])
assert r.get("faulted") == "PRESS-01"
assert len(store.data["flexScheduleVersions"]) == before_ver_n
for wid, (code, start, end) in snapshot.items():
wo = next(w for w in store.data["flexWorkOrders"] if w["id"] == wid)
assert wo["equipmentCode"] == code
assert wo["plannedStartTime"] == start
assert wo["plannedEndTime"] == end
def test_reschedule_is_p2_and_preview():
from server.agent_core import harness
from server.aps_domain.flex import preview_reschedule
assert harness.power_of("flex.reschedule") == "P2"
assert harness.power_of("flex.swap") == "P1"
store = _MemStore(seed_world())
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test")
title, lines = preview_reschedule(store, "L2")
assert "L2" in title
assert lines
# stage 不出执行:只登记确认卡
block = harness.stage_confirmation("test", "flex.reschedule", {"level": "L2"},
title=title, summary_lines=lines)
assert block.props["confirmId"]
assert harness.power_of("flex.reschedule") == "P2"
pending = harness.take_confirmation(block.props["confirmId"])
assert pending["action"] == "flex.reschedule"
# 取出后世界版本数不变(未执行)
assert len(store.data["flexScheduleVersions"]) == 1