|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Offline regressions for the static release card/version contract.""" |
| 3 | + |
| 4 | +import importlib.util |
| 5 | +import json |
| 6 | +from pathlib import Path |
| 7 | +import shutil |
| 8 | +import subprocess |
| 9 | +import tempfile |
| 10 | +import unittest |
| 11 | + |
| 12 | +ROOT = Path(__file__).resolve().parents[2] |
| 13 | +VALIDATOR = Path('.github/scripts/validate_release_metadata.py') |
| 14 | +CARD = Path('.well-known/agent.json') |
| 15 | + |
| 16 | + |
| 17 | +class ReleaseCardTests(unittest.TestCase): |
| 18 | + def setUp(self): |
| 19 | + self.temp = tempfile.TemporaryDirectory() |
| 20 | + self.addCleanup(self.temp.cleanup) |
| 21 | + self.root = Path(self.temp.name) |
| 22 | + for name in [VALIDATOR, CARD, Path('Cargo.toml'), Path('SKILL.md'), |
| 23 | + Path('scripts/bump-version.sh')]: |
| 24 | + target = self.root / name |
| 25 | + target.parent.mkdir(parents=True, exist_ok=True) |
| 26 | + shutil.copyfile(ROOT / name, target) |
| 27 | + self.policy = json.loads((ROOT / '.github/release-metadata-policy.json').read_text()) |
| 28 | + self.policy['rules'] = {'version_sync': self.policy['rules']['version_sync']} |
| 29 | + (self.root / '.github/release-metadata-policy.json').write_text(json.dumps(self.policy)) |
| 30 | + spec = importlib.util.spec_from_file_location('validator_fixture', self.root / VALIDATOR) |
| 31 | + self.validator = importlib.util.module_from_spec(spec) |
| 32 | + spec.loader.exec_module(self.validator) |
| 33 | + self.version = self.validator.extract_cargo_version('Cargo.toml') |
| 34 | + |
| 35 | + def validate(self, tag=None, card=None): |
| 36 | + command = ['python3', str(self.root / VALIDATOR), '--mode', 'release_tag', |
| 37 | + '--tag', 'v' + (tag or self.version)] |
| 38 | + if card: |
| 39 | + command += ['--agent-card', str(card)] |
| 40 | + return subprocess.run(command, cwd=self.root, capture_output=True, text=True) |
| 41 | + |
| 42 | + def test_current_source_and_staged_asset_match_tag(self): |
| 43 | + staged = self.root / 'release-files/agent.json' |
| 44 | + staged.parent.mkdir() |
| 45 | + shutil.copyfile(self.root / CARD, staged) |
| 46 | + result = self.validate(card=staged) |
| 47 | + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) |
| 48 | + |
| 49 | + def test_stale_source_and_staged_asset_are_blocking(self): |
| 50 | + for staged in [False, True]: |
| 51 | + with self.subTest(staged=staged): |
| 52 | + (self.root / CARD).write_text((ROOT / CARD).read_text()) |
| 53 | + target = self.root / ('staged-agent.json' if staged else CARD) |
| 54 | + card = json.loads((ROOT / CARD).read_text()) |
| 55 | + card['version'] = '0.10.0' |
| 56 | + target.write_text(json.dumps(card)) |
| 57 | + result = self.validate(card=target if staged else None) |
| 58 | + self.assertEqual(result.returncode, 1) |
| 59 | + self.assertIn('Agent card version', result.stdout) |
| 60 | + |
| 61 | + def test_card_only_pr_change_runs_blocking_rule(self): |
| 62 | + rule = self.policy['rules']['version_sync'] |
| 63 | + self.assertTrue(self.validator.should_run_rule( |
| 64 | + rule, 'pull_request', [str(CARD)], self.policy)) |
| 65 | + self.assertEqual(rule['level'], 'blocking') |
| 66 | + |
| 67 | + def test_bump_preserves_every_other_card_byte_and_checks_new_tag(self): |
| 68 | + before = (self.root / CARD).read_text() |
| 69 | + result = subprocess.run(['bash', 'scripts/bump-version.sh', '9.8.7'], |
| 70 | + cwd=self.root, capture_output=True, text=True) |
| 71 | + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) |
| 72 | + self.assertEqual((self.root / CARD).read_text(), before.replace( |
| 73 | + '"version": "' + self.version + '"', '"version": "9.8.7"', 1)) |
| 74 | + self.assertEqual(self.validate(tag='9.8.7').returncode, 0) |
| 75 | + self.assertEqual(self.validate(tag='9.8.6').returncode, 1) |
| 76 | + |
| 77 | + def test_bump_accepts_reformatted_json_and_preserves_other_bytes(self): |
| 78 | + card = json.loads((ROOT / CARD).read_text()) |
| 79 | + # Put another version before the top-level field to catch accidental |
| 80 | + # first-match replacement in nested objects. |
| 81 | + card = {'metadata': {'version': 'keep-me'}, **card} |
| 82 | + for indent, newline in [(None, '\n'), (4, '\n'), ('\t', '\n'), (2, '\r\n')]: |
| 83 | + with self.subTest(indent=indent, newline=newline): |
| 84 | + before = json.dumps(card, indent=indent).replace('\n', newline) |
| 85 | + (self.root / CARD).write_bytes(before.encode('utf-8')) |
| 86 | + result = subprocess.run(['bash', 'scripts/bump-version.sh', '9.8.7'], |
| 87 | + cwd=self.root, capture_output=True, text=True) |
| 88 | + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) |
| 89 | + self.assertEqual((self.root / CARD).read_bytes().decode('utf-8'), before.replace( |
| 90 | + '"version": "' + self.version + '"', '"version": "9.8.7"', 1)) |
| 91 | + self.assertEqual(self.validate(tag='9.8.7').returncode, 0) |
| 92 | + |
| 93 | + def test_invalid_card_leaves_every_version_file_unchanged(self): |
| 94 | + valid = json.loads((ROOT / CARD).read_text()) |
| 95 | + missing_version = dict(valid) |
| 96 | + del missing_version['version'] |
| 97 | + for card_text in ['{broken json', json.dumps(missing_version), |
| 98 | + json.dumps({**valid, 'version': 17}), |
| 99 | + '{"version": "0.1.0", "version": "0.2.0"}']: |
| 100 | + with self.subTest(card_text=card_text): |
| 101 | + for name in ['Cargo.toml', 'SKILL.md']: |
| 102 | + shutil.copyfile(ROOT / name, self.root / name) |
| 103 | + (self.root / CARD).write_text(card_text) |
| 104 | + paths = [Path('Cargo.toml'), Path('SKILL.md'), CARD] |
| 105 | + before = {path: (self.root / path).read_bytes() for path in paths} |
| 106 | + result = subprocess.run(['bash', 'scripts/bump-version.sh', '9.8.7'], |
| 107 | + cwd=self.root, capture_output=True, text=True) |
| 108 | + self.assertNotEqual(result.returncode, 0) |
| 109 | + self.assertEqual({path: (self.root / path).read_bytes() for path in paths}, |
| 110 | + before, 'validation failure must not partially bump versions') |
| 111 | + |
| 112 | + def test_unmatched_version_file_leaves_inputs_unchanged(self): |
| 113 | + for broken in ['Cargo.toml', 'SKILL.md']: |
| 114 | + with self.subTest(broken=broken): |
| 115 | + for name in ['Cargo.toml', 'SKILL.md']: |
| 116 | + shutil.copyfile(ROOT / name, self.root / name) |
| 117 | + (self.root / broken).write_text('no version field here\n') |
| 118 | + paths = [Path('Cargo.toml'), Path('SKILL.md'), CARD] |
| 119 | + before = {path: (self.root / path).read_bytes() for path in paths} |
| 120 | + result = subprocess.run(['bash', 'scripts/bump-version.sh', '9.8.7'], |
| 121 | + cwd=self.root, capture_output=True, text=True) |
| 122 | + self.assertNotEqual(result.returncode, 0) |
| 123 | + self.assertEqual({path: (self.root / path).read_bytes() for path in paths}, |
| 124 | + before) |
| 125 | + |
| 126 | + |
| 127 | +if __name__ == '__main__': |
| 128 | + unittest.main() |
0 commit comments