aps-agent/tests/golden/test_schedule_wizard.py

144 lines
5.8 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.

# ============================================================
# M-F 黄金测试:引导式排产向导(缺路线→模板推荐→P2生成→试排放行)
# ============================================================
from __future__ import annotations
import pytest
from server.agent_core import dialog, harness
from server.contracts import AgentReply, IntentResult
from server.state.seed import seed_world
class FakeStore:
def __init__(self):
self.data = seed_world()
self._counters: dict[str, int] = {}
def next_id(self, kind: str) -> int:
self._counters[kind] = self._counters.get(kind, 0) + 1
return self._counters[kind]
def save(self) -> None:
pass
@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "master.db"))
monkeypatch.delenv("APS_DB_DISABLED", raising=False)
from server.db.database import reset_engine
reset_engine()
dialog._SESSIONS.clear()
yield
dialog._SESSIONS.clear()
reset_engine()
def test_wizard_all_green_offers_run_then_releases_schedule_intent():
store = FakeStore()
reply = dialog.start_wizard(store, "w1")
assert isinstance(reply, AgentReply)
assert "可排" in reply.text or "齐备" in reply.text
out = dialog.pre_route(store, "w1", "排")
assert isinstance(out, IntentResult)
assert out.intent == "flex.schedule"
assert out.params.get("sortMode") == "BOTTLENECK"
def test_wizard_recommends_template_for_missing_routing():
store = FakeStore()
# 新产品 + 订单,无工艺路线 → 触发模板推荐
store.data["flexMaterials"].append({
"code": "SHAFT-01", "name": "传动轴(车削件)", "type": "FINISHED_PRODUCT",
"unit": "件", "stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0})
store.data["flexOrders"].append({
"id": 99, "orderNo": "FO-9901", "productCode": "SHAFT-01", "quantity": 20,
"dueDate": "2099-12-31", "priority": 1, "wbs": "", "productionController": "测试",
"status": "RELEASED"})
reply = dialog.start_wizard(store, "w2")
assert "没有工艺路线" in reply.text
assert reply.blocks and reply.blocks[0].type == "wizard"
tpls = reply.blocks[0].props.get("templates") or []
assert tpls, "应推荐至少一个行业模板"
# 产品名含「车削」→ 车削模板应在推荐里
assert any(t["code"] == "TPL-TURNING" for t in tpls)
# 用户选 1 → 出 P2 确认卡
reply2 = dialog.pre_route(store, "w2", "1")
assert isinstance(reply2, AgentReply)
confirm_blocks = [b for b in reply2.blocks if b.type == "confirm-card"]
assert confirm_blocks, "选模板后应出确认卡"
confirm_id = confirm_blocks[0].props["confirmId"]
# 批准 → 路线生成(工时标「模板」)
from server.aps_domain.workflow import execute_confirmed
msg = execute_confirmed(store, confirm_id, approve=True, actor="tester")
assert "工艺路线已生成" in msg
steps = [r for r in store.data["flexRoutings"] if r["productCode"] == "SHAFT-01"]
assert steps
assert all(s["stdTimeSource"] == "模板" for s in steps)
# 继续排产 → 全绿(或仅剩警告)→ 给试排确认
reply3 = dialog.pre_route(store, "w2", "继续排产")
assert isinstance(reply3, AgentReply)
assert "可排" in reply3.text or "齐备" in reply3.text
def test_wizard_asks_time_for_unmaintained_steps():
store = FakeStore()
# 摘掉 OP-WELD 设备默认工时 → PDU 产品该步「待维护」
for e in store.data["flexEquipment"]:
e.get("opStdTime", {}).pop("OP-WELD", None)
reply = dialog.start_wizard(store, "w3")
assert "没有工时" in reply.text or "工序" in reply.text
assert dialog._SESSIONS["w3"]["wizard"]["step"] == "fill_time"
# 回复分钟数 → 出 P2 工时确认卡
reply2 = dialog.pre_route(store, "w3", "12")
assert isinstance(reply2, AgentReply)
confirm_blocks = [b for b in reply2.blocks if b.type == "confirm-card"]
assert confirm_blocks
from server.aps_domain.workflow import execute_confirmed
msg = execute_confirmed(store, confirm_blocks[0].props["confirmId"], approve=True, actor="tester")
assert "工时已更新" in msg
weld_steps = [r for r in store.data["flexRoutings"] if r["operationCode"] == "OP-WELD"]
assert all(s.get("stdTimePerUnit") == 12.0 for s in weld_steps)
assert all(s.get("stdTimeSource") == "实测" for s in weld_steps)
def test_wizard_default_calendar():
store = FakeStore()
store.data["flexCalendar"] = []
reply = dialog.start_wizard(store, "w4")
assert "班次日历" in reply.text
reply2 = dialog.pre_route(store, "w4", "默认日历")
assert isinstance(reply2, AgentReply)
assert "默认日历" in reply2.text
assert store.data["flexCalendar"], "应写入默认日历"
def test_wizard_cancel():
store = FakeStore()
dialog.start_wizard(store, "w5")
reply = dialog.pre_route(store, "w5", "取消")
assert isinstance(reply, AgentReply)
assert "退出" in reply.text
assert dialog._SESSIONS["w5"]["wizard"] is None
def test_wizard_intent_rules():
from server.agent_core.intent import parse_fast
world = seed_world()
for phrase in ("我要排产", "怎么排产", "排产向导"):
r = parse_fast(phrase, world)
assert r is not None and r.intent == "schedule.wizard", phrase
# 「帮我排产 / 给我排产」是祈使开排,不再进向导菜单
for phrase in ("帮我排产", "给我排产", "我要你排产", "排产吧"):
r = parse_fast(phrase, world)
assert r is not None and r.intent in ("flex.schedule", "folder.schedule"), phrase
# 既有口令不受影响
r = parse_fast("柔性排产", world)
assert r is not None and r.intent == "flex.schedule"