diff --git a/.github/workflows/docker-runner-check.yml b/.github/workflows/docker-runner-check.yml index a90335fcb5..742d9732a1 100644 --- a/.github/workflows/docker-runner-check.yml +++ b/.github/workflows/docker-runner-check.yml @@ -32,7 +32,7 @@ on: type: boolean default: false diagnose_pi_startup: - description: "Instrument one retained W native Intel startup with unchanged deadlines (diagnostic only, no provider prompts)" + description: "Instrument one retained B9 profile12 native Intel startup with unchanged deadlines (diagnostic only, no provider prompts)" type: boolean default: false diagnose_ajv_pack: @@ -191,7 +191,7 @@ jobs: test "$SOURCE_E2E_SUPPORT_ONLY" != true test "$DIAGNOSE_AJV" != true test "$IMAGE_MODE" != true - test "$EXPECTED_SOURCE_SHA" = efe019a79f50440d7bd6c3bc6c75fb8f18953093 + test "$EXPECTED_SOURCE_SHA" = b9e5d6ecdb05ab7244c90976e07c950f8d09b15b test "$EXPECTED_LOCK_SHA256" = 38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba test "$(gh api "repos/$REPOSITORY" --jq '.visibility')" = public fi @@ -257,7 +257,7 @@ jobs: if-no-files-found: error manual_pi_startup_diagnostic: - name: Retained W native Intel startup timing diagnostic (not qualification) + name: Retained B9 profile12 native Intel startup timing diagnostic (not qualification) if: github.event_name == 'workflow_dispatch' && inputs.diagnose_pi_startup needs: authorize_manual runs-on: macos-15-intel @@ -278,16 +278,16 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - gh api repos/paperclipai/paperclip/actions/artifacts/11204971146/zip \ - > "$RUNNER_TEMP/pi-startup-36952019178.zip" - printf '%s %s\n' 5f9bfabcb5a3d1b3180c2eb6f0eb8ebe58770b0c63a8b7dcbd379729b6c468f9 \ - "$RUNNER_TEMP/pi-startup-36952019178.zip" | shasum -a 256 --check + gh api repos/paperclipai/paperclip/actions/artifacts/11208570718/zip \ + > "$RUNNER_TEMP/pi-startup-36959948724.zip" + printf '%s %s\n' b82cf6c843006781e2e6d5f0a06e0ff39497ffc457b94a511f8bdda66628aa43 \ + "$RUNNER_TEMP/pi-startup-36959948724.zip" | shasum -a 256 --check - name: Run one instrumented startup with original assertions and deadlines timeout-minutes: 18 run: | set -euo pipefail python3 -B trusted-ci/scripts/ci/pi-intel/startup_diagnostic.py \ - --archive "$RUNNER_TEMP/pi-startup-36952019178.zip" \ + --archive "$RUNNER_TEMP/pi-startup-36959948724.zip" \ --output "$GITHUB_WORKSPACE/pi-startup-diagnostic-evidence" - name: Admit complete diagnostic evidence within the storage bound if: always() diff --git a/scripts/ci/pi-intel/startup-diagnostic-inputs.json b/scripts/ci/pi-intel/startup-diagnostic-inputs.json index ffc48b2001..8ba1f5128c 100644 --- a/scripts/ci/pi-intel/startup-diagnostic-inputs.json +++ b/scripts/ci/pi-intel/startup-diagnostic-inputs.json @@ -1,57 +1,57 @@ { "schema": "paperclip.native-intel-startup-diagnostic-input/v1", - "sourceRevision": "efe019a79f50440d7bd6c3bc6c75fb8f18953093", + "sourceRevision": "b9e5d6ecdb05ab7244c90976e07c950f8d09b15b", "resolvedLockSha256": "38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba", - "artifactRunId": "36952019178", - "artifactId": "11204971146", - "artifactWorkflowRevision": "31e7bc15b00ba651b9c2e321d425a7e6320a48f9", - "artifactZipBytes": 1129002006, - "artifactZipSha256": "5f9bfabcb5a3d1b3180c2eb6f0eb8ebe58770b0c63a8b7dcbd379729b6c468f9", + "artifactRunId": "36959948724", + "artifactId": "11208570718", + "artifactWorkflowRevision": "9541eb18789bac35242eeea3e569b77fa90d1818", + "artifactZipBytes": 1076365959, + "artifactZipSha256": "b82cf6c843006781e2e6d5f0a06e0ff39497ffc457b94a511f8bdda66628aa43", "selectedFiles": { "receipt.json": { - "bytes": 26708, - "sha256": "338d7d93ba07b09d55de9a54640eb57c498bb4b35bc7ab1ad02d8fdbb72eb44a" + "bytes": 32763, + "sha256": "47ef1f494c59c1249ac42f08308a2fb83edcb0759890960059cc367ebdf12bf0" }, "provider-pack.tar.gz": { - "bytes": 1029188649, - "sha256": "680d5a7ed3cb7813fbce0a994d0b91b7ec4991b538da640ed9cc5175baf4b60e" + "bytes": 1029214229, + "sha256": "5a550004ca982c49a2d86b3196676f3ce1877b6655ec730d1971b4cec2ba547d" }, "provider-pack.json": { "bytes": 3629, - "sha256": "ffb81f911990dc66558a277ab8ec20698af03e671f44bdd8a41fda0f392e4e6a" + "sha256": "db5b1d7d825a0533a6506755c6209ccc4d45776c3abbf6c87ff845d1d47e99b2" }, "pack-inventory.json": { "bytes": 10633748, - "sha256": "5d2fbdb0976ae1f4431cf66ee3611c2104d23a3ce04910be8941f8a03e36d710" + "sha256": "172f0f4f2319f3716cca656cdeafd03b4f0db652def53101dfbf148850a20eaa" }, "paperclip-runnerd": { "bytes": 32035568, - "sha256": "a0c7ace7ad0be9ac5b5f9c58749d9c452cd3a1ef143a38d5120d8e4f2bf82d6a" + "sha256": "29d5b0fc2ae2a552b89a107e3777e2c8ee356ebdbb13a14ceab8865e1cea1d45" }, "source.tar": { - "bytes": 141690880, - "sha256": "587c0f60a30f0ca4b43361830cb611fcaa4a83f492cb7a9fd66b7908b45f72be" + "bytes": 141742080, + "sha256": "b32d8e32639e6527804fe11bfa1aaefcefa87fca31e9155499e6b9a28f69dbd4" }, "source-input-inventory.json": { - "bytes": 1033276, - "sha256": "1356cd9a81d4c94e477a50cf2879efc7002fd687337fdad121f0c2381689ab36" + "bytes": 1033702, + "sha256": "7c160bb1fc4d049378004d014e398d42dabf12e8a73ec9cdda7679113aaa6949" }, "resolved-pnpm-lock.yaml": { "bytes": 607788, "sha256": "38338a6867358440c5ab5993eaeb85fb501bc7df65d24acf52fbd63c880ee4ba" } }, - "originalSidecarSha256": "1f0ee222b2edaad997b898aec6580982afc179fc60e5abc87fa3e6f2386a691f", - "originalPackDigest": "sha256:ae455e79f07f189e6d5d2bb9eeb01d26040e4b19370bfcda47d288c7b1d60b24", - "daemonSha256": "a0c7ace7ad0be9ac5b5f9c58749d9c452cd3a1ef143a38d5120d8e4f2bf82d6a", + "originalSidecarSha256": "7462825f4b82bce8e2a69425eafe4ca8b2c8812df345269c82fb75aa21f3043c", + "originalPackDigest": "sha256:9cf4f88d9fcf63746f7716f773cb8d062544c19e0a78e111e60f659a3596b13f", + "daemonSha256": "29d5b0fc2ae2a552b89a107e3777e2c8ee356ebdbb13a14ceab8865e1cea1d45", "nodeSha256": "7abcf39bd37ab251015337ff75304d7555f0d8e88c6e0fbf04bce8ce34636f49", "testPath": "packages/paperclip-runner/test/pi-closed-startup.test.mjs", "testSha256": "8967cf9c8cd130b68bf9d64abef8cb8d352af00646e2288b341d8c6ae758b47a", "observerSha256": "1fcf353beb90722f90401a7792a5853bf0807d964a1065e3f55b76a63bc4b4cc", - "profileDigest": "sha256:5e276f48c8a87b3e6165369faac62d3925282c84b98934575b1b7b97ad50b309", - "closureDigest": "sha256:8c6e7802d47da9410af56e367f6cf7af572fcbf3e361627cee026e8cffc9bdd3", + "profileDigest": "sha256:47306e6d2a9b59e8f9189f725ebb7a0a7f91826044d1739e1a35ab31f228ba1f", + "closureDigest": "sha256:03351f4a250a8db0e79411a9079b43a0ff05f72a2aff41fae17f1fc2de24bd41", "model": "openrouter/deepseek/deepseek-v4-flash-0731", - "originalFailure": "Durable session.open failed: ACPX sidecar request timed out at session.open; drain issued 30.275s after open; secondary close could not durably suspend provider;44SDK contracts passed; cleanup verified; no report.json produced", + "originalFailure": "Original 30s session.open timeout; settled35032ms; runner exit0 and owned cleanup verified;48SDK contracts passed; fresh Rust daemon and complete pack passed.", "executionCount": 1, "providerCalls": 0, "timeoutChanges": false, diff --git a/scripts/ci/pi-intel/startup_diagnostic.py b/scripts/ci/pi-intel/startup_diagnostic.py index 106ced90af..58d9e036c8 100644 --- a/scripts/ci/pi-intel/startup_diagnostic.py +++ b/scripts/ci/pi-intel/startup_diagnostic.py @@ -58,13 +58,49 @@ def reject_sink_failure(logs): require(stat.S_ISREG(st.st_mode) and st.st_nlink==1 and st.st_size<=1024*1024,'Diagnostic stderr is missing, linked or oversized') require(b'PC_STARTUP_DIAGNOSTIC_SINK_FAILED' not in path.read_bytes(),'Explicit diagnostic sink failure marker') +def post_run_integrity(pack,inventory,daemon,original,test,pin): + # Every independent check runs even if another fails or timing was incomplete. + checks={};errors={} + actions={'pack':lambda:closed_tree(pack)==inventory, + 'daemon':lambda:sha(daemon)==pin['daemonSha256'], + 'test':lambda:sha(test)==pin['testSha256']} + for name in ['source.tar','source-input-inventory.json','resolved-pnpm-lock.yaml']: + actions[name]=lambda name=name:sha(original/name)==pin['selectedFiles'][name]['sha256'] + for name,check in actions.items(): + try: + require(check(),'Post-run identity mismatch: '+name);checks[name]=True + except BaseException as error:checks[name]=False;errors[name]=type(error).__name__+': '+str(error) + return {'complete':True,'passed':not errors,'checks':checks,'errors':errors} + +def finalize_diagnostic(proof,active,post_check,retain,remove): + errors=[] + try: + if active is not None: + if active.child is not None:active.retire(active.child) + proof['cleanupUncertain']=active.uncertain + except BaseException as error: + proof['cleanupUncertain']=True;errors.append('owned cleanup: '+repr(error)) + try: + proof['postRunIntegrity']=post_check() + if not proof['postRunIntegrity']['passed']:errors.append('post-run integrity failed') + except BaseException as error:errors.append('post-run integrity: '+repr(error)) + try:retain() + except BaseException as error:errors.append('evidence retention: '+repr(error)) + if not proof['cleanupUncertain']: + try:remove();proof['scratchRemoved']=True + except BaseException as error:proof['cleanupUncertain']=True;errors.append('scratch cleanup: '+repr(error)) + else:errors.append('scratch retained after uncertain owned cleanup') + if errors: + proof.update(status='diagnostic_failed',finalizationErrors=errors) + raise RuntimeError('; '.join(errors)) + def execute(args): out=args.output.resolve();out.mkdir(mode=0o700,parents=True,exist_ok=False) scratch=Path(tempfile.mkdtemp(prefix='pc-intel-diagnostic-',dir='/private/tmp'));scratch.chmod(0o700) scratch_id=(scratch.stat().st_dev,scratch.stat().st_ino,scratch.stat().st_uid) proof={'schema':'paperclip.native-intel-startup-diagnostic/v1','status':'preparing','sourceRevision':PIN['sourceRevision'],'runId':os.environ['GITHUB_RUN_ID'],'trustedWorkflowRevision':os.environ['GITHUB_WORKFLOW_SHA'],'originalQualificationRunId':PIN['artifactRunId'],'startedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'isQualification':False,'providerCalls':0,'credentialsRead':False,'runtimePromptsSubmitted':0,'nativeRecompiled':False,'dependencyInstallExecuted':False,'testSourceUnmodified':True,'deadlineChanged':False,'helpers':{p.name:sha(p) for p in HERE.iterdir() if p.is_file()},'commands':[],'cleanupUncertain':False,'scratchRemoved':False,'hardwareBareMetalClaim':False,'physicalColdDiskClaim':False,'cacheNote':'Artifact integrity verification necessarily reads payload before testing; no startup warmup is performed.'} def save():atomic_json(out/'receipt.json',proof) - active=None + active=None;pack=None;after=None;daemon=None;original=None;test=None def cancelled(sig,_): if active is not None:active.cancel('signal '+str(sig)) else:raise InterruptedError('Diagnostic cancelled') @@ -104,7 +140,7 @@ def execute(args): sidecar=pack/'dist/cli/acpx-runtime-sidecar.cjs';original_sidecar=sidecar.read_bytes();patched,patch=patch_sidecar(original_sidecar,identity) # Preserve original file mode; this one private copy now has an explicit diagnostic identity. sidecar.write_bytes(patched);atomic_json(out/'sidecar-patch.json',patch) - (out/'sidecar.diff').write_text(''.join(difflib.unified_diff(original_sidecar.decode().splitlines(True),patched.decode().splitlines(True),fromfile='original-W-sidecar',tofile='diagnostic-sidecar'))) + (out/'sidecar.diff').write_text(''.join(difflib.unified_diff(original_sidecar.decode().splitlines(True),patched.decode().splitlines(True),fromfile='original-B9-sidecar',tofile='diagnostic-sidecar'))) command('diagnostic-sidecar-syntax',[node,'--check',sidecar]) proof['diagnosticPackVerification']=json.loads(command('diagnostic-pack-rebind',[node,HERE/'rebind-diagnostic-pack.mjs',pack,PIN['originalSidecarSha256'],PIN['sourceRevision']],180)) after_manifest=json.loads((pack/'provider-pack.json').read_text());expected=copy.deepcopy(before_manifest) @@ -140,24 +176,25 @@ def execute(args): reject_sink_failure([out/'closed-startup.log',*retained.rglob('runnerd.stderr.log')]) proof['timing']=validate_timing_sink(sink,identity,set(patch['phases']));shutil.copy2(sink,out/'startup-timings.jsonl') atomic_json(out/'timing-summary.json',proof['timing']) - require(closed_tree(pack)==after and sha(daemon)==PIN['daemonSha256'],'Diagnostic test mutated pack or daemon') proof.update(status='diagnostic_complete_original_test_passed' if result==0 else 'diagnostic_complete_original_test_failed',isQualification=False,finishedAt=datetime.datetime.now(datetime.timezone.utc).isoformat());save() return result except BaseException as error: - proof.update(status='diagnostic_failed',errorType=type(error).__name__,error=str(error),finishedAt=datetime.datetime.now(datetime.timezone.utc).isoformat());raise + proof.update(status='diagnostic_failed',errorType=type(error).__name__,error=str(error),finishedAt=datetime.datetime.now(datetime.timezone.utc).isoformat()) + if 'timing' not in proof:proof['timing']={'completeSink':False,'error':type(error).__name__+': '+str(error),'partialEvidenceRetained':(scratch/'startup-timings.jsonl').is_file()} + raise finally: - try: - if active is not None: - if active.child is not None:active.retire(active.child) - proof['cleanupUncertain']=active.uncertain + def post_check(): + if after is None or test is None:return {'complete':False,'passed':False,'reason':'diagnostic pack/test preparation not completed'} + return post_run_integrity(pack,after,daemon,original,test,PIN) + def retain(): + if (out/'retained-test-state').is_dir():proof['retainedTestStateInventory']=closed_tree(out/'retained-test-state') if (scratch/'startup-timings.jsonl').is_file() and not (out/'startup-timings.jsonl').exists(): st=(scratch/'startup-timings.jsonl').lstat() if stat.S_ISREG(st.st_mode) and st.st_nlink==1 and st.st_size<=32768:shutil.copyfile(scratch/'startup-timings.jsonl',out/'startup-timings.jsonl') - require(not proof['cleanupUncertain'],'Retain scratch after uncertain owned process cleanup') + def remove(): st=scratch.lstat();require((st.st_dev,st.st_ino,st.st_uid)==scratch_id and stat.S_ISDIR(st.st_mode),'Scratch identity changed') - shutil.rmtree(scratch);proof['scratchRemoved']=True - except BaseException as cleanup_error: - proof.update(cleanupUncertain=True,cleanupError=type(cleanup_error).__name__+': '+str(cleanup_error));raise + shutil.rmtree(scratch) + try:finalize_diagnostic(proof,active,post_check,retain,remove) finally:save() if __name__=='__main__': diff --git a/scripts/ci/pi-intel/startup_timing_patch.py b/scripts/ci/pi-intel/startup_timing_patch.py index b8874cec6c..418cc825ce 100644 --- a/scripts/ci/pi-intel/startup_timing_patch.py +++ b/scripts/ci/pi-intel/startup_timing_patch.py @@ -1,7 +1,7 @@ """Sparse additive instrumentation of one pinned bundle; never edits vendor closure bytes.""" import hashlib,json from source_guard import require -ORIGINAL_SHA='1f0ee222b2edaad997b898aec6580982afc179fc60e5abc87fa3e6f2386a691f' +ORIGINAL_SHA='7462825f4b82bce8e2a69425eafe4ca8b2c8812df345269c82fb75aa21f3043c' PREFIX='__pcStartupDiagnostic' def patch_sidecar(original,sink): @@ -39,8 +39,9 @@ def patch_sidecar(original,sink): mark('// src/drivers/acpx/pi-verified-runtime.ts','async function verifyPiRuntimeLayoutForNativeSnapshot(root, manifest, nativeManifest, expectedClosureSha256) {','pi.layout.begin',True) mark('// src/drivers/acpx/pi-verified-runtime.ts',' return { manifestDigest };','pi.layout.end') mark('// src/drivers/acpx/native-distribution-integrity.ts','async function createNativeAcpxDistributionSnapshot(input, entries) {','snapshot.begin',True) + mark('// src/drivers/acpx/native-distribution-integrity.ts',' for (const depth of [...parentLevels.keys()].sort((a, b) => a - b)) {','snapshot.directories.begin') mark('// src/drivers/acpx/native-distribution-integrity.ts',' const copyEntry = async (entry) => {','snapshot.directories.end') - mark('// src/drivers/acpx/native-distribution-integrity.ts',' for (let start = 0; start < entries.length; ) {','snapshot.copy.begin') + mark('// src/drivers/acpx/native-distribution-integrity.ts',' const active = /* @__PURE__ */ new Set();','snapshot.copy.begin') mark('// src/drivers/acpx/native-distribution-integrity.ts',' const executable = (0, import_node_path5.join)(packageRoot, ...input.executable.split("/"));','snapshot.copy.end') mark('// src/drivers/acpx/native-distribution-integrity.ts',' await directoryBatch([...directories], (path3) => (0, import_promises6.chmod)(path3, 320));','snapshot.seal.begin') mark('// src/drivers/acpx/native-distribution-integrity.ts',' return { commandDirectory, bootstrap, snapshot:','snapshot.seal.end') diff --git a/scripts/ci/pi-intel/test_startup_diagnostic.py b/scripts/ci/pi-intel/test_startup_diagnostic.py index f19d2148c2..d309e9ab9e 100644 --- a/scripts/ci/pi-intel/test_startup_diagnostic.py +++ b/scripts/ci/pi-intel/test_startup_diagnostic.py @@ -1,7 +1,7 @@ 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,PIN +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 @@ -37,28 +37,28 @@ class DiagnosticTests(unittest.TestCase): 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']),46) + self.assertEqual(len(proof['phases']),47) 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_w_pack_identity_and_original_test_contract(self): - self.assertEqual(PIN['sourceRevision'],'efe019a79f50440d7bd6c3bc6c75fb8f18953093') - self.assertEqual(PIN['artifactRunId'],'36952019178') - self.assertEqual(PIN['artifactId'],'11204971146') + def test_retained_b9_pack_identity_and_original_test_contract(self): + self.assertEqual(PIN['sourceRevision'],'b9e5d6ecdb05ab7244c90976e07c950f8d09b15b') + self.assertEqual(PIN['artifactRunId'],'36959948724') + self.assertEqual(PIN['artifactId'],'11208570718') 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'],'8967cf9c8cd130b68bf9d64abef8cb8d352af00646e2288b341d8c6ae758b47a') self.assertEqual((PIN['executionCount'],PIN['providerCalls'],PIN['timeoutChanges']),(1,0,False)) self.assertEqual((PIN['archiveBytesMaximum'],PIN['retentionDays']),(268435456,7)) - def test_w_markers_follow_outer_layout_and_snapshot_boundaries(self): + def test_b9_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.copy.begin','snapshot.copy.end'),('snapshot.seal.begin','snapshot.seal.end')]: + 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) => {',' for (let start = 0; start < entries.length; ) {')]: + 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,[]) @@ -69,6 +69,37 @@ class DiagnosticTests(unittest.TestCase): 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):