from __future__ import annotations import json import hashlib import os from pathlib import Path import sys import tempfile import unittest from unittest import mock ROOT = Path(__file__).parents[1] sys.path.insert(0, str(ROOT / 'tools')) from guardrails import extract_rule, evaluate from build_site import build, load_manifest from test_publication import _fixture from test_release import _release from verify_release import verify CANON_FIXTURE = '---\nid: canon-build-mode-change-authorization\ntitle: Build-Mode Change Authorization\nstatus: accepted\nrevision: accepted-1\nowner: the-custodian\nlast_reviewed: 2026-10-11\nreview_interval: 3m\nexpires_on: production-close-down\n---\n\n# Build-Mode Change Authorization\n\n```yaml\nrule: build-mode-change-authorization\nversion: 0.1\napplies_while: build-mode\nblue_lane_requires: # every field must be present on the change record\n - diff_ref # path or commit of reviewed diff / dry-run output\n - rollback # command or procedure reference\n - abort_owner # repo or agent name\n - receipt_path # where the execution receipt is written\nred_lane_categories:\n - destructive-deletion\n - access-scope-expansion\n - secret-scope-expansion\n - spend-ceiling\n - public-publication\norange_lane_categories:\n - founder-identity-act\n - platform-admin-login\n```\n' class GuardrailTest(unittest.TestCase): def setUp(self): # Synthetic fixtures must not inherit the CI release source lock. source_env = mock.patch.dict(os.environ, {"POLICY_NEXUS_SOURCE_ROOT": ""}) source_env.start() self.addCleanup(source_env.stop) self.source = CANON_FIXTURE self.rule = extract_rule(self.source) self.record = {'categories': [], 'diff_ref': 'reviewed.patch', 'rollback': 'revert source/image', 'abort_owner': 'policy-nexus', 'receipt_path': 'receipt.json'} def test_canon_block_contract(self): self.assertEqual(self.rule, { 'rule': 'build-mode-change-authorization', 'version': 0.1, 'applies_while': 'build-mode', 'blue_lane_requires': ['diff_ref', 'rollback', 'abort_owner', 'receipt_path'], 'red_lane_categories': ['destructive-deletion', 'access-scope-expansion', 'secret-scope-expansion', 'spend-ceiling', 'public-publication'], 'orange_lane_categories': ['founder-identity-act', 'platform-admin-login']}) def test_blue_requires_complete_record_and_confirmed_build_mode(self): self.assertEqual(evaluate(self.rule, self.record, build_mode=True), {'lane': 'blue'}) self.assertEqual(evaluate(self.rule, self.record, build_mode=False)['lane'], 'incomplete') for key in self.rule['blue_lane_requires']: for invalid in ['', ' ', None, True, [], {}]: with self.subTest(key=key, value=invalid): result = evaluate(self.rule, self.record | {key: invalid}, build_mode=True) self.assertEqual(result['lane'], 'incomplete') self.assertIn(key, result['missing']) def test_all_red_and_orange_categories_and_red_precedence(self): for lane in ('red', 'orange'): for category in self.rule[f'{lane}_lane_categories']: self.assertEqual(evaluate(self.rule, {'categories': [category]}, build_mode=True)['lane'], lane) self.assertEqual(evaluate(self.rule, {'categories': ['public-publication', 'platform-admin-login']}, build_mode=True)['lane'], 'red') def test_unknown_or_undeclared_categories_cannot_grant_blue(self): for categories in [None, 'public-publication', [False], ['typo']]: self.assertEqual(evaluate(self.rule, self.record | {'categories': categories}, build_mode=True)['lane'], 'incomplete') def test_unsupported_yaml_duplicate_fields_and_duplicate_blocks_refused(self): block = self.source[self.source.index('```yaml'):self.source.index('```', self.source.index('```yaml') + 7) + 3] for source in [self.source + '\n' + block + '\n', self.source.replace('version: 0.1', 'version: 0.1\nversion: 0.1'), self.source.replace(' - rollback', ' - rollback: &alias'), self.source.replace('version: 0.1', 'version: 0.2')]: with self.assertRaises(ValueError): extract_rule(source) def test_build_published_artifact_equals_canon_and_expiry_is_prominent(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) manifest = _fixture(root) data = json.loads(manifest.read_text()) document = data['documents'][0] source = root / 'canon-repo' / document['source_path'] source.write_text(self.source) document.update(id='canon-build-mode-change-authorization', guardrail_path='guardrails/build-mode-change-authorization.json') manifest.write_text(json.dumps(data)) records = build(manifest, root / 'build') artifact = root / 'build' / document['guardrail_path'] self.assertEqual(json.loads(artifact.read_text()), extract_rule(source.read_text())) page = (root / 'build' / document['canonical_path']).read_text() self.assertIn('Expires on: production-close-down', page[:page.index('')]) self.assertIn('blue_lane_requires:', page) self.assertIn('guardrail_digest', records[0]) self.assertIn('Build-Mode Change Authorization', (root / 'build/index.html').read_text()) def test_release_verifier_rejects_missing_tampered_or_invalid_guardrail(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) output = _release(root) manifest_path = output / 'publication-manifest.json' manifest = json.loads(manifest_path.read_text()) artifact = output / 'guardrails/rule.json' artifact.parent.mkdir() good = (json.dumps(self.rule) + '\n').encode() artifact.write_bytes(good) document = manifest['documents'][0] document.update(guardrail_path='guardrails/rule.json', guardrail_digest=hashlib.sha256(good).hexdigest()) manifest_path.write_text(json.dumps(manifest)) verify(output) artifact.write_bytes(good + b' ') with self.assertRaisesRegex(ValueError, 'guardrail artifact digest'): verify(output) artifact.unlink() with self.assertRaises(OSError): verify(output) invalid = json.dumps(self.rule | {'version': 99}).encode() artifact.write_bytes(invalid) document['guardrail_digest'] = hashlib.sha256(invalid).hexdigest() manifest_path.write_text(json.dumps(manifest)) with self.assertRaisesRegex(ValueError, 'unsupported machine rule'): verify(output) def test_guardrail_path_collision_and_traversal_refused(self): with tempfile.TemporaryDirectory() as directory: manifest = _fixture(Path(directory)) original = json.loads(manifest.read_text()) for path in ['../outside.json', '/outside.json', 'example.html']: data = json.loads(json.dumps(original)) data['documents'][0]['guardrail_path'] = path manifest.write_text(json.dumps(data)) with self.assertRaises(ValueError): load_manifest(manifest)