204 lines
6.2 KiB
Python
204 lines
6.2 KiB
Python
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from app.application.agent import (
|
|
compose_system_messages,
|
|
next_step_for_tool,
|
|
step_event_payload,
|
|
task_from_deliberative_plan,
|
|
task_plan_payload,
|
|
)
|
|
from app.application.agent.skill_loader import load_skill_manifests
|
|
from app.modules.agent import (
|
|
AgentDefinition,
|
|
AgentTask,
|
|
ExecutionPolicy,
|
|
Plan,
|
|
PlanStep,
|
|
ToolPolicy,
|
|
)
|
|
from app.modules.planner import DeliberativePlan
|
|
from app.modules.planner import PlanStep as DeliberativePlanStep
|
|
from app.modules.skills import parse_skill_manifest
|
|
|
|
|
|
def test_agent_definition_keeps_minimal_execution_and_tool_policy() -> None:
|
|
agent = AgentDefinition(
|
|
name="工师",
|
|
system_prompt="完成已授权的工程任务。",
|
|
skill_refs=("代码审查",),
|
|
tool_policy=ToolPolicy(("read_file", "shell")),
|
|
execution_policy=ExecutionPolicy(max_iterations=3),
|
|
)
|
|
|
|
assert agent.skill_refs == ("代码审查",)
|
|
assert agent.tool_policy.allowed_tools == ("read_file", "shell")
|
|
assert agent.execution_policy.max_iterations == 3
|
|
|
|
|
|
def test_agent_task_keeps_plan_as_runtime_only_contract() -> None:
|
|
task = AgentTask(
|
|
id="task_1",
|
|
objective="审查当前变更",
|
|
agent_name="工师",
|
|
plan=Plan(
|
|
steps=(PlanStep(id="step_1", title="读取变更", tool="read_file"),),
|
|
status="approved",
|
|
),
|
|
)
|
|
|
|
assert task.status == "queued"
|
|
assert task.plan is not None
|
|
assert task.plan.steps[0].status == "pending"
|
|
|
|
|
|
def test_deliberative_plan_becomes_traceable_agent_task() -> None:
|
|
task = task_from_deliberative_plan(
|
|
DeliberativePlan(
|
|
title="调研计划",
|
|
steps=[
|
|
DeliberativePlanStep(
|
|
id=1,
|
|
title="检索资料",
|
|
description="读取相关资料",
|
|
tool="fetch_url",
|
|
risk_level="L2",
|
|
)
|
|
],
|
|
assumptions=["资料可访问"],
|
|
risks=["来源不完整"],
|
|
),
|
|
run_id="run_1",
|
|
agent_name="学士",
|
|
objective="调研主题",
|
|
)
|
|
|
|
assert task.status == "running"
|
|
assert task.plan is not None
|
|
assert task.plan.steps[0].id == "1"
|
|
assert task_plan_payload(task)["steps"] == [
|
|
{
|
|
"id": "1",
|
|
"title": "检索资料",
|
|
"description": "读取相关资料",
|
|
"tool": "fetch_url",
|
|
"risk_level": "L2",
|
|
"status": "pending",
|
|
}
|
|
]
|
|
|
|
|
|
def test_tool_maps_only_to_its_next_uncompleted_plan_step() -> None:
|
|
task = AgentTask(
|
|
id="task_1",
|
|
objective="执行计划",
|
|
agent_name="工师",
|
|
plan=Plan(
|
|
steps=(
|
|
PlanStep(id="1", title="读取", tool="read_file"),
|
|
PlanStep(id="2", title="再次读取", tool="read_file"),
|
|
)
|
|
),
|
|
)
|
|
|
|
first = next_step_for_tool(task, "read_file", set())
|
|
second = next_step_for_tool(task, "read_file", {"1"})
|
|
|
|
assert first is not None and first.id == "1"
|
|
assert second is not None and second.id == "2"
|
|
assert next_step_for_tool(task, "shell", set()) is None
|
|
assert step_event_payload(task, first, status="completed", result_summary="读取成功") == {
|
|
"task_id": "task_1",
|
|
"step_id": "1",
|
|
"title": "读取",
|
|
"tool": "read_file",
|
|
"risk_level": "L2",
|
|
"status": "completed",
|
|
"result_summary": "读取成功",
|
|
}
|
|
|
|
|
|
def test_execution_policy_rejects_invalid_iteration_limit() -> None:
|
|
with pytest.raises(ValueError, match="max_iterations"):
|
|
ExecutionPolicy(max_iterations=0)
|
|
|
|
|
|
def test_manifest_and_prompt_composer_keep_stable_order() -> None:
|
|
skill = SimpleNamespace(
|
|
name="代码审查",
|
|
description="审查变更",
|
|
trigger="审查, review",
|
|
prompt_fragment="先理解调用路径,再报告问题。",
|
|
risk_level="L1",
|
|
status="enabled",
|
|
tool_bindings_json='["read_file"]',
|
|
)
|
|
manifest = parse_skill_manifest(skill, ["read_file"])
|
|
|
|
messages = compose_system_messages(
|
|
["全局约束"],
|
|
AgentDefinition(name="工师", system_prompt="只执行已授权任务。"),
|
|
[manifest],
|
|
)
|
|
|
|
assert manifest.triggers == ("审查", "review")
|
|
assert messages == [
|
|
"全局约束",
|
|
"【Agent:工师】\n只执行已授权任务。",
|
|
"【代码审查·能力注入】\n先理解调用路径,再报告问题。",
|
|
]
|
|
|
|
|
|
def test_default_tools_filters_explicit_allowlist() -> None:
|
|
from app.application.agent.tools import default_tools
|
|
|
|
tools = default_tools(allowed_tools=("read_file",))
|
|
|
|
assert [tool["function"]["name"] for tool in tools] == ["read_file"]
|
|
|
|
|
|
async def test_skill_loader_unions_matches_with_explicit_references(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
skills = [
|
|
SimpleNamespace(
|
|
name="代码审查",
|
|
description="审查",
|
|
trigger="审查",
|
|
prompt_fragment="审查流程",
|
|
risk_level="L1",
|
|
status="enabled",
|
|
tool_bindings_json='["read_file"]',
|
|
),
|
|
SimpleNamespace(
|
|
name="网页研究",
|
|
description="研究",
|
|
trigger="研究",
|
|
prompt_fragment="研究流程",
|
|
risk_level="L2",
|
|
status="enabled",
|
|
tool_bindings_json='["web_search"]',
|
|
),
|
|
]
|
|
|
|
async def fake_match_active_skills(_context: str):
|
|
return [skills[0]]
|
|
|
|
async def fake_get_enabled_skills_by_names(names: tuple[str, ...]):
|
|
return [skill for skill in skills if skill.name in names]
|
|
|
|
monkeypatch.setattr(
|
|
"app.application.agent.skill_loader.match_active_skills",
|
|
fake_match_active_skills,
|
|
)
|
|
monkeypatch.setattr(
|
|
"app.application.agent.skill_loader.get_enabled_skills_by_names",
|
|
fake_get_enabled_skills_by_names,
|
|
)
|
|
|
|
manifests = await load_skill_manifests("请审查", skill_refs=("网页研究",))
|
|
|
|
assert [manifest.name for manifest in manifests] == ["代码审查", "网页研究"]
|
|
assert manifests[0].allowed_tools == ("read_file",)
|