1106 lines
58 KiB
Python
1106 lines
58 KiB
Python
#!/usr/bin/env python3
|
|
"""Whole-checker positive and semantic corruption tests; synthetic proof only."""
|
|
import copy
|
|
import re
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
from types import SimpleNamespace
|
|
import shutil
|
|
import contextlib
|
|
import io
|
|
import os
|
|
import gzip
|
|
import hashlib
|
|
import struct
|
|
|
|
sys.dont_write_bytecode = True
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
def module(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
value = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(value)
|
|
return value
|
|
C = module('placement_checker_test', ROOT/'tools/check_p08_normal_placement_evidence.py')
|
|
F = module('placement_synthetic_fixture', ROOT/'tests/fixtures/p08-03/checker_fixture.py')
|
|
D = module('placement_collector_test', ROOT/'tools/run_p08_normal_placement.py')
|
|
|
|
def check_passive_endpoint_types(passive, placement):
|
|
"""Focused source contract; native parsing and startup remain pending."""
|
|
if not re.search(r'^extends RefCounted\s*$', placement, re.M):
|
|
raise ValueError('placement inheritance')
|
|
observer = passive.split('class InputObserver extends Node:', 1)[1].split('\nfunc ', 1)[0]
|
|
field = re.search(r'^\tvar endpoint:\s*(\w+)', observer, re.M)
|
|
if not field or field[1] not in ('RefCounted', 'Object', 'Variant'):
|
|
raise ValueError('incompatible RefCounted endpoint')
|
|
if 'endpoint != null' not in observer:
|
|
raise ValueError('missing null guard')
|
|
if not re.search(r'input_observer\.endpoint\s*=\s*self', passive):
|
|
raise ValueError('missing root endpoint')
|
|
modal = passive.split('func button(', 1)[1].split('\nfunc ', 1)[0]
|
|
if not re.search(r'input_observer\.endpoint\s*=\s*null', modal):
|
|
raise ValueError('modal endpoint must remain null')
|
|
if 'input_observer.queue_free()' not in modal or 'input_observer = old_observer' not in modal:
|
|
raise ValueError('modal cleanup/restore')
|
|
if 'null_modal_receipts_only' not in passive:
|
|
raise ValueError('missing modal attribution')
|
|
|
|
|
|
class PassiveTypingTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.passive = (ROOT/'tests/fixtures/p08-03/passive_placement.gd').read_text()
|
|
self.placement = (ROOT/'tests/fixtures/p08-03/placement.gd').read_text()
|
|
|
|
def test_actual_fixture_types(self):
|
|
check_passive_endpoint_types(self.passive, self.placement)
|
|
|
|
def test_node_endpoint_rejected(self):
|
|
with self.assertRaisesRegex(ValueError, 'incompatible RefCounted'):
|
|
check_passive_endpoint_types(self.passive.replace('endpoint: RefCounted', 'endpoint: Node'), self.placement)
|
|
|
|
def test_missing_guard_rejected(self):
|
|
with self.assertRaisesRegex(ValueError, 'null guard'):
|
|
check_passive_endpoint_types(self.passive.replace('endpoint != null', 'true'), self.placement)
|
|
|
|
def test_modal_dispatch_rejected(self):
|
|
with self.assertRaisesRegex(ValueError, 'modal endpoint'):
|
|
check_passive_endpoint_types(self.passive.replace('input_observer.endpoint = null', 'input_observer.endpoint = self'), self.placement)
|
|
|
|
def test_modal_cleanup_rejected(self):
|
|
with self.assertRaisesRegex(ValueError, 'cleanup/restore'):
|
|
check_passive_endpoint_types(self.passive.replace('input_observer.queue_free()', 'pass'), self.placement)
|
|
|
|
|
|
class EvidenceTests(unittest.TestCase):
|
|
def setUp(self):
|
|
scratch = ROOT/'tests/fixtures/p08-03/.scratch'
|
|
scratch.mkdir(exist_ok=True)
|
|
self.temp = tempfile.TemporaryDirectory(dir=scratch)
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root, self.out = F.make(Path(self.temp.name), ROOT, C)
|
|
|
|
def edit(self, path, callback):
|
|
p = self.out/path
|
|
data = json.loads(p.read_text())
|
|
callback(data)
|
|
F.put(p, data)
|
|
F.repin(self.out)
|
|
|
|
def reject(self, text):
|
|
errors = C.check(self.root, self.out)
|
|
self.assertTrue(errors, 'corrupted proof accepted')
|
|
self.assertIn(text, ' '.join(errors))
|
|
|
|
def test_positive_whole_checker(self):
|
|
self.assertEqual([], C.check(self.root, self.out))
|
|
|
|
def edit_os_request(self, folder, callback):
|
|
self.edit(folder+'/request.json', callback)
|
|
self.edit(folder+'/ack.json', lambda d: d.update(request_sha256=C.sha(self.out/(folder+'/request.json'))))
|
|
|
|
def test_reveal_missing_thumb(self):
|
|
self.edit_os_request('os-pair/supported_connected-1', lambda d: d.pop('thumb_rect'))
|
|
self.reject('unverified palette reveal')
|
|
|
|
def test_reveal_missing_bar(self):
|
|
self.edit_os_request('os-pair/supported_connected-1', lambda d: d.pop('bar'))
|
|
self.reject('unverified palette reveal')
|
|
|
|
def test_reveal_not_visible(self):
|
|
self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(visible_after=False))
|
|
self.reject('unverified palette reveal')
|
|
|
|
def test_reveal_target_outside(self):
|
|
self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(target_rect=[110,0,20,20]))
|
|
self.reject('unverified palette reveal')
|
|
|
|
def test_reveal_unchanged_scroll(self):
|
|
self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(scroll_after=0))
|
|
self.reject('palette reveal direction mismatch')
|
|
|
|
def test_reveal_opposite_scroll(self):
|
|
self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(scroll_after=-1))
|
|
self.reject('palette reveal direction mismatch')
|
|
|
|
def test_reveal_next_wrong_node(self):
|
|
self.edit_os_request('os-pair/supported_connected-2', lambda d: d.update(node='/other'))
|
|
self.reject('unverified palette reveal')
|
|
|
|
def test_reveal_next_not_button(self):
|
|
self.edit_os_request('os-pair/supported_connected-2', lambda d: d.update(action='framing'))
|
|
self.reject('unverified palette reveal')
|
|
|
|
def terminal(self, sequence):
|
|
F.put(self.out/'os-pair/writer-terminal.json', {'pid':303, 'role':'writer',
|
|
'sequence':sequence, 'stem':'writer-request', 'reason':'timeout'})
|
|
F.repin(self.out)
|
|
|
|
def test_terminal_failure(self):
|
|
sequences = [C.load(p)['sequence'] for p in (self.out/'os-pair').glob('*/request.json')]
|
|
self.terminal(max(sequences))
|
|
self.reject('terminal OS failure')
|
|
|
|
def test_terminal_continuation(self):
|
|
self.terminal(1)
|
|
self.reject('terminal OS continuation')
|
|
|
|
def test_os_sequence_gap(self):
|
|
self.edit_os_request('os-pair/supported_connected-2', lambda d: d.update(sequence=999))
|
|
self.reject('OS request sequence gap')
|
|
|
|
def action_row(self, label):
|
|
rows = C.load(self.out/'pair/writer-actions.json')['actions']
|
|
return copy.deepcopy(next(row for row in rows if row['id'] == label))
|
|
|
|
def action_check(self, row, message=None, mutate_state=None):
|
|
def typed(name):
|
|
node = C.load(self.out/('pair/'+name+'.json'))
|
|
if mutate_state and name == row['after']:
|
|
mutate_state(node)
|
|
return node
|
|
if message:
|
|
with self.assertRaisesRegex(ValueError, message):
|
|
C.action_gate(row, typed, self.root)
|
|
else:
|
|
C.action_gate(row, typed, self.root)
|
|
|
|
def test_agreeing_preview_refusal(self):
|
|
row = self.action_row('missing_connection')
|
|
self.assertEqual(row['status'], '配置不可: '+row['report']['reasons'][0]['message'])
|
|
self.action_check(row)
|
|
|
|
def test_connection_status_corruptions(self):
|
|
for status in ('先に階段の一つ下の階に通路を作ってください。', '配置不可: STAIR_LANDING_MISSING', '配置不可: wrong message'):
|
|
with self.subTest(status=status):
|
|
row = self.action_row('missing_connection')
|
|
row['status'] = status
|
|
self.action_check(row, 'connection reason mismatch')
|
|
|
|
def test_connection_reason_missing_or_empty(self):
|
|
for reasons in ([], [{'code':'OTHER', 'message':'other'}], [{'code':'STAIR_LANDING_MISSING', 'message':''}]):
|
|
with self.subTest(reasons=reasons):
|
|
row = self.action_row('missing_connection')
|
|
row['report']['reasons'] = reasons
|
|
self.action_check(row, 'connection reason mismatch')
|
|
|
|
def test_connection_preview_rejection_bypassed(self):
|
|
row = self.action_row('missing_connection')
|
|
row['results'] = [{'code':'VALIDATION_FAILED', 'details':{'reason':'STAIR_LANDING_MISSING'}, 'message':row['status']}]
|
|
self.action_check(row, 'preview rejection bypassed')
|
|
|
|
def test_preview_command_disagreement(self):
|
|
row = self.action_row('missing_connection')
|
|
row['report']['can_build'] = True
|
|
row['results'] = [{'code':'VALIDATION_FAILED', 'details':{'reason':'STAIR_LANDING_MISSING'}, 'message':row['status']}]
|
|
self.action_check(row, 'preview/command disagreement')
|
|
|
|
def test_preview_runtime_reason_difference(self):
|
|
row = self.action_row('missing_connection')
|
|
row['report']['error_codes'] = ['OVERLAP']
|
|
row['results'] = [{'code':'VALIDATION_FAILED', 'details':{'reason':'STAIR_LANDING_MISSING'}, 'message':row['status']}]
|
|
self.action_check(row, 'preview reason differs from Runtime reason')
|
|
|
|
def test_preview_partial_state_change(self):
|
|
self.action_check(self.action_row('missing_connection'), 'cancel/preview partially applied',
|
|
lambda node: C.typed_member(node, 'game_state', 'resources', 'materials').update(text='999'))
|
|
|
|
def test_readable_demolition_success(self):
|
|
row = self.action_row('demolition')
|
|
self.assertEqual('撤去しました(返金: 資材 2)', row['status'])
|
|
self.action_check(row)
|
|
|
|
def test_demolition_missing_refund_amount(self):
|
|
row = self.action_row('demolition'); row['status'] = '撤去しました(返金: 資材)'
|
|
self.action_check(row, 'demolition refund display mismatch')
|
|
|
|
def test_demolition_wrong_refund_amount(self):
|
|
row = self.action_row('demolition'); row['status'] = '撤去しました(返金: 資材 3)'
|
|
self.action_check(row, 'demolition refund display mismatch')
|
|
|
|
def test_demolition_raw_enum_status(self):
|
|
row = self.action_row('demolition'); row['status'] = 'OK | |'
|
|
self.action_check(row, 'raw demolition token in status')
|
|
|
|
def test_readable_demolition_refusal(self):
|
|
row = self.action_row('refused_demolition')
|
|
self.assertEqual('STAIR_LANDING_IN_USE', row['results'][0]['details']['reason'])
|
|
self.assertEqual('撤去できません: '+row['results'][0]['message'], row['status'])
|
|
self.action_check(row)
|
|
|
|
def test_demolition_raw_reason_status(self):
|
|
row = self.action_row('refused_demolition'); row['status'] += ' STAIR_LANDING_IN_USE'
|
|
self.action_check(row, 'raw demolition token in status')
|
|
|
|
def test_demolition_altered_refusal_message(self):
|
|
for status in ('撤去できません', '撤去できません: altered'):
|
|
row = self.action_row('refused_demolition'); row['status'] = status
|
|
self.action_check(row, 'demolition refusal message mismatch')
|
|
|
|
def test_demolition_raw_reason_mismatch(self):
|
|
row = self.action_row('refused_demolition')
|
|
row['results'][0]['details']['reason'] = 'OTHER'
|
|
def mutate(node):
|
|
items = C.typed_member(node, 'systems', 'runtime', 'command_queue', 'processed')['items']
|
|
items[-1] = F.typed(row['results'][0])
|
|
self.action_check(row, 'reason mismatch', mutate)
|
|
|
|
def test_verify_only_cli_uses_build_output_directory(self):
|
|
output = io.StringIO()
|
|
with patch.dict(os.environ, {'BUILD_OUTPUT_DIR':str(self.out.parent)}), contextlib.redirect_stdout(output):
|
|
self.assertEqual(0,C.main(['--verify-only','--project-root',str(self.root)]))
|
|
self.assertEqual('pending',json.loads(output.getvalue())['acceptance'])
|
|
|
|
def test_verify_only_cli_rejects_semantic_corruption(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][0].update(status='配置可能'))
|
|
output = io.StringIO()
|
|
with patch.dict(os.environ, {'BUILD_OUTPUT_DIR':str(self.out.parent)}), contextlib.redirect_stdout(output):
|
|
self.assertEqual(1,C.main(['--verify-only','--project-root',str(self.root)]))
|
|
self.assertIn('reason mismatch',json.loads(output.getvalue())['deficits'])
|
|
|
|
def bootstrap_edit(self, mutate):
|
|
self.edit("os-pair/writer-bootstrap-checkpoint.json", mutate)
|
|
self.reject("bootstrap source-defined operation drift")
|
|
|
|
def test_bootstrap_missing_notification(self):
|
|
self.bootstrap_edit(lambda d: C.typed_member(d,"systems","runtime","domain_events","recent_events")["items"].pop())
|
|
|
|
def test_bootstrap_extra_notification(self):
|
|
self.bootstrap_edit(lambda d: C.typed_member(d,"systems","runtime","domain_events","recent_events")["items"].append(
|
|
copy.deepcopy(C.typed_member(d,"systems","runtime","domain_events","recent_events")["items"][-1])))
|
|
|
|
def test_bootstrap_counter_drift(self):
|
|
self.bootstrap_edit(lambda d: C.typed_member(d,"systems","runtime","id_generator","counters","domain_event").update(text="999"))
|
|
|
|
def test_bootstrap_payload_drift(self):
|
|
def mutate(d):
|
|
event = C.typed_member(d,"systems","runtime","domain_events","recent_events")["items"][-1]
|
|
C.typed_member(event,"payload","save_path").update(text='"/wrong/path"')
|
|
self.bootstrap_edit(mutate)
|
|
|
|
def test_bootstrap_unknown_field_drift(self):
|
|
self.bootstrap_edit(lambda d: d["entries"].append([F.typed("unknown"),F.typed(1)]))
|
|
|
|
def test_bootstrap_missing_raw(self):
|
|
(self.out/"os-pair/writer-bootstrap-checkpoint.var").unlink()
|
|
F.repin(self.out)
|
|
self.reject("writer-bootstrap-checkpoint.var")
|
|
|
|
def test_bootstrap_overwritten_raw(self):
|
|
shutil.copyfile(self.out/"os-pair/writer-checkpoint.var", self.out/"os-pair/writer-bootstrap-checkpoint.var")
|
|
F.repin(self.out)
|
|
self.reject("wrong bootstrap raw model")
|
|
|
|
def test_reason_mismatch(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][0].update(status='配置可能'))
|
|
self.reject('reason mismatch')
|
|
|
|
def test_world_cell_drift(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][0]['position'].update(selected_cell=[9, 9, 0]))
|
|
self.reject('world-cell drift')
|
|
|
|
def test_transformed_viewport_corruption(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][0]['position'].update(viewport=[1,2]))
|
|
self.reject('transformed viewport mismatch')
|
|
|
|
def test_stale_source(self):
|
|
(self.root/C.TEST).write_text('changed')
|
|
self.reject('stale source')
|
|
|
|
def test_stale_commit(self):
|
|
self.edit('index.json', lambda d: d.update(commit='0'*40))
|
|
self.reject('stale commit')
|
|
|
|
def test_non_reuse_head_mismatch(self):
|
|
self.edit('index.json', lambda d:d.update(commit='0'*40))
|
|
self.reject('stale commit')
|
|
|
|
def test_stale_input(self):
|
|
(self.root/'docs/traceability/original/BASELINE.txt').write_text('changed')
|
|
self.reject('stale input')
|
|
|
|
def test_missing_os(self):
|
|
self.edit('index.json', lambda d: d.update(os_proof=None))
|
|
self.reject('missing artifact')
|
|
|
|
def test_fake_os_method(self):
|
|
self.edit('os-pair/unsupported-os.json', lambda d: d.update(method='handler'))
|
|
self.reject('fake OS provenance')
|
|
|
|
def test_fake_os_pid(self):
|
|
self.edit('os-pair/unsupported-os.json', lambda d: d.update(target_pid=555))
|
|
self.reject('fake OS provenance')
|
|
|
|
def test_same_pid_resume(self):
|
|
self.edit('runs/reader/reader-receipt.json', lambda d: d.update(actual_pid=101))
|
|
self.reject('duplicate actual PID')
|
|
|
|
def test_orphan_occupancy(self):
|
|
path = 'pair/writer-demolition-after.json'
|
|
model = C.decode(json.loads((self.out/path).read_text()))
|
|
model['systems']['runtime']['world']['occupancies'].append({'entity_id': 'ghost', 'cells': [[-8, 0, 0]]})
|
|
F.put(self.out/path, F.typed(model)); F.repin(self.out)
|
|
self.reject('orphan/lost occupancy')
|
|
|
|
def test_partial_demolition(self):
|
|
path = 'pair/writer-demolition-after.json'
|
|
model = C.decode(json.loads((self.out/path).read_text()))
|
|
model['systems']['runtime']['facilities']['instances'].append({'instance_id': 'f1', 'occupied_cells': [[-8, 0, 0]]})
|
|
F.put(self.out/path, F.typed(model)); F.repin(self.out)
|
|
self.reject('partial demolition')
|
|
|
|
def test_refusal_partial_resources(self):
|
|
path = 'pair/writer-refused_demolition-after.json'
|
|
model = C.decode(json.loads((self.out/path).read_text()))
|
|
model['game_state']['resources']['gold'] -= 1
|
|
F.put(self.out/path, F.typed(model)); F.repin(self.out)
|
|
self.reject('refusal partially applied')
|
|
|
|
def test_wrong_resume(self):
|
|
path = 'pair/reader-checkpoint.json'
|
|
original = (self.out/path).read_bytes()
|
|
model = C.decode(json.loads((self.out/path).read_text()))
|
|
model['clock']['absolute_minute'] += 1
|
|
F.put(self.out/path, F.typed(model)); F.repin(self.out)
|
|
self.reject('wrong resume model')
|
|
(self.out/path).write_bytes(original); F.repin(self.out)
|
|
self.edit('pair/reader-save-result-0001.json', lambda d: d.update(result=F.typed({'ok':False,'operation':'load','code':'UNSAFE_PATH'})))
|
|
self.reject('actual save/load failed')
|
|
self.edit('pair/reader-save-result-0001.json', lambda d: d.update(result=F.typed({'ok':True,'operation':'load'}), requested_path='/outside/formal'))
|
|
self.reject('save outside private user profile')
|
|
|
|
def test_notification_payload_is_recomputed_from_returned_dict(self):
|
|
path = 'pair/reader-checkpoint.json'
|
|
original = (self.out/path).read_bytes()
|
|
fields = ('ok','code','message','operation','save_path','reason','request_id',
|
|
'checkpoint_absolute_minute','duration_us','runtime_duration_us',
|
|
'selected_candidate_id','selected_candidate_kind','selected_generation',
|
|
'save_id','saved_absolute_minute')
|
|
for field in fields:
|
|
with self.subTest(field=field):
|
|
data = json.loads(original)
|
|
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1]
|
|
value = C.typed_member(event,'payload',field)
|
|
decoded = C.decode(value)
|
|
changed = not decoded if isinstance(decoded,bool) else decoded+1 if isinstance(decoded,int) else decoded+'-drift'
|
|
value.update(C.scalar(changed,value['type']))
|
|
F.put(self.out/path,data); F.repin(self.out)
|
|
self.reject('source-defined operation drift')
|
|
(self.out/path).write_bytes(original); F.repin(self.out)
|
|
|
|
def test_notification_envelope_cannot_drift(self):
|
|
path = 'pair/writer-checkpoint.json'
|
|
original = (self.out/path).read_bytes()
|
|
for field in ('event_id','event_type','source_service','absolute_minute','causation_command_id','entity_ids'):
|
|
with self.subTest(field=field):
|
|
data = json.loads(original)
|
|
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1]
|
|
value = C.typed_member(event,field)
|
|
if field == 'entity_ids': value['items'].append(C.scalar('unexpected-entity'))
|
|
else:
|
|
old = C.decode(value)
|
|
value.update(C.scalar(old+1 if isinstance(old,int) else old+'-drift',value['type']))
|
|
F.put(self.out/path,data); F.repin(self.out)
|
|
self.reject('source-defined operation drift')
|
|
(self.out/path).write_bytes(original); F.repin(self.out)
|
|
|
|
def test_notification_counter_must_increment_exactly_once(self):
|
|
def corrupt(data):
|
|
C.typed_member(data,'systems','runtime','id_generator','counters','domain_event')['text'] = '9'
|
|
self.edit('pair/reader-checkpoint.json',corrupt)
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_notification_published_ids_cannot_be_omitted(self):
|
|
self.edit('pair/writer-checkpoint.json',lambda d: C.typed_member(d,'systems','runtime','domain_events','published_ids')['items'].clear())
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_extra_notification_is_rejected(self):
|
|
def corrupt(data):
|
|
events = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items']
|
|
events.append(copy.deepcopy(events[-1]))
|
|
self.edit('pair/reader-checkpoint.json',corrupt)
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_existing_event_history_cannot_be_rewritten(self):
|
|
def corrupt(data):
|
|
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][0]
|
|
C.typed_member(event,'event_type').update(text='"REWRITTEN_HISTORY"')
|
|
self.edit('pair/reader-checkpoint.json',corrupt)
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_postsave_unrelated_resource_drift_is_rejected(self):
|
|
self.edit('pair/writer-checkpoint.json',lambda d: C.typed_member(d,'game_state','resources','gold').update(text='9999'))
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_postload_unknown_field_drift_is_rejected(self):
|
|
self.edit('pair/reader-checkpoint.json',lambda d: C.typed_member(d,'systems','runtime','inventory')['entries'].append([C.scalar('unknown'),C.scalar(1)]))
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_presave_must_match_read_only_formal_model(self):
|
|
self.edit('pair/writer-presave.json',lambda d: C.typed_member(d,'clock','absolute_minute').update(text='361'))
|
|
self.reject('wrong presave formal model')
|
|
|
|
def test_read_only_inspection_must_match_complete_formal_model(self):
|
|
self.edit('pair/reader-save-result-0002.json',lambda d: C.typed_member(d['result'],'snapshot','clock','absolute_minute').update(text='361'))
|
|
self.reject('wrong read-only formal model')
|
|
|
|
def test_inspection_cannot_lose_read_only_attribution(self):
|
|
self.edit('pair/writer-save-result-0002.json',lambda d: C.typed_member(d['result'],'read_only').update(text='false'))
|
|
self.reject('missing read-only formal model')
|
|
|
|
def test_returned_result_payload_tampering_is_rejected(self):
|
|
self.edit('pair/reader-save-result-0001.json',lambda d: C.typed_member(d['result'],'duration_us').update(text='999'))
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_notification_payload_type_drift_is_rejected(self):
|
|
def corrupt(data):
|
|
event = C.typed_member(data,'systems','runtime','domain_events','recent_events')['items'][-1]
|
|
C.typed_member(event,'payload','operation')['type'] = 'String'
|
|
C.typed_member(event,'payload','operation')['text'] = '"recovery"'
|
|
self.edit('pair/reader-checkpoint.json',corrupt)
|
|
self.reject('source-defined operation drift')
|
|
|
|
def test_operation_count_is_source_derived_with_recent_limit(self):
|
|
model = C.decode(json.loads((self.out/'pair/writer-formal-model.json').read_text()))
|
|
runtime = model['systems']['runtime']
|
|
runtime['id_generator']['counters']['domain_event'] = 900
|
|
stream = runtime['domain_events']
|
|
stream['published_ids'] = ['domain_event.%012d' % n for n in range(1,901)]
|
|
stream['recent_events'] = [{'ordinal':n} for n in range(645,901)]
|
|
expected = C.decode(C.expected_operation(F.typed(model),{'ok':True},'SAVE_CHECKPOINT_SUCCEEDED'))
|
|
new = expected['systems']['runtime']
|
|
self.assertEqual(901,new['id_generator']['counters']['domain_event'])
|
|
self.assertEqual(901,len(new['domain_events']['published_ids']))
|
|
self.assertEqual(256,len(new['domain_events']['recent_events']))
|
|
self.assertEqual(stream['recent_events'][1:],new['domain_events']['recent_events'][:-1])
|
|
self.assertEqual('domain_event.000000000901',new['domain_events']['recent_events'][-1]['event_id'])
|
|
del runtime['id_generator']['counters']
|
|
with self.assertRaisesRegex(ValueError,'missing/duplicate typed model member'):
|
|
C.expected_operation(F.typed(model),{'ok':True},'SAVE_CHECKPOINT_SUCCEEDED')
|
|
|
|
def test_missing_formal_member(self):
|
|
for name in ('formal','formal.bak'):
|
|
with self.subTest(package=name):
|
|
file = self.out/('pair/'+name+'/journal.log')
|
|
original = file.read_bytes()
|
|
file.unlink(); F.repin(self.out)
|
|
self.reject('journal.log')
|
|
file.write_bytes(original); F.repin(self.out)
|
|
self.edit('pair/formal-export.json', lambda d: d['slot_metadata'].update(sha256='0'*64))
|
|
self.reject('slot metadata SHA')
|
|
|
|
def test_missing_model_member(self):
|
|
path = 'pair/writer-unsupported-before.json'
|
|
model = C.decode(json.loads((self.out/path).read_text()))
|
|
del model['systems']['runtime']['inventory']
|
|
F.put(self.out/path, F.typed(model)); F.repin(self.out)
|
|
self.reject('missing whole Runtime members')
|
|
|
|
def test_missing_clamp_boundary(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][-1].update(steps=[1]*18))
|
|
self.reject('wheel clamp sequence')
|
|
|
|
def test_no_actual_pan(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][-1].update(pan_after=[0,0]))
|
|
self.reject('move-mode wheel did not pan')
|
|
|
|
def test_handler_command_must_remain_headless(self):
|
|
self.edit('runs/writer/writer-receipt.json', lambda d: d['command'].remove('--headless'))
|
|
self.reject('handler command contract')
|
|
|
|
def test_os_headless_masquerade(self):
|
|
self.edit('os-runs/writer/ended.json', lambda d: d['command'].insert(1, '--headless'))
|
|
self.reject('OS native argv')
|
|
|
|
def test_stale_os_recorder(self):
|
|
self.edit('os-runs/writer/ended.json', lambda d: d.update(recorder_sha256='0'*64))
|
|
self.reject('OS controller/recorder SHA')
|
|
|
|
def test_os_process_unended(self):
|
|
self.edit('os-runs/writer/ended.json', lambda d: d.update(engine_ended=False))
|
|
self.reject('OS native failure')
|
|
|
|
def test_os_default_player_changed(self):
|
|
self.edit('os-runs/writer/ended.json', lambda d: d.update(default_after={'changed':True}))
|
|
self.reject('OS protected state changed')
|
|
|
|
def test_os_actual_observation_missing(self):
|
|
self.edit('os-pair/unsupported-1/observed.json', lambda d: d.update(events=[]))
|
|
self.reject('missing OS events')
|
|
|
|
def test_os_dispatch_missing(self):
|
|
self.edit('os-pair/writer-actions.json', lambda d: d['actions'][0]['position'].pop('dispatch'))
|
|
self.reject('dispatch')
|
|
|
|
def test_os_wrong_actual_input_cell(self):
|
|
# Keep event and action mutually consistent; independent conversion must reject.
|
|
self.edit('os-pair/writer-actions.json', lambda d: d['actions'][0]['position']['dispatch']['world_cell'].__setitem__(0, 99))
|
|
self.edit('os-pair/unsupported-1/observed.json', lambda d: d['events'][0]['dispatch']['world_cell'].__setitem__(0, 99))
|
|
self.reject('OS actual input cell drift')
|
|
|
|
def test_os_event_position_disagrees(self):
|
|
self.edit('os-pair/unsupported-1/observed.json', lambda d: d['events'][0].update(position=[0,0]))
|
|
self.reject('OS dispatch event mismatch')
|
|
|
|
def test_os_wrong_selected_entity(self):
|
|
self.edit('os-pair/writer-actions.json', lambda d: d['actions'][-1]['position']['selection'].update(entity_id='wrong'))
|
|
self.reject('OS selected entity mismatch')
|
|
|
|
def test_os_wrong_selected_occupancy(self):
|
|
self.edit('os-pair/writer-actions.json', lambda d: d['actions'][-1]['position']['selection'].update(entity_id='wrong', occupant_id='wrong'))
|
|
self.reject('OS selected occupancy mismatch')
|
|
|
|
def test_os_selection_requirement_cannot_be_removed(self):
|
|
self.edit('os-pair/writer-actions.json', lambda d: d['actions'][-1]['position']['selection'].update(required=False))
|
|
self.reject('OS missing entity selection')
|
|
|
|
def test_os_dispatch_transform_drift(self):
|
|
self.edit('os-pair/writer-actions.json', lambda d: d['actions'][0]['position']['dispatch'].update(pan=[100,0]))
|
|
self.edit('os-pair/unsupported-1/observed.json', lambda d: d['events'][0]['dispatch'].update(pan=[100,0]))
|
|
self.reject('OS dispatch transform drift')
|
|
|
|
def test_os_later_getter_is_diagnostic(self):
|
|
self.edit('os-pair/writer-actions.json', lambda d: d['actions'][-1]['position']['diagnostics'].update(getter_cell=[999,999,0]))
|
|
self.assertEqual([], C.check(self.root, self.out))
|
|
|
|
def test_os_ack_for_another_request(self):
|
|
self.edit('os-pair/unsupported-1/ack.json', lambda d: d.update(request_sha256='0'*64))
|
|
self.reject('OS ack request mismatch')
|
|
|
|
def test_os_canvas_mapping_drift(self):
|
|
self.edit('os-pair/unsupported-1/request.json', lambda d: d.update(viewport_to_screen=[1,0,0,1,32,0]))
|
|
# Repin acknowledgement too: test semantics, rather than checksum rejection.
|
|
self.edit('os-pair/unsupported-1/ack.json', lambda d: d.update(request_sha256=C.sha(self.out/'os-pair/unsupported-1/request.json')))
|
|
self.reject('OS canvas mapping drift')
|
|
|
|
def test_os_guide_interception(self):
|
|
self.edit('os-pair/unsupported-1/request.json', lambda d: d.update(guide_visible=True,guide_rect=[0,0,10000,10000]))
|
|
self.edit('os-pair/unsupported-1/ack.json', lambda d: d.update(request_sha256=C.sha(self.out/'os-pair/unsupported-1/request.json')))
|
|
self.reject('OS guide interception')
|
|
|
|
def test_whole_checker_missing_stair_projection(self):
|
|
self.edit('pair/writer-lower_stair-completed.json', lambda d: C.typed_member(d,'systems','runtime','facility_infrastructure','projections')['items'].pop())
|
|
self.reject('missing/extra infrastructure projection')
|
|
|
|
def test_whole_checker_wrong_stair_connector(self):
|
|
self.edit('pair/writer-lower_stair-completed.json', lambda d: C.typed_member(C.typed_member(d,'systems','runtime','path','connectors')['items'][0],'cost').update(text='999'))
|
|
self.reject('wrong infrastructure connector topology')
|
|
|
|
def test_refusal_cannot_be_relabelled(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][4].update(expected='OTHER_REASON'))
|
|
self.reject('required refusal reason changed')
|
|
|
|
def test_demolition_cannot_be_relabelled(self):
|
|
self.edit('pair/writer-actions.json', lambda d: d['actions'][9].update(kind='camera'))
|
|
self.reject('operation kind mismatch')
|
|
|
|
def test_png_signature_is_not_a_screenshot(self):
|
|
next((self.out/'os-pair').rglob('screen-before.png')).write_bytes(b'\x89PNG\r\n\x1a\nFAKE')
|
|
F.repin(self.out)
|
|
self.reject('invalid OS screenshot')
|
|
|
|
def test_omitted_os_lifecycle_request(self):
|
|
self.edit('os-pair/writer-requests.json', lambda d: d['requests'].pop())
|
|
self.reject('omitted OS lifecycle requests')
|
|
|
|
def test_os_audio_boolean_cannot_replace_owner_evidence(self):
|
|
self.edit('os-runs/writer/teardown/owned-audio/owner_1.json', lambda d: d['final_targets'][0].update(alive=True))
|
|
self.reject('real WeakRef release')
|
|
|
|
def collector_mock(self, fail_wait=False):
|
|
"""No native engine: Popen, profiles, live engine probes are all mocked."""
|
|
expected2400 = 2400
|
|
self.assertEqual(expected2400, D.OS_CHILD_TIMEOUT_SECONDS)
|
|
def assert2400(receipt):
|
|
self.assertEqual(expected2400, receipt['timeout_seconds'])
|
|
output = Path(self.temp.name)/'mock-collection'
|
|
stage = output/'stage'; stage.mkdir(parents=True)
|
|
shutil.copyfile(self.root/'project.godot',stage/'project.godot')
|
|
public = (stage/'project.godot').read_bytes()
|
|
(output/'pair').mkdir()
|
|
shutil.copytree(self.out/'pair/formal',output/'pair/initial-formal')
|
|
F.put(output/'pair/diagnostic-budget.json',{'setup':'synthetic mock'})
|
|
refs = C.runner.source_refs(self.root)
|
|
protected = {'mock_default_player':True}
|
|
captured = []; cleaned = []
|
|
def spawn(argv, **kw):
|
|
self.assertNotIn('--headless', argv)
|
|
self.assertEqual(['--path',str(stage),'--script'],argv[1:4])
|
|
role = kw['env']['P08_PLACEMENT_ROLE']
|
|
profile_text = (stage/'project.godot').read_text()
|
|
name = __import__('re').search(r'config/custom_user_dir_name="([^"]+)"',profile_text).group(1)
|
|
pid = 801+len(captured)
|
|
process = SimpleNamespace(pid=pid, role=role)
|
|
def wait(timeout):
|
|
self.assertEqual(expected2400,timeout)
|
|
assert2400(C.load(output/('os-runs/'+role+'/started.json')))
|
|
if fail_wait:
|
|
raise TimeoutError('mock Root acknowledgement timeout')
|
|
kw['stdout'].write('P08 passive OS placement: 2 checks, 0 failures\n')
|
|
F.put(output/('os-pair/'+role+'-profile.json'),{'pid':pid,'user_data_dir':str(Path(self.temp.name)/name),'display':'mock'})
|
|
if role == 'writer': shutil.copytree(output/'pair/initial-formal',output/'os-pair/formal')
|
|
return 0
|
|
process.wait = wait; captured.append(process)
|
|
return process
|
|
def cleanup(children):
|
|
cleaned.extend(p.pid for p in children)
|
|
return ([{'pid':p.pid,'engine_ended':True} for p in children], [])
|
|
r = SimpleNamespace(require=C.runner.require, inventory=C.runner.inventory,
|
|
sha=C.sha, read=C.load, write=C.runner.write, copy_source=shutil.copyfile,
|
|
source_refs=lambda root:refs, default_player=lambda root:protected,
|
|
profile_path=lambda name:Path(self.temp.name)/name,
|
|
private_project=C.runner.private_project, fresh_environment=C.runner.fresh_environment,
|
|
SUPPORT=ROOT/'tools/regression', module=lambda *a:SimpleNamespace(ordinary_gate=lambda *a:{'mock':True}))
|
|
outer = SimpleNamespace(native=SimpleNamespace(helper={'live_engines':lambda:[]}),finish_captured=cleanup)
|
|
# Disk admission is an environmental input to this fully mocked collector.
|
|
# Preserve the real collector's gate; make this fixture independent of host free space.
|
|
with patch.object(D.shutil,'disk_usage',return_value=SimpleNamespace(free=1024**3)), patch.object(D.subprocess,'Popen',side_effect=spawn), contextlib.redirect_stdout(io.StringIO()):
|
|
if fail_wait:
|
|
with self.assertRaisesRegex(AssertionError,'native OS collection failed'):
|
|
D.collect_os(r,self.root,output,stage,str(Path(self.temp.name)/'synthetic-engine-not-executable'),'1'*40,refs,protected,outer)
|
|
else:
|
|
phases = D.collect_os(r,self.root,output,stage,str(Path(self.temp.name)/'synthetic-engine-not-executable'),'1'*40,refs,protected,outer)
|
|
self.assertEqual(2,len(phases))
|
|
self.assertEqual([p.pid for p in captured],cleaned)
|
|
self.assertEqual(public,(stage/'project.godot').read_bytes())
|
|
self.assertTrue((output/'os-runs/writer/ended.json').exists())
|
|
self.assertEqual(['writer'] if fail_wait else ['writer', 'reader'],
|
|
[p.role for p in captured])
|
|
for process in captured:
|
|
for event in ('started', 'ended'):
|
|
assert2400(C.load(output/('os-runs/'+process.role+'/'+event+'.json')))
|
|
|
|
def test_mock_native_controller_two_private_pids(self):
|
|
self.collector_mock()
|
|
|
|
def test_mock_native_timeout_restores_project_and_cleans_owned_child(self):
|
|
self.collector_mock(fail_wait=True)
|
|
|
|
def image_put(path, value):
|
|
path.write_text(json.dumps(value, sort_keys=True)+'\n')
|
|
|
|
def negative_jpeg(width=1280, height=752, progressive=False):
|
|
def segment(marker, body):
|
|
return bytes((255, marker))+struct.pack('>H', len(body)+2)+body
|
|
frame = segment(0xc2 if progressive else 0xc0,
|
|
b'\x08'+struct.pack('>HH', height, width)+b'\x01\x01\x11\x00')
|
|
scan = segment(0xda, b'\x01\x01\x00\x00\x3f\x00')+b'\x12\xff\x00\x34\xff\xd0\x56'
|
|
tables = segment(0xdb, b'\x00'+bytes([1])*64)+segment(0xc4, b'\x00'+b'\x01'+bytes(15)+b'\x00'+b'\x10'+b'\x01'+bytes(15)+b'\x00')
|
|
return b'\xff\xd8'+tables+segment(0xe0, b'SYNTHETIC')+frame+scan+(scan if progressive else b'')+b'\xff\xd9'
|
|
|
|
def negative_images(base, candidate):
|
|
out, original = base/'canonical', base/'original'
|
|
out.mkdir(parents=True); index = {'commit':candidate, 'artifacts':{}}
|
|
measurement = {'scope':'synthetic post-collection measurement', 'images':[]}
|
|
for role, count in (('reader',5), ('writer',64)):
|
|
for i in range(1,count+1):
|
|
for side in ('before','after'):
|
|
name = f'os-pair/{role}-request-{i:04d}/screen-{side}.png'
|
|
path = out/name; path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(negative_jpeg(progressive=i%2 == 0))
|
|
measurement['images'].append({'file':name, 'source_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
'width':1280,'height':752,'format_by_magic':'JPEG'})
|
|
for name in ('runner-input.json','runner-ended.json','os-runs/writer/ended.json','os-runs/reader/ended.json'):
|
|
path = out/name; path.parent.mkdir(parents=True,exist_ok=True); image_put(path, {'synthetic':True})
|
|
refresh_images(out, index, measurement)
|
|
shutil.copytree(out, original)
|
|
def pin(name):
|
|
p=original/name
|
|
return {'path':str(p),'bytes':p.stat().st_size,'sha256':hashlib.sha256(p.read_bytes()).hexdigest()}
|
|
pins = {'original_root':str(original),'native_candidate':candidate,'index':pin('index.json'),
|
|
'runner_input':pin('runner-input.json'),'runner_ended':pin('runner-ended.json'),
|
|
'OS':{role:{'receipt':pin('os-runs/'+role+'/ended.json')} for role in ('writer','reader')}}
|
|
return out, original, index, measurement, pins
|
|
|
|
def refresh_images(out, index, measurement):
|
|
index['artifacts'] = {p.relative_to(out).as_posix():{'bytes':p.stat().st_size,
|
|
'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in out.rglob('*') if p.is_file() and p.name != 'index.json'}
|
|
image_put(out/'index.json',index)
|
|
measurement['original_index_sha256'] = hashlib.sha256((out/'index.json').read_bytes()).hexdigest()
|
|
|
|
|
|
class FreshRoutingTests(unittest.TestCase):
|
|
setUp = EvidenceTests.setUp
|
|
edit = EvidenceTests.edit
|
|
reject = EvidenceTests.reject
|
|
# Reuse the whole synthetic proof, not a boolean checker substitute.
|
|
def historical(self):
|
|
F.put(self.root/C.R09_DOCS/'ROOT_NATIVE_R09_COLLECTION_PINS.json', {'synthetic': True})
|
|
|
|
def fresh_check(self):
|
|
commit = C.load(self.out/'index.json')['commit']
|
|
original_load = C.load
|
|
def guarded_load(path):
|
|
self.assertNotEqual(Path(path).name, 'ROOT_R09_READONLY_IMAGE_DIMENSIONS.json')
|
|
return original_load(path)
|
|
with contextlib.ExitStack() as stack:
|
|
stack.enter_context(patch.object(C.subprocess, 'check_output', return_value=commit+'\n'))
|
|
stack.enter_context(patch.object(C, 'load', side_effect=guarded_load))
|
|
for name in ('r09_applicability', 'r09_runner_binding', 'r09_collection', 'r09_images'):
|
|
stack.enter_context(patch.object(C, name, side_effect=AssertionError('fresh used '+name)))
|
|
return C.check(self.root, self.out)
|
|
|
|
def test_historical_pins_fresh_positive(self):
|
|
self.historical()
|
|
self.assertEqual([], self.fresh_check())
|
|
|
|
def test_historical_pins_stale_commit(self):
|
|
self.historical()
|
|
self.edit('index.json', lambda d: d.update(commit='0'*40))
|
|
with patch.object(C.subprocess, 'check_output', return_value='1'*40+'\n'):
|
|
self.reject('stale commit')
|
|
|
|
def test_historical_pins_stale_source(self):
|
|
self.historical()
|
|
self.edit('index.json', lambda d: d['source_map'].update({'project.godot':'0'*64}))
|
|
self.assertIn('stale source', ' '.join(self.fresh_check()))
|
|
|
|
def test_historical_pins_stale_input(self):
|
|
self.historical()
|
|
self.edit('index.json', lambda d: d['inputs'].update({'AGENTS.md':'0'*64}))
|
|
self.assertIn('stale input', ' '.join(self.fresh_check()))
|
|
|
|
def test_reuse_missing_pins(self):
|
|
self.edit('index.json', lambda d: d.update(commit=C.R09_COMMIT))
|
|
self.reject('R09 reuse candidate')
|
|
|
|
def test_reuse_wrong_applicability(self):
|
|
self.historical()
|
|
F.put(self.root/C.R09_DOCS/'ROOT_R09_READONLY_IMAGE_DIMENSIONS.json', {'synthetic':True})
|
|
self.edit('index.json', lambda d: d.update(commit=C.R09_COMMIT))
|
|
with patch.object(C.subprocess, 'check_output', return_value=C.R09_COMMIT+'\n'), patch.object(
|
|
C.subprocess, 'run', return_value=SimpleNamespace(returncode=1)):
|
|
self.reject('R09_COMMIT not an ancestor')
|
|
|
|
def screenshots(self, data):
|
|
for path in self.out.rglob('screen-*.png'):
|
|
path.write_bytes(data)
|
|
F.repin(self.out)
|
|
|
|
def test_fresh_jpeg_positive(self):
|
|
self.historical()
|
|
self.screenshots(negative_jpeg())
|
|
self.assertEqual([], self.fresh_check())
|
|
|
|
def test_fresh_truncated_jpeg(self):
|
|
self.screenshots(negative_jpeg()[:-2])
|
|
self.assertIn('invalid JPEG SOI/EOI', ' '.join(self.fresh_check()))
|
|
|
|
def test_fresh_garbage(self):
|
|
self.screenshots(b'garbage')
|
|
self.assertIn('missing OS screenshot', ' '.join(self.fresh_check()))
|
|
|
|
def test_fresh_dimension_mismatch(self):
|
|
self.screenshots(negative_jpeg())
|
|
next(self.out.rglob('screen-after.png')).write_bytes(negative_jpeg(width=640))
|
|
F.repin(self.out)
|
|
self.assertIn('OS screenshot dimension mismatch', ' '.join(self.fresh_check()))
|
|
|
|
def test_fresh_unbound_path(self):
|
|
ack = next(self.out.rglob('ack.json'))
|
|
data = C.load(ack)
|
|
data['screen_before'] = data['screen_after']
|
|
F.put(ack, data)
|
|
F.repin(self.out)
|
|
self.assertIn('R09 screenshot request binding', ' '.join(self.fresh_check()))
|
|
|
|
|
|
class R09ImageTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory(dir=ROOT/'tests/fixtures/p08-03/.scratch')
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.fixture = SimpleNamespace(make=negative_images, jpeg=negative_jpeg, refresh=refresh_images)
|
|
self.out, self.original, self.index, self.measurement, self.pins = self.fixture.make(Path(self.temp.name), C.R09_COMMIT)
|
|
|
|
def images(self):
|
|
C.r09_images(self.out, self.index, self.measurement)
|
|
|
|
def change_bytes(self, data):
|
|
row = self.measurement['images'][0]
|
|
(self.out/row['file']).write_bytes(data)
|
|
row['source_sha256'] = hashlib.sha256(data).hexdigest()
|
|
self.fixture.refresh(self.out, self.index, self.measurement)
|
|
|
|
def test_baseline_progressive_and_collection_positive(self):
|
|
pins = C.load(ROOT/C.R09_DOCS/'ROOT_NATIVE_R09_COLLECTION_PINS.json')
|
|
original = Path(pins['original_root'])
|
|
if not (original/'index.json').exists():
|
|
self.skipTest('Root-executed original R09 capture hash re-check required')
|
|
C.r09_images(original, C.load(original/'index.json'),
|
|
C.load(ROOT/C.R09_DOCS/'ROOT_R09_READONLY_IMAGE_DIMENSIONS.json'), pins)
|
|
|
|
def test_jpeg_missing_tables_and_scan(self):
|
|
data = negative_jpeg()
|
|
def remove(marker):
|
|
start = data.index(bytes((255, marker)))
|
|
size = int.from_bytes(data[start+2:start+4], 'big')
|
|
return data[:start]+data[start+2+size:]
|
|
for stream, error in ((remove(0xdb), 'missing JPEG DQT'),
|
|
(remove(0xc4), 'missing JPEG DHT'),
|
|
(data[:data.index(b'\xff\xda')+10]+b'\xff\xd9', 'empty JPEG entropy stream')):
|
|
with self.subTest(error=error), self.assertRaisesRegex(ValueError, error):
|
|
C.jpeg_dimensions(stream)
|
|
start = data.index(b'\xff\xda')
|
|
for offset, value, error in ((5, 2, 'JPEG SOF/SOS component mismatch'),
|
|
(6, 0x11, 'JPEG SOS undefined table')):
|
|
stream = bytearray(data); stream[start+offset] = value
|
|
with self.assertRaisesRegex(ValueError, error): C.jpeg_dimensions(stream)
|
|
|
|
def test_truncated_or_non_jpeg(self):
|
|
good = self.fixture.jpeg()
|
|
for data in (good[:-1], b'NOT JPEG', b'\xff\xd8\xff\xe0\xff\xff\xff\xd9',
|
|
good[:15]+b'\xff\xd9', b'\xff\xd8\xff\xd9'):
|
|
with self.subTest(data=data):
|
|
self.change_bytes(data)
|
|
with self.assertRaisesRegex(ValueError, 'JPEG'):
|
|
self.images()
|
|
|
|
def test_sof_dimension_mismatch(self):
|
|
self.change_bytes(self.fixture.jpeg(height=720))
|
|
with self.assertRaisesRegex(ValueError,'SOF dimension mismatch'): self.images()
|
|
|
|
def test_before_after_dimension_drift(self):
|
|
self.change_bytes(self.fixture.jpeg(height=753))
|
|
self.measurement['images'][0]['height'] = 753
|
|
with self.assertRaisesRegex(ValueError,'before/after dimension drift'): self.images()
|
|
|
|
def test_missing_image_entry(self):
|
|
self.measurement['images'].pop()
|
|
with self.assertRaisesRegex(ValueError,'missing/extra image entry'): self.images()
|
|
|
|
def test_extra_image_entry(self):
|
|
row = dict(self.measurement['images'][0],file='os-pair/extra.png')
|
|
self.measurement['images'].append(row)
|
|
with self.assertRaisesRegex(ValueError,'missing/extra image entry'): self.images()
|
|
|
|
def test_duplicate_image_entry(self):
|
|
self.measurement['images'].append(self.measurement['images'][0])
|
|
with self.assertRaisesRegex(ValueError,'duplicate file'): self.images()
|
|
|
|
def test_extra_indexed_image(self):
|
|
(self.out/'os-pair/extra.png').write_bytes(self.fixture.jpeg())
|
|
self.fixture.refresh(self.out,self.index,self.measurement)
|
|
with self.assertRaisesRegex(ValueError,'missing/extra image entry'): self.images()
|
|
|
|
def test_source_sha256_mismatch(self):
|
|
self.measurement['images'][0]['source_sha256'] = '0'*64
|
|
with self.assertRaisesRegex(ValueError,'source_sha256 mismatch'): self.images()
|
|
|
|
def test_original_capture_hash_and_measurement_drift(self):
|
|
for alter_bytes in (True, False):
|
|
with self.subTest(alter_bytes=alter_bytes):
|
|
out, original, index, measurement, pins = negative_images(
|
|
Path(self.temp.name)/('case-'+str(alter_bytes)), C.R09_COMMIT)
|
|
row = sorted(measurement['images'], key=lambda r:r['file'])[0]
|
|
if alter_bytes:
|
|
(out/row['file']).write_bytes(negative_jpeg(height=720))
|
|
refresh_images(out, index, measurement)
|
|
error = 'original capture hash mismatch'
|
|
else:
|
|
row['source_sha256'] = '0'*64
|
|
error = 'measurement capture hash altered'
|
|
with self.assertRaisesRegex(ValueError, error):
|
|
C.r09_images(out, index, measurement, pins)
|
|
|
|
def test_stale_index_pin(self):
|
|
self.measurement['original_index_sha256'] = '0'*64
|
|
with self.assertRaisesRegex(ValueError,'stale image index pin'): self.images()
|
|
|
|
def test_original_canonical_divergence(self):
|
|
(self.out/self.measurement['images'][0]['file']).write_bytes(self.fixture.jpeg(height=720))
|
|
with self.assertRaisesRegex(ValueError,'original/canonical byte divergence'):
|
|
C.r09_collection(self.out,self.index,self.pins)
|
|
|
|
def test_collection_pin_mismatch(self):
|
|
self.pins['runner_input']['sha256'] = '0'*64
|
|
with self.assertRaisesRegex(ValueError,'collection pin SHA'):
|
|
C.r09_collection(self.out,self.index,self.pins)
|
|
|
|
|
|
class R09ApplicabilityTests(unittest.TestCase):
|
|
def test_commit_and_ancestor(self):
|
|
with patch.object(C.subprocess, 'check_output', return_value=C.R09_COMMIT+'\n'):
|
|
with self.assertRaisesRegex(ValueError, 'R09 reuse candidate'):
|
|
C.r09_applicability(ROOT, {'commit':'0'*40})
|
|
with patch.object(C.subprocess, 'run', return_value=SimpleNamespace(returncode=1)):
|
|
with self.assertRaisesRegex(ValueError, 'R09_COMMIT not an ancestor'):
|
|
C.r09_applicability(ROOT, {'commit':C.R09_COMMIT})
|
|
|
|
def test_non_whitelisted_committed_and_dirty_changes(self):
|
|
names = ['scenes/main/main.gd', 'tests/ui/test_p08_normal_placement_ops.gd',
|
|
'tests/fixtures/p08-03/added.py', 'tests/fixtures/p08-03/placement.gd',
|
|
'tools/run_p08_normal_placement.py', 'tools/regression/suite-policy.json']
|
|
for name in names:
|
|
for status in ('A', 'D', 'M'):
|
|
with self.subTest(name=name, status=status):
|
|
with patch.object(C.subprocess, 'run', return_value=SimpleNamespace(returncode=0)), patch.object(
|
|
C.subprocess, 'check_output', side_effect=[
|
|
C.R09_COMMIT+'\n', (status+'\0'+name+'\0').encode()]):
|
|
with self.assertRaisesRegex(ValueError, 'non-whitelisted committed change'):
|
|
C.r09_applicability(ROOT, {'commit':C.R09_COMMIT})
|
|
with patch.object(C.subprocess, 'run', return_value=SimpleNamespace(returncode=0)), patch.object(
|
|
C.subprocess, 'check_output', side_effect=[C.R09_COMMIT+'\n', b'', b' M scenes/main/main.gd\0']):
|
|
with self.assertRaisesRegex(ValueError, 'dirty working tree outside whitelist'):
|
|
C.r09_applicability(ROOT, {'commit':C.R09_COMMIT})
|
|
|
|
def test_engine_settings_environment_drift(self):
|
|
pins = {'runner_input':{'tag':'run'}, 'index':{'tag':'index'},
|
|
'OS':{'writer':{'receipt':{'tag':'receipt'}}}}
|
|
baseline = {'engine':'pinned-engine', 'engine_sha256':'1'*64}
|
|
for field, message in (('engine_sha256', 'R09 engine hash/version drift'),
|
|
('settings', 'R09 settings/config/environment drift'),
|
|
('environment', 'R09 settings/config/environment drift')):
|
|
altered = dict(baseline); altered[field] = 'drift'
|
|
with self.subTest(field=field), patch.object(C, 'load', return_value=altered), patch.object(
|
|
C, 'pinned_document', return_value=baseline):
|
|
with self.assertRaisesRegex(ValueError, message):
|
|
C.r09_runner_binding(ROOT, {}, pins)
|
|
# Version is in the SHA-pinned profile, not a worker engine invocation.
|
|
run = dict(baseline, commit=C.R09_COMMIT)
|
|
documents = [run, {}]
|
|
originals = [run, {'artifacts':{'os-pair/writer-profile.json':{'sha256':'1'*64}}}, {}]
|
|
with patch.object(C, 'load', side_effect=documents), patch.object(
|
|
C, 'pinned_document', side_effect=originals):
|
|
with self.assertRaisesRegex(ValueError, 'R09 engine hash/version drift'):
|
|
C.r09_runner_binding(ROOT, {'artifacts':{'os-pair/writer-profile.json':{'sha256':'2'*64}}}, pins)
|
|
|
|
def test_input_drift(self):
|
|
with patch.object(C.subprocess, 'run', return_value=SimpleNamespace(returncode=0)), patch.object(
|
|
C.subprocess, 'check_output', side_effect=[C.R09_COMMIT+'\n', b'', b'']), patch.object(
|
|
C, 'input_map', return_value={'fixture':'changed'}):
|
|
with self.assertRaisesRegex(ValueError, 'stale reuse input'):
|
|
C.r09_applicability(ROOT, {'commit':C.R09_COMMIT, 'inputs':{'fixture':'original'}})
|
|
|
|
|
|
class R16EnvironmentTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.original = Path('/recorded/r09')
|
|
env = {'PATH':'/opaque/original', 'P08_PLACEMENT_ROLE':'writer',
|
|
'P08_PLACEMENT_ROOT':str(self.original/'os-pair'),
|
|
'THE_TREE_AUDIO_TEARDOWN_PROOF_DIR':str(self.original/'os-runs/writer/teardown/owned-audio'),
|
|
'THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF':str(self.original/'os-runs/writer/teardown/control.json')}
|
|
self.pin = {'role':'writer', 'environment':{k:v for k,v in env.items() if k.startswith(('P08_', 'THE_TREE_'))},
|
|
'environment_sha256':hashlib.sha256(json.dumps(env, sort_keys=True).encode()).hexdigest(),
|
|
'controller_sha256':'driver', 'recorder_sha256':'recorder', 'engine_sha256':'engine',
|
|
'command':['engine', '--script', 'recorder'], 'source_map':{'source':'hash'}}
|
|
self.receipts = [copy.deepcopy(self.pin) for _ in range(3)]
|
|
|
|
def verify(self):
|
|
C.r09_environment(self.receipts, self.pin, 'writer', self.original)
|
|
|
|
def test_driver_recording_positive(self):
|
|
self.assertNotEqual(self.pin['environment_sha256'], hashlib.sha256(
|
|
json.dumps(self.pin['environment'], sort_keys=True).encode()).hexdigest())
|
|
self.verify()
|
|
|
|
def test_subset_one_receipt(self):
|
|
self.receipts[0]['environment']['P08_PLACEMENT_ROOT'] = '/changed'
|
|
with self.assertRaisesRegex(ValueError, 'R09 environment subset cross-receipt drift'): self.verify()
|
|
|
|
def test_subset_all_receipts_not_pin(self):
|
|
for r in self.receipts: r['environment']['P08_PLACEMENT_ROOT'] = '/changed'
|
|
with self.assertRaisesRegex(ValueError, 'R09 environment subset pin drift'): self.verify()
|
|
|
|
def test_hash_started_ended_difference(self):
|
|
self.receipts[0]['environment_sha256'] = '0'*64
|
|
with self.assertRaisesRegex(ValueError, 'R09 environment hash cross-receipt drift'): self.verify()
|
|
|
|
def test_hash_all_receipts_not_pin(self):
|
|
for r in self.receipts: r['environment_sha256'] = '0'*64
|
|
with self.assertRaisesRegex(ValueError, 'R09 environment hash pin drift'): self.verify()
|
|
|
|
def test_receipt_bytes_not_pin(self):
|
|
data = json.dumps(self.pin).encode()
|
|
pin = {'bytes':len(data), 'sha256':hashlib.sha256(data).hexdigest()}
|
|
C.r09_receipt_bytes(data, pin)
|
|
with self.assertRaisesRegex(ValueError, 'R09 receipt bytes drift'):
|
|
C.r09_receipt_bytes(data.replace(b'writer', b'reader'), pin)
|
|
|
|
def test_role_mismatch(self):
|
|
for r in [self.pin]+self.receipts: r['environment']['P08_PLACEMENT_ROLE'] = 'reader'
|
|
with self.assertRaisesRegex(ValueError, 'R09 environment role mismatch'): self.verify()
|
|
|
|
def test_proof_path_mismatch(self):
|
|
for field in ('THE_TREE_AUDIO_TEARDOWN_PROOF_DIR', 'THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF'):
|
|
with self.subTest(field=field):
|
|
self.setUp()
|
|
for r in [self.pin]+self.receipts: r['environment'][field] = '/wrong'
|
|
with self.assertRaisesRegex(ValueError, 'R09 environment proof path mismatch'): self.verify()
|
|
|
|
def test_driver_changed_at_head(self):
|
|
C.r09_driver_bytes(b'original', b'original', b'original')
|
|
with self.assertRaisesRegex(ValueError, 'R09 driver bytes drift'):
|
|
C.r09_driver_bytes(b'original', b'changed', b'original')
|
|
|
|
|
|
class RealR05InfrastructureTests(unittest.TestCase):
|
|
"""Actual native snapshot, never a fabricated acceptance specimen."""
|
|
def setUp(self):
|
|
folder = ROOT/'tests/fixtures/p08-03'
|
|
provenance = json.loads((folder/'r05-reader-checkpoint-provenance.json').read_bytes())
|
|
raw = gzip.decompress((folder/'r05-reader-checkpoint.json.gz').read_bytes())
|
|
self.assertEqual(provenance['raw_sha256'],hashlib.sha256(raw).hexdigest())
|
|
self.assertEqual(provenance['raw_bytes'],len(raw))
|
|
self.node = json.loads(raw)
|
|
self.model = C.decode(self.node)
|
|
|
|
def test_real_reader_checkpoint(self):
|
|
self.assertEqual(self.model,C.state(self.node))
|
|
|
|
def reject(self, mutate, message):
|
|
runtime = self.model['systems']['runtime']; mutate(runtime)
|
|
with self.assertRaisesRegex(ValueError,message): C.state(F.typed(self.model))
|
|
|
|
def test_missing_projection(self):
|
|
self.reject(lambda r:r['facility_infrastructure']['projections'].pop(),'missing/extra infrastructure projection')
|
|
|
|
def test_extra_projection(self):
|
|
def mutate(r):
|
|
p=copy.deepcopy(r['facility_infrastructure']['projections'][0]); p['instance_id']='ghost'
|
|
r['facility_infrastructure']['projections'].append(p)
|
|
self.reject(mutate,'missing/extra infrastructure projection')
|
|
|
|
def test_wrong_projected_cells(self):
|
|
self.reject(lambda r:r['facility_infrastructure']['projections'][0].update(cells=[[9,9,0]]),'wrong infrastructure projection')
|
|
|
|
def test_missing_route(self):
|
|
self.reject(lambda r:r['spatial_subslots']['entities'].pop(),'wrong infrastructure ROUTE topology')
|
|
|
|
def test_extra_route(self):
|
|
self.reject(lambda r:r['spatial_subslots']['entities'].append({'entity_id':'infra.route:ghost','category':'ROUTE','cells':[[8,4,0]]}),'extra infrastructure ROUTE topology')
|
|
|
|
def test_wrong_route(self):
|
|
self.reject(lambda r:r['spatial_subslots']['entities'][0].update(category='UTILITY'),'wrong infrastructure ROUTE topology')
|
|
|
|
def test_missing_connector(self):
|
|
self.reject(lambda r:r['path']['connectors'].pop(0),'wrong infrastructure connector topology')
|
|
|
|
def test_extra_connector(self):
|
|
def mutate(r):
|
|
c=copy.deepcopy(r['path']['connectors'][0]); c['id']='infra.connector:ghost'; r['path']['connectors'].append(c)
|
|
self.reject(mutate,'extra infrastructure connector topology')
|
|
|
|
def test_wrong_connector(self):
|
|
self.reject(lambda r:r['path']['connectors'][0].update(to=[8,0,0]),'wrong infrastructure connector topology')
|
|
|
|
def test_wrong_projected_walkable(self):
|
|
self.reject(lambda r:r['route_projection']['projected_walkable_cells'].pop(),'wrong projected walkable topology')
|
|
|
|
def test_wrong_path_cells(self):
|
|
self.reject(lambda r:r['path']['cell_traffic'].pop(),'wrong projected walkable topology')
|
|
|
|
def test_ordinary_facility_loss(self):
|
|
self.reject(lambda r:r['facilities']['instances'].clear(),'partial demolition/completion')
|
|
|
|
def test_proxy_occupancy_loss(self):
|
|
self.reject(lambda r:r['world']['occupancies'].pop(),'orphan/lost occupancy')
|
|
|
|
def test_proxy_operational_duplication(self):
|
|
self.reject(lambda r:r['facilities']['instances'].append(copy.deepcopy(r['construction']['facilities'][0])),'partial demolition/completion')
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|