Files
digital-drosophila/tools/normal-conditioning-audit/test_normal_finalize.py
2026-09-30 21:38:56 +08:00

122 lines
7.5 KiB
Python

"""Synthetic-only terminal guard tests, no science, training or final metrics."""
import copy
import json
import shlex
import sys
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import normal_finalize as F
class FinalizeTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
self.root=Path(self.temp.name).resolve();self.exp=self.root/'exp';self.control=self.root/'control';self.model=self.root/'model'
for p in (self.exp/'protocol',self.exp/'audit',self.control,self.model):p.mkdir(parents=True)
self.reg=dict(schema='normal-conditioning-registration-v1',scientific_scope='normal-acquisition-only',
subsequent_assays_authorized=False,normal_inventory={'fixture':True})
F.save(self.exp/'protocol/preregistered.json',self.reg)
self.sha=F.identity(self.exp/'protocol/preregistered.json')['sha256']
for name in ('launch-attempt.json','launch.json','supervisor-start.json'):
F.save(self.control/name,dict(synthetic=True,registration_sha256=self.sha))
self.pins={p.name:F.identity(p) for p in self.control.iterdir()}
self.kwargs=dict(exp=self.exp,control=self.control,model=self.model,regsha=self.sha,start_pins=self.pins,
census=lambda:dict(actual_OS_query=True,matching_live_processes=[]))
def terminal(self):
F.save(self.control/'exit.json',dict(registration_sha256=self.sha,
argv=[str(F.PYTHON),'-B',str(self.model/'normal_runner.py'),'--registration-sha256',self.sha],
exit_code=0,final_exit_observed=True,complete=True,error=None,
learning_success_claimed=False,scientific_analysis_required=True))
F.save(self.exp/'audit/execution-complete.json',dict(complete=True,registration_sha256=self.sha,
inventory=self.reg['normal_inventory'],learning_success_claimed=False))
def rewrite_fixture(self,path,update):
value=F.read(path);value.update(update)
# Test-owned file corruption, not application editing/retry behavior.
path.write_text(json.dumps(value),encoding='utf-8')
def test_actual_live_process_refuses_even_with_complete_files_and_no_writes(self):
self.terminal();before=sorted(str(p) for p in self.root.rglob('*'))
kwargs={**self.kwargs,'census':lambda:dict(actual_OS_query=True,matching_live_processes=[{'pid':123}])}
with self.assertRaisesRegex(ValueError,'still live'):F.readiness(**kwargs)
self.assertEqual(before,sorted(str(p) for p in self.root.rglob('*')))
def test_missing_process_without_real_exit_does_not_authorize_terminal_audit(self):
with self.assertRaisesRegex(ValueError,'missing process alone'):F.readiness(**self.kwargs)
def test_success_only_means_ready_not_learning_and_does_not_hash_brain_outputs(self):
self.terminal();F.save(self.exp/'unrelated-heavy-placeholder.json',{'not_read':True})
checked=[];original=F.identity
def identity(path):checked.append(str(path));return original(path)
with patch.object(F,'identity',side_effect=identity):result=F.readiness(**self.kwargs)
self.assertTrue(result['ready']);self.assertFalse(result['scientific_success_inferred'])
self.assertNotIn(str(self.exp/'unrelated-heavy-placeholder.json'),checked)
def test_changed_startup_binding_is_rejected(self):
self.terminal();self.rewrite_fixture(self.control/'launch.json',{'tampered':True})
with self.assertRaisesRegex(ValueError,'Startup'):F.readiness(**self.kwargs)
def test_nonzero_parent_exit_is_not_scientific_failure_or_success(self):
self.terminal();self.rewrite_fixture(self.control/'exit.json',{'exit_code':1,'complete':False})
with self.assertRaisesRegex(ValueError,'successful run exit'):F.readiness(**self.kwargs)
def test_completion_inventory_or_registration_mismatch_rejected(self):
self.terminal();self.rewrite_fixture(self.exp/'audit/execution-complete.json',{'inventory':{'shortened':True}})
with self.assertRaisesRegex(ValueError,'completion binding'):F.readiness(**self.kwargs)
def test_stopped_record_always_refuses_completion(self):
self.terminal();F.save(self.exp/'audit/stopped.json',dict(complete=False))
with self.assertRaisesRegex(ValueError,'Stopped run'):F.readiness(**self.kwargs)
def test_actual_process_query_required_not_caller_assumption(self):
self.terminal();kw={**self.kwargs,'census':lambda:dict(matching_live_processes=[])}
with self.assertRaisesRegex(ValueError,'Actual process'):F.readiness(**kw)
def test_argv_classifier_ignores_grep_substrings_and_includes_real_children(self):
worker=self.model.parent/'conditioning-interface-v2/isolated_workers.py'
validator=self.model.parent/'conditioning-execution-v1/validate_update_cpu.py'
def row(pid,*argv):return str(pid)+' '+shlex.join(argv)
rows='\n'.join([
row(10,'python','-B',str(self.model/'normal_runner.py'),'--registration-sha256','x'),
row(11,'python','-B',str(worker),'brain','--job',str(self.exp/'jobs/x.json')),
row(12,'python','-B',str(validator),'--authority',str(self.exp/'jobs/x.json')),
row(13,'bash','-c','grep '+str(self.model/'normal_runner.py')),
row(14,'python','-B',str(worker),'brain','--job','/different/exp/jobs/x.json'),
row(15,'python','-B',str(self.model/'normal_inputs.py'),'--registration-sha256','x')])
self.assertEqual([r['pid'] for r in F.live_runtime_rows(rows,self.model,self.exp)],[10,11,12,15])
def test_exclusive_save_never_overwrites(self):
path=self.root/'owned.json';F.save(path,{'first':True});before=path.read_bytes()
with self.assertRaises(FileExistsError):F.save(path,{'second':True})
self.assertEqual(before,path.read_bytes())
def test_real_harmless_CPU_child_exit_and_log_hashes_are_saved(self):
out=self.root/'new-audit';out.mkdir()
with patch.object(F,'OUT',out):
receipt=F.child('fixture',[sys.executable,'-B','-c','print("synthetic CPU fixture only")'],10)
self.assertEqual(receipt['exit_code'],0);self.assertTrue(receipt['actual_parent_observation'])
self.assertEqual(receipt['stdout'],F.identity(out/'fixture.stdout.log'))
self.assertIn('synthetic CPU fixture only',(out/'fixture.stdout.log').read_text())
def test_failed_CPU_child_preserves_exit_and_refuses_same_name_retry(self):
out=self.root/'failed-audit';out.mkdir()
with patch.object(F,'OUT',out):
with self.assertRaisesRegex(ValueError,'Audit child failed'):
F.child('fixture',[sys.executable,'-B','-c','raise SystemExit(7)'],10)
receipt=F.read(out/'fixture-exit.json')
self.assertEqual(receipt['exit_code'],7);self.assertFalse(receipt['complete'])
with self.assertRaises(FileExistsError):F.child('fixture',[sys.executable,'-B','-c','pass'],10)
def test_finalization_checks_live_guard_before_source_or_output_changes(self):
out=self.root/'should-stay-empty';out.mkdir()
with patch.object(F,'PYTHON',Path(sys.executable)),patch.object(F,'OUT',out),\
patch.object(F,'readiness',side_effect=ValueError('still live')),\
patch.object(F,'source_identity') as source:
with self.assertRaisesRegex(ValueError,'still live'):F.finalize()
source.assert_not_called()
self.assertEqual(list(out.iterdir()),[])
if __name__=='__main__':unittest.main()