aps-agent/tests/golden/test_order_management.py

235 lines
8.7 KiB
Python
Raw Permalink 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.

# ============================================================
# OR-01 订单管理黄金测试(moduleId: golden-order-management, 发版门禁 §14.3)
# 固化订单池写入的最小闭环:
# O1 新增订单进入后续排产输入
# O2 取消订单从后续排产输入排除
# O3 P2 确认卡摘要可生成并校验成品物料
# O4 审计事件同时满足 at/ts 契约且哈希链可校验
# ============================================================
from __future__ import annotations
import pytest
from server.agent_core.audit import write_audit
from server.agent_core.registry import verify_audit_chain
from server.aps_domain.orders import apply_order_action, confirmation_for_order_action
from server.engines import get_engine
from server.engines.base import EngineParams
from server.state.seed import seed_world
from server.timeutil import add_minutes, fmt_date, today0
def _next_id_factory(world):
"""按当前世界最大 ID 起号,避免新增订单与种子 ID 冲突。"""
tables = {
"audit": "auditEvents",
"conflict": "conflicts",
"log": "logs",
"productionOrder": "productionOrders",
"salesOrder": "salesOrders",
"scheduleVersion": "scheduleVersions",
"workOrder": "workOrders",
}
counters: dict[str, int] = {}
def next_id(kind: str) -> int:
if kind not in counters:
rows = world.get(tables.get(kind, kind + "s"), [])
counters[kind] = max((r.get("id", 0) for r in rows if isinstance(r, dict)), default=0)
counters[kind] += 1
return counters[kind]
return next_id
def _run(world):
"""执行一次规则引擎排产。"""
params = EngineParams(
orderIds=[],
engineType="RULE",
strategyTemplate="COMPREHENSIVE",
planningHorizonDays=14,
startDate=fmt_date(add_minutes(today0(), 24 * 60)),
)
return get_engine("RULE").solve(world, params, _next_id_factory(world))
def test_order_cancel_excludes_from_scheduling():
"""取消订单后,该订单不再生成 PO,参与排产订单数从 7 降为 6。"""
world = seed_world()
target = world["salesOrders"][0]
apply_order_action(world, _next_id_factory(world), "order.cancel", {"id": target["id"]})
result = _run(world)
assert result.orderCount == 6
assert all(po["salesOrderId"] != target["id"] for po in world["productionOrders"])
assert target["status"] == "CANCELLED"
assert all(item["status"] == "CANCELLED" for item in target["items"])
def test_order_upsert_create_enters_scheduling():
"""新增已确认订单后,下一版排产应把它纳入订单池。"""
world = seed_world()
payload = {
"customerName": "试点客户",
"customerLevel": "A",
"deliveryDate": fmt_date(add_minutes(today0(), 9 * 24 * 60)),
"priority": 2,
"status": "CONFIRMED",
"productId": 1,
"quantity": 120,
"isRush": False,
"specialRequirements": "OR-01 golden",
}
applied = apply_order_action(world, _next_id_factory(world), "order.upsert", payload)
order = applied["order"]
result = _run(world)
assert applied["created"] is True
assert len(world["salesOrders"]) == 8
assert result.orderCount == 8
assert any(po["salesOrderId"] == order["id"] for po in world["productionOrders"])
assert order["orderNo"].startswith("SO")
def test_order_confirmation_summary_validates_product():
"""订单确认卡必须可解释影响范围,并拒绝非成品物料。"""
world = seed_world()
title, lines = confirmation_for_order_action(world, "order.upsert", {
"customerName": "试点客户",
"customerLevel": "VIP",
"deliveryDate": fmt_date(add_minutes(today0(), 8 * 24 * 60)),
"priority": 1,
"status": "CONFIRMED",
"productId": 1,
"quantity": 80,
})
summary = "\n".join(lines)
assert "新增" in title
assert "智能控制器A型" in summary
assert "P2" in summary
with pytest.raises(ValueError):
confirmation_for_order_action(world, "order.upsert", {
"customerName": "错误物料客户",
"deliveryDate": fmt_date(add_minutes(today0(), 8 * 24 * 60)),
"productId": 10,
"quantity": 1,
})
def test_audit_event_exposes_at_and_ts_contract():
"""新增审计事件应同时提供 at/ts,前端治理页与旧数据均可读取。"""
world = seed_world()
event = write_audit(
world,
_next_id_factory(world),
actor="tester",
category="GATE",
action="order.upsert.stage",
target={"type": "SALES_ORDER", "id": None},
power="P2",
rationale={"confirmId": "unit-test"},
)
assert event["at"] == event["ts"]
assert verify_audit_chain(world["auditEvents"])["ok"] is True
def test_order_delete_removes_from_pool():
"""删除订单后从订单池物理移除,且不再参与排产。"""
world = seed_world()
target = world["salesOrders"][0]
oid = target["id"]
before = len(world["salesOrders"])
title, lines = confirmation_for_order_action(world, "order.delete", {"id": oid})
assert "删除" in title
assert any("物理" in s or "移除" in s for s in lines)
apply_order_action(world, _next_id_factory(world), "order.delete", {"id": oid})
assert len(world["salesOrders"]) == before - 1
assert all(so["id"] != oid for so in world["salesOrders"])
result = _run(world)
assert result.orderCount == 6
assert all(po["salesOrderId"] != oid for po in world["productionOrders"]
if po.get("schedulingVersionId") == world["scheduleVersions"][-1]["id"])
with pytest.raises(ValueError):
confirmation_for_order_action(world, "order.delete", {"id": oid})
def test_import_orders_and_materials():
"""Pi 已结构化的订单/物料行通过确定性校验并写入世界。"""
from server.aps_domain.intake import apply_import, parse_import
world = seed_world()
parsed = parse_import(
"orders",
world,
rows=[
{
"customerName": "比亚迪", "customerLevel": "VIP",
"productCode": "CTRL-C", "quantity": 200,
"deliveryDate": "2026-08-01",
},
{
"customerName": "华为", "productCode": "CTRL-A",
"quantity": 100, "deliveryDate": "2026-08-15",
},
],
)
assert parsed["kind"] == "orders" and len(parsed["rows"]) == 2
assert parsed["errors"] == []
before = len(world["salesOrders"])
applied = apply_import(world, _next_id_factory(world), parsed)
assert applied["count"] == 2
assert len(world["salesOrders"]) == before + applied["count"]
parsed2 = parse_import(
"materials",
rows=[{
"code": "SCR-NEW", "name": "不锈钢螺丝",
"type": "RAW_MATERIAL", "unit": "包", "stock": 800,
}],
)
assert parsed2["kind"] == "materials" and parsed2["rows"] and not parsed2["errors"]
apply_import(world, _next_id_factory(world), parsed2)
assert any(m["code"] == "SCR-NEW" for m in world["materials"])
missing = parse_import(None, world)
assert missing["rows"] == []
assert "kind" in missing["errors"][0]
legacy_text = parse_import("导入订单\n比亚迪,CTRL-C,200,2026-08-01,VIP", world)
assert legacy_text["rows"] == []
assert "kind" in legacy_text["errors"][0]
def test_order_and_master_clear():
"""一键清理:订单池清空;主数据工艺板块回种。"""
from server.aps_domain.masterdata import apply_master_action, confirmation_for_master_action
world = seed_world()
title, _ = confirmation_for_order_action(world, "order.clear", {})
assert "清理" in title
applied = apply_order_action(world, _next_id_factory(world), "order.clear", {})
assert applied["clearedOrders"] >= 1
assert world["salesOrders"] == []
assert world["purchaseOrders"] == []
world2 = seed_world()
apply_master_action(world2, _next_id_factory(world2), "master.material.upsert", {
"code": "TMP-CLR", "name": "临时料", "type": "RAW_MATERIAL", "unit": "件", "stock": 1,
})
assert any(m["code"] == "TMP-CLR" for m in world2["materials"])
t2, lines = confirmation_for_master_action(world2, "master.clear", {"scope": "process"})
assert "清理" in t2 and any("清空" in s for s in lines)
apply_master_action(world2, _next_id_factory(world2), "master.clear", {"scope": "process"})
assert world2["materials"] == []
assert world2["boms"] == []
world3 = seed_world()
assert world3["lines"]
apply_master_action(world3, _next_id_factory(world3), "master.clear", {"scope": "resource"})
assert world3["factories"] == [] and world3["lines"] == []