test_workshop_intake_runner.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """Exercise packaged r6 runner sequencing with synthetic child process outputs."""
  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. class RunnerTests(unittest.TestCase):
  15. def run_case(self, mode):
  16. with tempfile.TemporaryDirectory(prefix='ksf-intake-runner-') as temp:
  17. temp = Path(temp)
  18. package = temp / 'package'
  19. shutil.copytree(ROOT / 'docs/workshop-skill-release-20261109-r6/package', package)
  20. spec = importlib.util.spec_from_file_location('intake_runner_fixture', package / 'run-update.py')
  21. module = importlib.util.module_from_spec(spec)
  22. spec.loader.exec_module(module)
  23. calls = []
  24. def child(cmd, **kwargs):
  25. self.assertIsInstance(cmd, list)
  26. self.assertTrue(kwargs['capture_output'])
  27. self.assertNotIn('shell', kwargs)
  28. step = cmd[2]
  29. calls.append(step)
  30. if mode == 'dry-failure' and step == '--dry':
  31. return subprocess.CompletedProcess(cmd, 1, b'', b'{"ok":false}')
  32. if mode == 'apply-failure' and step == '--apply':
  33. return subprocess.CompletedProcess(cmd, 1, b'', b'{"ok":false}')
  34. payload = {'ok': True, 'readOnly': True, 'alreadyApplied': mode == 'already',
  35. 'baselineId': 'target' if mode == 'already' else 'r5',
  36. 'changedFiles': 0 if mode == 'already' else 7,
  37. 'changes': [] if mode == 'already' else [{'action': 'update'}] * 6 + [{'action': 'create'}],
  38. 'skillSha256': 'fixture-skill', 'manifestSha256': 'fixture-manifest'}
  39. if mode == 'bad-plan':
  40. payload['changedFiles'] = 6
  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 if mode == 'bad-verify' else 42,
  46. changedFilesVerified=7, runtimeAndBindingPreserved=True,
  47. researchFilesUnchanged=True, legacyEntrypointRetired=True)
  48. return subprocess.CompletedProcess(cmd, 0, json.dumps(payload).encode(), b'')
  49. output = temp / 'results'
  50. with patch.object(sys, 'argv', ['run-update.py', '--out', str(output)]), \
  51. patch.object(module.sys, 'platform', 'linux'), \
  52. patch.object(module.os, 'getuid', return_value=10000, create=True), \
  53. patch.object(module.subprocess, 'run', side_effect=child), \
  54. contextlib.redirect_stdout(io.StringIO()):
  55. code = module.main()
  56. receipt = json.loads((output / 'install-receipt.json').read_text(encoding='utf-8'))
  57. self.assertFalse(receipt['agentLoaded'])
  58. self.assertFalse(receipt['actualBusinessWriteVerified'])
  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'], 42)
  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()