test_workshop_report_runner.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """Exercise packaged r7 runner sequencing with synthetic child process outputs."""
  2. import contextlib
  3. import importlib.util
  4. import io
  5. import json
  6. import os
  7. from pathlib import Path
  8. import shutil
  9. import subprocess
  10. import sys
  11. import tempfile
  12. import unittest
  13. from unittest.mock import patch
  14. ROOT = Path(__file__).resolve().parents[1]
  15. class RunnerTests(unittest.TestCase):
  16. def run_case(self, mode):
  17. with tempfile.TemporaryDirectory(prefix='ksf-report-runner-') as temp:
  18. temp = Path(temp)
  19. package = temp / 'package'
  20. shutil.copytree(Path(os.environ.get('KSF_REPORT_PACKAGE_DIR', ROOT / 'docs/workshop-skill-release-20261109-r7/package')), package)
  21. spec = importlib.util.spec_from_file_location('report_runner_fixture', package / 'run-update.py')
  22. module = importlib.util.module_from_spec(spec)
  23. spec.loader.exec_module(module)
  24. calls = []
  25. def child(cmd, **kwargs):
  26. self.assertIsInstance(cmd, list)
  27. self.assertTrue(kwargs['capture_output'])
  28. self.assertNotIn('shell', kwargs)
  29. step = cmd[2]
  30. calls.append(step)
  31. if mode == 'dry-failure' and step == '--dry':
  32. return subprocess.CompletedProcess(cmd, 1, b'', b'{"ok":false}')
  33. if mode == 'apply-failure' and step == '--apply':
  34. return subprocess.CompletedProcess(cmd, 1, b'', b'{"ok":false}')
  35. payload = {'ok': True, 'readOnly': True, 'alreadyApplied': mode == 'already',
  36. 'baselineId': 'target' if mode == 'already' else 'r6',
  37. 'changedFiles': 0 if mode == 'already' else 23,
  38. 'changes': [] if mode == 'already' else [{'action': 'update'}] * 22 + [{'action': 'create'}],
  39. 'skillSha256': 'fixture-skill', 'manifestSha256': 'fixture-manifest'}
  40. if mode == 'bad-plan':
  41. payload['changedFiles'] = 22
  42. if step == '--apply':
  43. payload['backup'] = 'fixture-backup'
  44. if step == '--verify':
  45. self.assertEqual(cmd[-2:], ['--backup', 'fixture-backup'])
  46. payload.update(installedFilesVerified=42 if mode == 'bad-verify' else 43,
  47. changedFilesVerified=23, runtimeAndBindingPreserved=True,
  48. researchFilesUnchanged=True, legacyEntrypointRetired=True)
  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. self.assertFalse(receipt['agentLoaded'])
  59. for step in calls:
  60. label = step.removeprefix('--')
  61. self.assertTrue((output / (label + '.exit')).exists())
  62. self.assertTrue((output / (label + '.stdout.json')).exists())
  63. self.assertTrue((output / (label + '.stderr.json')).exists())
  64. return code, calls, receipt
  65. def test_success_is_dry_apply_once_then_verify(self):
  66. code, calls, receipt = self.run_case('success')
  67. self.assertEqual((code, calls), (0, ['--dry', '--apply', '--verify']))
  68. self.assertTrue(receipt['filesInstalled'])
  69. self.assertEqual(receipt['installedFilesVerified'], 43)
  70. def test_already_installed_only_runs_dry(self):
  71. code, calls, receipt = self.run_case('already')
  72. self.assertEqual((code, calls), (0, ['--dry']))
  73. self.assertTrue(receipt['alreadyApplied'])
  74. def test_dry_failure_stops(self):
  75. code, calls, receipt = self.run_case('dry-failure')
  76. self.assertEqual((code, calls), (1, ['--dry']))
  77. self.assertFalse(receipt['filesInstalled'])
  78. def test_wrong_plan_never_applies(self):
  79. code, calls, _ = self.run_case('bad-plan')
  80. self.assertEqual((code, calls), (1, ['--dry']))
  81. def test_apply_failure_never_retries(self):
  82. code, calls, _ = self.run_case('apply-failure')
  83. self.assertEqual((code, calls), (1, ['--dry', '--apply']))
  84. def test_wrong_verify_count_not_marked_installed(self):
  85. code, calls, receipt = self.run_case('bad-verify')
  86. self.assertEqual((code, calls), (1, ['--dry', '--apply', '--verify']))
  87. self.assertFalse(receipt['filesInstalled'])
  88. if __name__ == '__main__':
  89. unittest.main()