test_workshop_onboarding.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """Fresh-install and preservation tests using the integrity-checked official artifact."""
  2. import argparse
  3. import importlib.util
  4. import io
  5. import json
  6. from pathlib import Path
  7. import tarfile
  8. import tempfile
  9. import unittest
  10. from unittest.mock import patch
  11. ROOT = Path(__file__).resolve().parents[1]
  12. RELEASE = ROOT / 'docs/workshop-onboarding-20261109'
  13. spec = importlib.util.spec_from_file_location('ksf_onboarding', ROOT / 'scripts/workshop-onboarding/install.py')
  14. installer = importlib.util.module_from_spec(spec)
  15. spec.loader.exec_module(installer)
  16. class OnboardingTests(unittest.TestCase):
  17. def setUp(self):
  18. self.temp = tempfile.TemporaryDirectory(prefix='ksf-onboarding-test-')
  19. self.addCleanup(self.temp.cleanup)
  20. self.root = Path(self.temp.name)
  21. self.skills = self.root / 'host-skills'
  22. self.args = argparse.Namespace(skills_dir=str(self.skills), out=str(self.root / 'result'),
  23. voc_archive=str(RELEASE / 'source-audit/voc-skill-0.3.42.tgz'), apply=False, prepare_runtime=False)
  24. def run_install(self):
  25. return installer.run(self.args, RELEASE / 'package')
  26. def test_dry_checks_both_complete_sources_without_creating_targets(self):
  27. result = self.run_install()
  28. self.assertTrue(result['readOnly'])
  29. self.assertEqual([a['files'] for a in result['actions']], [43, 169])
  30. self.assertFalse(self.skills.exists())
  31. self.assertFalse(Path(self.args.out).exists())
  32. def test_fresh_install_complete_and_repeat_is_verify_only(self):
  33. self.args.apply = True
  34. result = self.run_install()
  35. self.assertTrue(result['filesInstalled'])
  36. self.assertEqual(result['hostSkillsLoaded'], 'not-verified')
  37. self.assertEqual(result['vocLiveAccess'], 'not-verified')
  38. config = json.loads((self.skills / 'voc-intelligence/.mcp.json').read_bytes())
  39. self.assertEqual(config['mcpServers']['voc']['args'],
  40. [str(self.skills / 'voc-intelligence/mcp/src/server.js')])
  41. marker = self.skills / 'ksf-workshop-runtime/credential-binding.json'
  42. marker.write_text('fixture-only-do-not-copy')
  43. before = (self.skills / 'voc-intelligence/package.json').read_bytes()
  44. self.args.apply = False
  45. repeated = self.run_install()
  46. self.assertTrue(all(a['action'] == 'verify-only' for a in repeated['actions']))
  47. self.assertEqual(marker.read_text(), 'fixture-only-do-not-copy')
  48. self.assertEqual((self.skills / 'voc-intelligence/package.json').read_bytes(), before)
  49. self.assertEqual(len(list(self.skills.glob('.ksf-init-*'))), 0)
  50. def test_existing_different_skill_blocks_before_other_skill_or_receipt_created(self):
  51. target = self.skills / 'ksf-workshop-runtime'
  52. target.mkdir(parents=True)
  53. (target / 'SKILL.md').write_text('unrecognized-existing-skill')
  54. self.args.apply = True
  55. with self.assertRaisesRegex(installer.InstallError, 'EXISTING_SKILL_DIFFERS'):
  56. self.run_install()
  57. self.assertEqual((target / 'SKILL.md').read_text(), 'unrecognized-existing-skill')
  58. self.assertFalse((self.skills / 'voc-intelligence').exists())
  59. self.assertFalse(Path(self.args.out).exists())
  60. def test_corrupt_archive_stops_before_writes(self):
  61. archive = self.root / 'bad.tgz'
  62. archive.write_bytes(b'bad official artifact')
  63. self.args.voc_archive = str(archive)
  64. self.args.apply = True
  65. with self.assertRaisesRegex(installer.InstallError, 'VOC_ARCHIVE_HASH_MISMATCH'):
  66. self.run_install()
  67. self.assertFalse(self.skills.exists())
  68. def test_archive_traversal_and_symlinks_rejected(self):
  69. for name, kind in [('package/../outside', tarfile.REGTYPE), ('package/link', tarfile.SYMTYPE)]:
  70. buffer = io.BytesIO()
  71. with tarfile.open(fileobj=buffer, mode='w:gz') as tar:
  72. info = tarfile.TarInfo(name)
  73. info.type = kind
  74. info.linkname = '../outside' if kind == tarfile.SYMTYPE else ''
  75. tar.addfile(info, io.BytesIO(b''))
  76. raw = buffer.getvalue()
  77. expected = {'bytes': len(raw), 'sha256': installer.digest(raw), 'files': {}}
  78. with self.assertRaises(installer.InstallError):
  79. installer.voc_archive_files(raw, expected)
  80. def test_failure_committing_second_skill_restores_first_and_preserves_unrelated(self):
  81. self.skills.mkdir()
  82. unrelated = self.skills / 'another-skill'
  83. unrelated.mkdir()
  84. (unrelated / 'SKILL.md').write_text('keep me')
  85. original = Path.rename
  86. def rename(source, dest):
  87. if source.name == 'voc-intelligence':
  88. raise OSError('synthetic failure')
  89. return original(source, dest)
  90. self.args.apply = True
  91. with patch.object(Path, 'rename', rename), self.assertRaises(OSError):
  92. self.run_install()
  93. self.assertFalse((self.skills / 'ksf-workshop-runtime').exists())
  94. self.assertFalse((self.skills / 'voc-intelligence').exists())
  95. self.assertEqual((unrelated / 'SKILL.md').read_text(), 'keep me')
  96. self.assertEqual(len(list(self.skills.glob('.ksf-init-*'))), 0)
  97. receipt = json.loads((Path(self.args.out) / 'install-receipt.json').read_bytes())
  98. self.assertFalse(receipt['ok'])
  99. def test_missing_node_does_not_claim_runtime_ready(self):
  100. self.args.apply = self.args.prepare_runtime = True
  101. with patch.object(installer, 'node_and_npm', return_value=None):
  102. result = self.run_install()
  103. self.assertTrue(result['filesInstalled'])
  104. self.assertEqual(result['vocRuntime'], {'status': 'pending', 'reason': 'NODE_OR_NPM_NOT_AVAILABLE'})
  105. self.assertEqual(result['businessSaveAccess'], 'not-verified')
  106. def test_result_directory_reuse_and_target_overlap_stop(self):
  107. self.args.apply = True
  108. Path(self.args.out).mkdir()
  109. with self.assertRaisesRegex(installer.InstallError, 'RESULT_DIRECTORY_EXISTS'):
  110. self.run_install()
  111. self.assertFalse(self.skills.exists())
  112. self.args.out = str(self.skills / 'results')
  113. with self.assertRaisesRegex(installer.InstallError, 'RESULT_TARGET_OVERLAP'):
  114. self.run_install()
  115. if __name__ == '__main__':
  116. unittest.main()