ReviewOS

also looking at this

stacks/drone

test: retain bounded browser launch evidence on failure

#70
Merged chris wants to merge codex/browser-startup-evidence into main
18 files +620 -23
scripts/qualify-runtime-cold-build.tsadded+134-0
Changes to scripts/qualify-runtime-cold-build.ts
@@ -0,0 +1,134 @@
1import assert from 'node:assert/strict'
2import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
3import { join, resolve } from 'node:path'
4import process from 'node:process'
5import { captureColdBuild, coldBuildQualificationTimeout } from '../tests/helpers/runtime-cold-build-supervisor'
6
7// Exercise actual build/test initialization across the former 20-second hook.
8// Tool witnesses forward to real CMake/CTest and retain actual child settlement.
9interface ToolEvent { event: 'start' | 'spawn' | 'exit', stage: string, groupId: number, pid: number, exitCode?: number, elapsedMs?: number }
10assert(process.platform !== 'win32', 'Cold build qualification requires POSIX process groups')
11assert(process.argv[2], 'Provide a new owned evidence directory')
12const reportingFault = process.argv[3] === '--interrupt-before-capture-report'
13assert(process.argv.length <= 4 && (!process.argv[3] || reportingFault), 'Unknown qualification mode')
14const repository = resolve(import.meta.dir, '..'), evidence = resolve(process.argv[2]!)
15await mkdir(evidence)
16if (reportingFault) await mkdir(join(evidence, 'capture-result.json'))
17const project = join(evidence, 'project'), tools = join(evidence, 'tools'), eventsPath = join(evidence, 'tool-events.ndjson')
18await mkdir(join(project, 'firmware'), { recursive: true })
19await mkdir(tools)
20await cp(join(repository, 'firmware/runtime'), join(project, 'firmware/runtime'), { recursive: true })
21await writeFile(join(project, 'bunfig.toml'), '[test]\npreload = []\n')
22const cmakePath = join(project, 'firmware/runtime/CMakeLists.txt'), cmake = await readFile(cmakePath, 'utf8')
23assert(cmake.startsWith('cmake_minimum_required(VERSION 3.20)'), 'Expected recorded runtime CMake entrypoint')
24await writeFile(cmakePath, cmake.replace('cmake_minimum_required(VERSION 3.20)', 'cmake_minimum_required(VERSION 3.20)\nexecute_process(COMMAND "${CMAKE_COMMAND}" -E sleep 21 COMMAND_ERROR_IS_FATAL ANY)'))
25assert(!await Bun.file(join(project, 'storage/runtime-build/host/CMakeCache.txt')).exists(), 'Qualification must start cold')
26assert(!/\s/.test(process.execPath), 'Tool witness needs a single executable shebang path')
27for (const tool of ['cmake', 'ctest']) {
28 const executable = Bun.which(tool)
29 assert(executable, `Missing native tool: ${tool}`)
30 const source = String.raw`#!${process.execPath}
31import { appendFileSync } from 'node:fs'
32const stage = ${JSON.stringify(tool)} === 'ctest' ? 'contracts' : process.argv[2] === '-S' ? 'configure' : 'compile'
33const began = performance.now()
34const owner = { stage, groupId: process.pid, pid: process.pid }
35appendFileSync(${JSON.stringify(eventsPath)}, JSON.stringify({ ...owner, event: 'start' }) + '\n')
36const child = Bun.spawn([${JSON.stringify(executable)}, ...process.argv.slice(2)], { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' })
37const event = { stage, groupId: process.pid, pid: child.pid }
38appendFileSync(${JSON.stringify(eventsPath)}, JSON.stringify({ ...event, event: 'spawn' }) + '\n')
39if (${reportingFault} && stage === 'configure') {
40 process.kill(child.pid, 0)
41 process.kill(process.ppid, 'SIGKILL')
42}
43const exitCode = await child.exited
44appendFileSync(${JSON.stringify(eventsPath)}, JSON.stringify({ ...event, event: 'exit', exitCode, elapsedMs: performance.now() - began }) + '\n')
45process.exit(exitCode)
46`
47 await writeFile(join(tools, tool), source, { mode: 0o700 })
48}
49const pattern = process.platform === 'linux' ? 'kernel-confirmed full input FIFO' : 'separate supervised boots'
50const buildCommandTimeoutMs = 120000, selectedTestTimeoutMs = process.platform === 'linux' ? 20000 : 5000
51const qualificationTimeoutMs = coldBuildQualificationTimeout(buildCommandTimeoutMs, selectedTestTimeoutMs)
52let result: Awaited<ReturnType<typeof captureColdBuild>> | undefined, failure: unknown
53const began = performance.now()
54try {
55 result = await captureColdBuild([process.execPath, 'test', join(repository, 'tests/unit/runtime.test.ts'), '--test-name-pattern', pattern], { cwd: project, env: { ...process.env, PATH: `${tools}:${process.env.PATH ?? ''}` }, timeoutMs: qualificationTimeoutMs })
56 failure = result.failure
57}
58catch (error) { failure = error }
59// Retain the capture outcome even if a later cleanup observation itself fails.
60const reportingErrors: { stage: string, code: string | null }[] = []
61try {
62 await writeFile(join(evidence, 'capture-result.json'), `${JSON.stringify({ exitCode: result?.exitCode ?? null, elapsedMs: performance.now() - began, failure: failure instanceof Error ? failure.message : failure ? String(failure) : null }, null, 2)}\n`)
63}
64catch (error) {
65 failure ??= error
66 reportingErrors.push({ stage: 'capture-result', code: (error as NodeJS.ErrnoException).code ?? null })
67}
68let eventText = ''
69try { eventText = await readFile(eventsPath, 'utf8') }
70catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }
71const events: ToolEvent[] = []
72for (const line of eventText.split('\n').filter(Boolean)) {
73 try { events.push(JSON.parse(line)) }
74 catch (error) { failure ??= error }
75}
76const groups = [...new Set(events.map(event => event.groupId))]
77assert(groups.every(group => Number.isSafeInteger(group) && group > 1), 'Tool witnesses must identify owned process groups')
78const probeErrors: { group: number, code: string }[] = []
79const cleanupErrors: { group: number, code: string | null }[] = []
80function alive(group: number): boolean {
81 try { process.kill(-group, 0); return true }
82 catch (error) {
83 const code = (error as NodeJS.ErrnoException).code
84 if (code === 'ESRCH') return false
85 if (code === 'EPERM') {
86 if (probeErrors.length < 20) probeErrors.push({ group, code })
87 return true // Unknown membership remains live until an actual absence probe.
88 }
89 failure ??= error
90 if (cleanupErrors.length < 20) cleanupErrors.push({ group, code: code ?? null })
91 return true
92 }
93}
94const beforeCleanup = groups.filter(alive)
95for (const group of beforeCleanup) {
96 try { process.kill(-group, 'SIGKILL') }
97 catch (error) {
98 if ((error as NodeJS.ErrnoException).code !== 'ESRCH') {
99 failure ??= error
100 cleanupErrors.push({ group, code: (error as NodeJS.ErrnoException).code ?? null })
101 }
102 }
103}
104const cleanupDeadline = performance.now() + 5000
105while (groups.some(alive) && performance.now() < cleanupDeadline) await Bun.sleep(25)
106const afterCleanup = groups.filter(alive)
107await writeFile(join(evidence, 'stdout.log'), result?.stdout ?? '')
108await writeFile(join(evidence, 'stderr.log'), result?.stderr ?? '')
109const report = { evidence: reportingFault ? 'controlled-cold-native-report-refusal' : 'controlled-cold-native-build', platform: process.platform, bun: Bun.version, pattern, reportingFault, configureDelaySeconds: 21, buildCommandTimeoutMs, selectedTestTimeoutMs, qualificationTimeoutMs, subjectExitCode: result?.exitCode ?? null, captureAvailable: result !== undefined, capture: result ? { pid: result.pid, deadlineReached: result.deadlineReached, stopRequested: result.stopRequested, rootCleanupRequired: result.rootCleanupRequired, rootGroupRemaining: result.rootGroupRemaining, probeErrors: result.probeErrors } : null, elapsedMs: performance.now() - began, events, beforeCleanup, afterCleanup, probeErrors, cleanupErrors, reportingErrors, failure: failure instanceof Error ? failure.message : failure ? String(failure) : null, physicalFlightEnabled: false }
110await writeFile(join(evidence, 'receipt.json'), `${JSON.stringify(report, null, 2)}\n`)
111assert.equal(afterCleanup.length, 0, 'Owned tool groups must settle before any evidence removal')
112assert.equal(cleanupErrors.length, 0, 'Unexpected tool cleanup errors must remain qualification failures')
113assert(result, 'Actual parent capture must be available')
114assert.equal(result.rootGroupRemaining, false, 'The actual parent process group must settle')
115if (reportingFault) {
116 assert.notEqual(result.exitCode, 0, 'The actual test parent must have been interrupted')
117 assert(failure instanceof Error && (failure as NodeJS.ErrnoException).code === 'EISDIR', 'Retain the actual report refusal')
118 assert(reportingErrors.some(error => error.stage === 'capture-result' && error.code === 'EISDIR'))
119 assert(beforeCleanup.length > 0, 'Reporting refusal must encounter a real live detached tool group')
120}
121else {
122 if (failure) throw failure
123 assert.equal(result.exitCode, 0, 'Original selected native runtime test must pass')
124 assert.equal(result.rootCleanupRequired, false, 'A successful qualification must require no parent group cleanup')
125 assert.match(result.stderr, /1 pass/)
126 assert.match(result.stderr, /0 fail/)
127 assert.equal(beforeCleanup.length, 0, 'A successful build must require no controller cleanup')
128 const exits = events.filter(event => event.event === 'exit')
129 assert.deepEqual(exits.map(event => event.stage), ['configure', 'compile', 'contracts'])
130 assert(exits.every(event => event.exitCode === 0), 'Real CMake and CTest must exit cleanly')
131 assert(exits[0]!.elapsedMs! >= 21000, 'Actual configure must cross the former setup hook')
132 assert(await Bun.file(join(project, 'storage/runtime-build/host/drone_runtime_host')).exists(), 'Real compiler output must exist')
133}
134console.log(JSON.stringify(report))