mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-01 13:37:24 +08:00
参考 Codex apply_patch 设计,支持单次调用对多个文本文件执行新增、 更新和删除:补丁以 *** Begin Patch / *** End Patch 包裹,Update 段 用 @@ 分隔 hunk,上下文行须与当前内容精确一致;先整体校验全部文件 操作,通过后才逐个原子写盘,并以校验期 sha256 防止并发修改。 - 工具描述与系统提示词明确 apply_patch / edit_file / write_file 分工: 跨多文件或增删文件用 apply_patch,单文件单处替换用 edit_file - 注册进工具工厂与 ALWAYS_INCLUDE 列表,加入策略 inventory 与 MCP 隐藏列表,流式回调归类为 file_write - 同步 create-moviepilot-plugin / publish-moviepilot-plugin / create-moviepilot-skill 技能的 allowed-tools 与版本 - 新增 tests/test_agent_apply_patch.py 覆盖解析、多文件应用、 整体拒绝、权限边界与版本冲突场景
102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
from pathlib import Path
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
SKILLS_ROOT = PROJECT_ROOT / "skills"
|
|
CORE_PROMPT_PATH = PROJECT_ROOT / "app/agent/prompt/System Core Prompt.txt"
|
|
|
|
|
|
def _read_skill(skill_name: str) -> str:
|
|
"""读取内置技能的 SKILL.md 内容。"""
|
|
return (SKILLS_ROOT / skill_name / "SKILL.md").read_text(encoding="utf-8")
|
|
|
|
|
|
def _frontmatter_value(content: str, key: str) -> str:
|
|
"""从 SKILL.md frontmatter 中读取单行字段值。"""
|
|
for line in content.splitlines():
|
|
if line.startswith(f"{key}:"):
|
|
return line.split(":", 1)[1].strip()
|
|
return ""
|
|
|
|
|
|
def test_modified_builtin_skills_have_incremented_versions() -> None:
|
|
"""本次修改过的内置技能必须递增版本,确保用户端同步更新。"""
|
|
expected_versions = {
|
|
"database-operation": "4",
|
|
"moviepilot-api": "13",
|
|
"moviepilot-cli": "7",
|
|
"moviepilot-update": "3",
|
|
"organize-files": "3",
|
|
"transfer-failed-retry": "4",
|
|
"generate-identifiers": "3",
|
|
"create-moviepilot-plugin": "4",
|
|
"create-moviepilot-skill": "2",
|
|
"publish-moviepilot-plugin": "2",
|
|
}
|
|
|
|
for skill_name, expected_version in expected_versions.items():
|
|
content = _read_skill(skill_name)
|
|
|
|
assert _frontmatter_value(content, "version") == expected_version
|
|
|
|
|
|
def test_moviepilot_cli_skill_uses_local_tool_boundary() -> None:
|
|
"""CLI 技能应只描述本地 MCP tool 边界,不再默认使用旧 Node 脚本。"""
|
|
content = _read_skill("moviepilot-cli")
|
|
|
|
assert "moviepilot tool" in content
|
|
assert "scripts/mp-cli.js" not in content
|
|
assert "Use `scripts/mp-cli.js`" not in content
|
|
assert "node scripts/mp-cli.js" not in content
|
|
assert "any request involving movies" not in content
|
|
assert "whenever the user explicitly mentions MoviePilot" not in content
|
|
assert "Do not ask the user" in content
|
|
assert "moviepilot-api" in content
|
|
assert "database-operation" in content
|
|
|
|
|
|
def test_api_and_database_skills_declare_fallback_boundaries() -> None:
|
|
"""API 和数据库技能应明确各自兜底边界,避免抢占普通产品操作。"""
|
|
api_content = _read_skill("moviepilot-api")
|
|
db_content = _read_skill("database-operation")
|
|
|
|
assert "REST API bridge" in api_content
|
|
assert "Do not use this skill just because MoviePilot is mentioned" in api_content
|
|
assert "moviepilot-cli" in api_content
|
|
assert "Direct SQL query or database update" in api_content
|
|
|
|
assert "direct SQL boundary" in db_content
|
|
assert "Use this skill as the final fallback" in db_content
|
|
assert "INSERT" in db_content
|
|
assert "UPDATE" in db_content
|
|
assert "DELETE" in db_content
|
|
|
|
|
|
def test_agent_core_prompt_does_not_block_plugin_source_edits() -> None:
|
|
"""核心提示词不应禁止插件开发技能写入源码。"""
|
|
core_prompt = CORE_PROMPT_PATH.read_text(encoding="utf-8")
|
|
plugin_skill = _read_skill("create-moviepilot-plugin")
|
|
allowed_tools = _frontmatter_value(plugin_skill, "allowed-tools")
|
|
|
|
assert "file editing tools, or generated patches to change code" not in core_prompt
|
|
assert "write_file" in allowed_tools
|
|
assert "edit_file" in allowed_tools
|
|
assert "apply_patch" in allowed_tools
|
|
assert "search_web" in allowed_tools
|
|
assert "browse_webpage" in allowed_tools
|
|
|
|
|
|
def test_agent_core_prompt_routes_code_tools_safely() -> None:
|
|
"""核心提示词应区分代码搜索、精确编辑和交互式命令场景。"""
|
|
core_prompt = CORE_PROMPT_PATH.read_text(encoding="utf-8")
|
|
|
|
assert '`execute_command(action="run")` with `rg`' in core_prompt
|
|
assert "`replace_all=true` only when every match must change" in core_prompt
|
|
assert "pick the editing tool by scope" in core_prompt
|
|
assert "Use `apply_patch` when one logical change spans multiple files" in core_prompt
|
|
assert "Use `action=run` for short bounded commands" in core_prompt
|
|
assert "including SSH" in core_prompt
|
|
assert "Never use shell redirection" in core_prompt
|
|
assert "matching version of the official documentation" in core_prompt
|
|
assert "Do not guess signatures from memory" in core_prompt
|