test_install_unified_skills.py 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import json
  2. from pathlib import Path
  3. import shutil
  4. import subprocess
  5. import sys
  6. import tempfile
  7. import unittest
  8. ROOT = Path(__file__).resolve().parents[1]
  9. SCRIPT = ROOT / 'scripts/install-unified-skills.py'
  10. class UnifiedSkillInstallerTests(unittest.TestCase):
  11. def setUp(self):
  12. self.tmp = tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
  13. self.root = Path(self.tmp.name)
  14. def run_cmd(self, command, skill, target, out, plan=None):
  15. selection = ['--component', 'research-runtime'] if skill == 'ksf-voc-library' else ['--skill', skill]
  16. argv = [sys.executable, '-B', '-X', 'utf8', str(SCRIPT), command, *selection, '--target', str(target), '--out', str(out)]
  17. if plan:
  18. argv += ['--plan', str(plan)]
  19. result = subprocess.run(argv, capture_output=True, encoding='utf-8')
  20. return result.returncode, json.loads(result.stdout)
  21. def target(self, skill):
  22. dest = self.root / skill
  23. shutil.copytree(ROOT / 'skills' / skill, dest)
  24. (dest / 'SKILL.md').write_text('previous-entry', encoding='utf-8')
  25. (dest / 'extra-local-file').write_text('preserve', encoding='utf-8')
  26. return dest
  27. def test_plan_apply_verify_preserve_other_files_and_business_code(self):
  28. for skill in ('ksf-voc-library', 'ksf-workshop-runtime'):
  29. target = self.target(skill)
  30. plans, applied = self.root / (skill + '-plan'), self.root / (skill + '-apply')
  31. self.assertEqual(self.run_cmd('plan', skill, target, plans)[0], 0)
  32. self.assertEqual((target / 'SKILL.md').read_text(), 'previous-entry')
  33. self.assertEqual(self.run_cmd('apply', skill, target, applied, plans / 'plan.json')[0], 0)
  34. self.assertEqual((applied / 'before/SKILL.md').read_text(), 'previous-entry')
  35. self.assertEqual((target / 'extra-local-file').read_text(), 'preserve')
  36. self.assertEqual(self.run_cmd('verify', skill, target, self.root / (skill + '-verify'))[0], 0)
  37. if skill == 'ksf-voc-library':
  38. self.assertFalse((target / 'SKILL.md').exists())
  39. self.assertNotIn('SKILL.md', (target / 'FILES.sha256').read_text())
  40. else:
  41. self.assertTrue((target / 'SKILL.md').exists())
  42. def test_already_merged_research_runtime_needs_no_skill_entry(self):
  43. skill = 'ksf-voc-library';target = self.target(skill)
  44. (target / 'SKILL.md').unlink()
  45. plans, applied = self.root / 'runtime-plan', self.root / 'runtime-apply'
  46. self.assertEqual(self.run_cmd('plan', skill, target, plans)[0], 0)
  47. self.assertEqual(self.run_cmd('apply', skill, target, applied, plans / 'plan.json')[0], 0)
  48. self.assertFalse((target / 'SKILL.md').exists())
  49. self.assertEqual(self.run_cmd('verify', skill, target, self.root / 'runtime-verify')[0], 0)
  50. def test_concurrent_change_and_protected_runtime_mismatch_stop_before_mutation(self):
  51. skill = 'ksf-workshop-runtime';target = self.target(skill)
  52. plans = self.root / 'plan'
  53. self.assertEqual(self.run_cmd('plan', skill, target, plans)[0], 0)
  54. (target / 'SKILL.md').write_text('concurrent-change', encoding='utf-8')
  55. self.assertNotEqual(self.run_cmd('apply', skill, target, self.root / 'apply', plans / 'plan.json')[0], 0)
  56. self.assertEqual((target / 'SKILL.md').read_text(), 'concurrent-change')
  57. protected = target / 'scripts/ksf-secure.mjs'
  58. protected.write_text('changed-runtime', encoding='utf-8')
  59. code, result = self.run_cmd('plan', skill, target, self.root / 'plan-bad-runtime')
  60. self.assertNotEqual(code, 0)
  61. self.assertEqual(result['error']['code'], 'PROTECTED_BUSINESS_RUNTIME_MISMATCH')
  62. if __name__ == '__main__':
  63. unittest.main()