test_workshop_platform_baseline_upgrade.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """Actual r6 documentation addition, private-file preservation and rollback in an isolated tree."""
  2. import importlib.util
  3. import json
  4. from pathlib import Path
  5. import shutil
  6. import tempfile
  7. import unittest
  8. from unittest.mock import patch
  9. ROOT = Path(__file__).resolve().parents[1]
  10. PACKAGE = ROOT / 'docs/ksf-workshop-r10-baseline-upgrade-20261110-r1/package'
  11. BASELINE = ROOT / 'docs/workshop-skill-release-20261109-r7/rehearsal-baseline-r6'
  12. spec = importlib.util.spec_from_file_location('platform_upgrade', PACKAGE / 'upgrade.py')
  13. up = importlib.util.module_from_spec(spec)
  14. spec.loader.exec_module(up)
  15. def tree(directory):
  16. return {p.relative_to(directory).as_posix(): (p.read_bytes(), p.stat().st_mode & 0o777)
  17. for p in directory.rglob('*') if p.is_file()}
  18. class PlatformBaselineTests(unittest.TestCase):
  19. def setUp(self):
  20. temporary = tempfile.TemporaryDirectory(prefix='ksf-r6-r10-test-')
  21. self.addCleanup(temporary.cleanup)
  22. self.root = Path(temporary.name)
  23. self.target = self.root / 'skills/ksf-workshop-runtime'
  24. shutil.copytree(BASELINE, self.target)
  25. self.output = self.root / 'results'
  26. self.binding = self.target / 'credential-binding.json'
  27. self.binding.write_text('fixture-only-private-binding')
  28. self.binding.chmod(0o600)
  29. (self.target / 'local-settings.json').write_text('fixture-local-settings')
  30. self.before = tree(self.target)
  31. def run_upgrade(self, command):
  32. return up.run(command, str(self.target), str(self.output), PACKAGE)
  33. def test_plan_recognizes_exact_r6_without_skill_writes(self):
  34. result = self.run_upgrade('plan')
  35. self.assertEqual(result['baselineId'], 'r6')
  36. self.assertEqual((result['changes'], result['creates']), (24, 1))
  37. self.assertEqual(tree(self.target), self.before)
  38. def test_apply_adds_missing_document_and_preserves_full_backup_and_private_files(self):
  39. self.run_upgrade('plan')
  40. self.run_upgrade('apply')
  41. result = self.run_upgrade('verify')
  42. self.assertTrue(result['ok'])
  43. self.assertEqual(result['publicFilesVerified'], 43)
  44. self.assertEqual(tree(self.output / 'before-full'), self.before)
  45. self.assertTrue((self.target / 'references/workshop-report-design.md').is_file())
  46. for name, value in self.before.items():
  47. if name.startswith('scripts/') or name in ['runtime-lock.json', 'credential-binding.json', 'local-settings.json']:
  48. self.assertEqual(tree(self.target)[name], value)
  49. after = tree(self.target)
  50. self.assertTrue(self.run_upgrade('apply')['alreadyApplied'])
  51. self.assertEqual(tree(self.target), after)
  52. def test_modified_document_rejected_without_skill_or_result_writes(self):
  53. p = self.target / 'references/workshop-tasks.md'
  54. p.write_bytes(p.read_bytes() + b'\nlocal modification\n')
  55. before = tree(self.target)
  56. with self.assertRaisesRegex(Exception, 'UPGRADE_BASELINE_CHANGED'):
  57. self.run_upgrade('plan')
  58. self.assertEqual(tree(self.target), before)
  59. self.assertFalse(self.output.exists())
  60. def test_modified_protected_code_rejected(self):
  61. p = self.target / 'scripts/ksf-secure.mjs'
  62. p.write_bytes(p.read_bytes() + b'\nchanged code\n')
  63. before = tree(self.target)
  64. with self.assertRaisesRegex(Exception, 'UPGRADE_BASELINE_CHANGED'):
  65. self.run_upgrade('plan')
  66. self.assertEqual(tree(self.target), before)
  67. self.assertFalse(self.output.exists())
  68. def test_new_document_path_collision_is_not_overwritten(self):
  69. p = self.target / 'references/workshop-report-design.md'
  70. p.write_text('independent local work')
  71. before = tree(self.target)
  72. with self.assertRaisesRegex(Exception, 'UPGRADE_NEW_FILE_ALREADY_EXISTS'):
  73. self.run_upgrade('plan')
  74. self.assertEqual(tree(self.target), before)
  75. self.assertFalse(self.output.exists())
  76. def test_change_after_plan_stops_before_backup_or_replacement(self):
  77. self.run_upgrade('plan')
  78. self.binding.write_text('fixture-binding-edited-after-plan')
  79. before = tree(self.target)
  80. with self.assertRaisesRegex(Exception, 'TARGET_CHANGED_SINCE_PLAN'):
  81. self.run_upgrade('apply')
  82. self.assertEqual(tree(self.target), before)
  83. self.assertFalse((self.output / 'before-full').exists())
  84. def test_failure_after_new_document_restores_old_files_and_removes_addition(self):
  85. self.run_upgrade('plan')
  86. core = up.core_at(PACKAGE)
  87. original_replace = core.os.replace
  88. def fail_entry(source, destination):
  89. if Path(destination) == self.target / 'SKILL.md':
  90. self.assertTrue((self.target / 'references/workshop-report-design.md').exists())
  91. raise OSError('fixture replacement failure')
  92. return original_replace(source, destination)
  93. with patch.object(up, 'core_at', return_value=core), patch.object(core.os, 'replace', fail_entry):
  94. with self.assertRaises(OSError):
  95. self.run_upgrade('apply')
  96. self.assertEqual(tree(self.target), self.before)
  97. self.assertEqual(tree(self.output / 'before-full'), self.before)
  98. self.assertTrue(json.loads((self.output / 'apply.json').read_bytes())['originalRestored'])
  99. self.assertFalse(list(self.target.rglob('*.ksf-upgrade-new')))
  100. def test_inventory_contains_only_declared_public_hashes(self):
  101. result = self.run_upgrade('inventory')
  102. self.assertEqual(len(result['files']), 43)
  103. self.assertIsNone(result['files']['references/workshop-report-design.md'])
  104. self.assertNotIn('credential-binding.json', result['files'])
  105. self.assertNotIn('fixture-only-private-binding', json.dumps(result))
  106. self.assertEqual(tree(self.target), self.before)
  107. self.assertFalse(self.output.exists())
  108. if __name__ == '__main__':
  109. unittest.main()