aps-agent/tests/golden/test_mesh_approval_resume.py

146 lines
4.9 KiB
Python

# ============================================================
# Mesh 人工审批恢复黄金测试
# 覆盖:批准后续跑依赖任务、驳回停止、非 Mesh 不命中、重复调用幂等。
# ============================================================
from __future__ import annotations
import pytest
from pydantic import ValidationError
from server.agent_core import mesh as mesh_module
@pytest.fixture()
def mesh_env(tmp_path, monkeypatch):
store = mesh_module.MeshStore(str(tmp_path / "agent_mesh.json"))
monkeypatch.setattr(mesh_module, "_MESH", store)
monkeypatch.setattr(mesh_module, "_DISPATCH_THREADS", {})
return store
def _approval_goal(store: mesh_module.MeshStore):
goal = store.create_goal(
"approval resume",
template=None,
tasks=[
{
"key": "analyze",
"title": "analyze",
"intent": "data.analyze",
"role": "data-analyst",
"dependsOn": [],
},
{
"key": "readiness",
"title": "readiness",
"intent": "readiness.query",
"role": "verifier",
"dependsOn": ["analyze"],
},
],
)
task = goal["tasks"][0]
task["status"] = "AWAITING_APPROVAL"
task["confirmationIds"] = ["confirm-approve"]
goal["status"] = "ATTENTION"
store.save()
return goal, task
def _join_dispatch(goal_id: str) -> None:
thread = mesh_module._DISPATCH_THREADS[goal_id]
thread.join(timeout=3)
assert not thread.is_alive(), "mesh dispatch thread did not finish"
def test_approved_confirmation_resumes_dependent_tasks(mesh_env, monkeypatch):
calls: list[str] = []
def fake_execute(session_id, intent, params, agent, **kwargs):
calls.append(intent)
return f"{intent} completed", []
monkeypatch.setattr(mesh_module, "_execute_intent", fake_execute)
goal, task = _approval_goal(mesh_env)
first = mesh_module.resume_goal_after_confirmation(
"confirm-approve", approve=True, actor="alice",
)
assert first["matched"] is True
assert first["resumed"] is True
assert first["started"] is True
assert first["alreadyRunning"] is False
assert task["status"] == "DONE"
assert task["confirmationIds"] == []
assert task["finishedAt"]
assert "人工已批准 confirm-approve by alice" in task["result"]
_join_dispatch(goal["id"])
assert calls == ["readiness.query"]
assert goal["tasks"][1]["status"] == "DONE"
assert goal["status"] == "ACHIEVED"
assert any(
message.get("kind") == "RESPONSE"
and "confirm:confirm-approve" in (message.get("evidenceRefs") or [])
for message in mesh_env.data["messages"]
)
first_thread = mesh_module._DISPATCH_THREADS[goal["id"]]
repeated = mesh_module.resume_goal_after_confirmation(
"confirm-approve", approve=True, actor="alice",
)
assert repeated["matched"] is True
assert repeated["resolved"] is True
assert repeated["resumed"] is False
assert repeated["reason"] == "ALREADY_RESOLVED"
assert mesh_module._DISPATCH_THREADS[goal["id"]] is first_thread
assert calls == ["readiness.query"]
def test_rejected_confirmation_stops_dispatch(mesh_env, monkeypatch):
calls: list[str] = []
def fake_execute(session_id, intent, params, agent, **kwargs):
calls.append(intent)
return f"{intent} completed", []
monkeypatch.setattr(mesh_module, "_execute_intent", fake_execute)
goal, task = _approval_goal(mesh_env)
result = mesh_module.resume_goal_after_confirmation(
"confirm-approve", approve=False, actor="bob",
)
assert result["matched"] is True
assert result["resumed"] is False
assert task["status"] == "FAILED"
assert task["confirmationIds"] == []
assert "人工驳回 confirm-approve by bob" in task["error"]
assert goal["tasks"][1]["status"] == "PENDING"
assert goal["status"] == "ATTENTION"
assert mesh_module._DISPATCH_THREADS == {}
assert calls == []
assert any(
message.get("kind") == "ALERT"
and "confirm:confirm-approve" in (message.get("evidenceRefs") or [])
for message in mesh_env.data["messages"]
)
def test_unrelated_confirmation_is_ignored(mesh_env):
result = mesh_module.resume_goal_after_confirmation(
"normal-p2-confirm", approve=True, actor="alice",
)
assert result == {"matched": False, "resolved": False, "resumed": False}
assert mesh_module._DISPATCH_THREADS == {}
def test_sop_stage_request_rejects_query_only_payload():
from server.gateway.app import SopStageRequest
with pytest.raises(ValidationError):
SopStageRequest(sessionId="session-sop", query="换线")
structured = SopStageRequest(sessionId="session-sop", assetId="sop-asset-1")
assert structured.assetId == "sop-asset-1"
assert structured.pack is None