mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
Refactor plugin cache handling and update related docs
This commit is contained in:
@@ -1,33 +1,113 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
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 _alist_skills
|
||||
from app.agent.middleware.skills import (
|
||||
SKILL_TOOL_NAME,
|
||||
SkillsMiddleware,
|
||||
_alist_skills,
|
||||
)
|
||||
from app.agent.tools.tags import ToolTag
|
||||
|
||||
|
||||
class SkillsMiddlewareAsyncTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_alist_skills_sorts_skill_directories_by_name(self):
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
root = Path(tempdir)
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
"""使用 asyncio 后端运行 anyio 异步测试。"""
|
||||
return "asyncio"
|
||||
|
||||
for skill_id in ("z-skill", "a-skill", "m-skill"):
|
||||
skill_dir = root / skill_id
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
f"""---
|
||||
name: {skill_id}
|
||||
description: test
|
||||
|
||||
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",
|
||||
)
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
skills = await _alist_skills(AsyncPath(str(root)))
|
||||
|
||||
self.assertEqual(
|
||||
["a-skill", "m-skill", "z-skill"],
|
||||
[skill["id"] for skill in skills],
|
||||
)
|
||||
@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
|
||||
|
||||
Reference in New Issue
Block a user