"""Pin skill_seed behaviour (C-line): SKILL.md-only, never overwrite. These are helper-level tests (no DB, no Worker) for cloud.skill_seed.ensure_codex_home_skills, complementing the worker-level tests in test_skill_seed_worker.py (which already cover flag-off / flag-on / tenant-edit-survives through _tenant_dirs). Two behaviours are pinned here, behaviour itself unchanged: 1. Only SKILL.md is ever copied. A skill directory may contain extra files (README, templates/, notes); none of them may land in the tenant home, and re-seeding must never resurrect them. Rationale (see the evaluation note in skill_seed.py): the pack today ships only SKILL.md per skill, so whole-directory copy has zero benefit today and would silently start copying future extras without review; per-file skip-if-differ cannot preserve a tenant deletion (missing destination is copied), so whole-dir copy would resurrect tenant-deleted auxiliary files on the next run. SKILL.md-only copy bounds that resurrection surface to the single SKILL.md file itself. 2. A tenant-modified SKILL.md is never overwritten. It is reported in skills_skipped_differ and left byte-identical, across repeats, while other skills still seed normally. Documented boundary, asserted explicitly rather than hidden: a tenant-DELETED SKILL.md is re-seeded on the next run (missing = copied). That is the existing, intended meaning of "missing is copied"; preserving a deletion would need a tombstone, which is new behaviour and out of scope for this convergence line. The test names it so a future reader does not mistake re-seeding for a bug -- or mistake the differ-guard for deletion preservation. """ import os import sys import tempfile import unittest from pathlib import Path try: from cloud.skill_seed import ensure_codex_home_skills, list_skills except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.skill_seed import ensure_codex_home_skills, list_skills def _pack(root): """Pack with extras that must never reach the tenant home.""" pack = Path(root) / "pack" skill_a = pack / "skill-a" (skill_a / "templates" / "roles").mkdir(parents=True, exist_ok=True) (skill_a / "SKILL.md").write_text("---\nname: skill-a\n---\nbody-a\n") (skill_a / "README.md").write_text("pack readme, must not seed\n") (skill_a / "notes.txt").write_text("pack notes, must not seed\n") (skill_a / "templates" / "roles" / "00-x.md").write_text("role\n") skill_b = pack / "skill-b" skill_b.mkdir(parents=True, exist_ok=True) (skill_b / "SKILL.md").write_text("---\nname: skill-b\n---\nbody-b\n") # Directory without SKILL.md: reported, never invented. (pack / "skill-broken").mkdir(parents=True, exist_ok=True) (pack / "skill-broken" / "other.txt").write_text("no SKILL.md here\n") return pack def _dest_tree(home): out = [] root = Path(home) / "skills" if root.is_dir(): for p in sorted(root.rglob("*")): if p.is_file(): out.append(str(p.relative_to(root))) return out class SkillSeedPinningTestCase(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() def tearDown(self): self.tmp.cleanup() def test_only_skill_md_is_copied_never_extras(self): home = os.path.join(self.tmp.name, "home") out = ensure_codex_home_skills(home, _pack(self.tmp.name)) self.assertEqual(sorted(out["skills_copied"]), ["skill-a", "skill-b"]) self.assertEqual(out["skills_skipped_missing"], ["skill-broken"]) self.assertEqual( _dest_tree(home), ["skill-a/SKILL.md", "skill-b/SKILL.md"]) # Second run: still no extras, and no resurrection of anything # the tenant never had -- copied is empty because both SKILL.md # files are now identical. out2 = ensure_codex_home_skills(home, _pack(self.tmp.name)) self.assertEqual(out2["skills_copied"], []) self.assertEqual( _dest_tree(home), ["skill-a/SKILL.md", "skill-b/SKILL.md"]) def test_tenant_extra_files_in_dest_are_left_alone(self): home = os.path.join(self.tmp.name, "home") ensure_codex_home_skills(home, _pack(self.tmp.name)) tenant_note = Path(home) / "skills" / "skill-a" / "tenant-notes.md" tenant_note.write_text("tenant's own file\n") ensure_codex_home_skills(home, _pack(self.tmp.name)) self.assertEqual(tenant_note.read_text(), "tenant's own file\n") self.assertIn("skill-a/tenant-notes.md", _dest_tree(home)) # Seeding never prunes the tenant directory either. self.assertIn("skill-a/SKILL.md", _dest_tree(home)) def test_tenant_modified_skill_never_overwritten_across_repeats(self): home = os.path.join(self.tmp.name, "home") pack = _pack(self.tmp.name) ensure_codex_home_skills(home, pack) dest = Path(home) / "skills" / "skill-a" / "SKILL.md" dest.write_text("tenant edited version\n") for _ in range(2): out = ensure_codex_home_skills(home, pack) self.assertEqual(out["skills_skipped_differ"], ["skill-a"]) self.assertEqual(dest.read_text(), "tenant edited version\n") # The other skill is unaffected by one tenant edit. dest_b = Path(home) / "skills" / "skill-b" / "SKILL.md" self.assertEqual(dest_b.read_text(), "---\nname: skill-b\n---\nbody-b\n") def test_identical_dest_is_noop_and_missing_is_recopied(self): home = os.path.join(self.tmp.name, "home") pack = _pack(self.tmp.name) ensure_codex_home_skills(home, pack) out = ensure_codex_home_skills(home, pack) self.assertEqual(out["skills_copied"], []) self.assertEqual(out["skills_skipped_differ"], []) # Documented boundary (see module docstring): a tenant-deleted # SKILL.md is re-seeded -- missing means copied. This is NOT # deletion preservation; only the differ-guard preserves edits. dest_b = Path(home) / "skills" / "skill-b" / "SKILL.md" dest_b.unlink() out2 = ensure_codex_home_skills(home, pack) self.assertEqual(out2["skills_copied"], ["skill-b"]) self.assertTrue(dest_b.is_file()) def test_list_skills_ignores_broken_and_non_directories(self): pack = _pack(self.tmp.name) (pack / "stray.txt").write_text("not a skill\n") names = [n for n, _p in list_skills(pack)] self.assertEqual(names, ["skill-a", "skill-b"]) if __name__ == "__main__": unittest.main()