"""Synthetic temporary Git repositories only; no real publication or science.""" import contextlib import io import json import os from pathlib import Path import shutil import subprocess import tempfile import unittest import verify_git_payload as verifier def encoded(value): return (json.dumps(value, sort_keys=True, indent=2) + '\n').encode('utf-8') def sealed(relative, payload): rows = [dict(path=path, bytes=len(data), sha256=verifier.sha(data)) for path, data in sorted(payload.items())] manifest = encoded(dict(root=verifier.PROJECT + '/' + relative, files=rows)) checksums = ''.join(row['sha256'] + ' ' + row['path'] + '\n' for row in rows) checksums += verifier.sha(manifest) + ' manifest.json\n' controls = {'manifest.json': manifest, 'checksums.sha256': checksums.encode('utf-8')} proof = dict(pass_all=True, root=verifier.PROJECT + '/' + relative, payload_file_count=len(rows), payload_bytes=sum(row['bytes'] for row in rows), manifest_sha256=verifier.sha(manifest), checksums_sha256=verifier.sha(controls['checksums.sha256']), excluded_pycache_files=0, verification_scope='synthetic test only') return rows, controls, proof @unittest.skipUnless(shutil.which('git'), 'Git executable is required') class GitPayloadTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory(prefix='phase4am-git-payload-synthetic-') self.addCleanup(temporary.cleanup) self.repo = Path(temporary.name).resolve() self.git('init', '--quiet') self.git('config', 'user.name', 'Synthetic Test') self.git('config', 'user.email', 'synthetic@example.invalid') self.git('config', 'core.autocrlf', 'false') self.git('config', 'commit.gpgsign', 'false') self.model = {'module.py': b'# synthetic, not a brain\nx = 1\n', 'config.json': b'{"synthetic": true}\n'} self.exp = {'audit/final-status.json': encoded(dict(status='FAIL-SYNTHETIC-AM-INTERFACE')), 'case/summary.json': b'{"synthetic": true}\n', 'bulk.npy': b'not-a-real-array\x00'} self.build() def git(self, *args, data=None): env = os.environ.copy() for key in ('GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES'): env.pop(key, None) result = subprocess.run(['git', '-C', str(self.repo), *args], input=data, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, check=False) self.assertEqual(result.returncode, 0, result.stderr.decode('utf-8', 'replace')) return result.stdout def write(self, path, data): target = self.repo / path target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(data) def build(self): _, model_controls, model_proof = sealed(verifier.MODEL, self.model) exp_rows, exp_controls, exp_proof = sealed(verifier.EXPERIMENT, self.exp) for path, data in {**self.model, **model_controls}.items(): self.write(verifier.MODEL + '/' + path, data) registry = {} index = [] omitted = [] for path, data in {**self.exp, **exp_controls}.items(): row = dict(path=path, bytes=len(data), sha256=verifier.sha(data)) if Path(path).suffix in verifier.SUFFIXES and len(data) <= verifier.MAX_EVIDENCE: registry['evidence/' + path] = data index.append(dict(source=verifier.EXPERIMENT + '/' + path, copy='evidence/' + path, bytes=len(data), sha256=verifier.sha(data))) else: omitted.append(dict(row, reason='bulk_or_non_evidence_extension')) for path, data in model_controls.items(): registry['model/' + path] = data index.append(dict(source=verifier.MODEL + '/' + path, copy='model/' + path, bytes=len(data), sha256=verifier.sha(data))) registry['evidence-index.json'] = encoded(index) registry['seal-verification.json'] = encoded(dict(model=model_proof, experiment=exp_proof)) self.receipt = dict(publication_complete=True, registry=verifier.REGISTRY, project_root=verifier.PROJECT, model_payload_remains_at=verifier.MODEL, bulk_remains_at=verifier.EXPERIMENT, model_manifest_sha256=model_proof['manifest_sha256'], experiment_manifest_sha256=exp_proof['manifest_sha256'], copied_evidence_files=len(index), copied_evidence_bytes=sum(row['bytes'] for row in index), files=[dict(path=path, bytes=len(data), sha256=verifier.sha(data)) for path, data in sorted(registry.items())], experiment_payload_not_copied=omitted, declared_audit_status='FAIL-SYNTHETIC-AM-INTERFACE') for path, data in registry.items(): self.write(verifier.REGISTRY + '/' + path, data) self.receipt_write() self.git('add', '--', verifier.MODEL, verifier.REGISTRY) def receipt_write(self): data = encoded(self.receipt) self.write(verifier.REGISTRY + '/publication-receipt.json', data) self.anchor = verifier.sha(data) def anchored_registry_change(self, relative, data): self.write(verifier.REGISTRY + '/' + relative, data) row = next(row for row in self.receipt['files'] if row['path'] == relative) row.update(bytes=len(data), sha256=verifier.sha(data)) self.receipt_write() self.git('add', '--', verifier.REGISTRY) def verify(self, **kwargs): return verifier.verify_git_payload(self.repo, self.anchor, **({'staged': True} if not kwargs else kwargs)) def test_staged_verifies_blob_bytes_and_leaves_index_unchanged(self): before = (self.repo / '.git/index').read_bytes() result = self.verify() self.assertTrue(result['pass_all']) self.assertEqual(result['model_payload_files'], 2) self.assertEqual(result['declared_audit_status'], 'FAIL-SYNTHETIC-AM-INTERFACE') self.assertFalse(result['experiment_bulk_verified_by_this_tool']) self.assertFalse(result['scientific_gate_executed']) self.assertTrue(result['publication_integrity_is_not_scientific_pass']) self.assertEqual(before, (self.repo / '.git/index').read_bytes()) self.assertFalse((self.repo / verifier.EXPERIMENT).exists()) def test_dirty_or_missing_worktree_payload_does_not_replace_good_index(self): (self.repo / verifier.MODEL / 'module.py').write_bytes(b'working copy differs') (self.repo / verifier.REGISTRY / 'publication-receipt.json').unlink() self.assertTrue(self.verify()['pass_all']) def test_correct_worktree_cannot_hide_corrupt_staged_blob(self): relative = verifier.MODEL + '/module.py' self.write(relative, self.model['module.py'].replace(b'x = 1', b'x = 2')) self.git('add', '--', relative) self.write(relative, self.model['module.py']) with self.assertRaisesRegex(ValueError, 'Git blob SHA-256 mismatch'): self.verify() def test_commit_verifies_immutable_commit_not_current_index(self): self.git('commit', '--quiet', '-m', 'synthetic only') commit = self.git('rev-parse', 'HEAD').decode().strip() self.write(verifier.MODEL + '/module.py', b'bad newer index') self.git('add', '--', verifier.MODEL) result = self.verify(commit='HEAD') self.assertEqual(result['commit'], commit) self.assertEqual(result['source'], 'commit') self.assertTrue(result['pass_all']) def test_external_receipt_anchor_is_mandatory_and_checked(self): for anchor in (None, '', '0' * 64, self.anchor.upper()): with self.subTest(anchor=anchor), self.assertRaises(ValueError): verifier.verify_git_payload(self.repo, anchor, staged=True) def test_extra_model_file_is_rejected_but_outside_scope_is_ignored(self): self.write('unrelated/old-file.txt', b'outside audit scope') self.git('add', '--', 'unrelated') self.assertTrue(self.verify()['pass_all']) self.write(verifier.MODEL + '/extra.py', b'extra') self.git('add', '--', verifier.MODEL) with self.assertRaisesRegex(ValueError, 'Model Git inventory'): self.verify() def test_extra_registry_file_is_rejected(self): self.write(verifier.REGISTRY + '/unlisted.json', b'{}') self.git('add', '--', verifier.REGISTRY) with self.assertRaisesRegex(ValueError, 'Registry Git inventory'): self.verify() def test_missing_staged_model_file_is_rejected(self): self.git('update-index', '--force-remove', verifier.MODEL + '/module.py') with self.assertRaisesRegex(ValueError, 'Model Git inventory'): self.verify() def test_symlink_blob_mode_is_rejected_without_following_it(self): path = verifier.MODEL + '/module.py' oid = self.git('rev-parse', ':' + path).decode().strip() self.git('update-index', '--cacheinfo', '120000,' + oid + ',' + path) with self.assertRaisesRegex(ValueError, 'Symlink/gitlink/nonregular'): self.verify() def test_gitlink_is_rejected(self): self.git('commit', '--quiet', '-m', 'synthetic only') oid = self.git('rev-parse', 'HEAD').decode().strip() self.git('update-index', '--add', '--cacheinfo', '160000,' + oid + ',' + verifier.MODEL + '/submodule') with self.assertRaisesRegex(ValueError, 'Symlink/gitlink/nonregular'): self.verify() def test_unmerged_index_is_rejected(self): path = verifier.MODEL + '/module.py' oid = self.git('rev-parse', ':' + path).decode().strip() self.git('update-index', '--force-remove', path) self.git('update-index', '--index-info', data=('100644 ' + oid + ' 1\t' + path + '\n').encode()) with self.assertRaisesRegex(ValueError, 'Unmerged index'): self.verify() def test_anchored_index_source_escape_is_rejected(self): data = json.loads((self.repo / verifier.REGISTRY / 'evidence-index.json').read_bytes()) data[0]['source'] = 'experiments/another-phase/audit/final-status.json' self.anchored_registry_change('evidence-index.json', encoded(data)) with self.assertRaisesRegex(ValueError, 'Evidence index differs'): self.verify() def test_anchored_registry_copy_cannot_disagree_with_model(self): data = (self.repo / verifier.REGISTRY / 'model/checksums.sha256').read_bytes() self.anchored_registry_change('model/checksums.sha256', b'0' * 64 + data[64:]) with self.assertRaisesRegex(ValueError, 'Checksum inventory'): self.verify() def test_anchored_receipt_cannot_change_model_manifest_anchor(self): self.receipt['model_manifest_sha256'] = '0' * 64 self.receipt_write(); self.git('add', '--', verifier.REGISTRY) with self.assertRaisesRegex(ValueError, 'Receipt-bound manifest'): self.verify() def test_registry_payload_corruption_is_rejected(self): path = verifier.REGISTRY + '/evidence/case/summary.json' self.write(path, b'{"synthetic":false}\n') self.git('add', '--', path) with self.assertRaisesRegex(ValueError, 'Git blob (byte count|SHA-256) mismatch'): self.verify() def test_anchored_receipt_path_escape_and_duplicate_fail(self): self.receipt['files'][0]['path'] = '../outside.json' self.receipt_write(); self.git('add', '--', verifier.REGISTRY) with self.assertRaisesRegex(ValueError, 'Noncanonical or escaping path'): self.verify() def test_duplicate_json_key_fails_even_with_external_anchor(self): data = encoded(self.receipt).replace(b'{\n', b'{\n "publication_complete": true,\n', 1) self.write(verifier.REGISTRY + '/publication-receipt.json', data) self.anchor = verifier.sha(data) self.git('add', '--', verifier.REGISTRY) with self.assertRaisesRegex(ValueError, 'Duplicate JSON key'): self.verify() def test_incomplete_status_is_preserved_not_regraded(self): self.receipt['declared_audit_status'] = 'INCOMPLETE-SYNTHETIC-AM-EXECUTION' self.receipt_write(); self.git('add', '--', verifier.REGISTRY) result = self.verify() self.assertEqual(result['declared_audit_status'], 'INCOMPLETE-SYNTHETIC-AM-EXECUTION') self.assertTrue(result['publication_integrity_is_not_scientific_pass']) def test_anchored_bulk_omission_inventory_mismatch_fails(self): self.receipt['experiment_payload_not_copied'] = [] self.receipt_write(); self.git('add', '--', verifier.REGISTRY) with self.assertRaisesRegex(ValueError, 'omitted-bulk inventory mismatch'): self.verify() def test_anchored_seal_proof_contradiction_fails(self): proof = json.loads((self.repo / verifier.REGISTRY / 'seal-verification.json').read_bytes()) proof['model']['payload_file_count'] += 1 self.anchored_registry_change('seal-verification.json', encoded(proof)) with self.assertRaisesRegex(ValueError, 'Seal-verification evidence contradicts'): self.verify() def test_both_or_neither_git_sources_fail(self): for kwargs in ({}, {'staged': True, 'commit': 'HEAD'}): with self.subTest(kwargs=kwargs), self.assertRaisesRegex(ValueError, 'Select exactly one'): verifier.verify_git_payload(self.repo, self.anchor, **kwargs) def test_cli_success_and_failure_are_json_with_exit_status(self): for anchor, expected in ((self.anchor, 0), ('0' * 64, 1)): with self.subTest(expected=expected), contextlib.redirect_stdout(io.StringIO()) as output: code = verifier.main(['--repo', str(self.repo), '--staged', '--receipt-sha256', anchor]) result = json.loads(output.getvalue()) self.assertEqual(code, expected) self.assertEqual(result['pass_all'], expected == 0) if __name__ == '__main__': unittest.main()