# ============================================================ # Agent 自动编排黄金测试(plan.md §3.1 / 矩阵 110 行剩余项) # 覆盖:出卡建 L2 节点(自动补 L0/L1 父链)、批准/驳回流转、 # 状态流转后重启加载(regenCount 单调不灭的 _load 校验)、 # 同 action 父链复用。 # ============================================================ from __future__ import annotations from pathlib import Path from server.agent_core.plan_orchestration import ( _ensure_l0_node, _ensure_l1_node, decide_plan_node, stage_plan_node, ) from server.agent_core.plan_runtime import PlanStore, get_plan_store def _clean_plan_stores(tmp_path: Path, monkeypatch) -> None: """隔离 PlanStore 单例;路径由 get_plan_store 的 world.path 决定。""" from server.agent_core import plan_runtime as pr monkeypatch.setattr(pr, "_plan_stores", {}) pr._plan_stores = {} monkeypatch.setattr(pr, "_plan_stores_lock", __import__("threading").RLock()) def test_stage_creates_l2_with_parent_chain(tmp_path: Path, monkeypatch): """出卡:创建 L2 节点,自动补 L0 意图 + L1 策略父链。""" _clean_plan_stores(tmp_path, monkeypatch) pid = stage_plan_node(session_id="s1", action="data.reset", params={"kind": "all"}, confirm_id="cf-1", power="P2") assert pid ps = get_plan_store() l0 = _ensure_l0_node(ps, "data.reset") l1 = _ensure_l1_node(ps, l0, "data.reset") assert l0.startswith("intent-") and l1.startswith("strategy-") node = ps.latest(pid) assert node.layer == "L2" and node.status == "DRAFT" assert node.parentId == l1 assert node.payload["confirmId"] == "cf-1" def test_approve_and_reject_flow(tmp_path: Path, monkeypatch): """批准 → APPROVED;驳回 → FAILED(同 store 实例)。""" _clean_plan_stores(tmp_path, monkeypatch) pid = stage_plan_node(session_id="s1", action="data.reset", params={}, confirm_id="cf-2", power="P2") pid2 = decide_plan_node(confirm_id="cf-2", approve=True, note="ok", actor="u") assert pid2 == pid assert get_plan_store().latest(pid).status == "APPROVED" pid3 = stage_plan_node(session_id="s1", action="order.cancel", params={"orderNo": "SO-1"}, confirm_id="cf-3", power="P2") pid4 = decide_plan_node(confirm_id="cf-3", approve=False, note="no", actor="u") assert pid4 == pid3 assert get_plan_store().latest(pid3).status == "FAILED" def test_reload_after_transition_is_valid(tmp_path: Path, monkeypatch): """状态流转后重启加载:regenCount 单调不灭(round-30 回归修复)。""" _clean_plan_stores(tmp_path, monkeypatch) pid = stage_plan_node(session_id="s1", action="schedule.publish", params={"versionId": 1}, confirm_id="cf-4", power="P2") decide_plan_node(confirm_id="cf-4", approve=True, note="ok", actor="u") # 重新实例化 PlanStore(模拟重启,用同一路径) ps2 = PlanStore(get_plan_store().path) node = ps2.latest(pid) assert node.status == "APPROVED" and node.version == 2 assert node.regenCount == 0 # 版本 1 保持 DRAFT 不可变 assert ps2.node_at(pid, 1).status == "DRAFT" def test_parent_chain_reuse_same_action(tmp_path: Path, monkeypatch): """同 action 二次出卡:复用 L0/L1 父链,仅新 L2 节点。""" _clean_plan_stores(tmp_path, monkeypatch) p1 = stage_plan_node(session_id="s1", action="data.reset", params={}, confirm_id="cf-5", power="P2") p2 = stage_plan_node(session_id="s1", action="data.reset", params={}, confirm_id="cf-6", power="P2") ps = get_plan_store() l0 = _ensure_l0_node(ps, "data.reset") l1 = _ensure_l1_node(ps, l0, "data.reset") assert p1 != p2 assert ps.latest(p1).parentId == ps.latest(p2).parentId == l1 # L0/L1 各只有一个 l0_count = sum(1 for pid in ps._plans if pid == l0) l1_count = sum(1 for pid in ps._plans if pid == l1) assert l0_count == 1 and l1_count == 1