aps-agent/tests/golden/test_wms_replay.py

127 lines
6.2 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.

# ============================================================
# round-38 方向 N:WMS 事件流重放黄金测试
# 固化:事件可重放(seq/arrival 顺序);重复重放幂等不重复触发;
# emit_shortage 幂等(idem_key);库存版本单调。
# ============================================================
from __future__ import annotations
from pathlib import Path
from server.aps_domain import wms_events
from server.integrations.wms_stub import reset_wms_client
from server.state.checkpoints import CheckpointStore
from server.state.seed import seed_world
class _MemStore:
def __init__(self, data, checkpoint_path: Path):
self.data = data
self.checkpoints = CheckpointStore(str(checkpoint_path))
def next_id(self, kind: str) -> int:
key = f"_c_{kind}"
self.data[key] = self.data.get(key, 4000) + 1
return self.data[key]
def save(self):
pass
def _seed_ledger(client):
client.upsert_inventory([
{"materialCode": "WIRE-HV", "name": "高压线材", "unit": "米",
"stock": 12000, "inTransit": 0, "safetyStock": 1000},
{"materialCode": "TERM-HV", "name": "高压端子", "unit": "个",
"stock": 6000, "inTransit": 2000, "safetyStock": 500},
{"materialCode": "BUSBAR", "name": "铜排", "unit": "根",
"stock": 1500, "inTransit": 400, "safetyStock": 200},
])
def test_emit_shortage_idempotent_by_idem_key(tmp_path: Path):
"""emit_shortage:相同 idem_key 只生成一次事件(重复调用返回同一 eventId)。"""
reset_wms_client(tmp_path / "wms_mirror.json")
client = reset_wms_client(tmp_path / "wms_mirror.json")
_seed_ledger(client)
ev1 = client.emit_shortage("WIRE-HV", new_stock=500, shortage_qty=220,
occurred_at="2026-08-02 09:00:00",
idem_key="k-wire")
ev2 = client.emit_shortage("WIRE-HV", new_stock=500, shortage_qty=220,
occurred_at="2026-08-02 09:00:00",
idem_key="k-wire")
assert ev1["duplicate"] is False and ev2["duplicate"] is True
assert ev2["eventId"] == ev1["eventId"]
assert ev2["seq"] == ev1["seq"]
# 缺省 idem_key(materialCode + occurredAt)同样幂等
ev3 = client.emit_shortage("TERM-HV", new_stock=3000, occurred_at="2026-08-02 10:00:00")
ev4 = client.emit_shortage("TERM-HV", new_stock=3000, occurred_at="2026-08-02 10:00:00")
assert ev4["duplicate"] is True and ev4["eventId"] == ev3["eventId"]
# 事件流单调:seq 递增、库存版本递增
assert [e["seq"] for e in client.replay()] == [1, 2]
assert client.inventory_version() == 2
def test_replay_stream_order_limit_and_version(tmp_path: Path):
"""replay:seq 逻辑顺序 / arrival 到达顺序 / limit 截断;版本单调。"""
reset_wms_client(tmp_path / "wms_mirror.json")
client = reset_wms_client(tmp_path / "wms_mirror.json")
_seed_ledger(client)
ev1 = client.emit_shortage("WIRE-HV", new_stock=500, shortage_qty=220,
occurred_at="2026-08-02 09:00:00")
ev2 = client.emit_shortage("TERM-HV", new_stock=3000, occurred_at="2026-08-02 10:00:00")
ev3 = client.emit_shortage("BUSBAR", new_stock=200, shortage_qty=180,
occurred_at="2026-08-02 11:00:00")
seq_order = client.replay(order="seq")
assert [e["eventId"] for e in seq_order] == [ev1["eventId"], ev2["eventId"], ev3["eventId"]]
arrival = client.replay(order="arrival")
assert [e["offset"] for e in arrival] == [0, 1, 2]
assert [e["eventId"] for e in client.replay(limit=2)] == [ev1["eventId"], ev2["eventId"]]
assert client.inventory_version() == 3
assert client.status()["eventCount"] == 3
assert client.status()["shortageCount"] == 3
# 事件带乱序容忍元数据(seq 逻辑顺序 / offset 到达顺序)
assert all(e.get("seq") is not None and e.get("offset") is not None for e in seq_order)
def test_replayed_events_consume_idempotently(tmp_path: Path):
"""重放的事件可重复消费:第一次消费落证据链,第二次全部幂等跳过(不重复触发)。"""
reset_wms_client(tmp_path / "wms_mirror.json")
store = _MemStore(seed_world(), tmp_path / "checkpoints.json")
from server.aps_domain.flex import run_flex_schedule
run_flex_schedule(store, sort_mode="BOTTLENECK", actor="test")
client = reset_wms_client(tmp_path / "wms_mirror.json")
_seed_ledger(client)
for i, (mat, ns, sq) in enumerate([("WIRE-HV", 500, 220), ("TERM-HV", 3000, 0),
("BUSBAR", 200, 180)]):
client.emit_shortage(mat, new_stock=ns, shortage_qty=sq,
occurred_at=f"2026-08-02 0{i + 9}:00:00")
stream = client.replay(order="seq")
# 第一次消费:3 条全部消费,库存版本 1→2→3,证据链逐条落 inventory-version
for ev in stream:
r = wms_events.consume_event(store, ev, actor="test")
assert r["consumed"] is True and r["duplicate"] is False
assert store.data["inventoryVersion"] == 3
assert len(store.data["wmsConsumed"]) == 3
consume_refs = [e["evidenceRefs"] for e in store.data["auditEvents"]
if e["action"] == "wms.event.consume"]
assert sorted(r[1] for r in consume_refs) == \
["inventory-version:1", "inventory-version:2", "inventory-version:3"]
assert len(store.data["wmsPending"]) == 0
# 第二次重放消费:全部幂等(duplicate),不再产生 consume 证据、版本不涨
for ev in stream:
r = wms_events.consume_event(store, ev, actor="test")
assert r["duplicate"] is True and r["consumed"] is False
assert store.data["inventoryVersion"] == 3
consume_after = [e for e in store.data["auditEvents"] if e["action"] == "wms.event.consume"]
assert len(consume_after) == 3 # 不重复触发
dup_count = sum(1 for e in store.data["auditEvents"] if e["action"] == "wms.event.duplicate")
assert dup_count == 3
# 重放投影接口
replay_view = wms_events.replay_events(limit=None, order="seq")
assert replay_view["count"] == 3
assert replay_view["inventoryVersion"] == 3