test_workshop_onboarding_upgrade.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. """Observable r7-to-r8 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-r3/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_r8_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_apply_upgrade_requires_flag_before_writes(self):
  55. self.old_install()
  56. before = (self.skills / 'ksf-workshop-runtime/SKILL.md').read_bytes()
  57. with self.assertRaisesRegex(installer.InstallError, 'UPGRADE_FLAG_REQUIRED'):
  58. installer.run(self.args, NEW)
  59. self.assertEqual((self.skills / 'ksf-workshop-runtime/SKILL.md').read_bytes(), before)
  60. self.assertFalse(Path(self.args.out).exists())
  61. def test_local_edit_or_code_edit_stops_without_overwrite(self):
  62. self.old_install()
  63. path = self.skills / 'ksf-workshop-runtime/references/workshop-tasks.md'
  64. original = path.read_bytes()
  65. for target in [path, self.skills / 'ksf-workshop-runtime/scripts/ksf-secure.mjs']:
  66. old = target.read_bytes()
  67. target.write_bytes(old + b'\nlocal edit\n')
  68. self.args.upgrade = True
  69. with self.assertRaisesRegex(installer.InstallError, 'UPGRADE_BASELINE_CHANGED'):
  70. installer.run(self.args, NEW)
  71. self.assertEqual(target.read_bytes(), old + b'\nlocal edit\n')
  72. self.assertFalse(Path(self.args.out).exists())
  73. target.write_bytes(old)
  74. self.assertEqual(path.read_bytes(), original)
  75. def test_replacement_failure_restores_original_and_keeps_binding(self):
  76. self.old_install()
  77. skill = self.skills / 'ksf-workshop-runtime'
  78. originals = {str(p.relative_to(skill)): p.read_bytes() for p in skill.rglob('*') if p.is_file()}
  79. real_replace = installer.os.replace
  80. def fail_entry(source, dest):
  81. if Path(dest).name == 'SKILL.md':
  82. raise OSError('synthetic replacement failure')
  83. return real_replace(source, dest)
  84. self.args.upgrade = True
  85. with patch.object(installer.os, 'replace', fail_entry), self.assertRaises(OSError):
  86. installer.run(self.args, NEW)
  87. self.assertEqual({str(p.relative_to(skill)): p.read_bytes() for p in skill.rglob('*') if p.is_file()}, originals)
  88. self.assertFalse(json.loads((Path(self.args.out) / 'install-receipt.json').read_bytes())['ok'])
  89. self.assertEqual(len(list(self.skills.glob('.ksf-init-*'))), 0)
  90. if __name__ == '__main__':
  91. unittest.main()