"""用户 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()