Files
MoviePilot/tests/test_agent_skills_middleware.py

114 lines
3.6 KiB
Python

import json
import pytest
from anyio import Path as AsyncPath
from langchain.agents.middleware.types import ModelRequest
from langchain_core.messages import SystemMessage
from app.agent.middleware.skills import (
SKILL_TOOL_NAME,
SkillsMiddleware,
_alist_skills,
)
from app.agent.tools.tags import ToolTag
@pytest.fixture
def anyio_backend():
"""使用 asyncio 后端运行 anyio 异步测试。"""
return "asyncio"
def _write_skill(root, skill_id: str, name: str | None = None) -> None:
"""写入测试用 Skill 文件。"""
skill_dir = root / skill_id
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text(
f"""---
name: {name or skill_id}
description: test skill {skill_id}
allowed-tools: "read_file execute_command"
---
# {skill_id}
Use this skill carefully.
""",
encoding="utf-8",
)
@pytest.mark.anyio
async def test_alist_skills_sorts_skill_directories_by_name(tmp_path):
"""异步扫描技能目录时应按目录名稳定排序。"""
for skill_id in ("z-skill", "a-skill", "m-skill"):
_write_skill(tmp_path, skill_id)
skills = await _alist_skills(AsyncPath(str(tmp_path)))
assert ["a-skill", "m-skill", "z-skill"] == [
skill["id"] for skill in skills
]
def test_skills_middleware_exposes_skill_tool(tmp_path):
"""SkillsMiddleware 应以中间件工具形式暴露 skill。"""
_write_skill(tmp_path, "moviepilot-cli")
middleware = SkillsMiddleware(sources=[str(tmp_path)])
assert [tool.name for tool in middleware.tools] == [SKILL_TOOL_NAME]
assert ToolTag.Read in middleware.tools[0].tags
assert ToolTag.Skill in middleware.tools[0].tags
assert "moviepilot-cli" in middleware.tools[0].description
@pytest.mark.anyio
async def test_skill_tool_loads_skill_by_id_and_name(tmp_path):
"""skill 工具应支持按 id 或 name 加载完整 SKILL.md。"""
_write_skill(tmp_path, "moviepilot-cli", name="MoviePilot CLI")
middleware = SkillsMiddleware(sources=[str(tmp_path)])
skill_tool = middleware.tools[0]
by_id = json.loads(await skill_tool.ainvoke({"name": "moviepilot-cli"}))
by_name = json.loads(await skill_tool.ainvoke({"name": "MoviePilot CLI"}))
assert by_id["success"] is True
assert by_id["skill"]["id"] == "moviepilot-cli"
assert "# moviepilot-cli" in by_id["content"]
assert by_name["success"] is True
assert by_name["skill"]["name"] == "MoviePilot CLI"
@pytest.mark.anyio
async def test_skill_tool_returns_not_found_for_unknown_skill(tmp_path):
"""skill 工具找不到技能时应返回结构化失败信息。"""
middleware = SkillsMiddleware(sources=[str(tmp_path)])
skill_tool = middleware.tools[0]
result = json.loads(await skill_tool.ainvoke({"name": "missing-skill"}))
assert result["success"] is False
assert "missing-skill" in result["message"]
def test_modify_request_instructs_model_to_use_skill_tool_without_paths(tmp_path):
"""系统提示应要求通过 skill 工具加载,而不是直接暴露文件读取路径。"""
_write_skill(tmp_path, "moviepilot-cli")
middleware = SkillsMiddleware(sources=[str(tmp_path)])
skills_metadata = middleware._load_skills_metadata()
request = ModelRequest(
model=None,
messages=[],
system_message=SystemMessage(content="BASE"),
state={"skills_metadata": skills_metadata},
runtime=None,
)
modified = middleware.modify_request(request)
system_content = str(modified.system_message.content)
assert "`skill` tool" in system_content
assert "moviepilot-cli" in system_content
assert "Read `" not in system_content
assert str(tmp_path) not in system_content