aps-agent/tests/golden/test_m2_state.py

89 lines
5.7 KiB
Python
Raw Normal View History

2026-07-21 11:05:57 +08:00
# ============================================================
# M2 状态治理黄金测试(moduleId: golden-m2-state, 发版门禁 §14.3)
# 固化三个核心不变式:
# S1 Explore 沙盒隔离:方案对比绝不污染主干世界(§5.1 铁律)
# S2 Checkpoint 往返:建档 → 破坏 → 回滚 → 完整恢复(§4.3)
# S3 审计哈希链:正常链校验通过;篡改任意字段即断链(§3.6)
# ============================================================
from __future__ import annotations # 前向类型引用
import copy # 世界对照拷贝
import json # 深比较辅助
from server.agent_core.audit import write_audit # 审计写入
from server.agent_core.registry import verify_audit_chain # 链校验
from server.aps_domain.scenario import compare_scenarios # 方案对比(沙盒)
from server.state.checkpoints import CheckpointStore # 快照仓(用临时路径实例化)
from server.state.seed import seed_world # 种子世界
def _next_id_factory():
"""独立发号器(用例内存态)。"""
counters: dict[str, int] = {} # 计数器
def next_id(kind: str) -> int: # 闭包发号
counters[kind] = counters.get(kind, 0) + 1 # 自增
return counters[kind] # 返回
return next_id # 闭包
# ---------------- S1:沙盒隔离 ----------------
def test_scenario_compare_does_not_mutate_world():
"""方案对比在深拷贝沙盒运行:主干世界的所有表必须逐字节不变。"""
world = seed_world() # 主干世界
before = json.dumps(world, ensure_ascii=False, sort_keys=True) # 运行前指纹
text, block = compare_scenarios(world) # 执行三策略沙盒对比
after = json.dumps(world, ensure_ascii=False, sort_keys=True) # 运行后指纹
assert before == after, "方案对比污染了主干世界(违反 §5.1 Explore 铁律)" # 逐字节一致
assert len(block.props["cards"]) == 3 # 产出三张方案卡
for card in block.props["cards"]: # 逐卡校验结构
assert card["kpi"]["woCount"] > 0 # 沙盒确实完成了排产
assert card["strategy"] in ("DELIVERY_FIRST", "CAPACITY_BALANCE", "COMPREHENSIVE") # 策略合法
assert "沙盒" in text # 文案明示未影响当前方案
# ---------------- S2:Checkpoint 往返 ----------------
def test_checkpoint_roundtrip(tmp_path):
"""建档 → 破坏世界 → 从档恢复:恢复结果与建档时刻逐字节一致。"""
store = CheckpointStore(path=str(tmp_path / "ckpt.json")) # 临时路径快照仓(隔离生产文件)
world = seed_world() # 初始世界
baseline = copy.deepcopy(world) # 建档时刻的对照副本
meta = store.create(world, label="测试基线", reason="manual") # 建档
assert meta["pairId"] # 拿到快照 ID
world["salesOrders"].clear() # 破坏:清空订单
world["scheduleVersions"].append({"id": 999}) # 破坏:塞入脏版本
pair = store.get(meta["pairId"]) # 取回快照
assert pair is not None # 快照存在
restored = pair["world"] # 快照中的世界
assert json.dumps(restored, ensure_ascii=False, sort_keys=True) == \
json.dumps(baseline, ensure_ascii=False, sort_keys=True), "快照未完整保留建档时刻状态" # 逐字节恢复
# 快照与运行态隔离:破坏运行态不应影响已存快照(深拷贝验证)
assert len(restored["salesOrders"]) == 7 # 快照内订单完好
# ---------------- S2b:容量淘汰 ----------------
def test_checkpoint_capacity_eviction(tmp_path):
"""快照数超过上限时淘汰最旧(防文件膨胀)。"""
store = CheckpointStore(path=str(tmp_path / "ckpt.json")) # 临时快照仓
world = seed_world() # 世界
ids = [store.create(world, label=f"c{i}", reason="manual")["pairId"] for i in range(25)] # 建 25 档(上限 20)
assert len(store.pairs) == 20 # 只保留 20 个
assert store.get(ids[0]) is None # 最旧的已被淘汰
assert store.get(ids[-1]) is not None # 最新的仍在
# ---------------- S3:审计哈希链 ----------------
def test_audit_chain_verify_and_tamper_detection():
"""正常链校验通过;篡改任一历史事件的任一字段必须被检出。"""
world = seed_world() # 世界(审计表随种子创建)
nid = _next_id_factory() # 发号器
for i in range(5): # 连续写 5 条审计
write_audit(world, nid, actor="tester", category="TOOL", action=f"op{i}",
target={"type": "T", "id": i}, power="P0", rationale={})
events = world["auditEvents"] # 审计链
assert verify_audit_chain(events)["ok"] is True # 完整链校验通过
tampered = copy.deepcopy(events) # 拷贝后篡改
tampered[2]["actor"] = "hacker" # 篡改第 3 条的行为人
result = verify_audit_chain(tampered) # 重新校验
assert result["ok"] is False # 必须检出断链
assert result["checked"] == 2 # 断点定位在被篡改处