also looking at this
test: retain bounded browser launch evidence on failure
#70
18 files
+620
-23
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -451,6 +451,8 @@ jobs: | ||
| 451 | 451 | path: | |
| 452 | 452 | ${{ runner.temp }}/workspace-browser-evidence/*.png |
| 453 | 453 | ${{ runner.temp }}/workspace-browser-evidence/compiled.html |
| 454 | ${{ runner.temp }}/workspace-browser-evidence/browser-launch.json | |
| 455 | ${{ runner.temp }}/workspace-browser-evidence/chrome-startup-stderr.log | |
| 454 | 456 | execution-browser: |
| 455 | 457 | runs-on: ubuntu-latest |
| 456 | 458 | timeout-minutes: 10 |
| @@ -480,6 +482,8 @@ jobs: | ||
| 480 | 482 | path: | |
| 481 | 483 | ${{ runner.temp }}/execution-browser-evidence/*.png |
| 482 | 484 | ${{ runner.temp }}/execution-browser-evidence/compiled.html |
| 485 | ${{ runner.temp }}/execution-browser-evidence/browser-launch.json | |
| 486 | ${{ runner.temp }}/execution-browser-evidence/chrome-startup-stderr.log | |
| 483 | 487 | purge-namespace: |
| 484 | 488 | runs-on: ubuntu-latest |
| 485 | 489 | timeout-minutes: 30 |
| @@ -1,5 +1,25 @@ | ||
| 1 | 1 | # Validation evidence |
| 2 | 2 | |
| 3 | ## Browser qualification diagnostics | |
| 4 | ||
| 5 | Browser fixtures keep their failed private directory after owned process-group | |
| 6 | cleanup. It contains native fixture credentials, database state and the browser | |
| 7 | profile, so CI does not upload that directory. Local retained paths are reported; | |
| 8 | CI runner disposal still ends access to private files that were not exported. | |
| 9 | ||
| 10 | CI exports a closed launch-child observation and at most 64 KiB of startup stderr. | |
| 11 | Public stderr capture stops before any page visit can receive credentials. Later | |
| 12 | browser stderr remains in the private fixture, while exported metadata reports | |
| 13 | that capture was withheld. Refused evidence destinations report unavailable and | |
| 14 | preserve the original browser failure. Launch exit observation, signal and cleanup | |
| 15 | intent are separate fields; they do not prove browser/page/thread liveness or the | |
| 16 | kernel's exact exit time. Post-renewal CDP liveness has its own operation label. | |
| 17 | ||
| 18 | Discovery, transport, stream and outer-test limits remain unchanged. These | |
| 19 | observations support the open investigations in [#44](https://github.com/stacksjs/drone/issues/44) | |
| 20 | and [#64](https://github.com/stacksjs/drone/issues/64); retaining a new attempt does | |
| 21 | not explain an earlier failure whose Chrome evidence was removed. | |
| 22 | ||
| 3 | 23 | ## Owned mission simulation slice |
| 4 | 24 | |
| 5 | 25 | - Full release suite: 211 Bun tests, 17,424 assertions, zero failures. Buddy lint, application/runtime/view |
| @@ -0,0 +1,59 @@ | ||
| 1 | import { Buffer } from 'node:buffer' | |
| 2 | import { writeSync } from 'node:fs' | |
| 3 | import { mkdir, open, writeFile } from 'node:fs/promises' | |
| 4 | import { join } from 'node:path' | |
| 5 | ||
| 6 | export interface BrowserLaunchObservation { | |
| 7 | phase: 'startup-unavailable' | 'discovery-failed' | 'closed' | |
| 8 | elapsedMs: number | |
| 9 | launch: { pid: number, exitObserved: boolean, settledExitCode: number | null, signalCode: string | number | null, exitObservedBeforeCleanup: boolean, cleanupRequested: boolean, exitElapsedMs: number | null } | |
| 10 | portProbe: { attempts: number, successfulReads: number, failedReads: number, lastReadError: string | null, discovered: boolean } | |
| 11 | } | |
| 12 | export function browserFileErrorCode(error: unknown): string { | |
| 13 | const code = (error as NodeJS.ErrnoException | null)?.code | |
| 14 | return typeof code === 'string' && /^[A-Z][A-Z0-9_]{0,31}$/.test(code) ? code : 'OTHER' | |
| 15 | } | |
| 16 | function unavailable(target: 'local' | 'export', error: unknown) { | |
| 17 | const bytes = new TextEncoder().encode(`${JSON.stringify({ oracle: 'browser-launch-evidence-unavailable', target, code: browserFileErrorCode(error) })}\n`) | |
| 18 | for (const descriptor of [2, 1]) { | |
| 19 | try { if (writeSync(descriptor, bytes) === bytes.byteLength) return } | |
| 20 | catch { /* Reporting must preserve the original browser failure. */ } | |
| 21 | } | |
| 22 | } | |
| 23 | /** Public stderr is captured only before any page can receive native fixture credentials. */ | |
| 24 | export async function retainBrowserLaunchEvidence(root: string, observation: BrowserLaunchObservation, options: { startupStderr: boolean, exportDirectory?: string }) { | |
| 25 | let stderr: { status: 'retained' | 'unavailable' | 'withheld', retainedBytes: number, totalBytes: number | null, truncated: boolean, code: string | null } = { status: 'withheld', retainedBytes: 0, totalBytes: null, truncated: false, code: null } | |
| 26 | let retained: Buffer | undefined | |
| 27 | if (options.startupStderr) { | |
| 28 | try { | |
| 29 | const handle = await open(join(root, 'chrome.log'), 'r') | |
| 30 | try { | |
| 31 | const stat = await handle.stat() | |
| 32 | if (!stat.isFile()) throw new Error('Startup stderr is not a regular file') | |
| 33 | const size = Math.min(stat.size, 65536), bytes = Buffer.alloc(size) | |
| 34 | const { bytesRead } = await handle.read(bytes, 0, size, Math.max(0, stat.size - size)) | |
| 35 | retained = bytes.subarray(0, bytesRead) | |
| 36 | stderr = { status: 'retained', retainedBytes: bytesRead, totalBytes: stat.size, truncated: stat.size > bytesRead, code: null } | |
| 37 | } | |
| 38 | finally { await handle.close() } | |
| 39 | } | |
| 40 | catch (error) { stderr = { status: 'unavailable', retainedBytes: 0, totalBytes: null, truncated: false, code: browserFileErrorCode(error) } } | |
| 41 | } | |
| 42 | const outcomes = { local: false, export: false } | |
| 43 | for (const target of ['local', 'export'] as const) { | |
| 44 | const directory = target === 'local' ? root : options.exportDirectory | |
| 45 | if (!directory) continue | |
| 46 | try { | |
| 47 | if (target === 'export') await mkdir(directory, { recursive: true }) | |
| 48 | let snapshot = stderr | |
| 49 | if (retained) { | |
| 50 | try { await writeFile(join(directory, 'chrome-startup-stderr.log'), retained, { mode: 0o600 }) } | |
| 51 | catch (error) { snapshot = { ...stderr, status: 'unavailable', retainedBytes: 0, code: browserFileErrorCode(error) } } | |
| 52 | } | |
| 53 | await writeFile(join(directory, 'browser-launch.json'), `${JSON.stringify({ version: 1, evidence: 'owned-browser-launch-observation', ...observation, stderr: snapshot }, null, 2)}\n`, { mode: 0o600 }) | |
| 54 | outcomes[target] = true | |
| 55 | } | |
| 56 | catch (error) { unavailable(target, error) } | |
| 57 | } | |
| 58 | return outcomes | |
| 59 | } | |
| @@ -89,7 +89,7 @@ try { | ||
| 89 | 89 | console.log(`QA driver33s timer drift ms: ${Date.now() - waitStart - 33000}`) |
| 90 | 90 | const status = await control('status') |
| 91 | 91 | console.log(`QA actual native recipient streams: ${status.recipientStreams}`) |
| 92 | assert.equal(await second.evaluate('1'), 1, 'Existing actual browser tab must stay responsive') | |
| 92 | await second.checkLiveness() | |
| 93 | 93 | assert(status.recipientStreams > baseline.recipientStreams, 'Actual native stream must reconnect during the measured lifetime window') |
| 94 | 94 | const starts = status.recipientStreamStarts as number[] |
| 95 | 95 | assert(starts.slice(baseline.recipientStreams).some((start, index) => start >= waitStart && start - starts[baseline.recipientStreams + index - 1]! >= 30000), 'Measured renewal must follow an actual default30s stream lifetime') |
| @@ -2,6 +2,8 @@ import assert from 'node:assert/strict' | ||
| 2 | 2 | import { mkdir, readFile } from 'node:fs/promises' |
| 3 | 3 | import { join } from 'node:path' |
| 4 | 4 | import { observeBrowserOperation } from './browser-operation' |
| 5 | import { browserFileErrorCode, retainBrowserLaunchEvidence } from './browser-launch-evidence' | |
| 6 | import type { BrowserLaunchObservation } from './browser-launch-evidence' | |
| 5 | 7 | |
| 6 | 8 | /** Dependency-free CDP, with a fresh profile. Credentials never enter command arguments. */ |
| 7 | 9 | async function openSocket(url: string): Promise<WebSocket> { |
| @@ -19,15 +21,24 @@ export async function startWorkspaceBrowser(root: string) { | ||
| 19 | 21 | await mkdir(profile) |
| 20 | 22 | const child = Bun.spawn([process.env.BROWSE_BROWSER ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '--headless=new', '--disable-gpu', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--disable-backgrounding-occluded-windows', '--disable-features=CalculateNativeWinOcclusion', '--disable-dev-shm-usage', '--disable-extensions', '--disable-component-update', '--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--remote-debugging-port=0', `--user-data-dir=${profile}`, 'about:blank'], { stdout: 'ignore', stderr: Bun.file(join(root, 'chrome.log')) }) |
| 21 | 23 | const started = Date.now() |
| 24 | const began = performance.now() | |
| 22 | 25 | let closing = false |
| 23 | void child.exited.then(code => { if (!closing) console.log(`QA isolated Chrome exited unexpectedly: ${code}, elapsedMs:${Date.now() - started}`) }) | |
| 26 | const launch: BrowserLaunchObservation['launch'] = { pid: child.pid, exitObserved: false, settledExitCode: null, signalCode: null, exitObservedBeforeCleanup: false, cleanupRequested: false, exitElapsedMs: null } | |
| 27 | const portProbe: BrowserLaunchObservation['portProbe'] = { attempts: 0, successfulReads: 0, failedReads: 0, lastReadError: null, discovered: false } | |
| 28 | let pageVisited = false | |
| 29 | const retain = (phase: BrowserLaunchObservation['phase']) => retainBrowserLaunchEvidence(root, { phase, elapsedMs: performance.now() - began, launch: { ...launch }, portProbe: { ...portProbe } }, { startupStderr: !pageVisited, exportDirectory: process.env.WORKSPACE_BROWSER_EVIDENCE ?? process.env.EXECUTION_BROWSER_EVIDENCE }) | |
| 30 | void child.exited.then(code => { | |
| 31 | launch.exitObserved = true; launch.settledExitCode = code; launch.signalCode = child.signalCode; launch.exitObservedBeforeCleanup = !closing; launch.exitElapsedMs = performance.now() - began | |
| 32 | if (!closing) console.log(`QA isolated Chrome exited unexpectedly: ${code}, elapsedMs:${Date.now() - started}`) | |
| 33 | }) | |
| 24 | 34 | let port = '' |
| 25 | 35 | for (let attempt = 0; attempt < 400; attempt++) { |
| 26 | port = await readFile(join(profile, 'DevToolsActivePort'), 'utf8').then(value => value.split('\n')[0]!).catch(() => '') | |
| 27 | if (port) break | |
| 36 | portProbe.attempts++ | |
| 37 | port = await readFile(join(profile, 'DevToolsActivePort'), 'utf8').then(value => { portProbe.successfulReads++; portProbe.lastReadError = null; return value.split('\n')[0]! }).catch(error => { portProbe.failedReads++; portProbe.lastReadError = browserFileErrorCode(error); return '' }) | |
| 38 | if (port) { portProbe.discovered = true; break } | |
| 28 | 39 | await Bun.sleep(50) |
| 29 | 40 | } |
| 30 | if (!port) { closing = true; if (child.exitCode === null) child.kill(); await Promise.race([child.exited, Bun.sleep(2000)]); throw new Error('Fresh browser must start') } | |
| 41 | if (!port) { closing = true; launch.cleanupRequested = true; if (child.exitCode === null) child.kill(); await Promise.race([child.exited, Bun.sleep(2000)]); await retain('startup-unavailable'); throw new Error('Fresh browser must start') } | |
| 31 | 42 | // Keep a browser-level session, as well as page sessions, for the owned lifecycle. |
| 32 | 43 | let browserSocket: WebSocket |
| 33 | 44 | try { |
| @@ -35,13 +46,14 @@ export async function startWorkspaceBrowser(root: string) { | ||
| 35 | 46 | const version = await observeBrowserOperation('version-json', () => response.json()) as { webSocketDebuggerUrl: string } |
| 36 | 47 | browserSocket = await observeBrowserOperation('browser-socket', () => openSocket(version.webSocketDebuggerUrl)) |
| 37 | 48 | } |
| 38 | catch (error) { closing = true; if (child.exitCode === null) child.kill(); await Promise.race([child.exited, Bun.sleep(2000)]); throw error } | |
| 49 | catch (error) { closing = true; launch.cleanupRequested = true; if (child.exitCode === null) child.kill(); await Promise.race([child.exited, Bun.sleep(2000)]); await retain('discovery-failed'); throw error } | |
| 39 | 50 | let browserSequence = 0 |
| 40 | 51 | browserSocket.onmessage = () => {} |
| 41 | 52 | const keepAlive = setInterval(() => { if (browserSocket.readyState === WebSocket.OPEN) browserSocket.send(JSON.stringify({ id: ++browserSequence, method: 'Browser.getVersion' })) }, 5000) |
| 42 | 53 | const pages: Array<{ close: () => void }> = [] |
| 43 | 54 | return { |
| 44 | 55 | async page(url: string, javascript = true) { |
| 56 | pageVisited = true | |
| 45 | 57 | const response = await observeBrowserOperation('target-fetch', () => fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT', signal: AbortSignal.timeout(5000) })) |
| 46 | 58 | const target = await observeBrowserOperation('target-json', () => response.json()) as { webSocketDebuggerUrl: string } |
| 47 | 59 | const socket = await observeBrowserOperation('page-socket', () => openSocket(target.webSocketDebuggerUrl)) |
| @@ -73,6 +85,7 @@ export async function startWorkspaceBrowser(root: string) { | ||
| 73 | 85 | while (Date.now() < end) { if (await evaluate(expression)) return; await Bun.sleep(50) } |
| 74 | 86 | throw new Error(message) |
| 75 | 87 | }, |
| 88 | async checkLiveness() { phase = 'post-renewal liveness'; assert.equal(await evaluate('1'), 1) }, | |
| 76 | 89 | async fill(selector: string, value: string) { phase = `field ${selector}`; await evaluate(`(() => { const element = document.querySelector(${JSON.stringify(selector)}); if (!element) throw Error('Missing form field'); element.value = ${JSON.stringify(value)}; element.dispatchEvent(new Event('input',{bubbles:true})); element.dispatchEvent(new Event('change',{bubbles:true})); })()`) }, |
| 77 | 90 | async click(text: string) { phase = `action ${text}`; await evaluate(`(() => { const element = [...document.querySelectorAll('button')].find(value => value.textContent.trim() === ${JSON.stringify(text)} && !value.disabled); if (!element) throw Error('Missing enabled action'); element.click(); })()`) }, |
| 78 | 91 | async screenshot(path: string, width = 1280, height = 1000) { await send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: width < 500 }); await Bun.sleep(100); const shot = await send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false }); await Bun.write(path, Buffer.from(shot.data, 'base64')) }, |
| @@ -83,6 +96,6 @@ export async function startWorkspaceBrowser(root: string) { | ||
| 83 | 96 | await page.wait(javascript ? "document.querySelector('#incident-key') !== null && document.querySelector('[data-workspace-status]')?.textContent.trim() === 'disconnected'" : "document.querySelector('#incident-key') !== null", 'Compiled workspace must initialize') |
| 84 | 97 | return page |
| 85 | 98 | }, |
| 86 | async close() { console.log(`QA browser.close requested, elapsedMs:${Date.now() - started}`); closing = true; clearInterval(keepAlive); browserSocket.close(); for (const page of pages) page.close(); if (child.exitCode === null) child.kill(); await Promise.race([child.exited, Bun.sleep(2000)]) }, | |
| 99 | async close() { console.log(`QA browser.close requested, elapsedMs:${Date.now() - started}`); closing = true; launch.cleanupRequested = true; clearInterval(keepAlive); browserSocket.close(); for (const page of pages) page.close(); if (child.exitCode === null) child.kill(); await Promise.race([child.exited, Bun.sleep(2000)]); await retain('closed') }, | |
| 87 | 100 | } |
| 88 | 101 | } |
| @@ -0,0 +1,59 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { Buffer } from 'node:buffer' | |
| 3 | import { mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises' | |
| 4 | import { tmpdir } from 'node:os' | |
| 5 | import { join } from 'node:path' | |
| 6 | import { retainBrowserLaunchEvidence } from '../helpers/browser-launch-evidence' | |
| 7 | import type { BrowserLaunchObservation } from '../helpers/browser-launch-evidence' | |
| 8 | ||
| 9 | const observation: BrowserLaunchObservation = { phase: 'startup-unavailable', elapsedMs: 20000, launch: { pid: 12345, exitObserved: true, settledExitCode: 23, signalCode: null, exitObservedBeforeCleanup: true, cleanupRequested: true, exitElapsedMs: 10 }, portProbe: { attempts: 400, successfulReads: 0, failedReads: 400, lastReadError: 'ENOENT', discovered: false } } | |
| 10 | ||
| 11 | test('startup capture retains only a bounded native stderr tail at both evidence destinations', async () => { | |
| 12 | const root = await mkdtemp(join(tmpdir(), 'drone-browser-evidence-')), exported = join(root, 'export') | |
| 13 | try { | |
| 14 | const source = 'unretained-prefix'.repeat(5000) + 'x'.repeat(65500) + 'controlled-tail' | |
| 15 | await writeFile(join(root, 'chrome.log'), source) | |
| 16 | expect(await retainBrowserLaunchEvidence(root, observation, { startupStderr: true, exportDirectory: exported })).toEqual({ local: true, export: true }) | |
| 17 | for (const directory of [root, exported]) { | |
| 18 | const report = JSON.parse(await readFile(join(directory, 'browser-launch.json'), 'utf8')) | |
| 19 | expect(report.stderr.totalBytes).toBe(Buffer.byteLength(source)) | |
| 20 | expect(report.stderr.retainedBytes).toBe(65536) | |
| 21 | expect(report.stderr.truncated).toBe(true) | |
| 22 | const captured = await readFile(join(directory, 'chrome-startup-stderr.log')) | |
| 23 | expect(captured.byteLength).toBe(65536) | |
| 24 | expect(captured.equals(Buffer.from(source).subarray(-65536))).toBe(true) | |
| 25 | expect((await stat(join(directory, 'browser-launch.json'))).mode & 0o777).toBe(0o600) | |
| 26 | } | |
| 27 | expect(await readFile(join(root, 'chrome.log'), 'utf8')).toBe(source) | |
| 28 | } | |
| 29 | finally { await rm(root, { recursive: true, force: true }) } | |
| 30 | }) | |
| 31 | ||
| 32 | test('post-navigation capture withholds private stderr and exports only lifecycle scalars', async () => { | |
| 33 | const root = await mkdtemp(join(tmpdir(), 'drone-browser-evidence-')), exported = join(root, 'export') | |
| 34 | try { | |
| 35 | await writeFile(join(root, 'chrome.log'), 'PRIVATE_FIXTURE_CREDENTIAL_SENTINEL') | |
| 36 | const result = await retainBrowserLaunchEvidence(root, { ...observation, phase: 'closed' }, { startupStderr: false, exportDirectory: exported }) | |
| 37 | expect(result).toEqual({ local: true, export: true }) | |
| 38 | expect(await readdir(exported)).toEqual(['browser-launch.json']) | |
| 39 | const publicText = await readFile(join(exported, 'browser-launch.json'), 'utf8') | |
| 40 | expect(publicText).not.toContain('PRIVATE_FIXTURE_CREDENTIAL_SENTINEL') | |
| 41 | expect(JSON.parse(publicText).stderr.status).toBe('withheld') | |
| 42 | expect(await readFile(join(root, 'chrome.log'), 'utf8')).toBe('PRIVATE_FIXTURE_CREDENTIAL_SENTINEL') | |
| 43 | } | |
| 44 | finally { await rm(root, { recursive: true, force: true }) } | |
| 45 | }) | |
| 46 | ||
| 47 | test('actual refused metadata and export paths report unavailable without replacing the browser failure', async () => { | |
| 48 | const root = await mkdtemp(join(tmpdir(), 'drone-browser-evidence-')), exported = join(root, 'not-a-directory') | |
| 49 | try { | |
| 50 | await writeFile(join(root, 'chrome.log'), 'controlled startup message') | |
| 51 | await mkdir(join(root, 'browser-launch.json')) | |
| 52 | await writeFile(exported, 'owned obstruction') | |
| 53 | const original = new Error('original browser failure') | |
| 54 | const run = async () => { await retainBrowserLaunchEvidence(root, observation, { startupStderr: true, exportDirectory: exported }); throw original } | |
| 55 | await expect(run()).rejects.toBe(original) | |
| 56 | expect(await readFile(exported, 'utf8')).toBe('owned obstruction') | |
| 57 | } | |
| 58 | finally { await rm(root, { recursive: true, force: true }) } | |
| 59 | }) | |
| @@ -0,0 +1,48 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' | |
| 3 | import { tmpdir } from 'node:os' | |
| 4 | import { join } from 'node:path' | |
| 5 | import process from 'node:process' | |
| 6 | import { startWorkspaceBrowser } from '../helpers/workspace-browser' | |
| 7 | ||
| 8 | test.skipIf(process.platform === 'win32')('failed native launch retains startup stderr, actual exit and port observations at the original discovery bound', async () => { | |
| 9 | const supplied = process.env.BROWSER_LAUNCH_EVIDENCE | |
| 10 | const root = supplied ? (await mkdir(supplied), supplied) : await mkdtemp(join(tmpdir(), 'drone-browser-launch-')) | |
| 11 | const executable = join(root, 'launch.ts'), pidPath = join(root, 'launch.pid') | |
| 12 | let passed = false | |
| 13 | const original = process.env.BROWSE_BROWSER | |
| 14 | try { | |
| 15 | await writeFile(executable, `#!${process.execPath}\nimport { writeFileSync } from 'node:fs'\nwriteFileSync(${JSON.stringify(pidPath)}, String(process.pid))\nconsole.error('controlled native browser startup refusal')\nprocess.exit(23)\n`, { mode: 0o700 }) | |
| 16 | process.env.BROWSE_BROWSER = executable | |
| 17 | await expect(startWorkspaceBrowser(root)).rejects.toThrow('Fresh browser must start') | |
| 18 | const actualPid = Number(await readFile(pidPath, 'utf8')) | |
| 19 | const report = JSON.parse(await readFile(join(root, 'browser-launch.json'), 'utf8')) | |
| 20 | expect(report.phase).toBe('startup-unavailable') | |
| 21 | expect(report.launch.pid).toBe(actualPid) | |
| 22 | expect(report.launch.exitObserved).toBe(true) | |
| 23 | expect(report.launch.settledExitCode).toBe(23) | |
| 24 | expect(report.launch.exitObservedBeforeCleanup).toBe(true) | |
| 25 | expect(report.launch.cleanupRequested).toBe(true) | |
| 26 | expect(report.portProbe.attempts).toBe(400) | |
| 27 | expect(report.portProbe.lastReadError).toBe('ENOENT') | |
| 28 | expect(report.stderr.status).toBe('retained') | |
| 29 | expect(report.stderr.retainedBytes).toBeLessThanOrEqual(65536) | |
| 30 | expect(await readFile(join(root, 'chrome-startup-stderr.log'), 'utf8')).toContain('controlled native browser startup refusal') | |
| 31 | expect(() => process.kill(actualPid, 0)).toThrow() | |
| 32 | passed = true | |
| 33 | } | |
| 34 | finally { | |
| 35 | if (original === undefined) delete process.env.BROWSE_BROWSER | |
| 36 | else process.env.BROWSE_BROWSER = original | |
| 37 | // The owned native program exits23 before discovery completes; never delete a live fixture. | |
| 38 | const actualPid = Number(await readFile(pidPath, 'utf8').catch(() => '0')) | |
| 39 | if (actualPid > 1) { | |
| 40 | let gone = false | |
| 41 | try { process.kill(actualPid, 0) } | |
| 42 | catch (error) { gone = (error as NodeJS.ErrnoException).code === 'ESRCH' } | |
| 43 | expect(gone).toBe(true) | |
| 44 | } | |
| 45 | if (passed && !supplied) await rm(root, { recursive: true, force: true }) | |
| 46 | else console.log(JSON.stringify({ evidence: 'controlled-native-launch', retainedRoot: root, passed })) | |
| 47 | } | |
| 48 | }, 30000) | |
| @@ -7,6 +7,7 @@ import { spawnWorkspaceFixture } from '../helpers/workspace-process' | ||
| 7 | 7 | test.skipIf(process.env.EXECUTION_BROWSER_QA !== 'true')('compiled execution panel uses real native routes and a genuine Worker', async () => { |
| 8 | 8 | const directory = await mkdtemp(join(tmpdir(), 'drone-incident-access-')) |
| 9 | 9 | let owned: ReturnType<typeof spawnWorkspaceFixture> | undefined |
| 10 | let passed = false | |
| 10 | 11 | try { |
| 11 | 12 | const config = join(directory, 'bunfig.toml'); await writeFile(config, 'preload=[]\n') |
| 12 | 13 | owned = spawnWorkspaceFixture([process.execPath, `--config=${config}`, '--no-env-file', `${import.meta.dir}/../fixtures/public-safety-access/execution-browser.ts`], directory, { ...process.env, APP_ENV: 'test', APP_DEBUG: 'false', NODE_ENV: 'test', DB_CONNECTION: 'sqlite', DB_DATABASE_PATH: join(directory, 'access.sqlite'), STX_DIR: join(directory, 'stx') }) |
| @@ -16,10 +17,15 @@ test.skipIf(process.env.EXECUTION_BROWSER_QA !== 'true')('compiled execution pan | ||
| 16 | 17 | const [code, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout as ReadableStream).text(), new Response(child.stderr as ReadableStream).text()]) |
| 17 | 18 | expect(code, `${stdout}\n${stderr}`).toBe(0) |
| 18 | 19 | expect(stdout).toContain('PASS actual execution browser: native live-before-return') |
| 20 | passed = true | |
| 19 | 21 | } |
| 20 | 22 | finally { clearTimeout(timeout) } |
| 21 | 23 | } |
| 22 | finally { await owned?.stop(); await rm(directory, { recursive: true, force: true }) } | |
| 24 | finally { | |
| 25 | await owned?.stop() | |
| 26 | if (passed) await rm(directory, { recursive: true, force: true }) | |
| 27 | else console.log(JSON.stringify({ evidence: 'retained-private-browser-fixture', directory })) | |
| 28 | } | |
| 23 | 29 | }, 110000) |
| 24 | 30 | |
| 25 | 31 | test('execution browser fixture refuses an unguarded directory before database or application writes', async () => { |
| @@ -8,6 +8,7 @@ import { spawnWorkspaceFixture } from '../helpers/workspace-process' | ||
| 8 | 8 | test.skipIf(process.env.WORKSPACE_BROWSER_QA !== 'true')('actual rendered workspace with native credentials and routes', async () => { |
| 9 | 9 | const directory = await mkdtemp(join(tmpdir(), 'drone-incident-access-')) |
| 10 | 10 | let owned: ReturnType<typeof spawnWorkspaceFixture> | undefined |
| 11 | let passed = false | |
| 11 | 12 | try { |
| 12 | 13 | const config = join(directory, 'bunfig.toml'); await writeFile(config, 'preload=[]\n') |
| 13 | 14 | owned = spawnWorkspaceFixture([process.execPath, `--config=${config}`, '--no-env-file', `${import.meta.dir}/../fixtures/public-safety-access/workspace-browser.ts`], directory, { ...process.env, APP_ENV: 'test', APP_DEBUG: 'false', NODE_ENV: 'test', DB_CONNECTION: 'sqlite', DB_DATABASE_PATH: join(directory, 'access.sqlite'), STX_DIR: join(directory, 'stx') }) |
| @@ -17,12 +18,14 @@ test.skipIf(process.env.WORKSPACE_BROWSER_QA !== 'true')('actual rendered worksp | ||
| 17 | 18 | const [code, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout as ReadableStream).text(), new Response(child.stderr as ReadableStream).text()]) |
| 18 | 19 | expect(code, `${stdout}\n${stderr}`).toBe(0) |
| 19 | 20 | expect(stdout).toContain('PASS actual compiled workspace browser') |
| 21 | passed = true | |
| 20 | 22 | } |
| 21 | 23 | finally { clearTimeout(timeout) } |
| 22 | 24 | } |
| 23 | 25 | finally { |
| 24 | 26 | await owned?.stop() |
| 25 | await rm(directory, { recursive: true, force: true }) | |
| 27 | if (passed) await rm(directory, { recursive: true, force: true }) | |
| 28 | else console.log(JSON.stringify({ evidence: 'retained-private-browser-fixture', directory })) | |
| 26 | 29 | } |
| 27 | 30 | }, 110000) |
| 28 | 31 | |