test_workshop_quality_runner.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """Runner control-flow checks; exact filesystem mutations are covered by Node."""
  2. import contextlib
  3. import importlib.util
  4. import io
  5. import json
  6. from pathlib import Path
  7. import shutil
  8. import subprocess
  9. import sys
  10. import tempfile
  11. import unittest
  12. from unittest.mock import patch
  13. ROOT = Path(__file__).resolve().parents[1]
  14. COUNTS = {'r2': (17, 14, 3), 'r3': (16, 14, 2), 'r4': (9, 8, 1)}
  15. class RunnerTests(unittest.TestCase):
  16. def run_case(self, mode='success', version='r4'):
  17. with tempfile.TemporaryDirectory(prefix='ksf-quality-runner-') as temp:
  18. temp = Path(temp)
  19. package = temp / 'package'
  20. shutil.copytree(ROOT / 'docs/workshop-skill-release-20261109-r5/package', package)
  21. spec = importlib.util.spec_from_file_location('quality_runner_fixture', package / 'run-update.py')
  22. module = importlib.util.module_from_spec(spec)
  23. spec.loader.exec_module(module)
  24. calls = []
  25. total, updates, creates = COUNTS[version]
  26. def child(cmd, **kwargs):
  27. self.assertIsInstance(cmd, list)
  28. self.assertTrue(kwargs['capture_output'])
  29. self.assertNotIn('shell', kwargs)
  30. step = cmd[2]
  31. calls.append(step)
  32. if mode == step.removeprefix('--') + '-failure':
  33. return subprocess.CompletedProcess(cmd, 1, b'', b'{"ok":false,"error":{"code":"FIXTURE_FAILURE"}}')
  34. payload = {'ok': True, 'readOnly': True, 'alreadyApplied': mode == 'already',
  35. 'baselineId': 'target' if mode == 'already' else version,
  36. 'changedFiles': 0 if mode == 'already' else total,
  37. 'changes': [] if mode == 'already' else [{'action':'update'}] * updates + [{'action':'create'}] * creates,
  38. 'skillSha256':'fixture-skill', 'manifestSha256':'fixture-manifest'}
  39. if mode == 'bad-plan':
  40. payload['changedFiles'] = 999
  41. if step == '--apply':
  42. payload['backup'] = 'fixture-backup'
  43. if step == '--verify':
  44. self.assertEqual(cmd[-2:], ['--backup', 'fixture-backup'])
  45. payload.update(installedFilesVerified=41, changedFilesVerified=total,
  46. runtimeAndBindingPreserved=True, researchFilesUnchanged=True, legacyEntrypointRetired=True)
  47. if mode == 'bad-verify':
  48. payload['installedFilesVerified'] = 40
  49. return subprocess.CompletedProcess(cmd, 0, json.dumps(payload).encode(), b'')
  50. output = temp / 'results'
  51. with patch.object(sys,'argv',['run-update.py','--out',str(output)]), \
  52. patch.object(module.sys,'platform','linux'), \
  53. patch.object(module.os,'getuid',return_value=10000,create=True), \
  54. patch.object(module.subprocess,'run',side_effect=child), \
  55. contextlib.redirect_stdout(io.StringIO()):
  56. code = module.main()
  57. receipt = json.loads((output / 'install-receipt.json').read_text(encoding='utf-8'))
  58. for step in calls:
  59. for suffix in ('.stdout.json','.stderr.json','.exit'):
  60. self.assertTrue((output / (step.removeprefix('--') + suffix)).is_file())
  61. self.assertFalse(receipt['agentLoaded'])
  62. self.assertFalse(receipt['currentReadVerified'])
  63. return code, calls, receipt
  64. def test_three_baselines_apply_once(self):
  65. for version in COUNTS:
  66. with self.subTest(version=version):
  67. code,calls,receipt = self.run_case(version=version)
  68. self.assertEqual((code,calls),(0,['--dry','--apply','--verify']))
  69. self.assertEqual(receipt['changedFiles'],COUNTS[version][0])
  70. def test_failed_dry_stops(self):
  71. code,calls,_=self.run_case('dry-failure')
  72. self.assertEqual((code,calls),(1,['--dry']))
  73. def test_bad_plan_stops(self):
  74. code,calls,_=self.run_case('bad-plan')
  75. self.assertEqual((code,calls),(1,['--dry']))
  76. def test_failed_apply_not_repeated(self):
  77. code,calls,_=self.run_case('apply-failure')
  78. self.assertEqual((code,calls),(1,['--dry','--apply']))
  79. def test_failed_verify_not_reinstalled(self):
  80. code,calls,receipt=self.run_case('verify-failure')
  81. self.assertEqual((code,calls),(1,['--dry','--apply','--verify']))
  82. self.assertFalse(receipt['filesInstalled'])
  83. def test_bad_verify_not_success(self):
  84. code,_,receipt=self.run_case('bad-verify')
  85. self.assertEqual(code,1)
  86. self.assertFalse(receipt['ok'])
  87. def test_already_installed_does_not_apply(self):
  88. code,calls,receipt=self.run_case('already')
  89. self.assertEqual((code,calls),(0,['--dry']))
  90. self.assertTrue(receipt['alreadyApplied'])
  91. if __name__ == '__main__':
  92. unittest.main()