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",)