aps-agent/tests/golden/test_plan_orchestration.py

89 lines
4.0 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.

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