206 lines
7.5 KiB
Python
206 lines
7.5 KiB
Python
"""用户 skill: 多来源覆盖(user wins)、加载失败收集、save_skill / fork_skill。"""
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from core.skills import SkillRegistry, SkillSource
|
|
from tools.skill_authoring import ForkSkillTool, SaveSkillTool
|
|
|
|
|
|
def _write_skill(root: Path, name: str, desc: str, body: str = "body") -> Path:
|
|
d = root / name
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
(d / "SKILL.md").write_text(
|
|
f"---\nname: {name}\ndescription: {desc}\n---\n\n# {name}\n\n{body}\n",
|
|
encoding="utf-8",
|
|
)
|
|
return d
|
|
|
|
|
|
class TestMultiSourceRegistry(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tmp.name)
|
|
self.builtin = self.root / "builtin"
|
|
self.user = self.root / "user"
|
|
self.builtin.mkdir()
|
|
self.user.mkdir()
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def _registry(self):
|
|
return SkillRegistry([
|
|
SkillSource(self.builtin, "builtin"),
|
|
SkillSource(self.user, "user"),
|
|
])
|
|
|
|
def test_user_overrides_builtin(self):
|
|
_write_skill(self.builtin, "ppt", "内置 ppt")
|
|
_write_skill(self.user, "ppt", "我的 ppt")
|
|
reg = self._registry()
|
|
self.assertEqual(reg.get("ppt").source, "user")
|
|
self.assertEqual(reg.get("ppt").description, "我的 ppt")
|
|
self.assertIn("ppt", reg.user_overrides)
|
|
|
|
def test_distinct_names_coexist(self):
|
|
_write_skill(self.builtin, "ppt", "内置 ppt")
|
|
_write_skill(self.user, "ppt-mine", "我的 ppt")
|
|
reg = self._registry()
|
|
self.assertEqual(reg.get("ppt").source, "builtin")
|
|
self.assertEqual(reg.get("ppt-mine").source, "user")
|
|
self.assertNotIn("ppt-mine", reg.user_overrides)
|
|
|
|
def test_discovery_block_tags_user_skills(self):
|
|
_write_skill(self.builtin, "coding", "内置 coding")
|
|
_write_skill(self.user, "ppt", "我的") # 不撞内置
|
|
_write_skill(self.builtin, "ppt", "内置 ppt")
|
|
reg = self._registry()
|
|
block = reg.discovery_block()
|
|
self.assertIn("[你的·已覆盖内置]", block) # user ppt 覆盖 builtin ppt
|
|
|
|
def test_bad_user_skill_collected_not_crash(self):
|
|
_write_skill(self.builtin, "coding", "内置")
|
|
# 用户 skill: 缺 description
|
|
bad = self.user / "broken"
|
|
bad.mkdir()
|
|
(bad / "SKILL.md").write_text("---\nname: broken\n---\nbody", encoding="utf-8")
|
|
reg = self._registry()
|
|
self.assertIn("coding", reg.skills) # 没崩,内置正常
|
|
self.assertNotIn("broken", reg.skills) # 坏的没收
|
|
self.assertTrue(any(n == "broken" for n, _ in reg.load_errors))
|
|
self.assertIn("未加载", reg.discovery_block())
|
|
|
|
def test_bad_yaml_user_skill_collected(self):
|
|
bad = self.user / "badyaml"
|
|
bad.mkdir()
|
|
(bad / "SKILL.md").write_text("---\nname: [unclosed\n---\nbody", encoding="utf-8")
|
|
reg = self._registry()
|
|
self.assertTrue(any(n == "badyaml" for n, _ in reg.load_errors))
|
|
|
|
def test_missing_user_dir_is_noop(self):
|
|
_write_skill(self.builtin, "coding", "内置")
|
|
reg = SkillRegistry([
|
|
SkillSource(self.builtin, "builtin"),
|
|
SkillSource(self.root / "does-not-exist", "user"),
|
|
])
|
|
self.assertIn("coding", reg.skills)
|
|
self.assertEqual(reg.load_errors, [])
|
|
|
|
|
|
class TestSaveSkillTool(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tmp.name)
|
|
self.user_skills = self.root / ".skills"
|
|
self.builtin = self.root / "builtin"
|
|
self.builtin.mkdir()
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def _tool(self):
|
|
reg = SkillRegistry([
|
|
SkillSource(self.builtin, "builtin"),
|
|
SkillSource(self.user_skills, "user"),
|
|
])
|
|
return SaveSkillTool(self.user_skills, reg)
|
|
|
|
def test_save_writes_file(self):
|
|
out = self._tool().execute(
|
|
name="mine",
|
|
content="---\nname: mine\ndescription: 我的 skill\n---\n\n# Mine\n",
|
|
)
|
|
self.assertIn("saved", out)
|
|
self.assertTrue((self.user_skills / "mine" / "SKILL.md").exists())
|
|
|
|
def test_reject_bad_name(self):
|
|
out = self._tool().execute(name="../evil", content="---\ndescription: x\n---\n")
|
|
self.assertIn("[Error]", out)
|
|
self.assertFalse((self.user_skills / "../evil").exists())
|
|
|
|
def test_reject_missing_description(self):
|
|
out = self._tool().execute(name="mine", content="---\nname: mine\n---\nbody")
|
|
self.assertIn("[Error]", out)
|
|
self.assertIn("description", out)
|
|
|
|
def test_reject_bad_yaml(self):
|
|
out = self._tool().execute(name="mine", content="---\nname: [bad\n---\nbody")
|
|
self.assertIn("[Error]", out)
|
|
|
|
def test_reject_name_mismatch(self):
|
|
out = self._tool().execute(
|
|
name="mine",
|
|
content="---\nname: other\ndescription: x\n---\n",
|
|
)
|
|
self.assertIn("[Error]", out)
|
|
self.assertIn("不一致", out)
|
|
|
|
def test_warns_on_builtin_override(self):
|
|
_write_skill(self.builtin, "ppt", "内置 ppt")
|
|
out = self._tool().execute(
|
|
name="ppt",
|
|
content="---\nname: ppt\ndescription: 我的 ppt\n---\n",
|
|
)
|
|
self.assertIn("saved", out)
|
|
self.assertIn("覆盖内置", out)
|
|
|
|
|
|
class TestForkSkillTool(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tmp.name)
|
|
self.user_skills = self.root / ".skills"
|
|
self.builtin = self.root / "builtin"
|
|
self.builtin.mkdir()
|
|
# 带脚本的内置 skill
|
|
d = _write_skill(self.builtin, "ppt", "内置 ppt")
|
|
(d / "scripts").mkdir()
|
|
(d / "scripts" / "helper.py").write_text("X = 1\n", encoding="utf-8")
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def _tool(self):
|
|
reg = SkillRegistry([
|
|
SkillSource(self.builtin, "builtin"),
|
|
SkillSource(self.user_skills, "user"),
|
|
])
|
|
return ForkSkillTool(self.user_skills, reg)
|
|
|
|
def test_fork_copies_scripts_and_renames(self):
|
|
out = self._tool().execute(src="ppt", new_name="ppt-mine")
|
|
self.assertIn("forked", out)
|
|
dest = self.user_skills / "ppt-mine"
|
|
self.assertTrue((dest / "scripts" / "helper.py").exists()) # 脚本带过来
|
|
md = (dest / "SKILL.md").read_text(encoding="utf-8")
|
|
self.assertIn("name: ppt-mine", md) # frontmatter name 对齐新名
|
|
self.assertNotIn("name: ppt\n", md)
|
|
|
|
def test_forked_renamed_skill_does_not_shadow_builtin(self):
|
|
self._tool().execute(src="ppt", new_name="ppt-mine")
|
|
reg = SkillRegistry([
|
|
SkillSource(self.builtin, "builtin"),
|
|
SkillSource(self.user_skills, "user"),
|
|
])
|
|
# 改了名,内置 ppt 不被遮,新名独立存在
|
|
self.assertEqual(reg.get("ppt").source, "builtin")
|
|
self.assertEqual(reg.get("ppt-mine").source, "user")
|
|
|
|
def test_reject_existing_dest(self):
|
|
self._tool().execute(src="ppt", new_name="ppt-mine")
|
|
out = self._tool().execute(src="ppt", new_name="ppt-mine")
|
|
self.assertIn("[Error]", out)
|
|
self.assertIn("已存在", out)
|
|
|
|
def test_reject_unknown_src(self):
|
|
out = self._tool().execute(src="nope", new_name="x")
|
|
self.assertIn("[Error]", out)
|
|
self.assertIn("不存在", out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|