diff --git a/src/main/orcad/orcad-bun-launcher.integration.test.ts b/src/main/orcad/orcad-bun-launcher.integration.test.ts index 82272700f16..8ee18d53293 100644 --- a/src/main/orcad/orcad-bun-launcher.integration.test.ts +++ b/src/main/orcad/orcad-bun-launcher.integration.test.ts @@ -1,5 +1,5 @@ import { build } from 'esbuild' -import { existsSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import { copyFile, mkdtemp, readFile, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' @@ -23,6 +23,22 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => { await copyFile(runtimePath, join(directory, orcadBunRuntimeFilename(process.platform))) await writeFile(join(directory, '.build-target'), `${process.platform}-${process.arch}\n`) await build({ + // Record entry before imports without relying on either process's stdout. + banner: { + js: ` + function traceLauncherPhase(phase) { + const runtime = process.versions.bun ? 'bun' : 'node' + try { + require('node:fs').appendFileSync(process.env.ORCA_TEST_PHASES + '.' + runtime, + JSON.stringify({ at: Date.now(), phase, pid: process.pid, runtime, arch: process.arch }) + '\\n') + } catch (error) { + console.error('launcher fixture trace failed:', error) + } + } + traceLauncherPhase('entry') + process.once('exit', code => traceLauncherPhase('exit:' + code)) + ` + }, stdin: { contents: ` import {handoffToBundledOrcad, OrcadBundledRuntimeError} from './src/main/orcad/orcad-bundled-runtime' @@ -30,12 +46,16 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => { import {resolveOrcadExitCode} from './src/main/orcad/orcad-exit-code' import {writeFile} from 'node:fs/promises' if (!process.versions.bun) { + traceLauncherPhase('before-handoff') if (!handoffToBundledOrcad()) throw new Error('Missing bundled runtime') + traceLauncherPhase('after-handoff') process.on('message', signal => process.emit(signal)) } else { + traceLauncherPhase('before-booting') console.log('booting:' + process.pid) console.log('runtime:' + process.versions.bun) console.log('channel-env:' + (process.env.ORCA_BUNDLED_LAUNCHER_CHANNEL ?? 'absent')) + traceLauncherPhase('booting-written') process.on('exit', code => console.log('runtime-exit:' + code)) const keepalive = setInterval(() => {}, 1_000) const install = async () => { @@ -99,6 +119,13 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => { const runtime = options.direct ? join(directory, orcadBunRuntimeFilename(process.platform)) : nodePath + const startedAt = Date.now() + const events: { at: number; event: string }[] = [] + const record = (event: string): void => { + if (events.length < 16) { + events.push({ at: Date.now(), event }) + } + } const child = spawnProcess({ program: options.nohup ? 'nohup' : runtime, args: [...(options.nohup ? [runtime] : []), join(directory, 'orcad.js')], @@ -106,6 +133,7 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ORCA_TEST_DONE: join(directory, 'done'), + ORCA_TEST_PHASES: join(directory, 'phases'), ORCA_TEST_DELAY_INSTALL: options.delay ? '1' : '0', ORCA_TEST_STALL: options.stall ? '1' : '0', ORCA_TEST_FAIL_STARTUP: options.failStartup ? '1' : '0' @@ -114,8 +142,10 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => { stdio: ['ignore', 'pipe', 'pipe', 'ipc'] }) children.add(child) + child.once('spawn', () => record('spawn')) let closed = false child.once('close', () => { + record('close') closed = true }) let output = '' @@ -128,18 +158,57 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => { runtimes.delete(Number(pid)) } } - child.stdout.on('data', capture) - child.stderr.on('data', capture) + const streams = [ + ['stdout', child.stdout], + ['stderr', child.stderr] + ] as const + for (const [name, stream] of streams) { + stream.on('data', capture) + stream.once('data', () => record(`${name}:data`)) + stream.once('end', () => record(`${name}:end`)) + stream.once('close', () => record(`${name}:close`)) + } const exit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>( (resolve, reject) => { - child.once('error', reject) + child.once('error', (error) => { + record(`error:${error.message}`) + reject(error) + }) child.once('exit', (code, signal) => { + record(`exit:${code}:${signal}`) children.delete(child) resolve({ code, signal }) }) } ) - return { child, output: () => output, exit, isClosed: () => closed } + const diagnostics = () => ({ + startedAt, + elapsedMs: Date.now() - startedAt, + events, + pid: child.pid, + exitCode: child.exitCode, + signalCode: child.signalCode, + connected: child.connected, + closed, + streams: streams.map(([name, stream]) => ({ + name, + ended: stream.readableEnded, + destroyed: stream.destroyed, + error: stream.errored?.message, + bufferedBytes: stream.readableLength + })), + phases: ['node', 'bun'].map((runtime) => { + try { + return { + runtime, + trace: readFileSync(join(directory, `phases.${runtime}`), 'utf8').slice(0, 4096) + } + } catch (error) { + return { runtime, error: String(error) } + } + }) + }) + return { child, output: () => output, exit, isClosed: () => closed, diagnostics } } it.each([false, true])( @@ -214,7 +283,12 @@ describe.skipIf(!existsSync(runtimePath))('real Bun launcher lifecycle', () => { it('preserves a startup configuration verdict after early launcher loss', async () => { const h = launch({ delay: true, failStartup: true }) - await vi.waitFor(() => expect(h.output()).toContain('booting:'), { timeout: 5_000 }) + try { + await vi.waitFor(() => expect(h.output()).toContain('booting:'), { timeout: 5_000 }) + } catch (error) { + console.error('Bun launcher startup diagnostics:', JSON.stringify(h.diagnostics())) + throw error + } h.child.kill('SIGKILL') await h.exit await vi.waitFor(() => expect(h.output()).toContain('runtime-exit:78'), { timeout: 5_000 })