test_workshop_onboarding_upgrade_r10.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. """Observable r7/r8/r9-to-r10 upgrade, preservation and rollback using the official artifact."""
  2. import argparse
  3. import importlib.util
  4. import json
  5. from pathlib import Path
  6. import tempfile
  7. import unittest
  8. from unittest.mock import patch
  9. ROOT = Path(__file__).resolve().parents[1]
  10. OLD = ROOT / 'docs/workshop-onboarding-20261109-r2/package'
  11. NEW = ROOT / 'docs/workshop-onboarding-20261109-r5/package'
  12. ARCHIVE = ROOT / 'docs/workshop-onboarding-20261109/source-audit/voc-skill-0.3.42.tgz'
  13. spec = importlib.util.spec_from_file_location('upgrade_installer', ROOT / 'scripts/workshop-onboarding/install.py')
  14. installer = importlib.util.module_from_spec(spec)
  15. spec.loader.exec_module(installer)
  16. class UpgradeTests(unittest.TestCase):
  17. def setUp(self):
  18. self.temp = tempfile.TemporaryDirectory(prefix='ksf-upgrade-test-')
  19. self.addCleanup(self.temp.cleanup)
  20. self.root = Path(self.temp.name)
  21. self.skills = self.root / 'skills'
  22. self.args = argparse.Namespace(skills_dir=str(self.skills), out=str(self.root / 'old-result'),
  23. voc_archive=str(ARCHIVE), apply=True, upgrade=False, prepare_runtime=False)
  24. def old_install(self):
  25. installer.run(self.args, OLD)
  26. self.args.out = str(self.root / 'new-result')
  27. self.binding = self.skills / 'ksf-workshop-runtime/credential-binding.json'
  28. self.binding.write_text('local-fixture-binding')
  29. def test_fresh_r10_and_repeat(self):
  30. result = installer.run(self.args, NEW)
  31. self.assertTrue(result['filesInstalled'])
  32. self.args.apply = False
  33. repeat = installer.run(self.args, NEW)
  34. self.assertEqual([r['action'] for r in repeat['actions']], ['verify-only', 'verify-only'])
  35. self.assertEqual(len(list(self.skills.glob('*/SKILL.md'))), 2) # workshop plus official external VOC
  36. self.assertFalse((self.skills / 'ksf-voc-library').exists())
  37. def test_r7_plan_no_write_upgrade_backup_and_binding_preserved(self):
  38. self.old_install()
  39. before = (self.skills / 'ksf-workshop-runtime/SKILL.md').read_bytes()
  40. self.args.apply = False
  41. plan = installer.run(self.args, NEW)
  42. self.assertEqual(plan['actions'][0]['action'], 'upgrade')
  43. self.assertEqual((self.skills / 'ksf-workshop-runtime/SKILL.md').read_bytes(), before)
  44. self.assertFalse(Path(self.args.out).exists())
  45. self.args.apply = self.args.upgrade = True
  46. applied = installer.run(self.args, NEW)
  47. self.assertTrue(applied['filesInstalled'])
  48. self.assertEqual(self.binding.read_text(), 'local-fixture-binding')
  49. self.assertEqual((Path(self.args.out) / 'before/ksf-workshop-runtime/SKILL.md').read_bytes(), before)
  50. installer.verify_component(self.skills / 'ksf-workshop-runtime',
  51. installer.source_files(NEW, installer.verify_package(NEW), ARCHIVE)['ksf-workshop-runtime'])
  52. self.args.apply = False
  53. self.assertEqual(installer.run(self.args, NEW)['actions'][0]['action'], 'verify-only')
  54. def test_r8_upgrade_preserves_binding_and_verifies(self):
  55. installer.run(self.args, ROOT / 'docs/workshop-onboarding-20261109-r3/package')
  56. self.args.out = str(self.root / 'r9-result')
  57. binding = self.skills / 'ksf-workshop-runtime/credential-binding.json'
  58. binding.write_text('local-fixture-binding')
  59. self.args.upgrade = True
  60. applied = installer.run(self.args, NEW)
  61. self.assertTrue(applied['filesInstalled'])
  62. self.assertEqual(binding.read_text(), 'local-fixture-binding')
  63. self.args.apply = False
  64. self.assertEqual(installer.run(self.args, NEW)['actions'][0]['action'], 'verify-only')
  65. def test_r9_upgrade_preserves_identity_config_and_runtime(self):
  66. installer.run(self.args, ROOT / 'docs/workshop-onboarding-20261109-r4/package')
  67. self.args.out = str(self.root / 'r10-result')
  68. skill = self.skills / 'ksf-workshop-runtime'
  69. protected = {str(p.relative_to(skill)): p.read_bytes() for p in (skill / 'scripts').iterdir() if p.is_file()}
  70. protected['runtime-lock.json'] = (skill / 'runtime-lock.json').read_bytes()
  71. for name in ['credential-binding.json', 'private-local.json']:
  72. (skill / name).write_text('fixture-not-a-real-secret')
  73. self.args.upgrade = True
  74. applied = installer.run(self.args, NEW)
  75. self.assertTrue(applied['filesInstalled'])
  76. self.assertEqual(applied['actions'][0]['fromVersion'], 'r9')
  77. for name, before in protected.items():
  78. self.assertEqual((skill / name).read_bytes(), before)
  79. for name in ['credential-binding.json', 'private-local.json']:
  80. self.assertEqual((skill / name).read_text(), 'fixture-not-a-real-secret')
  81. self.args.apply = False
  82. self.assertEqual(installer.run(self.args, NEW)['actions'][0]['action'], 'verify-only')
  83. def test_apply_upgrade_requires_flag_before_writes(self):
  84. self.old_install()
  85. before = (self.skills / 'ksf-workshop-runtime/SKILL.md').read_bytes()
  86. with self.assertRaisesRegex(installer.InstallError, 'UPGRADE_FLAG_REQUIRED'):
  87. installer.run(self.args, NEW)
  88. self.assertEqual((self.skills / 'ksf-workshop-runtime/SKILL.md').read_bytes(), before)
  89. self.assertFalse(Path(self.args.out).exists())
  90. def test_local_edit_or_code_edit_stops_without_overwrite(self):
  91. self.old_install()
  92. path = self.skills / 'ksf-workshop-runtime/references/workshop-tasks.md'
  93. original = path.read_bytes()
  94. for target in [path, self.skills / 'ksf-workshop-runtime/scripts/ksf-secure.mjs']:
  95. old = target.read_bytes()
  96. target.write_bytes(old + b'\nlocal edit\n')
  97. self.args.upgrade = True
  98. with self.assertRaisesRegex(installer.InstallError, 'UPGRADE_BASELINE_CHANGED'):
  99. installer.run(self.args, NEW)
  100. self.assertEqual(target.read_bytes(), old + b'\nlocal edit\n')
  101. self.assertFalse(Path(self.args.out).exists())
  102. target.write_bytes(old)
  103. self.assertEqual(path.read_bytes(), original)
  104. def test_replacement_failure_restores_original_and_keeps_binding(self):
  105. self.old_install()
  106. skill = self.skills / 'ksf-workshop-runtime'
  107. originals = {str(p.relative_to(skill)): p.read_bytes() for p in skill.rglob('*') if p.is_file()}
  108. real_replace = installer.os.replace
  109. def fail_entry(source, dest):
  110. if Path(dest).name == 'SKILL.md':
  111. raise OSError('synthetic replacement failure')
  112. return real_replace(source, dest)
  113. self.args.upgrade = True
  114. with patch.object(installer.os, 'replace', fail_entry), self.assertRaises(OSError):
  115. installer.run(self.args, NEW)
  116. self.assertEqual({str(p.relative_to(skill)): p.read_bytes() for p in skill.rglob('*') if p.is_file()}, originals)
  117. self.assertFalse(json.loads((Path(self.args.out) / 'install-receipt.json').read_bytes())['ok'])
  118. self.assertEqual(len(list(self.skills.glob('.ksf-init-*'))), 0)
  119. if __name__ == '__main__':
  120. unittest.main()