Files
2026-09-30 21:06:22 +08:00

388 lines
25 KiB
Python

"""Pure temporary-file and mock-process tests; no sampler, Torch, brain or GPU."""
import copy
import importlib.util
from pathlib import Path
import subprocess
import tempfile
import time
from types import SimpleNamespace
import unittest
from unittest.mock import patch
import numpy as np
import normal_runner as R
import normal_prepare as P
def load_readonly(name, path):
spec=importlib.util.spec_from_file_location(name,path)
module=importlib.util.module_from_spec(spec)
exec(compile(path.read_bytes(),str(path),'exec'),module.__dict__)
return module
LAB=Path(__file__).resolve().parent.parent/'olfactory-conditioning-lab-v1'
COMPOSER=load_readonly('_runner_test_original_composer',LAB/'conditioning_inputs.py')
WORKER=load_readonly('_runner_test_original_worker',LAB.parent/'conditioning-interface-v2/isolated_workers.py')
def task(arm='paired', *, requested=True, kind='train', dan='PAM15', number=1):
value=dict(task_id=f'block/normal/{arm}/epoch-{number:03d}',kind=kind,steps=1200,
block_id='block',arm=arm,mode='physiological',dan_type=dan,
compartment_verbatim="y5B'2a" if dan=='PAM15' else "y2a'1",
delta_dependency=dict(kind='zero',source=None),feedback_to_training_or_idle=kind=='train')
if kind=='probe':
value['recipe']=dict(US_requested=False,D_command=0,weight_updates_enabled=False)
else:
value['recipe']=dict(us=dict(nominal_slot_tick=900,rng_seed=None,rng_draws=0,
requested=requested,tick=900 if requested else None),
D_command=1. if requested and arm!='d_zero' else 0.,update_enabled=arm!='frozen')
return value
class RunnerTests(unittest.TestCase):
def test_runtime_array_closure_matches_original_worker_not_directory_glob(self):
self.assertEqual(P.GRAPH_FILES,WORKER.GRAPH_FILES)
self.assertEqual(P.APL_FILES,WORKER.APL_FILES)
with tempfile.TemporaryDirectory() as folder:
root=Path(folder);graph=root/'graph';apl=root/'apl'
graph.mkdir();apl.mkdir()
# Additional reference arrays are neither consumed nor required.
for name in ('apl_out_ids.npy','input_raw.npy','output_raw.npy','pn_globals.npy'):
(apl/name).touch()
template=dict(graph_dir=str(graph),apl_bundle_dir=str(apl),coefficients_path=str(root/'coeff.json'))
paths=P.runtime_array_paths(template)
expected={str(graph/name) for name in WORKER.GRAPH_FILES} | {
str(apl/name) for name in WORKER.APL_FILES} | {template['coefficients_path']}
self.assertEqual(paths,expected);self.assertEqual(len(paths),12)
def setUp(self):
temporary=tempfile.TemporaryDirectory();self.addCleanup(temporary.cleanup)
self.root=Path(temporary.name).resolve()
for rel in ('protocol','jobs','cases','runs','updates','validation','results','receipts','logs','audit'):
(self.root/rel).mkdir()
self.addCleanup(patch.stopall)
patch.object(R,'EXP',self.root).start()
self.regfile=self.root/'protocol/preregistered.json';R.save(self.regfile,{'synthetic':True})
self.sha=R.digest(self.regfile)
self.w=R.Workflow.__new__(R.Workflow);self.w.sha=self.sha
self.w.start=time.perf_counter();self.w.completed=set();self.w.expected=[];self.w.tasks={}
self.w.overlay_dependency_pins={};self.w.case_dependency_pins={}
self.w.reg=dict(budget=dict(output_cap_bytes=8<<20,minimum_free_reserve_bytes=0,
workflow_timeout_seconds=60,child_timeout_seconds=10,not_after_utc='2099-01-01T00:00:00+00:00',
next_child_headroom_bytes=65536),runtime_input_sha256={},
input_sha256={},initial_overlay=dict(overlay_file=str(self.root/'protocol/zero.npz'),
overlay_manifest=str(self.root/'protocol/zero.json')))
R.save_npz(self.root/'protocol/zero.npz',dict(plastic_delta_mv=np.zeros(2,np.float32)))
R.save(self.root/'protocol/zero.json',dict(synthetic=True))
initial={p.relative_to(self.root).as_posix():R.identity(p) for p in (self.root/'protocol').iterdir()}
self.w.ledger=R.Ledger(self.root,self.sha,initial,8<<20,0)
def test_original_composer_no_US_D0_frozen_and_PPL_requests(self):
cs=dict(tick=np.array([10,20],np.int32),neuron_idx=np.array([3,4],np.int32))
for arm,requested,dan in (('paired',False,'PAM15'),('d_zero',True,'PAM15'),
('frozen',True,'PAM15'),('paired',True,'PPL103')):
item=task(arm,requested=requested,dan=dan)
ids=np.array([10,11,12,13] if dan=='PAM15' else [14,15],np.int32)
composed,us=R.compose_requests(cs,ids,item,COMPOSER)
self.assertEqual(len(us['tick']),len(ids) if requested else 0)
self.assertEqual(composed['audit']['US_attempts'],len(us['tick']))
self.assertEqual(item['recipe']['D_command'],0. if not requested or arm=='d_zero' else 1.)
self.assertEqual(item['recipe']['update_enabled'],arm!='frozen')
if requested:self.assertTrue(np.array_equal(us['neuron_idx'],ids))
bad=task('d_zero');bad['recipe']['D_command']=1.
with self.assertRaisesRegex(ValueError,'D/update'):R.compose_requests(cs,ids,bad,COMPOSER)
def test_probe_refuses_US_update_or_feedback(self):
cs=dict(tick=np.array([7],np.int32),neuron_idx=np.array([3],np.int32))
value=task(kind='probe');combined,us=R.compose_requests(cs,np.array([9,10],np.int32),value,COMPOSER)
self.assertEqual(len(us['tick']),0);self.assertEqual(combined['audit']['US_attempts'],0)
for role in ('US_requested','weight_updates_enabled','feedback_to_training_or_idle'):
bad=copy.deepcopy(value)
(bad if role=='feedback_to_training_or_idle' else bad['recipe'])[role]=True
with self.assertRaisesRegex(ValueError,'Probe command'):R.compose_requests(cs,np.array([9,10],np.int32),bad,COMPOSER)
def test_frozen_clock_and_reset_seed_retained(self):
original=dict(kind='fullgraph',dt_ms=.1,apl_delay_steps=18,precision='float64',
apl_precision='float32',record_history=False,seed=1234,training=False,cs_input_seed=999)
actual=R.derive_spec(original,task(),budget_root=self.root,output_cap=123456)
self.assertEqual(actual['seed'],1234);self.assertEqual(actual['steps'],1200)
self.assertEqual(actual['duration_ms'],120.);self.assertNotIn('cs_input_seed',actual)
self.assertFalse(actual['within_trial_weight_updates']);self.assertFalse(original['training'])
with self.assertRaisesRegex(ValueError,'fixed D18'):R.derive_spec(dict(original,apl_delay_steps=0),task(),budget_root=self.root,output_cap=1)
def test_actual_successful_parent_receipt_commits_ledger(self):
output=self.root/'runs/synthetic.txt'
job=dict(kind='cpu_verify',input_sha256={str(self.regfile):self.sha})
def fake_run(argv,**kwargs):
self.assertEqual(kwargs['env']['CUDA_VISIBLE_DEVICES'],'')
self.assertEqual(kwargs['env']['PYTHONDONTWRITEBYTECODE'],'1')
kwargs['stdout'].write(b'synthetic child only\n');output.write_bytes(b'new immutable output')
return SimpleNamespace(returncode=0)
with patch.object(R.subprocess,'run',side_effect=fake_run) as run:
self.w.launch('unit',job,['not-a-real-executable'],[output])
self.assertEqual(run.call_count,1);self.assertIn('unit',self.w.completed)
self.assertTrue(self.w.ledger.final_verify(['unit'])['pass_all'])
process=R.read(self.root/'receipts/unit/process.json')
self.assertEqual(process['exit_code'],0);self.assertTrue(process['complete'])
with self.assertRaisesRegex(ValueError,'rerun committed'):
self.w.launch('unit',job,['not-real'],[output])
def test_failed_child_is_uncommitted_unknown_and_cannot_rerun(self):
job=dict(kind='cpu_verify',input_sha256={str(self.regfile):self.sha})
with patch.object(R.subprocess,'run',return_value=SimpleNamespace(returncode=9)):
with self.assertRaisesRegex(ValueError,'Child failed'):self.w.launch('failed',job,['not-real'],[])
receipt=R.read(self.root/'receipts/failed/process.json')
self.assertFalse(receipt['complete']);self.assertEqual(receipt['exit_code'],9)
self.assertEqual(self.w.ledger.inspect()['uncommitted_entry_ids'],['failed'])
with self.assertRaises(FileExistsError):self.w.launch('failed',job,['not-real'],[])
def test_timed_out_child_never_gets_success_commit(self):
job=dict(kind='cpu_verify',input_sha256={str(self.regfile):self.sha})
with patch.object(R.subprocess,'run',side_effect=subprocess.TimeoutExpired('not-real',10)):
with self.assertRaisesRegex(ValueError,'Child failed'):self.w.launch('timeout',job,['not-real'],[])
receipt=R.read(self.root/'receipts/timeout/process.json')
self.assertTrue(receipt['timed_out']);self.assertIsNone(receipt['exit_code'])
self.assertNotIn('timeout',self.w.completed)
def test_spawn_OSerror_retains_failed_receipt_without_fake_exit(self):
job=dict(kind='cpu_verify',input_sha256={str(self.regfile):self.sha})
with patch.object(R.subprocess,'run',side_effect=OSError('synthetic spawn failure')):
with self.assertRaisesRegex(ValueError,'Child failed'):self.w.launch('spawn-error',job,['not-real'],[])
receipt=R.read(self.root/'receipts/spawn-error/process.json')
self.assertFalse(receipt['complete']);self.assertIsNone(receipt['exit_code'])
self.assertFalse(receipt['exit_observed_by_parent']);self.assertIn('synthetic spawn failure',receipt['launch_error'])
self.assertEqual(self.w.ledger.inspect()['uncommitted_entry_ids'],['spawn-error'])
def test_after_process_input_mutation_stops_before_commit(self):
dependency=self.root/'protocol/input.json';R.save(dependency,dict(value=1))
job=dict(kind='cpu_verify',input_sha256={str(dependency):R.digest(dependency)})
def fake_run(*a,**k):
dependency.write_bytes(b'{"value":2}');return SimpleNamespace(returncode=0)
with patch.object(R.subprocess,'run',side_effect=fake_run):
with self.assertRaisesRegex(ValueError,'Pinned input changed'):self.w.launch('mutation',job,['not-real'],[])
self.assertNotIn('mutation',self.w.completed)
def _delta_fixture(self):
old=task();new=task(number=2);new['delta_dependency']=dict(kind='epoch_delta_after',source=old['task_id']+'/delta-after')
self.w.tasks={old['task_id']:old};key=R.task_key(old)
self.w.completed.add(key+'--verify')
output=self.root/'updates'/key;output.mkdir()
R.save_npz(output/'delta-after.npz',dict(plastic_delta_mv=np.array([-.25,0.],np.float32)))
R.save(output/'overlay-manifest.json',dict(synthetic=True))
validation=self.root/'validation/previous.json';R.save(validation,dict(complete=True,
after_overlay_sha256=R.digest(output/'delta-after.npz'),after_manifest_sha256=R.digest(output/'overlay-manifest.json')))
process=self.root/f'receipts/{key}--verify/process.json';process.parent.mkdir()
R.save(process,dict(complete=True,exit_code=0,
entry_id=key+'--verify',registration_sha256=self.sha))
overlay=dict(overlay_file=str(output/'delta-after.npz'),overlay_manifest=str(output/'overlay-manifest.json'))
result=self.root/f'results/{key}.json'
R.save(result,dict(complete=True,task=old,cpu_validation=R.pin(validation),after_overlay=overlay,
cpu_process_receipt_path=str(process),update_dir=str(output)))
for path in (result,validation,process,output/'delta-after.npz',output/'overlay-manifest.json'):
self.w.ledger._tracked[path.relative_to(self.root).as_posix()]=R.identity(path)
return old,new,overlay,result,validation
def test_delta_dependency_requires_same_branch_completed_verify_and_receipt(self):
old,new,overlay,result,validation=self._delta_fixture()
self.w.completed.clear()
with self.assertRaisesRegex(ValueError,'CPU validation missing'):self.w.overlay(new)
self.w.completed.add(R.task_key(old)+'--verify')
self.assertEqual(self.w.overlay(new),overlay)
self.assertIn(str(result),self.w.overlay_dependency_pins)
self.assertIn(str(validation),self.w.overlay_dependency_pins)
for field in ('arm','block_id'):
altered=copy.deepcopy(new);altered[field]='unrelated'
with self.assertRaisesRegex(ValueError,'unrelated branch'):self.w.overlay(altered)
validation.write_bytes(b'{}')
with self.assertRaisesRegex(ValueError,'committed bytes'):self.w.overlay(new)
def test_alternate_valid_overlay_cannot_replace_previous_verified_delta(self):
_,new,_,result,_=self._delta_fixture()
alternative=self.root/'updates/other-valid';alternative.mkdir()
R.save_npz(alternative/'delta-after.npz',dict(plastic_delta_mv=np.array([-.5,0.],np.float32)))
R.save(alternative/'overlay-manifest.json',dict(synthetic=True,other=True))
for path in alternative.iterdir():self.w.ledger._tracked[path.relative_to(self.root).as_posix()]=R.identity(path)
record=R.read(result);record['update_dir']=str(alternative)
record['after_overlay']={k:str(alternative/name) for k,name in
(('overlay_file','delta-after.npz'),('overlay_manifest','overlay-manifest.json'))}
result.write_text(R.canonical(record),encoding='utf-8')
with self.assertRaisesRegex(ValueError,'committed bytes'):self.w.overlay(new)
# Even a synthetic "committed" bad result must fail its independent CPU output binding.
self.w.ledger._tracked[result.relative_to(self.root).as_posix()]=R.identity(result)
with self.assertRaisesRegex(ValueError,'independently verified output'):self.w.overlay(new)
def test_zero_dependency_does_not_carry_previous_lineage_pins(self):
self.w.overlay_dependency_pins={'stale':'0'*64}
self.assertEqual(self.w.overlay(task()),self.w.reg['initial_overlay'])
self.assertEqual(self.w.overlay_dependency_pins,{})
def _case_fixture(self):
item=task();template=self.root/'protocol/case-template.json'
R.save(template,dict(kind='fullgraph',dt_ms=.1,apl_delay_steps=18,precision='float64',
apl_precision='float32',record_history=False,seed=1234))
population=self.root/'protocol/populations.npz'
self.w.populations=dict(PAM15=np.array([10,11,12,13],np.int32),PPL103=np.array([14,15],np.int32))
R.save_npz(population,self.w.populations);self.w.reg['population_file']=str(population)
self.w.reg['input_sha256'][str(population)]=R.digest(population)
directory=self.root/'inputs/generated';directory.mkdir(parents=True)
source=directory/'source.npz';audit=directory/'source.json'
R.save_npz(source,dict(tick=np.array([5],np.int32),neuron_idx=np.array([3],np.int32)))
descriptor=dict(synthetic=True)
R.save(audit,dict(descriptor=descriptor,requests_sha256=R.digest(source)))
self.w.input_plan=dict(task_inputs={item['task_id']:'source'},descriptors={'source':descriptor})
self.w.inputs=COMPOSER
for path in (template,population,source,audit):
self.w.ledger._tracked[path.relative_to(self.root).as_posix()]=R.identity(path)
return item,template,source,audit
def test_prepare_case_reads_only_committed_template_and_shared_CS(self):
item,template,source,audit=self._case_fixture()
directory=self.w.prepare_case(item)
self.assertEqual(R.read(directory/'spec.json')['seed'],1234)
self.assertTrue({str(template),str(source),str(audit)} <= set(self.w.case_dependency_pins))
self.assertEqual(R.npz(directory/'us_requests.npz')['tick'].tolist(),[900]*4)
def test_changed_case_template_is_rejected_before_new_spec_write(self):
item,template,_,_=self._case_fixture();value=R.read(template);value['seed']=9876
template.write_text(R.canonical(value),encoding='utf-8')
with self.assertRaisesRegex(ValueError,'committed bytes'):self.w.prepare_case(item)
self.assertFalse((self.root/'cases'/R.task_key(item)/'spec.json').exists())
def test_changed_CS_and_self_rehashed_audit_are_both_rejected(self):
item,_,source,audit=self._case_fixture()
with source.open('wb') as stream:np.savez_compressed(stream,tick=np.array([9],np.int32),neuron_idx=np.array([4],np.int32))
value=R.read(audit);value['requests_sha256']=R.digest(source)
audit.write_text(R.canonical(value),encoding='utf-8')
with self.assertRaisesRegex(ValueError,'committed bytes'):self.w.prepare_case(item)
self.assertFalse((self.root/'cases'/R.task_key(item)/'requests.npz').exists())
def test_exclusive_helpers_reject_linked_parent_before_writes(self):
destination=self.root/'external';destination.mkdir();link=self.root/'alias'
try:link.symlink_to(destination,target_is_directory=True)
except OSError:self.skipTest('Platform disallows temporary symlinks')
for filename,write in (('x.json',lambda p:R.save(p,{'a':1})),
('x.npz',lambda p:R.save_npz(p,{'a':np.array([1],np.int32)}))):
with self.assertRaisesRegex(ValueError,'linked path'):write(link/filename)
self.assertFalse((destination/filename).exists())
def _mock_task(self,item):
"""Exercise real orchestration/control closures, with no scientific child."""
validator=self.root/'validator.py';validator.write_bytes(b'# not executed\n')
self.w.reg['input_sha256'][str(validator)]=R.digest(validator)
patch.object(R,'AQ',self.root).start()
target=self.root/'validate_update_cpu.py';target.write_bytes(validator.read_bytes())
self.w.reg['input_sha256'][str(target)]=R.digest(target)
for name in ('conditioning_epoch.py','conditioning_preflight_compare.py','engineering_compare.py'):
self.w.reg['input_sha256'][str(LAB/name)]=R.digest(LAB/name)
R.save(self.root/'protocol/epoch-template.json',dict(schema='synthetic',files={},frozen_inputs={}))
self.w.ledger._tracked['protocol/epoch-template.json']=R.identity(self.root/'protocol/epoch-template.json')
calls=[];authorities=[]
def case(task_value):
result=self.root/'cases'/R.task_key(task_value);result.mkdir()
R.save(result/'spec.json',dict(steps=1200));R.save_npz(result/'requests.npz',dict(tick=np.empty(0,np.int32),neuron_idx=np.empty(0,np.int32)))
return result
def ap_job(name,kind,output,**kwargs):
return dict(kind=kind,output_dir=str(output),receipt_file=str(self.root/f'receipts/{name}/worker.json'),
input_sha256={},authority_file=str(kwargs.get('authority','')))
def launch(name,job,argv,outputs,*,pre_outputs=(),post=None,ap=False):
calls.append(job['kind'])
if job['kind']=='brain':
out=Path(job['output_dir']);out.mkdir();R.save(out/'checkpoint-manifest.json',dict(synthetic=True))
R.save_npz(out/'state.npz',dict(step=np.empty(0,np.int32)))
for filename in ('process.json','worker.json'):
receipt=self.root/f'receipts/{name}/{filename}';receipt.parent.mkdir(exist_ok=True)
R.save(receipt,dict(synthetic=True))
self.w.ledger._tracked[receipt.relative_to(self.root).as_posix()]=R.identity(receipt)
elif job['kind']=='update':
authorities.append(R.read(job['authority_file']))
out=Path(job['output_dir']);out.mkdir()
R.save_npz(out/'delta-after.npz',dict(plastic_delta_mv=np.zeros(2,np.float32)))
R.save(out/'overlay-manifest.json',{});R.save(out/'chain-receipt.json',{})
else:
out=Path(job['update_dir'])
R.save(job['receipt_file'],dict(complete=True,frozen_U_verify_receipt_recomputed=True,
actual_sealed_events_used=True,after_overlay_sha256=R.digest(out/'delta-after.npz'),
epoch_authority_sha256=job['authority_sha256']))
if post is not None:post()
self.w.completed.add(name)
with patch.object(self.w,'prepare_case',side_effect=case),patch.object(self.w,'ap_job',side_effect=ap_job),\
patch.object(self.w,'launch',side_effect=launch),patch.object(self.w,'brain_verify',return_value={'synthetic':True}):
self.w.task(item)
return calls,authorities,R.read(self.root/f'results/{R.task_key(item)}.json')
def test_probe_orchestration_never_calls_E_or_CPU_update(self):
calls,authorities,record=self._mock_task(task(kind='probe'))
self.assertEqual(calls,['brain']);self.assertEqual(authorities,[])
self.assertNotIn('after_overlay',record);self.assertNotIn('cpu_validation',record)
def test_training_no_US_preserves_None_and_zero_D(self):
calls,authorities,record=self._mock_task(task(requested=False))
self.assertEqual(calls,['brain','update','cpu_verify'])
self.assertIsNone(authorities[0]['command']['us_tick'])
self.assertEqual(authorities[0]['command']['d_command'],0.)
self.assertFalse(authorities[0]['command']['frozen']);self.assertIn('after_overlay',record)
def test_D0_keeps_US_but_passes_zero_D_to_original_E(self):
calls,authorities,_=self._mock_task(task('d_zero'))
self.assertEqual(calls,['brain','update','cpu_verify'])
self.assertEqual(authorities[0]['command']['us_tick'],900)
self.assertEqual(authorities[0]['command']['d_command'],0.)
self.assertFalse(authorities[0]['command']['frozen'])
def test_frozen_keeps_D_request_but_explicitly_disables_update(self):
_,authorities,_=self._mock_task(task('frozen'))
self.assertEqual(authorities[0]['command']['d_command'],1.)
self.assertTrue(authorities[0]['command']['frozen'])
def test_PPL_compartment_not_replaced_by_PAM_or_sign_flip(self):
_,authorities,_=self._mock_task(task(dan='PPL103'))
self.assertEqual(authorities[0]['command']['compartment'],"y2a'1")
self.assertEqual(authorities[0]['command']['d_command'],1.)
self.assertEqual(authorities[0]['command']['trace_backend'],'torch')
def test_partial_task_cannot_silently_resume_remaining_children(self):
item=task();self.w.completed.add(R.task_key(item)+'--brain')
with patch.object(self.w,'launch') as launch:
with self.assertRaisesRegex(ValueError,'Partially completed task'):self.w.task(item)
launch.assert_not_called()
def test_real_AP_validation_accepts_brain_job_and_rejects_protected_outputs(self):
# Actual frozen parser only, using small synthetic files, no runtime load.
frozen=self.root/'frozen';frozen.mkdir();graph=frozen/'graph';graph.mkdir();apl=frozen/'apl';apl.mkdir()
for name in WORKER.GRAPH_FILES:(graph/name).write_bytes(b'synthetic graph bytes')
for name in WORKER.APL_FILES:(apl/name).write_bytes(b'synthetic APL bytes')
for name in ('mask.npy','base.npy','coeff.json'):(frozen/name).write_bytes(b'synthetic authority')
case=self.root/'cases/first';case.mkdir()
R.save(case/'spec.json',dict(graph_dir=str(graph),apl_bundle_dir=str(apl),coefficients_path=str(frozen/'coeff.json')))
R.save_npz(case/'requests.npz',dict(tick=np.empty(0,np.int32),neuron_idx=np.empty(0,np.int32)))
overlay=self.w.reg['initial_overlay'];manifest=Path(overlay['overlay_manifest'])
am=LAB.parent/'wholebrain-plastic-interface-v1/overlay.py'
manifest.write_text(R.canonical(dict(mask_path=str(frozen/'mask.npy'),mask_sha256=R.digest(frozen/'mask.npy'),
base_candidate_path=str(frozen/'base.npy'),base_candidate_sha256=R.digest(frozen/'base.npy'),
am_overlay_path=str(am),am_overlay_sha256=R.digest(am))),encoding='utf-8')
self.w.reg['runtime_input_sha256']={p:v['sha256'] for p,v in R.inventory(frozen).items()}
self.w.reg['runtime_input_sha256'][str(am)]=R.digest(am);self.w.h=WORKER
name='parser-brain';job=self.w.ap_job(name,'brain',self.root/'runs/first',case=case,overlay=overlay)
Path(job['receipt_file']).parent.mkdir()
file=self.root/'jobs/parser.json';R.save(file,job)
parsed,paths,_,_=WORKER.validate_job(file,R.digest(file),'brain')
self.assertEqual(parsed['diagnostic_mode'],'light');self.assertFalse(paths['output'].exists())
for field in ('output_dir','receipt_file','build_root'):
altered=copy.deepcopy(job);altered[field]=str(case/('forbidden-'+field))
alternative=self.root/'jobs'/(field+'.json');R.save(alternative,altered)
with self.assertRaisesRegex(ValueError,'protected input/source'):
WORKER.validate_job(alternative,R.digest(alternative),'brain')
self.assertFalse((case/'forbidden-output_dir').exists())
def test_separate_receipt_directories_allow_original_worker_lineage_protection(self):
# Real AP path validator should not interpret a previous receipt as
# authorizing writes into its parent. New receipts must be siblings.
prior=self.root/'receipts/previous';prior.mkdir()
previous=prior/'process.json';R.save(previous,dict(synthetic=True))
self.w.overlay_dependency_pins={str(previous):R.digest(previous)}
self.test_real_AP_validation_accepts_brain_job_and_rejects_protected_outputs()
if __name__=='__main__':unittest.main()