aps-agent/tests/golden/test_plan_orchestration.py

89 lines
4.0 KiB
Python
Raw Normal View History

# ============================================================
# 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