import json import sys import tempfile import unittest import zipfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts')) import evidence class EvidenceTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.root = Path(self.temp.name) (self.root / 'observation.txt').write_text('measured=1', encoding='utf-8') def tearDown(self): self.temp.cleanup() def run_bundle(self, assertions): manifest = self.root / 'input.json' manifest.write_text(json.dumps({'schema_version': 1, 'run_id': 'LAB-001', 'evidence_root': '.', 'assertions': assertions}), encoding='utf-8') return evidence.bundle(manifest, self.root / 'output') def assertion(self, verdict='passed', refs=None): return {'id': 'live', 'expected': '1', 'actual': '1', 'verdict': verdict, 'evidence': refs if refs is not None else ['observation.txt']} def test_success_has_hash_and_portable_archive(self): result = self.run_bundle([self.assertion()]) self.assertEqual(result['verdict'], 'passed') self.assertEqual(len(result['artifacts'][0]['sha256']), 64) with zipfile.ZipFile(self.root / 'output' / 'bundle.zip') as z: self.assertIn('report.md', z.namelist()) self.assertIn('evidence/observation.txt', z.namelist()) def test_missing_file_does_not_pass(self): result = self.run_bundle([self.assertion(refs=['missing.txt'])]) self.assertEqual(result['verdict'], 'inconclusive') def test_empty_evidence_does_not_pass(self): self.assertEqual(self.run_bundle([self.assertion(refs=[])])['verdict'], 'inconclusive') def test_expected_hash_mismatch_does_not_pass(self): result = self.run_bundle([self.assertion(refs=[{'path': 'observation.txt', 'sha256': '0' * 64}])]) self.assertEqual(result['verdict'], 'inconclusive') self.assertEqual(result['assertions'][0]['requested_evidence'][0]['sha256'], '0' * 64) self.assertEqual(result['artifacts'][0]['integrity'], 'mismatch') self.assertTrue((self.root / 'output' / 'evidence' / 'observation.txt').is_file()) def test_real_failure_preserved_alongside_blocked(self): blocked = self.assertion('blocked', []) blocked['id'] = 'later' self.assertEqual(self.run_bundle([self.assertion('failed'), blocked])['verdict'], 'failed') def test_no_assertions_not_pass(self): self.assertEqual(self.run_bundle([])['verdict'], 'inconclusive') def test_path_escape_rejected(self): with self.assertRaises(ValueError): self.run_bundle([self.assertion(refs=['../outside.txt'])]) def test_cannot_overwrite_run(self): self.run_bundle([self.assertion()]) with self.assertRaises(FileExistsError): evidence.bundle(self.root / 'input.json', self.root / 'output') def test_invalid_verdict_rejected(self): with self.assertRaises(ValueError): self.run_bundle([self.assertion('completed')]) def test_duplicate_ids_rejected(self): with self.assertRaises(ValueError): self.run_bundle([self.assertion(), self.assertion()]) def test_optional_checks_alone_cannot_pass(self): a = self.assertion() a['required'] = False self.assertEqual(self.run_bundle([a])['verdict'], 'inconclusive') def test_source_named_bundle_zip_is_preserved(self): (self.root / 'bundle.zip').write_bytes(b'external evidence archive') self.run_bundle([self.assertion(refs=['bundle.zip'])]) with zipfile.ZipFile(self.root / 'output' / 'bundle.zip') as z: self.assertIn('evidence/bundle.zip', z.namelist()) def test_simulation_is_visible_in_human_report(self): manifest = self.root / 'input.json' manifest.write_text(json.dumps({'schema_version': 1, 'run_id': 'EXAMPLE', 'context': {'execution_mode': 'simulation', 'real_device_test_executed': False}, 'assertions': [self.assertion()]}), encoding='utf-8') evidence.bundle(manifest, self.root / 'output') self.assertIn('simulation', (self.root / 'output' / 'report.md').read_text(encoding='utf-8')) if __name__ == '__main__': unittest.main()