tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

unit.py (100932B)


      1 #!/usr/bin/env python3
      2 """Fast host-side policy, provenance, RSS and native-scope controls; no builds."""
      3 
      4 # native_header_equivalence_check
      5 import hashlib
      6 
      7 import json
      8 
      9 from pathlib import Path
     10 
     11 import tempfile
     12 
     13 import unittest
     14 
     15 from native_header_equivalence import native_header_equivalence, digest
     16 
     17 class HeaderEquivalenceTests(unittest.TestCase):
     18     root_kind = 'unit'
     19 
     20     def setUp(self):
     21         self.temporary = tempfile.TemporaryDirectory()
     22         self.addCleanup(self.temporary.cleanup)
     23         root = Path(self.temporary.name)
     24         self.repo, self.generation = root / 'repo', root / 'generation'
     25         self.repo.mkdir(); self.generation.mkdir()
     26         (self.generation / 'overlay').mkdir()
     27         (self.generation / 'cas/cache/source-build').mkdir(parents=True)
     28         self.map = self.repo / 'map.v1'
     29         old_map = ('arboricx.source-map.v1\nunit\tbase\tbase\tbase.tri\t-\n' +
     30                    self.root_kind + '\tsource_map\tsource.map\troot.tri\tbase\n')
     31         self.map.write_text(old_map.replace('unit\t', 'native\t').replace('reexport\t', 'native\t'))
     32         (self.generation / 'source-map.v1').write_text(old_map)
     33         raw = {'base': b'x = t\n', 'source_map': b'!import "base" !Local\nf x = x\n'}
     34         staged = {'base': b'!export *\nx = t\n\n',
     35                   'source_map': (b'!import base *\n!export *\n' +
     36                                  (b'!export * from base\n' if self.root_kind == 'reexport' else b'') +
     37                                  b'f x = x\n\n')}
     38         compiler = '1' * 64
     39         provenance = dict(map_sha256=hashlib.sha256(old_map.encode()).hexdigest(),
     40                           sources={k: hashlib.sha256(v).hexdigest() for k, v in raw.items()},
     41                           compiler_bundle_sha256=compiler)
     42         (self.generation / 'inputs.json').write_text(json.dumps(provenance))
     43         (self.generation / 'status.json').write_text(json.dumps(dict(complete=True, root='source_map')))
     44         for alias, path in [('base', 'base.tri'), ('source_map', 'root.tri')]:
     45             self.repo.joinpath(path).write_bytes(staged[alias])
     46             self.generation.joinpath('overlay', alias + '.tri').write_bytes(staged[alias])
     47             tree = self.put('arboricx.tree-term.v1', b'\0')
     48             manifest = self.put('arboricx.module-manifest.v1', ('arboricx.module-manifest.v1\nexport\tmain\tarboricx.tree-term.v1\t' + tree + '\tarboricx.abi.tree.v1\n').encode())
     49             rows = ['source\t' + digest('arboricx.source.v1', staged[alias]),
     50                     'compiler\t' + compiler,
     51                     'options\t' + digest('arboricx.build-options.v1', b'source-module.reference.v1')]
     52             receipt = self.put('arboricx.build-receipt.v1', ('arboricx.build-receipt.v1\n' + '\n'.join(rows) + '\noutput\t' + manifest + '\n').encode())
     53             key = digest('arboricx.source-build-key.v1', ('arboricx.source-build-key.v1\n' + '\n'.join(rows) + '\n').encode())
     54             self.generation.joinpath('cas/cache/source-build', alias).write_text('\t'.join(['arboricx.source-build-cache.v1', key, manifest, receipt]))
     55 
     56     def put(self, domain, payload):
     57         pin = digest(domain, payload)
     58         path = self.generation / 'cas/objects' / pin[:3] / pin
     59         path.parent.mkdir(parents=True, exist_ok=True); path.write_bytes(payload)
     60         return pin
     61 
     62     def check(self):
     63         return native_header_equivalence(self.generation, self.repo, self.map)
     64 
     65     def test_valid_staged_bytes(self):
     66         rows, proof = self.check()
     67         self.assertEqual(proof['changed'], ['base', 'source_map'])
     68         self.assertEqual(len(rows), 2)
     69 
     70     def test_changed_body(self):
     71         with (self.repo / 'root.tri').open('a') as f: f.write('-- body change\n')
     72         with self.assertRaisesRegex(ValueError, 'staged bytes'): self.check()
     73 
     74     def test_changed_namespace(self):
     75         p = self.repo / 'root.tri'; p.write_text(p.read_text().replace('!import base *', '!import base as B'))
     76         with self.assertRaisesRegex(ValueError, 'staged bytes'): self.check()
     77 
     78     def test_changed_exports(self):
     79         p = self.repo / 'root.tri'; p.write_text(p.read_text().replace('!export *', '!export f'))
     80         with self.assertRaisesRegex(ValueError, 'staged bytes|invalid native export'): self.check()
     81 
     82     def test_changed_map_path(self):
     83         (self.repo / 'other.tri').write_bytes((self.repo / 'base.tri').read_bytes())
     84         self.map.write_text(self.map.read_text().replace('base.tri', 'other.tri'))
     85         with self.assertRaisesRegex(ValueError, 'edges, paths'): self.check()
     86 
     87     def test_incomplete_generation(self):
     88         (self.generation / 'status.json').write_text('{"complete":false,"root":"source_map"}')
     89         with self.assertRaisesRegex(ValueError, 'completed'): self.check()
     90 
     91     def test_overlay_receipt_corruption(self):
     92         # Keep current source equal to the modified overlay: the receipt still
     93         # authenticates the old compiler input and must reject this pair.
     94         for p in [self.repo / 'root.tri', self.generation / 'overlay/source_map.tri']:
     95             with p.open('a') as f: f.write('-- forged overlay\n')
     96         with self.assertRaisesRegex(ValueError, 'receipt mismatch'): self.check()
     97 
     98     def test_checkpoint_key_corruption(self):
     99         p = self.generation / 'cas/cache/source-build/source_map'
    100         parts = p.read_text().split('\t'); parts[1] = '2' * 64; p.write_text('\t'.join(parts))
    101         with self.assertRaisesRegex(ValueError, 'key mismatch'): self.check()
    102 
    103 class ReexportHeaderEquivalenceTests(HeaderEquivalenceTests):
    104     # Exercise the same fail-closed controls with the prelude's re-export policy.
    105     root_kind = 'reexport'
    106 
    107 
    108 # native_library_migration_check
    109 import unittest
    110 
    111 from native_library_migration import native_candidate, alias, terminal_export, reexport_candidate
    112 
    113 class HeaderTests(unittest.TestCase):
    114     def test_local_and_namespace_preserved(self):
    115         body = b'-- body\nf x = N.id x\n'
    116         raw = b'!import "prelude" !Local\n!import "source.core" N\n' + body
    117         candidate, imports = native_candidate(raw, {'prelude': 'prelude', 'source.core': 'source_core'})
    118         self.assertEqual(candidate, b'!import prelude *\n!import source_core as N\n!export *\n'+body)
    119         self.assertEqual(imports, [('prelude', '!Local'), ('source_core', 'N')])
    120 
    121     def test_comments_before_import_preserved(self):
    122         raw = b'-- header comment\n!import "prelude" !Local\nx = t\n'
    123         candidate, _ = native_candidate(raw, {'prelude': 'prelude'})
    124         self.assertTrue(candidate.endswith(b'-- header comment\nx = t\n'))
    125 
    126     def test_explicit_transitive_dependency_is_recorded(self):
    127         raw = b'!import "patterns" !Local\nmain = equal? t t\n'
    128         candidate, imports = native_candidate(raw, {'patterns': 'patterns'}, [('prelude', '!Local')])
    129         self.assertEqual(imports, [('prelude', '!Local'), ('patterns', '!Local')])
    130         self.assertTrue(candidate.startswith(b'!import prelude *\n!import patterns *\n'))
    131         self.assertTrue(candidate.endswith(b'main = equal? t t\n'))
    132 
    133     def test_unknown_dependency_rejected(self):
    134         with self.assertRaisesRegex(ValueError, 'unknown dependency'):
    135             native_candidate(b'!import "missing" !Local\nx = t\n', {})
    136 
    137     def test_malformed_import_rejected(self):
    138         with self.assertRaisesRegex(ValueError, 'unsupported legacy'):
    139             native_candidate(b'!import "prelude"\nx = t\n', {'prelude': 'prelude'})
    140 
    141     def test_native_policy_not_widened(self):
    142         raw = b'!import dep as D\n!export f\nf = D.f\n'
    143         candidate, imports = native_candidate(raw, {'dep': 'dep'})
    144         self.assertEqual(candidate, raw)
    145         self.assertEqual(imports, [('dep', 'D')])
    146 
    147     def test_unknown_native_dependency_rejected(self):
    148         with self.assertRaisesRegex(ValueError, 'unknown native'):
    149             native_candidate(b'!import dep *\n!export f\nf = t\n', {'other': 'other'})
    150 
    151     def test_terminal_selector_requires_explicit_exact_symbol(self):
    152         raw = b'program x = x\nprogram\n\n'
    153         self.assertEqual(terminal_export(raw, 'program'), b'program x = x\n\n')
    154         with self.assertRaisesRegex(ValueError, 'exact bare'):
    155             terminal_export(b'program x = x\nprogram t\n', 'program')
    156 
    157     def test_terminal_selector_requires_authored_definition(self):
    158         with self.assertRaisesRegex(ValueError, 'authored definition'):
    159             terminal_export(b'program\n', 'program')
    160 
    161     def test_terminal_selector_rejects_expression_names(self):
    162         with self.assertRaisesRegex(ValueError, 'invalid terminal'):
    163             terminal_export(b'program t\n', 'program t')
    164 
    165     def test_reexport_directives_precede_body_comments(self):
    166         raw = b'-- example\n!import "dep" !Local\n'
    167         candidate, imports = native_candidate(raw, {'dep':'dep'})
    168         result = reexport_candidate(raw, candidate, imports)
    169         self.assertIn(b'!export *\n!export * from dep\n-- example', result)
    170         self.assertEqual(reexport_candidate(result,result,imports),result)
    171 
    172     def test_reexport_policy_rejects_authored_definitions(self):
    173         with self.assertRaisesRegex(ValueError,'only comments'):
    174             reexport_candidate(b'f = t\n',b'!export *\nf = t\n',[])
    175 
    176     def test_reexport_policy_rejects_namespace_widening(self):
    177         with self.assertRaisesRegex(ValueError,'local imports'):
    178             reexport_candidate(b'-- comment\n',b'!export *\n',[('dep','N')])
    179 
    180     def test_flat_aliases(self):
    181         self.assertEqual(alias('source.export-cache'), 'source_export_cache')
    182 
    183 
    184 # process_rss_check
    185 import subprocess
    186 
    187 import unittest
    188 
    189 from types import SimpleNamespace
    190 
    191 from unittest.mock import patch
    192 
    193 from process_rss import ChildRSS, status_fields
    194 
    195 class ChildRSSTests(unittest.TestCase):
    196     def monitor(self, candidates=(7, 1001)):
    197         monitor = ChildRSS.__new__(ChildRSS)
    198         def still_running(timeout):
    199             raise subprocess.TimeoutExpired('fake-child', timeout)
    200         monitor.proc = SimpleNamespace(pid=7, poll=lambda: None, wait=still_running)
    201         monitor.parent, monitor.namespace_index, monitor.pid = 1000, 1, None
    202         monitor.candidates = lambda: iter(candidates)
    203         return monitor
    204 
    205     def test_namespace_translation_ignores_wrong_host_pid(self):
    206         def read(path):
    207             if str(path) == '/proc/7/status':
    208                 return 'Pid: 7\nPPid: 2\nNSpid: 7\nVmRSS: 999999 kB\n'
    209             return 'Pid: 1001\nPPid: 1000\nNSpid: 1001 7\nVmRSS: 512 kB\n'
    210         monitor = self.monitor()
    211         with patch('pathlib.Path.read_text', read):
    212             self.assertEqual(monitor.sample(), 512)
    213             self.assertEqual(monitor.pid, 1001)
    214 
    215     def test_missing_rss_fails_closed(self):
    216         with patch('pathlib.Path.read_text', return_value='Pid: 1001\nPPid: 1000\nNSpid: 1001 7\n'):
    217             with self.assertRaisesRegex(RuntimeError, 'cannot monitor'):
    218                 self.monitor((1001,)).sample()
    219 
    220     def test_inaccessible_rss_fails_closed(self):
    221         with patch('pathlib.Path.read_text', side_effect=FileNotFoundError):
    222             with self.assertRaisesRegex(RuntimeError, 'cannot monitor'):
    223                 self.monitor().sample()
    224 
    225     def test_cached_pid_reuse_fails_closed(self):
    226         monitor = self.monitor(); monitor.pid = 1001
    227         with patch('pathlib.Path.read_text', return_value='Pid: 1001\nPPid: 1000\nNSpid: 1001 8\nVmRSS: 10 kB\n'):
    228             with self.assertRaisesRegex(RuntimeError, 'cannot monitor'):
    229                 monitor.sample()
    230 
    231     def test_exiting_leader_uses_shared_thread_memory(self):
    232         from pathlib import Path
    233         monitor = self.monitor((1001,))
    234         def read(path):
    235             if str(path) == '/proc/1001/status':
    236                 return 'Pid: 1001\nPPid: 1000\nNSpid: 1001 7\n'
    237             return 'Tgid: 1001\nVmRSS: 321 kB\n'
    238         with patch('pathlib.Path.read_text', read), patch('pathlib.Path.glob', return_value=[Path('/proc/1001/task/1002/status')]):
    239             self.assertEqual(monitor.sample(), 321)
    240 
    241     def test_identified_running_exit_with_no_address_space(self):
    242         monitor = self.monitor((1001,))
    243         def read(path):
    244             if str(path).endswith('/statm'):
    245                 return '0 0 0 0 0 0 0\n'
    246             return 'Pid: 1001\nPPid: 1000\nNSpid: 1001 7\nState: R (running)\n'
    247         with patch('pathlib.Path.read_text', read), patch('pathlib.Path.glob', return_value=[]):
    248             self.assertEqual(monitor.sample(), 0)
    249 
    250     def test_identified_zombie_can_finish_kernel_cleanup(self):
    251         monitor = self.monitor((1001,))
    252         with patch('pathlib.Path.read_text', return_value='Pid: 1001\nPPid: 1000\nNSpid: 1001 7\nState: Z (zombie)\n'), patch('pathlib.Path.glob', return_value=[]):
    253             self.assertIsNone(monitor.sample())
    254 
    255     def test_exited_child_has_no_sample(self):
    256         monitor = self.monitor(); monitor.proc.poll = lambda: 0
    257         self.assertIsNone(monitor.sample())
    258 
    259     def test_address_space_disappears_just_before_reap(self):
    260         monitor = self.monitor()
    261         monitor.proc.wait = lambda timeout: 0
    262         with patch('pathlib.Path.read_text', side_effect=FileNotFoundError):
    263             self.assertIsNone(monitor.sample())
    264 
    265     def test_real_child_is_visible(self):
    266         proc = subprocess.Popen(['sleep', '2'])
    267         try:
    268             monitor = ChildRSS(proc)
    269             sample = monitor.sample()
    270             self.assertIsNotNone(sample)
    271             self.assertGreater(sample, 0)
    272             self.assertTrue(monitor.matches(status_fields(__import__('pathlib').Path(f'/proc/{monitor.pid}/status').read_text())))
    273         finally:
    274             proc.terminate(); proc.wait()
    275 
    276 
    277 # research_session_check
    278 import hashlib
    279 
    280 from pathlib import Path
    281 
    282 import subprocess
    283 
    284 import tempfile
    285 
    286 import time
    287 
    288 import unittest
    289 
    290 from research_session import ResearchSession, ROOT
    291 
    292 class ResearchTests(unittest.TestCase):
    293     def test_prepared_fixture_selections_never_invoke_compiler(self):
    294         with tempfile.TemporaryDirectory() as directory:
    295             source = Path(directory) / 'fixture.tri'
    296             source.write_text('!export *\none = t\ntwo = t t\n')
    297             session = ResearchSession.__new__(ResearchSession)
    298             session.records = {source: {'after_sha256': hashlib.sha256(source.read_bytes()).hexdigest(),
    299                                         'manifest': 'manifest'}}
    300             session.tricu, session.store = 'tricu', Path(directory)
    301             session.object = lambda domain, pin: b'header\nexport\tone\tkind\troot1\tabi\nexport\ttwo\tkind\troot2\tabi\n'
    302             calls = []
    303             session.run = lambda command, seconds: (calls.append(command) or b'', .01)
    304             for name in ['one', 'two', 'one']:
    305                 session.compile(source, [name], Path(directory) / (name + '.arboricx'))
    306             self.assertEqual(len(calls), 3)
    307             self.assertTrue(all(command[1:3] == ['arboricx', 'export'] for command in calls))
    308 
    309     def test_changed_prepared_source_is_rejected(self):
    310         with tempfile.TemporaryDirectory() as directory:
    311             source = Path(directory) / 'fixture.tri'; source.write_text('changed')
    312             session = ResearchSession.__new__(ResearchSession)
    313             session.records = {source: {'after_sha256': '0' * 64}}
    314             with self.assertRaisesRegex(ValueError, 'changed during session'):
    315                 session.compile(source, ['main'], Path(directory) / 'out')
    316 
    317     def test_job_above_twenty_minutes_is_rejected(self):
    318         session = ResearchSession.__new__(ResearchSession)
    319         with self.assertRaisesRegex(ValueError, '1200'):
    320             session.run(['never-started'], 1201)
    321 
    322     def test_expired_whole_harness_cannot_start_another_child(self):
    323         session = ResearchSession.__new__(ResearchSession)
    324         session.deadline = time.monotonic() - 1
    325         with self.assertRaises(subprocess.TimeoutExpired):
    326             session.run(['never-started'], 1200)
    327 
    328     def test_ir_harness_uses_original_authenticated_fixture_path(self):
    329         text = (ROOT / 'test/staging_experiment.py').read_text()
    330         self.assertIn("source = ROOT / 'test/stagingExperiment.tri'", text)
    331         self.assertNotIn("source = root / 'fixtures.tri'", text)
    332         self.assertNotIn('zig', text.lower())
    333 
    334     def test_conditional_fixtures_are_batched(self):
    335         text = (ROOT / 'test/staged_conditional.py').read_text()
    336         body = text.split('    def fixture_for(', 1)[1].split('    for entry in ', 1)[0]
    337         self.assertNotIn('compile(', body)
    338         self.assertIn('arboricx', body)
    339 
    340 
    341 # standard_native_migration_check
    342 from pathlib import Path
    343 
    344 import unittest
    345 
    346 ROOT = Path(__file__).resolve().parent.parent
    347 
    348 DEFERRED_LIBRARIES = {}
    349 
    350 LEGACY_DEMO_FIXTURES = set()
    351 
    352 def legacy(path):
    353     return any(line.startswith('!import "') for line in path.read_text().splitlines())
    354 
    355 class StandardMigrationTests(unittest.TestCase):
    356     def test_remaining_library_imports_are_explicitly_scoped(self):
    357         remaining = {str(path.relative_to(ROOT)) for path in (ROOT / 'lib').rglob('*.tri')
    358                      if legacy(path)}
    359         self.assertEqual(remaining, set(DEFERRED_LIBRARIES))
    360 
    361     def test_application_demos_are_native(self):
    362         demos = [*(ROOT / 'demos').glob('*.tri'), *(ROOT / 'demos/interactionTrees').glob('*.tri')]
    363         self.assertEqual(len(demos), 23)
    364         for path in demos:
    365             with self.subTest(path=path):
    366                 self.assertFalse(legacy(path))
    367                 self.assertTrue(any(line.startswith('!export ') for line in path.read_text().splitlines()))
    368 
    369     def test_demo_inputs_have_no_legacy_imports(self):
    370         remaining = {str(path.relative_to(ROOT)) for path in (ROOT / 'demos').rglob('*.tri')
    371                      if legacy(path)}
    372         self.assertEqual(remaining, LEGACY_DEMO_FIXTURES)
    373 
    374     def test_formatter_and_recursion_are_native(self):
    375         for directory in ['lib/tools', 'lib/recursion']:
    376             for path in (ROOT / directory).rglob('*.tri'):
    377                 with self.subTest(path=path):
    378                     self.assertFalse(legacy(path))
    379                     self.assertTrue(any(line.startswith('!export ') for line in path.read_text().splitlines()))
    380 
    381 
    382 class NativeHeaderTests(unittest.TestCase):
    383     def test_order_and_deduplication(self):
    384         from native_header import native_header
    385         edges, imports = native_header("-- header\n!import math\n!export * from base\n!import math as M\n!source build_only\n!export main\nmain = t\n")
    386         self.assertEqual(edges, ['math', 'base', 'build_only'])
    387         self.assertEqual(imports, [('math', 'math'), ('math', 'M')])
    388 
    389     def test_hash_imports_and_barrels(self):
    390         from native_header import native_header
    391         pin = 'a' * 64
    392         self.assertEqual(native_header('!export * from dep\n')[0], ['dep'])
    393         self.assertEqual(native_header('!import '+pin+' as M\n!export * from '+pin+'\n')[0], [])
    394         with self.assertRaises(ValueError): native_header('!import '+pin+'\n')
    395 
    396     def test_redundant_build_only_edges(self):
    397         from native_header import native_header
    398         for source in ['!source dep\n!import dep\n', '!import dep\n!source dep\n',
    399                        '!source dep\n!export * from dep\n', '!source dep\n!source dep\n']:
    400             with self.subTest(source=source), self.assertRaisesRegex(ValueError, 'redundant'):
    401                 native_header(source)
    402 
    403     def test_old_native_syntax_and_recovery_spelling_rejected(self):
    404         from native_header import native_header
    405         for source in ['!use dep *\n', '!import "dep" !Local\n']:
    406             with self.subTest(source=source), self.assertRaises(ValueError): native_header(source)
    407 
    408 
    409 class GuardedJobTests(unittest.TestCase):
    410     def test_output_and_nonzero_exit(self):
    411         import subprocess
    412         import sys
    413         from process_rss import run_guarded
    414         output, peak = run_guarded([sys.executable, '-c', 'print("transport-ok")'],
    415                                   memory_mib=64, seconds=5)
    416         self.assertEqual(output.strip(), 'transport-ok')
    417         self.assertGreaterEqual(peak, 0)
    418         with self.assertRaises(subprocess.CalledProcessError) as failure:
    419             run_guarded([sys.executable, '-c', 'print("diagnostic"); raise SystemExit(3)'],
    420                         memory_mib=64, seconds=5)
    421         self.assertIn('diagnostic', failure.exception.output)
    422 
    423     def test_time_and_memory_guards(self):
    424         import sys
    425         from process_rss import run_guarded
    426         with self.assertRaisesRegex(RuntimeError, 'wall-time guard'):
    427             run_guarded([sys.executable, '-c', 'import time; time.sleep(10)'],
    428                         memory_mib=64, seconds=0.05)
    429         with self.assertRaisesRegex(RuntimeError, 'RSS guard'):
    430             run_guarded([sys.executable, '-c', 'import time; data=bytearray(8*1024*1024); time.sleep(10)'],
    431                         memory_mib=1, seconds=5)
    432 
    433 
    434 class BootstrapAcceptanceTests(unittest.TestCase):
    435     def setUp(self):
    436         from bootstrap_generations import source_map, snapshot
    437         from bootstrap_acceptance import ARTIFACTS, sha
    438         self.temporary = tempfile.TemporaryDirectory()
    439         self.addCleanup(self.temporary.cleanup)
    440         self.root = Path(self.temporary.name)
    441         self.source = self.root / 'source'
    442         self.source.mkdir()
    443         (self.source / 'root.tri').write_text('!export main\nmain = t\n')
    444         self.runtime = self.root / 'host'
    445         self.runtime.write_bytes(b'host-identity')
    446         self.first, self.second = self.root / 'first', self.root / 'second'
    447         for index, run in enumerate((self.first, self.second)):
    448             (run / 'cas/cache/source-build').mkdir(parents=True)
    449             (run / 'source-map.v1').write_text('arboricx.source-map.v1\nnative\tsource_map\troot\troot.tri\t-\n')
    450             (run / 'cas/cache/source-build/source_map').write_text('version\tkey\tidentical-manifest\treceipt')
    451             (run / 'compiler.arboricx').write_bytes(b'bridge' if index == 0 else b'rebuilt-compiler.arboricx')
    452             (run / 'io-adapter.arboricx').write_bytes(b'adapter')
    453             for name in ARTIFACTS:
    454                 (run / name).write_bytes(name.encode())
    455             text, entries, _ = source_map(run / 'source-map.v1', self.source, 'source_map')
    456             inputs = dict(source_root=str(self.source), root='source_map', backend='stable', fresh_store=True,
    457                           runtime_sha256=sha(self.runtime), adapter_sha256=sha(run / 'io-adapter.arboricx'),
    458                           compiler_bundle_sha256=sha(run / 'compiler.arboricx'), **snapshot(text, entries, self.source))
    459             (run / 'inputs.json').write_text(json.dumps(inputs))
    460             (run / 'status.json').write_text(json.dumps(dict(complete=True, **inputs,
    461                 artifacts={name: dict(sha256=sha(run / name)) for name in ARTIFACTS})))
    462 
    463     def verify(self):
    464         from bootstrap_acceptance import verify
    465         from unittest.mock import patch
    466         # Synthetic outputs test gate policy only; real CAS auditing is separate.
    467         with patch('bootstrap_acceptance.run_guarded', return_value=('synthetic audit', 0)):
    468             return verify(self.first, self.second, self.runtime)
    469 
    470     def test_accepts_matching_policy_records(self):
    471         self.assertTrue(self.verify()['accepted'])
    472 
    473     def test_rejects_wrong_compiler_and_artifact(self):
    474         from bootstrap_acceptance import sha
    475         path = self.second / 'compiler.arboricx'
    476         path.write_bytes(b'unrelated-worker')
    477         for filename in ('inputs.json', 'status.json'):
    478             record_path = self.second / filename
    479             record = json.loads(record_path.read_text())
    480             record['compiler_bundle_sha256'] = sha(path)
    481             record_path.write_text(json.dumps(record))
    482         with self.assertRaisesRegex(ValueError, 'did not execute'):
    483             self.verify()
    484         path.write_bytes(b'rebuilt-compiler.arboricx')
    485         (self.first / 'build-stable.arboricx').write_bytes(b'corrupt')
    486         with self.assertRaisesRegex(ValueError, 'artifact identity'):
    487             self.verify()
    488 
    489     def test_rejects_seeded_or_failed_runs(self):
    490         for field, value, diagnostic in [('fresh_store', False, 'fresh-store'),
    491                                           ('complete', False, 'completed stable')]:
    492             path = self.second / 'status.json'
    493             original = path.read_bytes()
    494             data = json.loads(original)
    495             data[field] = value
    496             path.write_text(json.dumps(data))
    497             with self.assertRaisesRegex(ValueError, diagnostic):
    498                 self.verify()
    499             path.write_bytes(original)
    500 
    501     def test_rejects_changed_source_and_actual_manifests(self):
    502         path = self.source / 'root.tri'
    503         original = path.read_bytes()
    504         path.write_bytes(original + b'-- changed\n')
    505         with self.assertRaisesRegex(ValueError, 'fingerprint'):
    506             self.verify()
    507         path.write_bytes(original)
    508         (self.second / 'cas/cache/source-build/source_map').write_text('version\tkey\tdifferent-manifest\treceipt')
    509         with self.assertRaisesRegex(ValueError, 'manifest reproduction'):
    510             self.verify()
    511 
    512 
    513 class SourceMappedKitTests(unittest.TestCase):
    514     def test_projection_keeps_authored_sources_and_explicit_framing(self):
    515         from prepare_compiler_kit import project
    516         _, sources, worker, adapter = project(ROOT/'bootstrap/compiler-source-map.v1', ROOT, 2, 'stable')
    517         self.assertEqual(sources['compiler_io'], (ROOT/'lib/source/io.tri').read_bytes())
    518         self.assertIn('sourceMapBuildStable = M.sourceMapBuildStable', worker)
    519         self.assertIn('!import io as IO', adapter)
    520         self.assertEqual(sources['source_object'], (ROOT/'lib/source/object.tri').read_bytes())
    521         with self.assertRaisesRegex(ValueError, 'requires IO protocol 2'):
    522             project(ROOT/'bootstrap/compiler-source-map.v1', ROOT, 1, 'stable')
    523         self.assertEqual(sources['io'], (ROOT/'lib/io.tri').read_bytes())
    524         self.assertIn('!import compiler_io *', sources['source_object'].decode())
    525         _, _, worker, adapter = project(ROOT/'bootstrap/compiler-source-map.v1', ROOT, 2, 'optimized')
    526         self.assertIn('sourceMapBuild = M.sourceMapBuild', worker)
    527         self.assertIn('!import io as IO', adapter)
    528 
    529 
    530 class ArchiveRecoveryPolicyTests(unittest.TestCase):
    531     def test_recovery_binds_produced_version_and_adapter(self):
    532         from bootstrap_archive import recovery_profile
    533         import hashlib
    534         with tempfile.TemporaryDirectory() as directory:
    535             root = Path(directory); adapter = root/'adapter'
    536             adapter.write_bytes(b'v2')
    537             pin = hashlib.sha256(b'v2').hexdigest()
    538             (root/'produced-io-adapter.arboricx').write_bytes(b'v2')
    539             inputs = dict(execution_io_version=1, produced_io_version=2,
    540                           produced_adapter_sha256=pin, adapter_sha256='0'*64)
    541             self.assertEqual(recovery_profile(root, inputs, adapter, True), 2)
    542             with self.assertRaisesRegex(ValueError, 'accepted pair'):
    543                 recovery_profile(root, inputs, adapter, False)
    544             adapter.write_bytes(b'wrong')
    545             with self.assertRaisesRegex(ValueError, 'produced adapter'):
    546                 recovery_profile(root, inputs, adapter, True)
    547             adapter.write_bytes(b'v2')
    548             (root/'produced-io-adapter.arboricx').write_bytes(b'corrupt')
    549             with self.assertRaisesRegex(ValueError, 'identity mismatch'):
    550                 recovery_profile(root, inputs, adapter, True)
    551             del inputs['execution_io_version']
    552             with self.assertRaisesRegex(ValueError, 'incomplete'):
    553                 recovery_profile(root, inputs, adapter, True)
    554             self.assertEqual(recovery_profile(root, {}, adapter, False), 1)
    555             with self.assertRaisesRegex(ValueError, 'accepted output'):
    556                 recovery_profile(root, {'adapter_sha256':'0'*64}, adapter, True)
    557 
    558     def test_scp_execution_is_not_a_compiler_restore_gate(self):
    559         archive = (ROOT / 'test/bootstrap_archive.py').read_text()
    560         self.assertNotIn('scp_native_validation.py', archive)
    561         self.assertIn('scp_execution_required=False', archive)
    562 
    563 
    564 class SupportProjectionTests(unittest.TestCase):
    565     def setUp(self):
    566         from bundle_dag import merge_bundles, read
    567         self.temporary = tempfile.TemporaryDirectory()
    568         self.addCleanup(self.temporary.cleanup)
    569         self.directory = Path(self.temporary.name)
    570         self.original = self.directory / 'original.arboricx'
    571         self.projected = self.directory / 'projected.arboricx'
    572         leaf = ROOT / 'test/fixtures/false.arboricx'
    573         other = ROOT / 'test/fixtures/true.arboricx'
    574         merge_bundles(self.original, [(leaf, [(next(iter(read(leaf)[3])), 'keep.value')]),
    575                                      (other, [(next(iter(read(other)[3])), 'drop.value')])])
    576         merge_bundles(self.projected, [(self.original, [('keep.value', 'keep.value')])],
    577                       prune_unreachable=True)
    578 
    579     def test_exact_selected_exports_without_unreachable_nodes(self):
    580         from project_support_fixture import verify_projection
    581         self.assertEqual(verify_projection(self.original, self.projected, {'keep'}), 1)
    582 
    583     def test_missing_export_and_changed_kernel_rejected(self):
    584         from bundle_dag import merge_bundles
    585         from project_support_fixture import verify_projection
    586         with self.assertRaisesRegex(ValueError, 'export set'):
    587             verify_projection(self.original, self.projected, {'keep', 'drop'})
    588         merge_bundles(self.projected, [(self.original, [('drop.value', 'keep.value')])],
    589                       prune_unreachable=True)
    590         with self.assertRaisesRegex(ValueError, 'changed a kernel'):
    591             verify_projection(self.original, self.projected, {'keep'})
    592 
    593     def test_unreachable_archived_nodes_rejected(self):
    594         from bundle_dag import merge_bundles
    595         from project_support_fixture import verify_projection
    596         merge_bundles(self.projected, [(self.original, [('keep.value', 'keep.value')])])
    597         with self.assertRaisesRegex(ValueError, 'unreachable'):
    598             verify_projection(self.original, self.projected, {'keep'})
    599 
    600     def test_immutable_projection_origin_fingerprints(self):
    601         # The extraction ledger remains historical: current/rebuilt outputs have
    602         # since been genuinely rebuilt. Never rewrite the original snapshots or
    603         # pretend new compilation is an unchanged projection. Current outputs
    604         # and every rebuilt receipt are authenticated by the Haskell cohort gate.
    605         fixtures = ROOT / 'test/fixtures'
    606         evidence = json.loads((fixtures / 'support-projection.json').read_text())
    607         self.assertEqual(evidence['format'], 'tricu.support-projection.v1')
    608         for name in ['support-origin.v1', 'support-origin.arboricx', 'support-origin-compiler.arboricx']:
    609             self.assertEqual(hashlib.sha256((fixtures / name).read_bytes()).hexdigest(),
    610                              evidence['outputs_sha256'][name], name)
    611 
    612     def test_active_support_is_dependency_closed_without_archive(self):
    613         from native_header import native_header
    614         fixtures = ROOT / 'test/fixtures'
    615         rows = [line.split('\t') for name in ['compiler-library.v1', 'support-library.v1']
    616                 for line in (fixtures / name).read_text().splitlines() if line.startswith('module\t')]
    617         providers = {row[2] for row in rows}
    618         for _, _, alias, path, pin in rows:
    619             self.assertNotIn('consumer', path)
    620             self.assertNotIn('archive/', path)
    621             from fixture_sources import snapshot_bytes
    622             dependencies, _ = native_header(snapshot_bytes(fixtures, pin).decode())
    623             self.assertLessEqual(set(dependencies), providers, alias)
    624         self.assertNotIn('ScpCommand', (ROOT / 'tricu.cabal').read_text())
    625         self.assertFalse((ROOT / 'src/ScpCommand.hs').exists())
    626 
    627 
    628 class LibraryPublicationAliases(unittest.TestCase):
    629     def test_authored_aliases_are_not_filename_guesses(self):
    630         import importlib.util
    631         spec = importlib.util.spec_from_file_location('prepare_libraries', ROOT/'ext/demo-site/prepare-libraries.py')
    632         module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
    633         rows = module.library_sources("tricu.workspace.v1\nhandler_upload\tlib/handlers/upload.tri\nhandler_testing\tlib/handlers/testing.tri\n")
    634         self.assertEqual(next(alias for alias,path,_ in rows if path=='lib/handlers/upload.tri'), 'handler_upload')
    635         self.assertEqual(next(alias for alias,path,_ in rows if path=='lib/handlers/testing.tri'), 'handler_testing')
    636         self.assertEqual({path for _,path,_ in rows}, {str(path.relative_to(ROOT)) for path in (ROOT/'lib').rglob('*.tri')})
    637         for text in ['bad', 'tricu.workspace.v1\na\tx\na\ty', 'tricu.workspace.v1\na\tx\nb\tx', 'tricu.workspace.v1\na\t../x']:
    638             with self.assertRaises(ValueError): module.workspace_aliases(text)
    639 
    640 
    641 class SupportPublicationControls(unittest.TestCase):
    642     def test_resource_limits_fail_before_build_or_publication(self):
    643         import contextlib
    644         import io
    645         from native_library_migration import main as compile_candidates
    646         from repair_support_fixture import main as publish_candidates
    647         cases = [
    648             (compile_candidates, ['tool', 'host', 'generation', 'output', '--library', 'socket', '--capabilities', '0']),
    649             (publish_candidates, ['tool', 'host', 'candidates', '--output-directory', 'output', '--capabilities', '0']),
    650             (publish_candidates, ['tool', 'host', 'candidates', '--output-directory', 'output', '--verification-memory-mib', '65537']),
    651         ]
    652         for main, argv in cases:
    653             with self.subTest(argv=argv), patch('sys.argv', argv), contextlib.redirect_stderr(io.StringIO()):
    654                 with self.assertRaises(SystemExit) as error:
    655                     main()
    656                 self.assertEqual(error.exception.code, 2)
    657 
    658     def test_publication_cannot_overwrite_an_existing_directory(self):
    659         from repair_support_fixture import main
    660         with tempfile.TemporaryDirectory() as directory:
    661             marker = Path(directory) / 'marker'
    662             marker.write_bytes(b'keep')
    663             with patch('sys.argv', ['tool', 'host', 'candidates', '--output-directory', directory]):
    664                 with self.assertRaises(FileExistsError):
    665                     main()
    666             self.assertEqual(marker.read_bytes(), b'keep')
    667             self.assertEqual(list(Path(directory).iterdir()), [marker])
    668 
    669 
    670 class CompilerDependencyTests(unittest.TestCase):
    671     def test_current_map_is_exact_compiler_closure_without_patterns(self):
    672         from bootstrap_generations import source_map
    673         _, entries, required = source_map(
    674             ROOT / 'bootstrap/compiler-source-map.v1', ROOT, 'source_map')
    675         self.assertEqual(entries['compiler_io'][2], ['base', 'io', 'io_storage', 'io_text'])
    676         self.assertEqual(entries['io'][1], 'lib/io.tri')
    677         self.assertNotIn('patterns', entries)
    678         self.assertEqual(set(entries), required)
    679 
    680 
    681 class FixtureSourceTests(unittest.TestCase):
    682     def setUp(self):
    683         from fixture_sources import digest
    684         self.temporary = tempfile.TemporaryDirectory()
    685         self.addCleanup(self.temporary.cleanup)
    686         self.root = Path(self.temporary.name)
    687         self.fixtures, self.source = self.root / 'fixtures', self.root / 'source'
    688         self.fixtures.mkdir()
    689         self.source.mkdir()
    690         for cohort, alias in [('compiler-library', 'compiler'), ('support-library', 'support')]:
    691             source = b'!export main\nmain = t\n' + ('-- ' + alias + '\n').encode()
    692             (self.source / (alias + '.tri')).write_bytes(source)
    693             bundle = b'synthetic-policy-bundle-' + alias.encode()
    694             (self.fixtures / (cohort + '.arboricx')).write_bytes(bundle)
    695             (self.fixtures / (cohort + '.v1')).write_text(
    696                 'tricu.' + cohort + '-fixture.v1\nmodule\t' + alias + '\t' + alias + '\t' + alias + '.tri\t'
    697                 + digest('arboricx.source.v1', source) + '\nbundle\t'
    698                 + digest('tricu.' + cohort + '-fixture.v1', bundle) + '\n')
    699         (self.source / 'bootstrap').mkdir()
    700         (self.source / 'bootstrap/compiler-source-map.v1').write_text(
    701             'arboricx.source-map.v1\nnative\tcompiler\tcompiler\tcompiler.tri\t-\n')
    702 
    703     def capture(self):
    704         from fixture_sources import capture
    705         return capture(self.fixtures, self.source)
    706 
    707     def test_reference_and_freshness_are_distinct(self):
    708         from fixture_sources import verify, current_mismatches
    709         self.capture()
    710         self.assertEqual(current_mismatches(self.fixtures, self.source), [])
    711         (self.source / 'compiler.tri').write_text('!import changed *\n!export main\nmain = changed\n')
    712         self.assertEqual(len(verify(self.fixtures)), 2)
    713         self.assertEqual(current_mismatches(self.fixtures, self.source), ['compiler.tri'])
    714 
    715     def test_materialize_authenticates_reference_without_blessing_worktree(self):
    716         from fixture_sources import materialize, current_mismatches
    717         self.capture()
    718         (self.source / 'support.tri').write_bytes(b'changed work')
    719         target = self.root / 'reference'
    720         materialize(self.fixtures, target)
    721         self.assertEqual(current_mismatches(self.fixtures, target), [])
    722         self.assertEqual(current_mismatches(self.fixtures, self.source), ['support.tri'])
    723         with self.assertRaises(FileExistsError): materialize(self.fixtures, target)
    724         next((self.fixtures / 'reference-source').glob('*.tri')).write_bytes(b'corrupt')
    725         with self.assertRaises(ValueError): materialize(self.fixtures, self.root / 'bad')
    726         self.assertFalse((self.root / 'bad').exists())
    727 
    728     def test_capture_cannot_bless_edited_sources(self):
    729         (self.source / 'support.tri').write_text('changed')
    730         with self.assertRaisesRegex(ValueError, 'does not match'):
    731             self.capture()
    732         self.assertFalse((self.fixtures / 'reference-source').exists())
    733 
    734     def test_missing_snapshot_never_falls_back_to_matching_working_source(self):
    735         from fixture_sources import verify
    736         with self.assertRaises(FileNotFoundError):
    737             verify(self.fixtures)
    738 
    739     def test_corrupt_snapshot_is_rejected_not_repaired(self):
    740         from fixture_sources import verify
    741         self.capture()
    742         next((self.fixtures / 'reference-source').glob('*.tri')).write_bytes(b'corrupt')
    743         with self.assertRaisesRegex(ValueError, 'digest mismatch'):
    744             verify(self.fixtures)
    745         with self.assertRaisesRegex(ValueError, 'existing reference'):
    746             self.capture()
    747 
    748     def test_corrupt_bundle_is_rejected(self):
    749         from fixture_sources import verify
    750         self.capture()
    751         (self.fixtures / 'compiler-library.arboricx').write_bytes(b'corrupt')
    752         with self.assertRaisesRegex(ValueError, 'bundle digest'):
    753             verify(self.fixtures)
    754 
    755     def test_unsafe_or_duplicate_metadata_is_rejected(self):
    756         from fixture_sources import source_rows
    757         path = self.fixtures / 'compiler-library.v1'
    758         original = path.read_text()
    759         for value in ['../outside', '/outside']:
    760             path.write_text(original.replace('compiler.tri', value))
    761             with self.assertRaisesRegex(ValueError, 'path or digest'):
    762                 source_rows(self.fixtures)
    763         path.write_text(original + original.splitlines()[1] + '\n')
    764         with self.assertRaisesRegex(ValueError, 'duplicate'):
    765             source_rows(self.fixtures)
    766 
    767     def test_cli_reports_reference_success_but_current_failure(self):
    768         import sys
    769         self.capture()
    770         (self.source / 'compiler.tri').write_bytes(b'edited')
    771         base = [sys.executable, str(ROOT / 'test/fixture_sources.py')]
    772         common = ['--fixtures', str(self.fixtures), '--source-root', str(self.source)]
    773         reference = subprocess.run(base + ['verify'] + common, capture_output=True, text=True)
    774         current = subprocess.run(base + ['current'] + common, capture_output=True, text=True)
    775         self.assertEqual(reference.returncode, 0, reference.stderr)
    776         self.assertIn('freshness NOT checked', reference.stdout)
    777         self.assertEqual(current.returncode, 1)
    778         self.assertIn('compiler.tri', current.stderr)
    779 
    780     def test_map_change_is_freshness_failure(self):
    781         from fixture_sources import current_mismatches
    782         self.capture()
    783         (self.source / 'bootstrap/compiler-source-map.v1').write_text('arboricx.source-map.v1\n')
    784         self.assertEqual(current_mismatches(self.fixtures, self.source), ['bootstrap/compiler-source-map.v1'])
    785 
    786 
    787 class SupportPublicationTests(unittest.TestCase):
    788     def test_patterns_relocation_is_explicit_and_identity_checked(self):
    789         from repair_support_fixture import relocate_patterns, updated_module_rows
    790         old = ['module\thttp\thttp\tlib/http.tri\told-source']
    791         record = dict(legacy='patterns', path='lib/patterns.tri')
    792         candidates = {'patterns': (record, b'!export main\nmain = t\n')}
    793         with self.assertRaisesRegex(ValueError, 'membership'):
    794             updated_module_rows(old, candidates)
    795         relocated = relocate_patterns(old, candidates)
    796         self.assertEqual(updated_module_rows(relocated, candidates), relocated)
    797         self.assertEqual(old, ['module\thttp\thttp\tlib/http.tri\told-source'])
    798         with self.assertRaisesRegex(ValueError, 'requires rebuilt'):
    799             relocate_patterns(old, {})
    800         with self.assertRaisesRegex(ValueError, 'identity'):
    801             relocate_patterns(old, {'patterns': (dict(record, path='other.tri'), b't')})
    802         with self.assertRaisesRegex(ValueError, 'already belongs'):
    803             relocate_patterns(relocated, candidates)
    804 
    805     def test_changed_source_has_a_new_authenticated_fingerprint(self):
    806         from repair_support_fixture import updated_module_rows
    807         old = ['module\thttp\thttp\tlib/http.tri\told-source',
    808                'module\tsocket\tsocket\tlib/socket.tri\toriginal-socket']
    809         record = dict(legacy='http', path='lib/http.tri')
    810         payload = b'!export f\nf x = x\n'
    811         rows = updated_module_rows(old, {'http': (record, payload)})
    812         self.assertEqual(rows[0].split('\t')[-1], digest('arboricx.source.v1', payload))
    813         self.assertEqual(rows[1], old[1])
    814         self.assertEqual(old[0].split('\t')[-1], 'old-source')
    815         with self.assertRaisesRegex(ValueError, 'identity'):
    816             updated_module_rows(old, {'http': (dict(record, path='other.tri'), payload)})
    817         with self.assertRaisesRegex(ValueError, 'membership'):
    818             updated_module_rows(old, {'unknown': (record, payload)})
    819 
    820     def test_candidate_replacement_is_explicit_and_complete(self):
    821         from repair_support_fixture import select_candidates
    822         with tempfile.TemporaryDirectory() as directory:
    823             roots = [Path(directory) / str(n) for n in range(2)]
    824             for root in roots:
    825                 root.mkdir()
    826                 (root / 'migration.json').write_text(json.dumps([dict(alias='http', complete=True)]))
    827             with self.assertRaisesRegex(ValueError, 'requires --replace'):
    828                 select_candidates(roots, [])
    829             self.assertEqual(select_candidates(roots, ['http'])['http'][0], roots[1])
    830             with self.assertRaisesRegex(ValueError, 'exactly'):
    831                 select_candidates(roots[:1], ['http'])
    832             (roots[1] / 'migration.json').write_text(json.dumps([dict(alias='http', complete=False)]))
    833             with self.assertRaisesRegex(ValueError, 'incomplete'):
    834                 select_candidates(roots, ['http'])
    835 
    836     def test_receipt_cannot_relabel_source_compiler_or_output(self):
    837         from repair_support_fixture import checked_receipt
    838         source = b'f x = x\n'
    839         payload = ('arboricx.build-receipt.v1\nsource\t' + digest('arboricx.source.v1', source) +
    840                    '\ncompiler\tcompiler\noutput\tmanifest\noptions\t' +
    841                    digest('arboricx.build-options.v1', b'source-module.stable.v1') + '\n').encode()
    842         checked_receipt(payload, source, 'manifest', 'compiler')
    843         for raw, text, manifest, compiler in [
    844             (payload, source + b'-- changed\n', 'manifest', 'compiler'),
    845             (payload, source, 'wrong', 'compiler'),
    846             (payload, source, 'manifest', 'wrong'),
    847             (payload + b'output\tmanifest\n', source, 'manifest', 'compiler'),
    848             (payload.replace(digest('arboricx.build-options.v1', b'source-module.stable.v1').encode(),
    849                              digest('arboricx.build-options.v1', b'source-module.reference.v1').encode()),
    850              source, 'manifest', 'compiler'),
    851         ]:
    852             with self.subTest(manifest=manifest, compiler=compiler), self.assertRaisesRegex(ValueError, 'receipt mismatch'):
    853                 checked_receipt(raw, text, manifest, compiler)
    854 
    855     def test_old_consumer_receipt_cannot_pin_an_old_support_provider(self):
    856         from repair_support_fixture import check_support_dependencies
    857         providers = dict(http='current-http', socket='current-socket')
    858         rows = [['local', 'http', 'current-http', 'facade-source'],
    859                 ['pin', 'http', 'current-http', 'parseHeaders', 'tree']]
    860         check_support_dependencies(rows, providers)
    861         for bad in ([['local', 'http', 'old-http', 'source']],
    862                     [rows[0], ['pin', 'http', 'old-http', 'parseHeaders', 'tree']],
    863                     [rows[0], rows[0]], [['local', 'http']], [['pin', 'http']]):
    864             with self.subTest(bad=bad), self.assertRaises(ValueError):
    865                 check_support_dependencies(bad, providers)
    866 
    867 
    868 class LocalControlHarnessTests(unittest.TestCase):
    869     def test_independent_case_diagnostic_and_ast_corpus(self):
    870         from local_control import cases, failures, ast_cases
    871         rows = cases()+failures()+ast_cases()
    872         self.assertEqual(len(rows),len({row[0] for row in rows}))
    873         labels = {row[0] for row in rows}
    874         for label in ('case-ignored-scrutinee', 'case-unused-loop', 'case-pin-collision',
    875                       'rec-nested-capture', 'rec-no-mutual', 'plain-let-shadow',
    876                       'rec-value-before-missing', 'case-ast', 'rec-ast'):
    877             self.assertIn(label,labels)
    878         from http_headers import expect
    879         for _,_,expected in ast_cases():
    880             with self.assertRaises(ValueError):
    881                 expect(expected+b'\0', expected, 'extra AST field')
    882 
    883     def test_explicit_source_dependencies_cannot_override_or_escape(self):
    884         from native_library_migration import dependency_sources, check_source_dependencies
    885         import tempfile, json
    886         from pathlib import Path
    887         with tempfile.TemporaryDirectory() as directory:
    888             root = Path(directory); (root/'extra.tri').write_text('!export main\nmain = 42\n')
    889             rows = dependency_sources(root, ['extra=extra.tri'], {'base'})
    890             workspace = root/'workspace'; workspace.mkdir()
    891             (workspace/'extra.tri').write_bytes((root/'extra.tri').read_bytes())
    892             (root/'selection.json').write_text(json.dumps(dict(source_dependencies=rows)))
    893             check_source_dependencies(root, root)
    894             for entries in [['base=extra.tri'], ['extra=extra.tri']*2, ['extra=../escape'], ['extra=/absolute'], ['!bad=extra.tri'], ['extra']]:
    895                 with self.assertRaises(ValueError): dependency_sources(root, entries, {'base'})
    896             (root/'outside.tri').symlink_to('/etc/passwd')
    897             with self.assertRaises(ValueError): dependency_sources(root, ['extra=outside.tri'], set())
    898             (root/'extra.tri').write_text('!export main\nmain = 43\n')
    899             with self.assertRaises(ValueError): check_source_dependencies(root, root)
    900 
    901     def test_candidate_source_guard_rejects_old_facade_and_source_edits(self):
    902         from native_library_migration import checked_candidate_source
    903         import tempfile, hashlib
    904         from pathlib import Path
    905         with tempfile.TemporaryDirectory() as directory:
    906             root = Path(directory); workspace = root/'workspace'; workspace.mkdir()
    907             source = b'!export value\nvalue = 23\n'
    908             (root/'library.tri').write_bytes(source)
    909             staged = workspace/'source_ast.tri'; staged.write_bytes(source)
    910             record = dict(path='library.tri', alias='source_ast', after_sha256=hashlib.sha256(source).hexdigest())
    911             self.assertEqual(checked_candidate_source(workspace, record, root), source)
    912             staged.write_bytes(b'!export * from '+b'0'*64+b'\n')
    913             with self.assertRaises(ValueError): checked_candidate_source(workspace, record, root)
    914             staged.write_bytes(source); (root/'library.tri').write_bytes(source+b'-- changed\n')
    915             with self.assertRaises(ValueError): checked_candidate_source(workspace, record, root)
    916 
    917 
    918 class RegistryServerHarnessTests(unittest.TestCase):
    919     def test_independent_oracles_and_explicit_changes(self):
    920         from registry_server import pure_cases, effect_cases, demo_cases
    921         rows = pure_cases() + effect_cases() + list(demo_cases())
    922         self.assertEqual(len(rows), len({row.label for row in rows}))
    923         self.assertEqual(sum(c.label.startswith('hex-byte-') for c in rows), 256)
    924         self.assertEqual(sum(c.label.startswith('hash-invalid-at-') for c in rows), 64)
    925         self.assertEqual(sum(c.label.startswith('demo-init-failure-') for c in rows), 7)
    926         for c in rows:
    927             self.assertIsInstance(c.expected, bytes)
    928             self.assertEqual(bool(c.change), c.previous is not None)
    929             if c.previous is not None:
    930                 self.assertNotEqual(c.expected, c.previous)
    931 
    932     def test_traversal_never_requests_filesystem_io(self):
    933         import posixpath
    934         from registry_server import effect_cases, finished, result, number, bs, DIAGNOSTIC, action, ROOT, path
    935         rows = effect_cases()
    936         for c in rows:
    937             if c.label.startswith('get-reject-'):
    938                 self.assertEqual(c.expected, finished(result(False, number(400), bs(DIAGNOSTIC))))
    939             if c.label.startswith('write-reject-'):
    940                 self.assertEqual(c.expected, finished(result(False, bs(b'sha256Hex: '+DIAGNOSTIC)),
    941                                                       [action(28, bs(b'body'))]))
    942                 self.assertEqual(c.previous_export, 'putBundleWithHash')
    943         self.assertFalse(posixpath.normpath(path(b'../../../escaped')).startswith(ROOT+b'/'))
    944 
    945     def test_prior_defects_cannot_pass_current_oracles(self):
    946         from registry_server import effect_cases, demo_cases
    947         from http_pipeline import check_case
    948         for c in effect_cases()+list(demo_cases()):
    949             if c.previous is not None:
    950                 check_case(c, c.expected, c.previous)
    951                 with self.assertRaises(ValueError):
    952                     check_case(c, c.previous, c.previous)
    953 
    954     def test_wrong_path_and_atomic_operation_are_detected(self):
    955         from registry_server import action, bs, pair, finished, result, expect
    956         output = result(True, bs(b'hash'))
    957         write = action(33, pair(bs(b'/registry/objects/abc/hash.arboricx'), bs(b'data')))
    958         expected = finished(output, [write])
    959         for trace in ([], [action(22, pair(bs(b'/registry/tmp/hash.tmp'), bs(b'data')))],
    960                       [action(33, pair(bs(b'/outside'), bs(b'data')))], [write, write]):
    961             with self.assertRaises(ValueError):
    962                 expect(finished(output, trace), expected, 'mutated publication')
    963 
    964 
    965 class HttpPipelineHarnessTests(unittest.TestCase):
    966     def test_changes_have_explicit_old_and_independent_new_results(self):
    967         from http_pipeline import pure_cases, effect_cases
    968         rows = pure_cases() + effect_cases()
    969         self.assertEqual(len(rows), len({row.label for row in rows}))
    970         changes = {row.label for row in rows if row.previous is not None}
    971         self.assertEqual(changes, {'body-eof-', 'body-eof-6162', 'receive-recv-failure',
    972             'handler-pure-too-large', 'handler-io-too-large', 'handler-pure-header-limit',
    973             'handler-io-header-limit', 'handler-pure-body-eof', 'handler-io-body-eof', 'status-413'})
    974         for row in rows:
    975             self.assertIsInstance(row.expected, bytes)
    976             if row.previous is not None:
    977                 self.assertNotEqual(row.expected, row.previous)
    978 
    979     def test_reference_modes_never_disable_subject_oracle(self):
    980         from http_pipeline import Case, check_case
    981         row = Case('changed', 'probe', (), b'\0', previous=b'\1\0')
    982         check_case(row, row.expected, row.previous)
    983         check_case(row, row.expected, row.expected, reference_current=True)
    984         for actual, before, current in [(row.previous, row.previous, False),
    985                                         (row.previous, row.expected, True),
    986                                         (row.expected, row.previous, True),
    987                                         (row.expected, row.expected, False)]:
    988             with self.assertRaises(ValueError):
    989                 check_case(row, actual, before, current)
    990 
    991     def test_both_router_paths_have_success_failure_and_cleanup_cases(self):
    992         from http_pipeline import effect_cases
    993         labels = {row.label for row in effect_cases()}
    994         for flavor in ('pure', 'io'):
    995             for suffix in ('get', 'post', 'head', 'bad-line', 'bad-header-head', 'bad-method',
    996                            'bad-version-head', 'bad-target-head', 'duplicate', 'coding-head',
    997                            'too-large', 'header-eof', 'header-limit', 'body-fragmented',
    998                            'body-recv-error', 'body-eof', 'body-send-error', 'body-close-error'):
    999                 self.assertIn('handler-' + flavor + '-' + suffix, labels)
   1000         for control in ('pure', 'extra-script', 'unscripted', 'wrong-tag', 'abort', 'bind', 'fuel'):
   1001             self.assertIn('driver-' + control, labels)
   1002 
   1003     def test_trace_oracle_detects_missing_reordered_or_wrong_effect_payloads(self):
   1004         from http_pipeline import action, finished, number, byte_tree, pair, result, LEAF
   1005         from http_headers import expect
   1006         send = action(77, pair(number(7), byte_tree(b'response')))
   1007         close = action(71, number(7))
   1008         expected = finished(result(True, LEAF), [send, close])
   1009         for trace in ([send], [close, send], [send, close, close],
   1010                       [action(77, pair(number(7), byte_tree(b'wrong'))), close]):
   1011             with self.assertRaises(ValueError):
   1012                 expect(finished(result(True, LEAF), trace), expected, 'trace mutation')
   1013 
   1014 
   1015 class HttpLengthHarnessTests(unittest.TestCase):
   1016     def test_length_corpus_covers_bytes_and_policies_without_running_artifacts(self):
   1017         from http_lengths import cases, BAD_REQUEST
   1018         rows = cases(include_costs=False)
   1019         self.assertEqual(len(rows), len({row.label for row in rows}))
   1020         self.assertEqual(sum(row.label.startswith('value-byte-') for row in rows), 256)
   1021         self.assertEqual(sum(row.label.startswith('duplicate-') for row in rows), 25)
   1022         for row in rows:
   1023             if row.label.startswith(('duplicate-', 'coding-')):
   1024                 self.assertEqual(row.expected, BAD_REQUEST)
   1025                 self.assertFalse(row.compare_reference)
   1026             self.assertEqual(len(row.arguments), 3 if row.export == 'readBody' else 1)
   1027 
   1028     def test_rejections_require_complete_pure_action_not_just_a_status(self):
   1029         from http_lengths import cases, pair, number, BAD_REQUEST, TOO_LARGE
   1030         from http_headers import expect
   1031         rows = cases(include_costs=False)
   1032         by_name = {row.label: row for row in rows}
   1033         for row in rows:
   1034             if row.label.startswith('no-io-'):
   1035                 original = by_name[row.label[len('no-io-'):]]
   1036                 self.assertIn(original.expected, (BAD_REQUEST, TOO_LARGE))
   1037                 self.assertEqual(row.expected, pair(number(0), original.expected))
   1038                 with self.assertRaises(ValueError):
   1039                     expect(pair(number(1), original.expected), row.expected, row.label)
   1040 
   1041     def test_reference_comparisons_never_feed_large_valid_numbers_to_old_parser(self):
   1042         from http_lengths import cases, length_result, BAD_REQUEST, pair, number, result, byte_tree, LEAF
   1043         safe_results = {BAD_REQUEST, length_result(None), length_result(0), length_result(1), length_result(42),
   1044                         pair(number(0), result(True, LEAF, byte_tree(b'body')))}
   1045         for row in cases(include_costs=False):
   1046             if row.compare_reference:
   1047                 self.assertIn(row.expected, safe_results, row.label)
   1048 
   1049 
   1050 class HttpHeaderHarnessTests(unittest.TestCase):
   1051     """Cheap oracle/identity controls only; artifact execution stays opt-in."""
   1052 
   1053     def test_syntax_changes_have_explicit_reasons_and_independent_oracles(self):
   1054         from http_headers import cases, syntax_cases
   1055         old = {case.label: case for case in cases()}
   1056         rows = syntax_cases()
   1057         self.assertEqual(len(rows), len({row.label for row in rows}))
   1058         self.assertEqual(sum(row.label.startswith('name-byte-') for row in rows), 256)
   1059         self.assertEqual(sum(row.label.startswith('value-byte-') for row in rows), 256)
   1060         self.assertTrue(any(row.export == 'parseRequestLine' for row in rows))
   1061         for row in rows:
   1062             if row.change:
   1063                 self.assertIsNotNone(row.expected, row.label)
   1064                 if row.label in old:
   1065                     self.assertEqual(row.reference_expected, old[row.label].expected)
   1066             elif row.label in old:
   1067                 self.assertEqual(row, old[row.label])
   1068 
   1069     def test_only_approved_nonbyte_cases_are_retired(self):
   1070         from http_headers import cases, syntax_cases, retired_case
   1071         historical = cases()
   1072         current = {case.label for case in syntax_cases()}
   1073         retired = {case.label for case in historical if retired_case(case)}
   1074         self.assertEqual(len(retired), 103)
   1075         self.assertEqual({case.label for case in historical} - current, retired)
   1076         self.assertEqual(sum(case.label.startswith('ascii-') for case in syntax_cases()), 256)
   1077         self.assertIn('headers-shape-5', current)
   1078         self.assertIn('headers-shape-6', current)
   1079 
   1080     def test_changed_expectations_do_not_disable_result_or_reference_checks(self):
   1081         from dataclasses import replace
   1082         from http_headers import syntax_cases, check_case, LEAF
   1083         changed = next(row for row in syntax_cases() if row.label == 'bare-lf')
   1084         check_case(changed, changed.reference_expected, changed.expected)
   1085         for before, after in [(changed.reference_expected, changed.reference_expected),
   1086                               (LEAF, changed.expected)]:
   1087             with self.assertRaisesRegex(ValueError, 'complete result differs'):
   1088                 check_case(changed, before, after)
   1089         with self.assertRaisesRegex(ValueError, 'independent oracle'):
   1090             check_case(replace(changed, expected=None), LEAF, LEAF)
   1091         unchanged = next(row for row in syntax_cases() if row.label == 'headers-shape-5')
   1092         with self.assertRaisesRegex(ValueError, 'differential'):
   1093             check_case(unchanged, LEAF, b'\x01\x00')
   1094         with self.assertRaisesRegex(ValueError, 'independent oracle'):
   1095             check_case(unchanged, None, LEAF)
   1096 
   1097     def test_independent_tree_encodings(self):
   1098         from http_headers import number, pair, tree_list, byte_tree, parse_tree_output
   1099         self.assertEqual(number(0), b'\x00')
   1100         self.assertEqual(number(1), b'\x02\x01\x00\x00')
   1101         self.assertEqual(number(2), b'\x02\x00\x02\x01\x00\x00')
   1102         self.assertEqual(pair(b'\x00', b'\x01\x00'), b'\x02\x00\x01\x00')
   1103         self.assertEqual(tree_list([number(65)]), byte_tree(b'A'))
   1104         self.assertEqual(parse_tree_output(' 2010\n'), b'\x02\x00\x01\x00')
   1105 
   1106     def test_renderer_rejects_partial_or_extra_output(self):
   1107         from http_headers import parse_tree_output
   1108         for text in ('', '1', '20', '00', '0 error', '3', '0\n0'):
   1109             with self.subTest(text=text), self.assertRaises(ValueError):
   1110                 parse_tree_output(text)
   1111 
   1112     def test_corpus_is_named_and_policy_characterization_is_explicit(self):
   1113         from http_headers import cases, EXPORTS
   1114         rows = cases()
   1115         self.assertEqual(len(rows), len({row.label for row in rows}))
   1116         self.assertEqual({row.export for row in rows}, set(EXPORTS))
   1117         self.assertEqual(sum(row.label.startswith('ascii-') for row in rows), 256)
   1118         for row in rows:
   1119             self.assertEqual(row.expected is None, row.category == 'differential-only')
   1120         by_name = {row.label: row for row in rows}
   1121         for label in ('embedded-cr', 'bare-lf', 'space-name', 'eof-header', 'duplicates'):
   1122             # Duplicate preservation is ordinary parser behavior; deciding how
   1123             # particular headers interact belongs to a later semantic slice.
   1124             expected = 'independent' if label == 'duplicates' else 'characterization'
   1125             self.assertEqual(by_name[label].category, expected)
   1126 
   1127     def test_complete_result_comparison_rejects_wrong_fields(self):
   1128         from http_headers import cases, expect, headers, reply, diagnostic
   1129         by_name = {row.label: row for row in cases()}
   1130         wrong = [
   1131             ('ascii-065', by_name['ascii-064'].expected),
   1132             ('blank', headers([], b'wrong remainder')),
   1133             ('reply-missing-colon', reply(False, diagnostic(1, b'colon'), consumed=False)),
   1134             ('reply-missing-colon', reply(False, diagnostic(2, b'colon'))),
   1135         ]
   1136         for label, value in wrong:
   1137             with self.subTest(label=label), self.assertRaisesRegex(ValueError, 'complete result differs'):
   1138                 expect(by_name[label].expected, value, label)
   1139 
   1140     def test_reference_integrity_and_recorded_source(self):
   1141         from http_headers import reference, ROOT
   1142         bundle, metadata = reference(ROOT / 'test/fixtures')
   1143         self.assertTrue(bundle.is_file())
   1144         self.assertEqual(metadata['source_path'], 'lib/http.tri')
   1145 
   1146     def test_corrupt_reference_metadata_bundle_and_source_fail(self):
   1147         import shutil
   1148         from http_headers import reference, ROOT
   1149         fixtures = ROOT / 'test/fixtures'
   1150         metadata = json.loads((fixtures / 'http-headers-reference.json').read_text())
   1151         paths = ['http-headers-reference.json', 'http-headers-reference.arboricx',
   1152                  'reference-source/' + metadata['source_digest'] + '.tri']
   1153         for damaged in paths:
   1154             with self.subTest(damaged=damaged), tempfile.TemporaryDirectory() as directory:
   1155                 destination = Path(directory)
   1156                 for path in paths:
   1157                     (destination / path).parent.mkdir(parents=True, exist_ok=True)
   1158                     shutil.copyfile(fixtures / path, destination / path)
   1159                 with (destination / damaged).open('ab') as output:
   1160                     output.write(b'corrupt')
   1161                 with self.assertRaises(ValueError):
   1162                     reference(destination)
   1163 
   1164 
   1165 class ArboricxReaderOracleTests(unittest.TestCase):
   1166     def test_cases_have_unique_names_and_independent_truncation_oracles(self):
   1167         from arboricx_readers import cases
   1168         rows = list(cases())
   1169         self.assertEqual(len(rows), 401)
   1170         self.assertEqual(len({r[1] for r in rows}), len(rows))
   1171         for module in ['common', 'nodes', 'manifest']:
   1172             self.assertTrue(any(r[0] == module and '-cut-' in r[1] and r[4] is not None for r in rows))
   1173 
   1174     def test_binary_failure_keeps_field_start_not_consumed_prefix(self):
   1175         from arboricx_readers import Reader, Failure
   1176         from http_headers import result, number
   1177         from stable_bundle import byte_tree
   1178         r = Reader(b'xx')
   1179         r.take(1)
   1180         with self.assertRaises(Failure) as caught:
   1181             r.take(2)
   1182         self.assertEqual(caught.exception.tree, result(False, number(1), byte_tree(b'x')))
   1183 
   1184 
   1185 class SocketConvenienceOracleTests(unittest.TestCase):
   1186     def test_only_post_acquisition_failures_change(self):
   1187         from socket_conveniences import cases
   1188         rows = list(cases())
   1189         self.assertEqual(len(rows), 14)
   1190         self.assertEqual(len({r.label for r in rows}), len(rows))
   1191         changed = [r for r in rows if r.previous is not None]
   1192         self.assertEqual(len(changed), 6)
   1193         self.assertTrue(all('failure-close-' in r.label and r.previous != r.expected for r in changed))
   1194 
   1195 
   1196 class ParserControlOracleTests(unittest.TestCase):
   1197     def test_complete_reply_and_poison_probe_inventory(self):
   1198         from parser_control import cases
   1199         rows = list(cases())
   1200         self.assertEqual(len(rows), 252)
   1201         self.assertEqual(len({r[0] for r in rows}), len(rows))
   1202         self.assertEqual(sum('ignored-fields-' in r[0] for r in rows), 4)
   1203         self.assertTrue(any(r[0] == 'unselected-failure' for r in rows))
   1204         self.assertTrue(any(r[0] == 'unselected-success' for r in rows))
   1205 
   1206 
   1207 
   1208 class BaseWorkloadTests(unittest.TestCase):
   1209     def test_generated_worker_variants(self):
   1210         from base_workloads import candidate, WORKERS, reference_source
   1211         source = reference_source()
   1212         for variant in ['manual', 'direct', 'patterned', 'captured']:
   1213             text = candidate(source, variant)
   1214             for public in WORKERS:
   1215                 self.assertIn(public + ' ', text)
   1216             if variant != 'manual':
   1217                 self.assertNotRegex(text, r'\bself\b')
   1218         self.assertIn('map f l = (let rec', candidate(source, 'captured'))
   1219         self.assertIn('foldl f x l = (let rec', candidate(source, 'captured'))
   1220         self.assertIn('reverse_ other acc = other', candidate(source, 'patterned'))
   1221 
   1222     def test_large_rendered_result_before_rts_residency(self):
   1223         from base_workloads import measurements
   1224         with tempfile.TemporaryDirectory() as d:
   1225             path = Path(d) / 'sample.log'
   1226             rows = [dict(profile=name, wall_ns=i * 100, allocated_bytes=i * 10,
   1227                          gc_elapsed_ns=i, live_bytes=7)
   1228                     for i, name in enumerate(['load-begin', 'load-end',
   1229                                               'invoke-begin-1', 'invoke-end-1',
   1230                                               'observe-begin-1', 'observe-end-1'])]
   1231             # Must not perform an unanchored numeric regex search over this line.
   1232             path.write_text('\n'.join(json.dumps(r) for r in rows) + '\n' +
   1233                             '0' * 200000 + ' 9,999 bytes allocated in the heap\n' +
   1234                             '   1,234 bytes maximum residency (2 samples)\n')
   1235             actual = measurements(path)
   1236             self.assertEqual(actual['rts_max_residency_bytes'], 1234)
   1237             self.assertEqual(actual['invoke_wall_ns'], 300)
   1238             self.assertEqual(actual['invoke_allocated_bytes'], 30)
   1239             self.assertEqual(actual['measurement_scope'], 'invocation-through-rendered-output-v1')
   1240 
   1241     def test_workload_independent_oracles(self):
   1242         from base_workloads import workloads, encode
   1243         rows = {name: (argument, expected) for name, argument, expected in workloads(1024)}
   1244         data = bytes(rows['lower'][0])
   1245         self.assertEqual(bytes(rows['lower'][1]), data.lower())
   1246         self.assertEqual(rows['splitLines'][1], [list(x) for x in data.split(b'\n')])
   1247         self.assertEqual(rows['sumRows'][1], [sum(x) for x in rows['sumRows'][0]])
   1248         self.assertEqual(len(rows['prefixDigits'][1]), 20)
   1249         self.assertEqual(encode([0, 1]), b'\2\0\2\2\1\0\0\0')
   1250 
   1251     def test_summary_discards_warmup(self):
   1252         from base_workloads import summarize
   1253         fields = ['invoke_wall_ns', 'invoke_allocated_bytes', 'invoke_gc_elapsed_ns',
   1254                   'load_wall_ns', 'load_allocated_bytes', 'post_invoke_live_bytes',
   1255                   'rts_max_residency_bytes', 'peak_kib', 'seconds']
   1256         rows = [dict(variant='manual', workload='lower', size=64, warmup=(v == 999),
   1257                      **{k: v for k in fields}) for v in [999, 2, 4, 8]]
   1258         result = summarize(rows)[0]
   1259         self.assertEqual(result['repetitions'], 3)
   1260         self.assertEqual(result['invoke_wall_ns'], dict(median=4, minimum=2, maximum=8))
   1261 
   1262 
   1263 
   1264 class BaseReadableOracleTests(unittest.TestCase):
   1265     def test_small_bounded_inventory(self):
   1266         from base_readable import cases, edges
   1267         from source_order import end
   1268         rows = list(edges())
   1269         self.assertEqual(len(rows), 30)
   1270         self.assertEqual(len({row[0] for row in rows}), 30)
   1271         for _, _, argument, expected in rows:
   1272             self.assertEqual(end(argument), len(argument))
   1273             self.assertEqual(end(expected), len(expected))
   1274         for size in [64, 1024]:
   1275             workloads = list(cases(size))
   1276             self.assertEqual(len(workloads), 8)
   1277             for _, argument, expected in workloads:
   1278                 self.assertEqual(end(argument), len(argument))
   1279                 self.assertEqual(end(expected), len(expected))
   1280 
   1281 
   1282 class CollectionBatchTests(unittest.TestCase):
   1283     def test_oracles_are_complete_trees_with_unique_short_labels(self):
   1284         from collections_batch import cases, effects
   1285         from source_order import end
   1286         rows = cases() + effects()
   1287         self.assertEqual(len({row[0] for row in rows}), len(rows))
   1288         for label, _, argument, expected in rows:
   1289             self.assertLess(len(label), 120)
   1290             self.assertEqual(end(argument), len(argument))
   1291             self.assertEqual(end(expected), len(expected))
   1292 
   1293     def test_workspace_audit_rejects_changed_sources_and_objects(self):
   1294         from collections_batch import audit_workspace
   1295         with tempfile.TemporaryDirectory() as directory:
   1296             workspace, store, compiler, tree, restore = self.make_workspace(Path(directory))
   1297             self.assertEqual(set(audit_workspace(workspace, store, compiler)), {'a', 'b'})
   1298             source = workspace/'a.tri'
   1299             source.write_text('!export value\nvalue = t t\n')
   1300             with self.assertRaisesRegex(ValueError, 'source'):
   1301                 audit_workspace(workspace, store, compiler)
   1302             source.write_text('!export value\nvalue = t\n')
   1303             (store/'objects'/tree[:3]/tree).write_bytes(b'corrupt')
   1304             with self.assertRaisesRegex(AssertionError, 'object digest'):
   1305                 audit_workspace(workspace, store, compiler)
   1306 
   1307     def test_workspace_audit_rejects_stale_dependency_and_export_pins(self):
   1308         from collections_batch import audit_workspace
   1309         with tempfile.TemporaryDirectory() as directory:
   1310             workspace, store, compiler, tree, restore = self.make_workspace(Path(directory))
   1311             restore(stale_receipt=True)
   1312             with self.assertRaisesRegex(AssertionError, 'stale candidate dependency'):
   1313                 audit_workspace(workspace, store, compiler)
   1314             restore(stale_pin=True)
   1315             with self.assertRaisesRegex(AssertionError, 'stale candidate export pin'):
   1316                 audit_workspace(workspace, store, compiler)
   1317 
   1318     @staticmethod
   1319     def make_workspace(root):
   1320         workspace, store = root/'workspace', root/'store'
   1321         workspace.mkdir();(store/'cache/source-build').mkdir(parents=True)
   1322         compiler = 'a'*64
   1323         def obj(domain, payload):
   1324             pin = digest(domain, payload)
   1325             path = store/'objects'/pin[:3]/pin;path.parent.mkdir(parents=True, exist_ok=True)
   1326             path.write_bytes(payload);return pin
   1327         tree = obj('arboricx.tree-term.v1', b'\0')
   1328         sources = {'a': '!export value\nvalue = t\n',
   1329                    'b': '!import a *\n!export main\nmain = value\n'}
   1330         manifests = {alias: obj('arboricx.module-manifest.v1',
   1331             ('arboricx.module-manifest.v1\nexport\t'+name+'\tarboricx.tree-term.v1\t'+tree+'\tarboricx.abi.tree.v1\n').encode())
   1332             for alias, name in [('a', 'value'), ('b', 'main')]}
   1333         for alias, source in sources.items():
   1334             (workspace/(alias+'.tri')).write_text(source)
   1335         def receipt(alias, extra=''):
   1336             payload = ('arboricx.build-receipt.v1\nsource\t'+digest('arboricx.source.v1', sources[alias].encode())+
   1337                 '\ncompiler\t'+compiler+'\noptions\t'+digest('arboricx.build-options.v1', b'source-module.stable.v1')+
   1338                 '\noutput\t'+manifests[alias]+'\n'+extra).encode()
   1339             pin = obj('arboricx.build-receipt.v1', payload)
   1340             (store/'cache/source-build'/alias).write_text('arboricx.source-build-cache.v1\t'+'0'*64+'\t'+manifests[alias]+'\t'+pin)
   1341             return pin
   1342         a_receipt = receipt('a')
   1343         def restore(stale_receipt=False, stale_pin=False):
   1344             receipt('b', 'local\ta\t'+manifests['a']+'\t'+('b'*64 if stale_receipt else a_receipt)+
   1345                 '\npin\ta\t'+manifests['a']+'\tvalue\t'+('c'*64 if stale_pin else tree)+'\n')
   1346         restore()
   1347         return workspace, store, compiler, tree, restore
   1348 
   1349 
   1350 class NumericBatchTests(unittest.TestCase):
   1351     def test_independent_numeric_oracles_are_complete_trees(self):
   1352         from numeric_batch import cases
   1353         from source_order import end
   1354         rows = cases()
   1355         self.assertEqual(len({row[0] for row in rows}), len(rows))
   1356         for label, _, argument, expected in rows:
   1357             self.assertLess(len(label), 120)
   1358             self.assertEqual(end(argument), len(argument))
   1359             self.assertEqual(end(expected), len(expected))
   1360 
   1361     def test_zero_divisor_and_shape_oracles_are_distinct(self):
   1362         from numeric_batch import cases, error, n, pair, tree_list
   1363         rows = cases()
   1364         checked = next(expected for _, name, argument, expected in rows
   1365                        if name == 'checkedBinary' and argument == pair(n(7), n(0)))
   1366         self.assertTrue(checked.endswith(tree_list([error('non-zero')]*3)))
   1367         self.assertNotEqual(error('non-zero'), n(0))
   1368         self.assertNotEqual(error('not a natural number'), error('non-zero'))
   1369 
   1370 
   1371 class BundleBatchTests(unittest.TestCase):
   1372     def test_complete_independent_oracles(self):
   1373         from bundle_batch import cases
   1374         from source_order import end
   1375         rows = cases()
   1376         self.assertEqual(len(rows), len({row[0] for row in rows}))
   1377         for label, _, _, arguments, expected in rows:
   1378             self.assertLess(len(label), 120)
   1379             for tree in arguments + [expected]:
   1380                 self.assertEqual(end(tree), len(tree))
   1381 
   1382     def test_suffix_and_rejection_controls_remain_present(self):
   1383         from bundle_batch import cases, byte_tree, n
   1384         from source_order import field
   1385         rows = cases()
   1386         self.assertTrue(any(export == 'prefix' and field(expected, 1, 1) == byte_tree(b'TAIL')
   1387                             for _, _, export, _, expected in rows))
   1388         for code in [7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]:
   1389             self.assertTrue(any(expected.startswith(b'\2\0\2') and field(expected, 1, 0) == n(code)
   1390                                 for _, _, _, _, expected in rows), code)
   1391 
   1392 
   1393 class ToolingBatchTests(unittest.TestCase):
   1394     def test_complete_models_and_graph_ready_order(self):
   1395         from tooling_batch_models import cases, graph_result, n, result
   1396         from source_order import end
   1397         # A newly ready earlier entry precedes the already-ready later one.
   1398         self.assertEqual(graph_result([(0,[1]),(1,[]),(2,[])]), result(True,n([1,0,2])))
   1399         self.assertEqual(graph_result([(0,[1]),(1,[2]),(2,[1])]), result(False,n(1)))
   1400         rows=cases()
   1401         self.assertEqual(len(rows),len({row[0] for row in rows}))
   1402         for _,_,argument,expected in rows:
   1403             for tree in [argument,expected]:self.assertEqual(end(tree),len(tree))
   1404 
   1405     def test_formatter_oracles_preserve_tokens_and_line_counts(self):
   1406         from tooling_batch_models import format_cases
   1407         import re
   1408         tokens=re.compile(r'"(?:\\.|[^"\\])*"|\|-.*?-\||--[^\r\n]*|[^\s]',re.S)
   1409         for label,source,expected in format_cases():
   1410             self.assertEqual(tokens.findall(source),tokens.findall(expected),label)
   1411             self.assertEqual(source.count('\n'),expected.count('\n'),label)
   1412 
   1413 
   1414 class SourceFoundationOracleTests(unittest.TestCase):
   1415     def test_corpus_has_complete_trees_and_unique_labels(self):
   1416         from source_foundations_models import corpus
   1417         from source_order import end
   1418         rows = corpus(consumers=True)
   1419         self.assertEqual(len(rows), len({row[0] for row in rows}))
   1420         for label, _, argument, expected in rows:
   1421             self.assertEqual(end(argument), len(argument), label)
   1422             self.assertEqual(end(expected), len(expected), label)
   1423 
   1424     def test_shift_preserves_bound_indices_and_opaque_trees(self):
   1425         from source_foundations_models import Node, shift, substitute
   1426         bound = Node(4, Node(2, 0))
   1427         free = Node(4, Node(2, 1))
   1428         opaque = Node(5, free.tree())
   1429         self.assertEqual(shift(bound, 7), bound)
   1430         self.assertEqual(shift(free, 7), Node(4, Node(2, 8)))
   1431         self.assertEqual(shift(opaque, 7), opaque)
   1432         self.assertEqual(substitute(free, [Node(2, 3)], 5), Node(4, Node(2, 4)))
   1433         self.assertEqual(shift(free, 2), shift(shift(free)))
   1434 
   1435 
   1436 class CandidateResourceTests(unittest.TestCase):
   1437     def test_memory_selection_reaches_heap_and_rss_guards(self):
   1438         from collections_batch import ProbeRunner
   1439         from unittest.mock import patch
   1440         with tempfile.TemporaryDirectory() as directory:
   1441             root = Path(directory)
   1442             for selected in [8192, 65536]:
   1443                 runner = ProbeRunner(root/'tricu', root, 60, memory_mib=selected)
   1444                 with patch('source_order.run_guarded', return_value=('', 123)) as run:
   1445                     runner.run('probe', ['run'], 30)
   1446                     command = run.call_args.args[0]
   1447                     self.assertIn(f'-M{selected}m', command)
   1448                     self.assertEqual(run.call_args.kwargs['memory_mib'], selected)
   1449                     self.assertLessEqual(run.call_args.kwargs['seconds'], 30)
   1450             self.assertEqual(ProbeRunner(root/'tricu', root, 60).memory_mib, 8192)
   1451 
   1452     def test_memory_selection_rejects_out_of_policy_limits(self):
   1453         from source_order import Runner
   1454         for value in [-1, 0, 65537]:
   1455             with self.assertRaises(ValueError):
   1456                 Runner(Path('tricu'), Path('.'), memory_mib=value)
   1457 
   1458 
   1459 class GrammarOracleTests(unittest.TestCase):
   1460     def test_authored_replies_and_lookahead_vectors_are_whole_trees(self):
   1461         from source_grammar_models import authored, lookahead_rows
   1462         from source_order import end
   1463         cases = authored()
   1464         self.assertEqual(len(cases), len({row[0] for row in cases}))
   1465         for label, _, argument, expected in cases:
   1466             self.assertEqual(end(argument), len(argument), label)
   1467             self.assertEqual(end(expected), len(expected), label)
   1468         for row in lookahead_rows():
   1469             for tree in row:
   1470                 self.assertEqual(end(tree), len(tree))
   1471 
   1472     def test_layout_oracles_are_complete_and_independent(self):
   1473         from source_layout_cases import semantic_cases, parser_cases
   1474         from source_order import end
   1475         values, replies = semantic_cases(), parser_cases()
   1476         self.assertEqual(len(values + replies), len({row[0] for row in values + replies}))
   1477         for label, _, value in values:
   1478             self.assertEqual(end(value), len(value), label)
   1479         for label, _, _, expected in replies:
   1480             self.assertEqual(end(expected), len(expected), label)
   1481         aligned = next(row[3] for row in replies if row[0] == 'ast-layout-2')
   1482         deeper = next(row[3] for row in replies if row[0] == 'ast-layout-4')
   1483         self.assertNotEqual(aligned, deeper)  # equal structure, different spans
   1484         self.assertTrue(any(row[0] == 'unindented-body' for row in replies))
   1485         self.assertTrue(any(row[0] == 'nested-do-indent' for row in replies))
   1486 
   1487     def test_lookahead_boundaries_do_not_consume_or_guess_assignments(self):
   1488         from source_grammar_models import lookahead
   1489         self.assertTrue(lookahead([2, 2, 5], [1, 3, 5])[0][0])
   1490         self.assertFalse(lookahead([2, 6, 5], [1, 2, 3])[0][0])
   1491         self.assertFalse(lookahead([2, 6, 11], [1, 2, 3])[0][1])
   1492         self.assertTrue(lookahead([2, 3, 6, 4, 11], [1]*5)[0][1])
   1493         self.assertFalse(lookahead([2, 12, 11], [1]*3)[0][1])
   1494         self.assertEqual(lookahead([6, 2, 11], [1, 3, 5])[1],
   1495                          [True, True, True, False, True, True])
   1496 
   1497 
   1498 class ResolutionOracleTests(unittest.TestCase):
   1499     def test_models_emit_complete_trees(self):
   1500         from source_resolution_models import (ast_cases, scope_cases, pattern_cases,
   1501             resolve_result, free, pattern_result, graph_cases, graph_model)
   1502         from source_order import end
   1503         for node, environment in ast_cases():
   1504             for tree in [node.tree(), resolve_result(node, environment)]:
   1505                 self.assertEqual(end(tree), len(tree))
   1506         for node in scope_cases():
   1507             for name in free(node):
   1508                 self.assertEqual(name.kind, 2)
   1509                 self.assertEqual(end(name.tree()), len(name.tree()))
   1510         for node, binders, environment in pattern_cases():
   1511             tree = pattern_result(node, binders, environment)
   1512             self.assertEqual(end(tree), len(tree))
   1513         for definitions, environment, final in graph_cases():
   1514             for tree in graph_model(definitions, environment, final):
   1515                 self.assertEqual(end(tree), len(tree))
   1516 
   1517     def test_scope_and_duplicate_priority(self):
   1518         from source_resolution_models import Ast, free, resolved, graph_model
   1519         from source_order import field, number
   1520         name = Ast(2, 'x', 9)
   1521         self.assertEqual(free(Ast(9, ('x', name, name))), [name])
   1522         self.assertEqual(free(Ast(15, ('x', name, name))), [])
   1523         bound = resolved(name, [], ('y', 'x', 'x'))
   1524         self.assertEqual(number(field(bound, 1, 0)), 1)
   1525         definitions = [(Ast(2, 'x', 1), Ast(2, 'missing', 2)),
   1526                        (Ast(2, 'x', 3), Ast(1))]
   1527         ordered, analyzed = graph_model(definitions, [], Ast(2, 'absent'))
   1528         self.assertEqual(number(field(ordered, 1, 0, 0)), 30)
   1529         self.assertEqual(number(field(analyzed, 1, 0, 0)), 20)
   1530 
   1531 
   1532 class LoweringOracleTests(unittest.TestCase):
   1533     def test_identity_and_constant(self):
   1534         from source_lowering_models import tc_apply, I, K, n, LEAF
   1535         for value in [LEAF, n(23), I]:
   1536             self.assertEqual(tc_apply(I, value), value)
   1537             self.assertEqual(tc_apply(tc_apply(K, value), n(99)), value)
   1538 
   1539     def test_complete_residual_models(self):
   1540         from source_lowering_models import specimens, bracket, eliminate, run_result
   1541         from source_order import end
   1542         terms, cores = specimens()
   1543         for tree in [bracket(t).tree() for t in terms] + [eliminate(c).tree() for c in cores] + [run_result(t) for t in terms]:
   1544             self.assertEqual(end(tree), len(tree))
   1545 
   1546 
   1547 class ModuleOracleTests(unittest.TestCase):
   1548     def test_roundtrip_and_whole_expectations(self):
   1549         from source_modules import tree_cases, decode, manifest_cases, header_cases
   1550         from source_order import end
   1551         from http_headers import result
   1552         for tree in tree_cases():
   1553             self.assertEqual(decode(list(tree), True), result(True, tree))
   1554         for text, expected in manifest_cases() + header_cases():
   1555             self.assertEqual(end(expected), len(expected))
   1556 
   1557 
   1558 class BuildOracleTests(unittest.TestCase):
   1559     def test_bundle_layout_and_missing_dependency(self):
   1560         from source_building import bundle_bytes, map_cases
   1561         from stable_bundle import sections
   1562         from source_order import field
   1563         from http_headers import LEAF
   1564         with tempfile.TemporaryDirectory() as directory:
   1565             path = Path(directory) / 'golden.arboricx'
   1566             path.write_bytes(bundle_bytes([('x', LEAF), ('y', LEAF)]))
   1567             self.assertEqual(len(sections(path)), 2)
   1568         text, outcome = next((text, outcome) for text, outcome in map_cases() if '\tmissing\n' in text)
   1569         self.assertEqual(field(outcome, 0), LEAF)
   1570 
   1571 
   1572 class BackendDefaultTests(unittest.TestCase):
   1573     def test_historical_provenance_is_not_reinterpreted(self):
   1574         from generation_backend import DEFAULT_BACKEND, recorded_backend
   1575         self.assertEqual(DEFAULT_BACKEND, 'stable')
   1576         self.assertEqual(recorded_backend({}), 'optimized')
   1577         for backend in ['stable', 'optimized']:
   1578             self.assertEqual(recorded_backend({'backend': backend}), backend)
   1579         with self.assertRaises(ValueError):
   1580             recorded_backend({'backend': 'unknown'})
   1581 
   1582     def test_real_runner_parsers_default_stable_and_keep_overrides(self):
   1583         # Stop at parse_args: exercise the actual CLI declarations without
   1584         # reading artifacts, spawning hosts or building source in the fast gate.
   1585         import argparse
   1586         import runpy
   1587         from unittest.mock import patch
   1588 
   1589         class Parsed(Exception):
   1590             pass
   1591 
   1592         parse = argparse.ArgumentParser.parse_args
   1593         scripts = {
   1594             'bootstrap_generations.py': ['previous', 'output'],
   1595             'bootstrap_generation_cli.py': ['previous'],
   1596             'native_library_migration.py': ['tricu', 'generation', 'output', '--library', 'socket'],
   1597             'portable_cli.py': ['tricu'],
   1598             'source_equations.py': ['tricu', '--generation', 'generation'],
   1599         }
   1600         for script, required in scripts.items():
   1601             for override in [None, 'stable', 'optimized']:
   1602                 with self.subTest(script=script, override=override):
   1603                     captured = []
   1604                     argv = required + (['--backend', override] if override else [])
   1605 
   1606                     def intercept(parser, *args, **kwargs):
   1607                         captured.append(parse(parser, argv))
   1608                         raise Parsed()
   1609 
   1610                     with patch.object(argparse.ArgumentParser, 'parse_args', intercept):
   1611                         with self.assertRaises(Parsed):
   1612                             runpy.run_path(str(Path(__file__).parent / script), run_name='__main__')
   1613                     self.assertEqual(captured[0].backend, override or 'stable')
   1614 
   1615 
   1616 class HistoricalLibraryReferenceTests(unittest.TestCase):
   1617     def test_historical_paths_are_labelled_without_source_substitution(self):
   1618         import importlib.util
   1619         spec = importlib.util.spec_from_file_location('site_reference', ROOT/'ext/demo-site/reference.py')
   1620         module = importlib.util.module_from_spec(spec)
   1621         spec.loader.exec_module(module)
   1622         source, note = module.source_reference(ROOT, 'lib/io-v2.tri')
   1623         self.assertIsNone(source)
   1624         self.assertIn('Historical source path', note)
   1625         source, note = module.source_reference(ROOT, 'lib/io.tri')
   1626         self.assertEqual(source, ROOT/'lib/io.tri')
   1627         self.assertIn('Source SHA-256', note)
   1628         for path in ['lib/missing-current.tri', '../outside.tri', '/etc/passwd']:
   1629             with self.assertRaises(ValueError):
   1630                 module.source_reference(ROOT, path)
   1631 
   1632 
   1633 class CheckpointProgressTests(unittest.TestCase):
   1634     def test_private_and_unselected_files_are_never_read_as_checkpoints(self):
   1635         from bootstrap_generations import published_checkpoints
   1636         with tempfile.TemporaryDirectory() as temporary:
   1637             root = Path(temporary)
   1638             cache = root / 'cas/cache/source-build'
   1639             cache.mkdir(parents=True)
   1640             (cache/'app').write_bytes(b'committed')
   1641             # Directories would fail read_bytes if the watcher touched them.
   1642             (cache/'473-17.tricu-stage-').mkdir()
   1643             (cache/'unselected').mkdir()
   1644             self.assertEqual(dict(published_checkpoints(root, ['app', 'not-yet-published'])),
   1645                              {'app': b'committed'})
   1646 
   1647 
   1648 class PublishedInventoryTests(unittest.TestCase):
   1649     def test_only_reserved_unpublished_stage_names_are_excluded(self):
   1650         from shared_library_generation import published_objects
   1651         with tempfile.TemporaryDirectory() as temporary:
   1652             root = Path(temporary)
   1653             shard = root / 'objects' / 'aaa'
   1654             shard.mkdir(parents=True)
   1655             (shard / ('a' * 64)).write_bytes(b'committed')
   1656             (shard / '.tricu-stage-interrupted').write_bytes(b'partial')
   1657             (shard / '473-17.tricu-stage-').write_bytes(b'GHC temporary naming')
   1658             self.assertEqual(published_objects(root), {'a' * 64: b'committed'})
   1659             (shard / ('a' * 63 + 'b')).write_bytes(b'extra committed object')
   1660             self.assertEqual(len(published_objects(root)), 2)
   1661             (shard / 'unexpected').write_bytes(b'not a reserved stage')
   1662             with self.assertRaises(AssertionError):
   1663                 published_objects(root)
   1664 
   1665 
   1666 class RuntimeRetirementTests(unittest.TestCase):
   1667     def test_generation_parser_rejects_retired_runtime_controls_before_output(self):
   1668         import runpy
   1669         import sys
   1670         from contextlib import redirect_stderr
   1671         from io import StringIO
   1672         from unittest.mock import patch
   1673         with tempfile.TemporaryDirectory() as temporary:
   1674             output = Path(temporary) / 'not-created'
   1675             for options in [('--execution-host', 'zig'), ('--zig-library', '/missing'),
   1676                             ('--zig-gc-policy', '1', '1')]:
   1677                 with self.subTest(options=options), patch.object(sys, 'argv',
   1678                         ['bootstrap_generations.py', 'previous', str(output), *options]):
   1679                     with redirect_stderr(StringIO()), self.assertRaises(SystemExit) as error:
   1680                         runpy.run_path(str(Path(__file__).parent / 'bootstrap_generations.py'), run_name='__main__')
   1681                     self.assertEqual(error.exception.code, 2)
   1682                     self.assertFalse(output.exists())
   1683 
   1684 
   1685 class BootstrapKitTests(unittest.TestCase):
   1686     def setUp(self):
   1687         from bootstrap_inputs import SCHEMA, ROLES
   1688         self.temporary = tempfile.TemporaryDirectory()
   1689         self.addCleanup(self.temporary.cleanup)
   1690         self.directory = Path(self.temporary.name)
   1691         self.metadata = dict(schema=SCHEMA, backend='stable', artifacts={})
   1692         for role, filename in ROLES.items():
   1693             payload = (role + '-opaque-test-bytes').encode()
   1694             (self.directory / filename).write_bytes(payload)
   1695             self.metadata['artifacts'][role] = dict(file=filename,
   1696                 export='main' if role == 'adapter' else 'sourceMapBuildStable',
   1697                 sha256=hashlib.sha256(payload).hexdigest())
   1698         self.save()
   1699 
   1700     def save(self):
   1701         (self.directory / 'bootstrap-kit.json').write_text(json.dumps(self.metadata))
   1702 
   1703     def load(self, backend='stable'):
   1704         from bootstrap_inputs import load_kit
   1705         return load_kit(self.directory, backend)
   1706 
   1707     def test_captures_exact_manifest_and_artifacts(self):
   1708         raw, payloads, pin = self.load()
   1709         self.assertEqual(raw, (self.directory / 'bootstrap-kit.json').read_bytes())
   1710         self.assertEqual(pin, hashlib.sha256(raw).hexdigest())
   1711         (self.directory / 'rebuilt-compiler.arboricx').write_bytes(b'changed')
   1712         self.assertEqual(payloads['compiler'], b'compiler-opaque-test-bytes')
   1713         with self.assertRaisesRegex(ValueError, 'digest mismatch'): self.load()
   1714 
   1715     def test_backend_is_explicit(self):
   1716         with self.assertRaisesRegex(ValueError, 'backend mismatch'): self.load('optimized')
   1717         self.metadata.pop('backend'); self.save()
   1718         with self.assertRaisesRegex(ValueError, 'backend mismatch'): self.load()
   1719 
   1720     def test_rejects_invalid_manifest_schema(self):
   1721         for schema in ('old', None, 1):
   1722             with self.subTest(schema=schema):
   1723                 self.metadata['schema'] = schema; self.save()
   1724                 with self.assertRaisesRegex(ValueError, 'schema'): self.load()
   1725 
   1726     def test_requires_both_roles_and_no_extra_role(self):
   1727         adapter = self.metadata['artifacts'].pop('adapter'); self.save()
   1728         with self.assertRaisesRegex(ValueError, 'exactly'): self.load()
   1729         self.metadata['artifacts']['adapter'] = adapter
   1730         self.metadata['artifacts']['extra'] = adapter; self.save()
   1731         with self.assertRaisesRegex(ValueError, 'exactly'): self.load()
   1732 
   1733     def test_rejects_path_traversal_and_role_swap(self):
   1734         for filename in ('../elsewhere', '/tmp/elsewhere', 'io-adapter.arboricx'):
   1735             with self.subTest(filename=filename):
   1736                 self.metadata['artifacts']['compiler']['file'] = filename; self.save()
   1737                 with self.assertRaisesRegex(ValueError, 'filename'): self.load()
   1738 
   1739     def test_rejects_wrong_export(self):
   1740         self.metadata['artifacts']['compiler']['export'] = 'sourceMapBuild'; self.save()
   1741         with self.assertRaisesRegex(ValueError, 'export'): self.load()
   1742 
   1743     def test_rejects_invalid_digest(self):
   1744         for pin in ('a' * 63, 'A' * 64, '../x', None):
   1745             with self.subTest(pin=pin):
   1746                 self.metadata['artifacts']['adapter']['sha256'] = pin; self.save()
   1747                 with self.assertRaisesRegex(ValueError, 'digest'): self.load()
   1748 
   1749     def test_explicit_versioned_kit(self):
   1750         self.metadata.update(schema='tricu.bootstrap-kit.v2', io_version=2); self.save()
   1751         self.load()
   1752         for bad in [True, '2', 0, 3, None]:
   1753             self.metadata['io_version'] = bad; self.save()
   1754             with self.assertRaises(ValueError): self.load()
   1755         self.metadata.pop('io_version'); self.save()
   1756         with self.assertRaises(ValueError): self.load()
   1757         self.metadata.update(schema='tricu.bootstrap-kit.v1', io_version=2); self.save()
   1758         with self.assertRaises(ValueError): self.load()
   1759 
   1760     def test_missing_or_corrupt_adapter_fails(self):
   1761         path = self.directory / 'io-adapter.arboricx'
   1762         path.write_bytes(b'corrupt')
   1763         with self.assertRaisesRegex(ValueError, 'digest mismatch'): self.load()
   1764         path.unlink()
   1765         with self.assertRaises(FileNotFoundError): self.load()
   1766 
   1767 
   1768 class GenerationIOTests(unittest.TestCase):
   1769     def test_strict_versions_and_historical_default(self):
   1770         from generation_io import versions, version
   1771         self.assertEqual(versions({}), (1, 1))
   1772         for bad in [True, False, 0, 3, '2', None, 2.0]:
   1773             with self.subTest(bad=bad), self.assertRaises(ValueError): version(bad)
   1774         for incomplete in [{'execution_io_version': 2}, {'produced_adapter_sha256': 'a'*64}]:
   1775             with self.assertRaises(ValueError): versions(incomplete)
   1776         with self.assertRaises(ValueError): versions(dict(execution_io_version=2,
   1777             produced_io_version=2, produced_adapter_sha256='A'*64))
   1778 
   1779     def test_host_and_authority_are_explicit(self):
   1780         from generation_io import execution_flags, compiler_flags
   1781         self.assertEqual(compiler_flags(1), [])
   1782         self.assertEqual(compiler_flags(2), ['--toolchain-io-version', '2'])
   1783         self.assertEqual(execution_flags(1), ['--unsafe-io'])
   1784         with self.assertRaises(ValueError): execution_flags(1, 'haskell-ffi')
   1785         self.assertEqual(execution_flags(2), [])
   1786         self.assertEqual(execution_flags(2, 'haskell-ffi'), [])
   1787         for host in ['browser', 'zig', 'unknown']:
   1788             with self.assertRaises(ValueError): execution_flags(2, host)
   1789 
   1790     def test_invocation_grants_and_canonical_cli_selection(self):
   1791         from generation_io import cli_version, check_invocation
   1792         import argparse
   1793         v2 = dict(execution_io_version=2, produced_io_version=2, produced_adapter_sha256='a'*64)
   1794         check_invocation(v2, ['tricu', 'arboricx', 'run', '--io'])
   1795         check_invocation({}, ['tricu', '--unsafe-io'])
   1796         for flag in ['--unsafe-io', '--allow-read=/tmp', '--allow-write', '--allow-read-all', '--allow-write-all']:
   1797             with self.assertRaises(ValueError): check_invocation(v2, ['tricu', flag])
   1798         with self.assertRaises(ValueError): check_invocation({}, ['tricu', '--io'])
   1799         for text in ['01', '+2', '2.0', ' 2', '3', '-1']:
   1800             with self.assertRaises(argparse.ArgumentTypeError): cli_version(text)
   1801         self.assertEqual(cli_version('2'), 2)
   1802 
   1803     def test_transition_chain_not_identical_execution_framing(self):
   1804         from generation_io import check_pair
   1805         first = dict(adapter_sha256='a'*64, execution_io_version=1,
   1806                      produced_io_version=2, produced_adapter_sha256='b'*64)
   1807         second = dict(adapter_sha256='b'*64, execution_io_version=2,
   1808                       produced_io_version=2, produced_adapter_sha256='b'*64)
   1809         check_pair(first, second)
   1810         for key, value in [('execution_io_version', 1), ('produced_io_version', 1),
   1811                            ('adapter_sha256', 'a'*64), ('produced_adapter_sha256', 'c'*64)]:
   1812             with self.subTest(key=key), self.assertRaises(ValueError):
   1813                 check_pair(first, {**second, key: value})
   1814         check_pair(dict(adapter_sha256='a'*64), dict(adapter_sha256='a'*64))
   1815 
   1816     def test_produced_adapter_is_authenticated_not_input_adapter(self):
   1817         from generation_io import produced_adapter
   1818         with tempfile.TemporaryDirectory() as directory:
   1819             root = Path(directory)
   1820             (root/'io-adapter.arboricx').write_bytes(b'v1')
   1821             (root/'produced-io-adapter.arboricx').write_bytes(b'v2')
   1822             old = dict(adapter_sha256=hashlib.sha256(b'v1').hexdigest())
   1823             new = dict(**old, execution_io_version=1, produced_io_version=2,
   1824                        produced_adapter_sha256=hashlib.sha256(b'v2').hexdigest())
   1825             self.assertEqual(produced_adapter(root, old), b'v1')
   1826             self.assertEqual(produced_adapter(root, new), b'v2')
   1827             (root/'produced-io-adapter.arboricx').write_bytes(b'v1')
   1828             with self.assertRaises(ValueError): produced_adapter(root, new)
   1829 
   1830 
   1831 class IndexedObjectAuditTests(unittest.TestCase):
   1832     def test_independent_writer_and_complete_audit(self):
   1833         from native_indexed_store import indexed_bytes
   1834         from executable_object import verify_payload, INDEXED, FLAT
   1835         for tree in [b'\0', b'\1\0', b'\2\1\0\2\0\0']:
   1836             verify_payload(FLAT, tree)
   1837             data = indexed_bytes(tree)
   1838             verify_payload(INDEXED, data)
   1839             for at in [0, 8, 15, 31, 32, 95, 96, len(data) - 1]:
   1840                 changed = bytearray(data); changed[at] ^= 255
   1841                 with self.subTest(tree=tree, at=at):
   1842                     with self.assertRaises((AssertionError, ValueError)):
   1843                         verify_payload(INDEXED, bytes(changed))
   1844             for changed in [data[:-1], data + b'\0']:
   1845                 with self.assertRaises(AssertionError): verify_payload(INDEXED, changed)
   1846         with self.assertRaises(AssertionError): verify_payload('wrong.kind', b'\0')
   1847 
   1848 
   1849 class InspectorStagingTests(unittest.TestCase):
   1850     def test_staging_is_opt_in_immutable_and_authenticates_bytes(self):
   1851         from library_inspector import stage_site
   1852         with tempfile.TemporaryDirectory() as temporary:
   1853             root = Path(temporary)
   1854             (root / 'producer').mkdir()
   1855             site = root / 'site'; (site / 'bundles').mkdir(parents=True)
   1856             index = site / 'packages.json'
   1857             index.write_text(json.dumps(dict(schema=1, releases=[])))
   1858             payload = root / 'producer/tricu-base.arboricx'
   1859             payload.write_bytes(b'fixture bytes')
   1860             digest = hashlib.sha256(payload.read_bytes()).hexdigest()
   1861             report = dict(complete=True, releases={'tricu-base': dict(version='0.1.0', manifest='a'*64, bundle=digest)})
   1862             with self.assertRaises(ValueError): stage_site(root, site, dict(report, complete=False))
   1863             for version in (0, 3, True, "2"):
   1864                 with self.assertRaises(ValueError): stage_site(root, site, dict(report, io_version=version))
   1865             self.assertEqual(list((site / "bundles").iterdir()), [])
   1866             stage_site(root, site, report)
   1867             before = index.read_bytes(); stage_site(root, site, dict(report, io_version=2))
   1868             self.assertEqual(index.read_bytes(), before)
   1869             self.assertEqual((site / 'bundles' / (digest + '.arboricx')).read_bytes(), payload.read_bytes())
   1870             payload.write_bytes(b'changed bytes')
   1871             with self.assertRaises(ValueError): stage_site(root, site, report)
   1872             changed = hashlib.sha256(payload.read_bytes()).hexdigest()
   1873             report['releases']['tricu-base']['bundle'] = changed
   1874             with self.assertRaises(ValueError): stage_site(root, site, report)
   1875             self.assertEqual(index.read_bytes(), before)
   1876             self.assertFalse((site / 'bundles' / (changed + '.arboricx')).exists())
   1877             report['releases']['tricu-base']['version'] = '0.2.0'
   1878             stage_site(root, site, report)
   1879             self.assertEqual(len(json.loads(index.read_text())['releases']), 2)
   1880             self.assertTrue((site / 'bundles' / (digest + '.arboricx')).exists())
   1881 
   1882 
   1883 if __name__ == '__main__':
   1884     unittest.main()