Files
PaperClipAI/scripts/ci/pi-intel/test_startup_diagnostic.py
2026-10-02 09:09:30 -05:00

294 lines
21 KiB
Python

import copy,hashlib,json,os,subprocess,tempfile,unittest
from pathlib import Path
from startup_timing_patch import patch_sidecar,ORIGINAL_SHA
from startup_diagnostic import validate_pack_delta,validate_timing_sink,reject_sink_failure,post_run_integrity,finalize_diagnostic,PIN
from diagnostic_lifecycle import DiagnosticChild,DiagnosticInspection
from owned_processes import process_table,command_tokens,ProcessInspectionUnavailable
from unittest.mock import patch
from types import SimpleNamespace
class FakeProcess:
pid=111;returncode=0
def __init__(self,fail_wait=False):self.fail_wait=fail_wait
def poll(self):return 0
def wait(self,timeout):
if self.fail_wait:raise RuntimeError('wait failed')
return 0
class FakeOwner:
def __init__(self,fail_stop=False):self.fail_stop=fail_stop;self.stops=0
def observe(self,pid):pass
def stop(self,pid):
self.stops+=1
if self.fail_stop:raise RuntimeError('stop failed')
return {'status':'stopped'}
def live(self):return set()
class DiagnosticTests(unittest.TestCase):
@classmethod
def setUpClass(cls):cls.original=Path(os.environ['PI_DIAGNOSTIC_ORIGINAL_SIDECAR']).read_bytes()
def binding(self,root):
p=root/'startup-timings.jsonl';p.touch(mode=0o600);s=p.stat()
return p,{'path':str(p),**{k:str(getattr(s,'st_'+k)) for k in ['dev','ino','uid']}}
def test_patch_is_exact_additions_only(self):
sink={'path':'/private/tmp/pc-intel-diagnostic-fixture/startup-timings.jsonl','dev':'1','ino':'2','uid':'501'}
patched,proof=patch_sidecar(self.original,sink);text=patched.decode();shift=0
for change in proof['insertions']:
at=change['offset']+shift;self.assertEqual(text[at:at+len(change['inserted'])],change['inserted']);shift+=len(change['inserted'])
for change in reversed(proof['insertions']):
shift-=len(change['inserted']);at=change['offset']+shift;text=text[:at]+text[at+len(change['inserted']):]
self.assertEqual(text.encode(),self.original);self.assertEqual(hashlib.sha256(text.encode()).hexdigest(),ORIGINAL_SHA)
self.assertEqual(len(proof['phases']),54)
def test_current_bba_patch_and_exact_retained_provenance(self):
from startup_timing_patch import BBA_SHA
from retained_qualification import validate_original,RetainedChild
current=json.loads((Path(__file__).parent/'startup-diagnostic-bba-inputs.json').read_text())
original=Path(os.environ['PI_DIAGNOSTIC_BBA_SIDECAR']).read_bytes()
modified,proof=patch_sidecar(original,{'path':'/private/tmp/pc-intel-diagnostic-fixture/startup-timings.jsonl','dev':'1','ino':'2','uid':'501'},BBA_SHA)
self.assertEqual(proof['originalSha256'],BBA_SHA)
text=modified.decode();shift=sum(len(x['inserted']) for x in proof['insertions'])
for change in reversed(proof['insertions']):
shift-=len(change['inserted']);at=change['offset']+shift
self.assertEqual(text[at:at+len(change['inserted'])],change['inserted'])
text=text[:at]+text[at+len(change['inserted']):]
self.assertEqual(text.encode(),original);self.assertEqual(len(proof['phases']),54)
self.assertEqual(current['sourceRevision'],'bba63f51207de8513ad2d9593694f718fc60ef17')
self.assertEqual(current['artifactRunId'],'37004499595')
self.assertTrue(current['repairedObserver']);self.assertTrue(current['compilerProvenanceValidated'])
self.assertEqual(current['originalSidecarSha256'],BBA_SHA)
self.assertEqual(current['testSha256'],PIN['testSha256'])
validate_original(Path(os.environ['PI_DIAGNOSTIC_BBA_ORIGINAL_ARTIFACT']),current)
with self.assertRaisesRegex(RuntimeError,'original digest mismatch'):
patch_sidecar(original,{'path':'unused','dev':'1','ino':'2','uid':'3'})
workflow=(Path(__file__).parents[3]/'.github/workflows/docker-runner-check.yml').read_text()
job=workflow.split(' manual_pi_startup_diagnostic:',1)[1].split(' manual_pi_startup_overlap:',1)[0]
self.assertIn('--inputs bba',job);self.assertIn('artifacts/11226760000/zip',job)
self.assertIn(current['artifactZipSha256'],job)
fallback=workflow.split(' manual_image:',1)[1].split('\n needs:',1)[0]
self.assertIn('!inputs.diagnose_pi_startup',fallback)
def test_wrong_original_rejected(self):
with self.assertRaises(RuntimeError):patch_sidecar(self.original+b' ',{'path':'unused','dev':'1','ino':'2','uid':'3'})
def test_retained_f5_pack_identity_and_original_test_contract(self):
self.assertEqual(PIN['sourceRevision'],'f5c5fde380f60937ef26cc5a92e1d6a043e9cddc')
self.assertEqual(PIN['artifactRunId'],'36984851998')
self.assertEqual(PIN['artifactId'],'11218057612')
self.assertEqual(PIN['originalSidecarSha256'],ORIGINAL_SHA)
self.assertEqual(PIN['selectedFiles']['paperclip-runnerd']['sha256'],PIN['daemonSha256'])
self.assertEqual(PIN['selectedFiles']['resolved-pnpm-lock.yaml']['sha256'],PIN['resolvedLockSha256'])
self.assertEqual(PIN['testSha256'],'32bb057e1505082c0ed6b1d71cdd8cdb3271d920c2bea9cdc1ebddedf568c0c4')
self.assertEqual((PIN['executionCount'],PIN['providerCalls'],PIN['timeoutChanges']),(1,0,False))
self.assertEqual((PIN['archiveBytesMaximum'],PIN['retentionDays']),(268435456,7))
def test_f5_markers_follow_outer_layout_and_snapshot_boundaries(self):
_,proof=patch_sidecar(self.original,{'path':'/private/tmp/pc-intel-diagnostic-fixture/startup-timings.jsonl','dev':'1','ino':'2','uid':'501'})
phases=proof['phases']
self.assertFalse(any(p.startswith('pi.hash.') for p in phases))
for begin,end in [('pi.native_manifest.begin','pi.native_manifest.end'),('pi.discovery.begin','pi.discovery.end'),('pi.layout.begin','pi.layout.end'),('snapshot.directories.begin','snapshot.directories.end'),('snapshot.copy.begin','snapshot.copy.end'),('snapshot.seal.begin','snapshot.seal.end')]:
self.assertEqual(phases.count(begin),1);self.assertEqual(phases.count(end),1)
# Discovery recursion and descriptor-copy loops must not emit one row per file.
source=self.original.decode()
for begin,end in [(' const visit = async (directory) => {',' await visit(physicalRoot);'),(' const copyEntry = async (entry) => {',' const active = /* @__PURE__ */ new Set();')]:
start=source.index(begin);finish=source.index(end,start)
interior=[x for x in proof['insertions'] if start < x['offset'] < finish]
self.assertEqual(interior,[])
def test_pi_acp_markers_preserve_actual_admission_and_no_native_rpc_claim(self):
_,proof=patch_sidecar(self.original,{'path':'/private/tmp/pc-intel-diagnostic-fixture/startup-timings.jsonl','dev':'1','ino':'2','uid':'501'})
for phase in ['acp.initialize.begin','acp.initialize.response','pi.acp.session.request','pi.acp.correlated.response','pi.acp.mode.verified','pi.acp.thought_level.begin','pi.acp.thought_level.verified']:
self.assertEqual(proof['phases'].count(phase),1)
self.assertFalse(any('get_state' in phase for phase in proof['phases']))
source=self.original.decode()
selected=next(x for x in proof['insertions'] if 'Mark("pi.acp.thought_level.verified")' in x['inserted'])
self.assertTrue(source[:selected['offset']].endswith(' piThinking.assertReady();'))
def test_unknown_rebind_profile_rejects_before_any_pack_access(self):
result=subprocess.run([os.environ['PI_DIAGNOSTIC_TEST_NODE'],str(Path(__file__).parent/'rebind-diagnostic-pack.mjs'),'/nonexistent-pack','0'*64,'0'*40,'unknown'],capture_output=True,text=True,timeout=5)
self.assertNotEqual(result.returncode,0);self.assertIn('Unexpected diagnostic profile mode',result.stderr);self.assertNotIn('ENOENT',result.stderr)
def test_missing_terminal_retains_incomplete_classification(self):
with tempfile.TemporaryDirectory() as tmp:
p,b=self.binding(Path(tmp))
p.write_text(''.join(json.dumps({'phase':phase,'ns':str(i)})+'\n' for i,phase in enumerate(['sidecar.entry','session.open.begin','snapshot.copy.begin'])))
with self.assertRaisesRegex(RuntimeError,'terminal marks missing'):
validate_timing_sink(p,b,{'sidecar.entry','session.open.begin','snapshot.copy.begin','sidecar.exit'})
self.assertIn('snapshot.copy.begin',p.read_text())
def test_post_run_integrity_drains_checks_after_pack_failure(self):
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);names=['daemon','test','source.tar','source-input-inventory.json','resolved-pnpm-lock.yaml']
for name in names:(root/name).write_bytes(name.encode())
digest=lambda name:hashlib.sha256(name.encode()).hexdigest()
pin={'daemonSha256':digest('daemon'),'testSha256':digest('test'),'selectedFiles':{name:{'sha256':digest(name)} for name in names[2:]}}
with patch('startup_diagnostic.closed_tree',side_effect=RuntimeError('mutation')):
result=post_run_integrity(root,[],root/'daemon',root,root/'test',pin)
self.assertFalse(result['passed']);self.assertTrue(result['complete'])
self.assertEqual(result['checks'],{'pack':False,**{name:True for name in names}})
(root/'test').write_bytes(b'drift')
with patch('startup_diagnostic.closed_tree',return_value=[]):
result=post_run_integrity(root,[],root/'daemon',root,root/'test',pin)
self.assertFalse(result['passed']);self.assertFalse(result['checks']['test']);self.assertTrue(result['checks']['resolved-pnpm-lock.yaml'])
def test_partial_sink_finally_keeps_integrity_and_cleanup(self):
proof={'status':'diagnostic_failed','timing':{'completeSink':False},'cleanupUncertain':False};calls=[]
finalize_diagnostic(proof,None,lambda:(calls.append('integrity') or {'passed':True}),lambda:calls.append('retain'),lambda:calls.append('remove'))
self.assertEqual(calls,['integrity','retain','remove']);self.assertTrue(proof['scratchRemoved']);self.assertFalse(proof['timing']['completeSink']);self.assertEqual(proof['status'],'diagnostic_failed')
def test_finalization_integrity_and_retention_errors_do_not_block_cleanup(self):
for failed_phase in ['integrity','retain']:
proof={'cleanupUncertain':False};calls=[]
def step(name):
calls.append(name)
if name==failed_phase:raise RuntimeError(name)
return {'passed':True}
with self.assertRaises(RuntimeError):finalize_diagnostic(proof,None,lambda:step('integrity'),lambda:step('retain'),lambda:step('remove'))
self.assertEqual(calls,['integrity','retain','remove']);self.assertTrue(proof['scratchRemoved']);self.assertFalse(proof['cleanupUncertain'])
def test_uncertain_cleanup_still_records_integrity_but_retains_scratch(self):
active=DiagnosticChild(FakeOwner(fail_stop=True));active.child=FakeProcess();proof={'cleanupUncertain':False};calls=[]
with self.assertRaises(RuntimeError):finalize_diagnostic(proof,active,lambda:(calls.append('integrity') or {'passed':True}),lambda:calls.append('retain'),lambda:self.fail('uncertain scratch removed'))
self.assertEqual(calls,['integrity','retain']);self.assertTrue(proof['cleanupUncertain'])
def test_numeric_inode_rejected(self):
with self.assertRaises(RuntimeError):patch_sidecar(self.original,{'path':'/private/tmp/pc-intel-diagnostic-x/startup-timings.jsonl','dev':1,'ino':2,'uid':3})
def run_prelude(self,extra='',identity_mutation=None):
with tempfile.TemporaryDirectory(prefix='pc-intel-diagnostic-',dir='/private/tmp') as tmp:
root=Path(tmp);sink,binding=self.binding(root)
if identity_mutation:identity_mutation(binding)
_,proof=patch_sidecar(self.original,binding);prelude=next(r['inserted'] for r in proof['insertions'] if '// Explicit diagnostic derivative' in r['inserted'])
script=root/'fixture.cjs';script.write_text(prelude+'\n__pcStartupDiagnosticMark("sidecar.entry");\n'+extra+'\n__pcStartupDiagnosticMark("session.open.begin");\n')
run=subprocess.run([os.environ['PI_DIAGNOSTIC_TEST_NODE'],script],env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=10)
self.assertEqual(run.returncode,0)
if identity_mutation or extra:
with self.assertRaises(RuntimeError):validate_timing_sink(sink,{k:str(getattr(sink.stat(),'st_'+k)) for k in ['dev','ino','uid']},set(proof['phases']))
else:
rows=validate_timing_sink(sink,binding,set(proof['phases']))['rows'];self.assertEqual([r['phase'] for r in rows],['sidecar.entry','session.open.begin','sidecar.exit'])
return run.stderr,sink.stat().st_size
def test_sink_exact_identity_and_terminal(self):self.run_prelude()
def test_sink_wrong_inode_is_failure(self):self.assertIn('SINK_FAILED',self.run_prelude(identity_mutation=lambda b:b.update(ino='0'))[0])
def test_sink_late_mode_change_prevents_complete_diagnosis(self):self.assertIn('SINK_FAILED',self.run_prelude('__pcStartupDiagnosticFs.chmodSync(__pcStartupDiagnosticBinding.path,0o400);')[0])
def test_sink_existing_near_cap_cannot_overflow(self):
stderr,size=self.run_prelude('__pcStartupDiagnosticFs.appendFileSync(__pcStartupDiagnosticBinding.path,"x".repeat(32760-__pcStartupDiagnosticFs.statSync(__pcStartupDiagnosticBinding.path).size));')
self.assertIn('SINK_FAILED',stderr);self.assertLessEqual(size,32768)
def test_sink_row_cap_prevents_complete_diagnosis(self):self.assertIn('SINK_FAILED',self.run_prelude('for(let n=0;n<130;n++)__pcStartupDiagnosticMark("sidecar.dispatch");')[0])
def test_sink_parser_rejects_bad_rows(self):
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);p,b=self.binding(root)
for rows in [[{'phase':'sidecar.entry','ns':'2'},{'phase':'sidecar.exit','ns':'1'}],[{'phase':'unknown','ns':'1'}],[{'phase':'sidecar.entry','ns':'1','path':'secret'}]]:
p.write_text(''.join(json.dumps(r)+'\n' for r in rows))
with self.assertRaises(RuntimeError):validate_timing_sink(p,b,{'sidecar.entry','sidecar.exit'})
def test_explicit_failure_marker_rejects_even_with_valid_terminal(self):
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);p,b=self.binding(root)
p.write_text(''.join(json.dumps({'phase':phase,'ns':str(i)})+'\n' for i,phase in enumerate(['sidecar.entry','session.open.begin','sidecar.exit'])))
self.assertTrue(validate_timing_sink(p,b,{'sidecar.entry','session.open.begin','sidecar.exit'})['completeSink'])
log=root/'runnerd.stderr.log';log.write_text('PC_STARTUP_DIAGNOSTIC_SINK_FAILED\n')
with self.assertRaises(RuntimeError):reject_sink_failure([log])
def test_only_sidecar_and_manifest_delta(self):
before=[{'path':p,'kind':'file','mode':0o644,'sha256':'a','size':1} for p in ['dist/cli/acpx-runtime-sidecar.cjs','provider-pack.json','vendor']]
after=copy.deepcopy(before)
for row in after[:2]:row['sha256']='b'
self.assertEqual(len(validate_pack_delta(before,after)),2)
after[2]['sha256']='b'
with self.assertRaises(RuntimeError):validate_pack_delta(before,after)
def test_complete_small_evidence_admitted_with_diagnostic_cap(self):
from admit_evidence import admit_evidence
from admit_diagnostic_evidence import MAX_DIAGNOSTIC_BYTES
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);(root/'unique-sidecar').write_bytes(b'unique')
result=admit_evidence(root,MAX_DIAGNOSTIC_BYTES)
self.assertEqual(result['status'],'admitted_complete_evidence');self.assertEqual(result['maximumBytes'],268435456);self.assertEqual(result['retentionDays'],7)
self.assertEqual((root/'unique-sidecar').read_bytes(),b'unique')
def test_oversized_diagnostic_evidence_rejected_without_truncation(self):
from admit_evidence import admit_evidence
from admit_diagnostic_evidence import MAX_DIAGNOSTIC_BYTES
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);p=root/'oversized-unique-evidence'
with p.open('wb') as f:f.truncate(MAX_DIAGNOSTIC_BYTES+1)
result=admit_evidence(root,MAX_DIAGNOSTIC_BYTES)
self.assertEqual(result['status'],'rejected_storage_cap');self.assertFalse(result['truncated']);self.assertEqual(p.stat().st_size,MAX_DIAGNOSTIC_BYTES+1)
def test_cancel_before_spawn(self):
c=DiagnosticChild(FakeOwner());c.cancel('before');calls=[]
with self.assertRaises(RuntimeError):c.execute(lambda:calls.append(True),1)
self.assertEqual(calls,[]);self.assertFalse(c.uncertain)
def test_cancel_during_spawn_publishes_then_retires(self):
owner=FakeOwner();c=DiagnosticChild(owner);p=FakeProcess()
def factory():c.cancel('during');return p
with self.assertRaises(RuntimeError):c.execute(factory,1)
self.assertIs(c.child,p);self.assertTrue(c.retired);self.assertEqual(owner.stops,1)
def test_cancel_after_publication_retires_once(self):
owner=FakeOwner();c=DiagnosticChild(owner)
owner.observe=lambda pid:c.cancel('published')
with self.assertRaises(RuntimeError):c.execute(lambda:FakeProcess(),1)
self.assertTrue(c.retired);self.assertEqual(owner.stops,1)
def test_stop_exception_sticky_and_no_second_launch(self):
c=DiagnosticChild(FakeOwner(fail_stop=True))
with self.assertRaises(RuntimeError):c.execute(lambda:FakeProcess(),1)
self.assertTrue(c.uncertain)
with self.assertRaises(RuntimeError):c.execute(lambda:self.fail('second launch'),1)
def test_wait_exception_sticky(self):
c=DiagnosticChild(FakeOwner())
with self.assertRaises(RuntimeError):c.execute(lambda:FakeProcess(fail_wait=True),1)
self.assertTrue(c.uncertain)
def test_factory_exception_uncertain(self):
c=DiagnosticChild(FakeOwner())
def factory():raise OSError('spawn uncertainty')
with self.assertRaises(OSError):c.execute(factory,1)
self.assertTrue(c.uncertain)
class DiagnosticInspectionTests(unittest.TestCase):
def fixture(self,now=10,active_end=80,cleanup_end=None):
self.now=now
owner=SimpleNamespace(stop_deadline=cleanup_end)
child=SimpleNamespace(deadline=active_end,clock=lambda:self.now)
return owner,child,DiagnosticInspection(owner,child)
def test_shared_inspection_defaults_and_exact_commands(self):
with patch('owned_processes.subprocess.check_output',return_value='') as run:
self.assertEqual(process_table(),{})
run.assert_called_once_with(['/bin/ps','-axo','pid=,ppid=,lstart=,stat=,comm='],text=True,timeout=2)
with patch('owned_processes.subprocess.check_output',return_value='node /owned/sidecar') as run:
self.assertEqual(command_tokens(123),['node','/owned/sidecar'])
run.assert_called_once_with(['/bin/ps','-p','123','-o','command='],text=True,stderr=subprocess.DEVNULL,timeout=2)
def test_diagnostic_maximum_and_remaining_active_budget(self):
_,_,inspection=self.fixture()
with patch('owned_processes.subprocess.check_output',return_value='node') as run:
inspection.table();self.assertEqual(run.call_args.kwargs['timeout'],5)
inspection.argv(123);self.assertEqual(run.call_args.kwargs['timeout'],5)
self.now=79.75;inspection.table();self.assertEqual(run.call_args.kwargs['timeout'],.25)
self.now=79.9;inspection.argv(123);self.assertAlmostEqual(run.call_args.kwargs['timeout'],.1)
def test_cleanup_budget_replaces_expired_active_deadline(self):
owner,_,inspection=self.fixture(now=90,active_end=80,cleanup_end=190)
with patch('owned_processes.subprocess.check_output',return_value='node') as run:
inspection.table();self.assertEqual(run.call_args.kwargs['timeout'],5)
self.now=189.75;inspection.argv(123);self.assertEqual(run.call_args.kwargs['timeout'],.25)
def test_expired_or_unpublished_deadlines_never_launch_inspection(self):
for active_end,cleanup_end in [(10,None),(None,None),(80,10)]:
_,_,inspection=self.fixture(active_end=active_end,cleanup_end=cleanup_end)
with patch('owned_processes.subprocess.check_output') as run:
for invoke in [inspection.table,lambda:inspection.argv(123)]:
with self.assertRaises(RuntimeError):invoke()
run.assert_not_called()
def test_active_argv_failure_fatal_cleanup_policy_preserved(self):
owner,_,inspection=self.fixture()
with patch('owned_processes.subprocess.check_output',side_effect=subprocess.TimeoutExpired('ps',5)) as run:
with self.assertRaisesRegex(RuntimeError,'active argv inspection'):inspection.argv(123)
self.assertEqual(run.call_count,1)
owner.stop_deadline=110
with self.assertRaises(ProcessInspectionUnavailable):inspection.argv(123)
self.assertEqual(run.call_count,2)
def test_active_cadence_half_second_and_deadline_capped(self):
owner=FakeOwner();child=DiagnosticChild(owner);process=FakeProcess();process.poll=lambda:None
self.now=0;sleeps=[]
def sleep(duration):sleeps.append(duration);self.now+=duration
with self.assertRaisesRegex(RuntimeError,'outer deadline'):
child.execute(lambda:process,1.2,clock=lambda:self.now,sleep=sleep)
self.assertEqual(len(sleeps),3);self.assertEqual(sleeps[:2],[.5,.5]);self.assertAlmostEqual(sleeps[2],.2)
self.assertTrue(child.retired);self.assertEqual(owner.stops,1)
def test_active_table_timeout_fatal_retired_once_without_retry(self):
owner=FakeOwner();child=DiagnosticChild(owner);process=FakeProcess();process.poll=lambda:None
calls=[]
def observe(pid):
calls.append(pid)
if len(calls)==2:raise subprocess.TimeoutExpired('ps',5)
owner.observe=observe
with self.assertRaises(subprocess.TimeoutExpired):child.execute(lambda:process,70)
self.assertEqual(calls,[111,111]);self.assertEqual(owner.stops,1);self.assertTrue(child.retired)
with self.assertRaises(RuntimeError):child.execute(lambda:self.fail('second launch'),70)
def test_inspection_failure_with_cleanup_error_stays_uncertain(self):
owner=FakeOwner(fail_stop=True);child=DiagnosticChild(owner)
owner.observe=lambda pid:(_ for _ in ()).throw(subprocess.TimeoutExpired('ps',5))
with self.assertRaisesRegex(RuntimeError,'stop failed'):child.execute(lambda:FakeProcess(),70)
self.assertTrue(child.uncertain);self.assertEqual(owner.stops,1)
with self.assertRaises(RuntimeError):child.execute(lambda:self.fail('second launch'),70)
if __name__=='__main__':unittest.main()