Files

180 lines
7.8 KiB
Python
Raw Permalink Normal View History

2026-10-30 12:40:16 +08:00
"""Behavioral checks for sync ownership, read-only modes, and delivery commands."""
from pathlib import Path
import importlib.machinery
import importlib.util
import subprocess
import tempfile
import unittest
REPO = Path(__file__).resolve().parents[1]
loader = importlib.machinery.SourceFileLoader('catalog_validator', str(REPO / 'bin/validate-skills'))
spec = importlib.util.spec_from_loader(loader.name, loader)
validator = importlib.util.module_from_spec(spec)
loader.exec_module(validator)
class ToolTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='global-skills-test-')
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.global_dir = self.root / 'global'
self.jev_dir = self.root / 'jev'
self.target = self.root / 'target'
self.global_dir.mkdir()
self.jev_dir.mkdir()
def skill(self, root, name):
path = root / name
path.mkdir()
(path / 'SKILL.md').write_text(f'---\nname: {name}\ndescription: fixture\nscope: global\n---\n')
return path
def sync(self, *flags):
return subprocess.run(['bash', str(REPO / 'sync-skills.sh'), '--global-dir', str(self.global_dir),
'--jev-dir', str(self.jev_dir), '--target', str(self.target), *flags],
text=True, capture_output=True)
def test_sync_rejects_target_overlapping_catalog(self):
source = self.skill(self.global_dir, 'example')
self.target = self.global_dir
self.assertEqual(self.sync().returncode, 2)
self.assertTrue((source / 'SKILL.md').is_file())
def test_metadata_rejects_invalid_yaml_scalar(self):
for field in ['name: "unterminated', 'description: bad: scalar']:
with self.assertRaises(ValueError):
validator.metadata('---\n' + field + '\n---\n')
def test_install_and_idempotence(self):
source = self.skill(self.global_dir, 'example')
self.assertEqual(self.sync().returncode, 0)
self.assertEqual((self.target / 'example').resolve(), source)
self.assertIn('0 changes', self.sync().stdout)
self.assertEqual(self.sync('--check').returncode, 0)
def test_external_symlink_collision_preserves_user_link(self):
self.skill(self.global_dir, 'example')
self.target.mkdir()
external = self.root / 'user-skill'
external.mkdir()
dest = self.target / 'example'
dest.symlink_to(external)
self.assertEqual(self.sync().returncode, 1)
self.assertEqual(dest.resolve(), external)
def test_dangling_external_symlink_is_preserved(self):
self.skill(self.global_dir, 'example')
self.target.mkdir()
dest = self.target / 'example'
dest.symlink_to(self.root / 'missing-user-skill')
self.assertEqual(self.sync().returncode, 1)
self.assertTrue(dest.is_symlink())
self.assertEqual(dest.readlink(), self.root / 'missing-user-skill')
def test_unrelated_directory_is_preserved(self):
self.skill(self.global_dir, 'example')
(self.target / 'example').mkdir(parents=True)
file = self.target / 'example' / 'user.txt'
file.write_text('user work')
self.assertEqual(self.sync().returncode, 1)
self.assertEqual(file.read_text(), 'user work')
def test_deleted_skill_link_is_pruned(self):
source = self.skill(self.global_dir, 'example')
self.sync()
(source / 'SKILL.md').unlink()
source.rmdir()
self.assertEqual(self.sync().returncode, 0)
self.assertFalse((self.target / 'example').is_symlink())
def test_relative_managed_link_is_pruned(self):
self.target.mkdir()
dest = self.target / 'gone'
dest.symlink_to('../global/gone')
self.assertEqual(self.sync().returncode, 0)
self.assertFalse(dest.is_symlink())
def test_move_between_managed_sources(self):
old = self.skill(self.jev_dir, 'example')
self.sync()
(old / 'SKILL.md').unlink()
old.rmdir()
new = self.skill(self.global_dir, 'example')
self.assertEqual(self.sync().returncode, 0)
self.assertEqual((self.target / 'example').resolve(), new)
def test_unavailable_jev_does_not_prune_its_links(self):
self.target.mkdir()
dest = self.target / 'example'
dest.symlink_to(self.jev_dir / 'example')
self.jev_dir.rmdir()
self.assertEqual(self.sync().returncode, 0)
self.assertTrue(dest.is_symlink())
def test_dry_run_and_check_do_not_create_targets(self):
self.skill(self.global_dir, 'example')
self.assertEqual(self.sync('--dry-run').returncode, 0)
self.assertFalse(self.target.exists())
self.assertEqual(self.sync('--check').returncode, 1)
self.assertFalse(self.target.exists())
def test_read_only_modes_do_not_remove_stale_links(self):
self.target.mkdir()
dest = self.target / 'gone'
dest.symlink_to(self.global_dir / 'gone')
self.assertEqual(self.sync('--check').returncode, 1)
self.assertEqual(self.sync('--dry-run').returncode, 0)
self.assertTrue(dest.is_symlink())
def test_source_collision_fails_before_writing(self):
self.skill(self.global_dir, 'example')
self.skill(self.jev_dir, 'example')
self.assertEqual(self.sync().returncode, 2)
self.assertFalse(self.target.exists())
def test_invalid_source_fails_before_writing(self):
(self.global_dir / 'empty').mkdir()
self.assertEqual(self.sync().returncode, 2)
self.assertFalse(self.target.exists())
def test_file_and_directory_delivery_support_spaces(self):
file = self.root / 'report with spaces.md'
file.write_text('report')
result = subprocess.run(['bash', str(REPO / 'bin/t3-deliver'), str(file)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0)
self.assertEqual(result.stdout.splitlines()[-1], f'scp "vasily@msi.tail0b014a.ts.net:{file}" .')
result = subprocess.run(['bash', str(REPO / 'bin/t3-deliver'), str(self.root)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0)
self.assertEqual(result.stdout.splitlines()[-1], f'scp -r "vasily@msi.tail0b014a.ts.net:{self.root}" .')
def test_delivery_rejects_shell_expansion_in_names(self):
for name in ['cash$report', 'percent%report', 'tick`report', 'quote"report', 'bang!report', 'newline\nreport']:
path = self.root / name
path.write_text('fixture')
result = subprocess.run(['bash', str(REPO / 'bin/t3-deliver'), str(path)], text=True, capture_output=True)
self.assertNotEqual(result.returncode, 0, name)
self.assertNotIn('scp ', result.stdout)
def test_validator_reports_missing_links_and_collision(self):
source = self.skill(self.global_dir, 'example')
self.skill(self.jev_dir, 'example')
(source / 'SKILL.md').write_text((source / 'SKILL.md').read_text() + '\n[missing](no-such-file.md)\n')
result = subprocess.run(['python3', str(REPO / 'bin/validate-skills'), '--global-dir', str(self.global_dir),
'--jev-dir', str(self.jev_dir)], text=True, capture_output=True)
self.assertEqual(result.returncode, 1)
self.assertIn('collides', result.stderr)
self.assertIn('missing link target', result.stderr)
def test_metadata_rejects_duplicate_keys(self):
with self.assertRaises(ValueError):
validator.metadata('---\nname: example\nname: other\n---\n')
def test_validator_ignores_example_links_inside_fences(self):
path = self.root / 'reference.md'
path.write_text('```markdown\n[example](not-a-real-file.md)\n```\n')
self.assertEqual(list(validator.local_links(path)), [])
if __name__ == '__main__':
unittest.main()