#!/usr/bin/env node /** * Integration smoke: boot a real seeder + booter pair with isolated stores. * Runs `npm run os:seeder` and `npm run os:booter` against temp Corestores. */ import { spawn } from 'node:child_process' import { mkdtemp, rm } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const tmpBase = await mkdtemp(path.join(os.tmpdir(), 'bare-os-integration-lab-')) const hostData = path.join(tmpBase, 'host-data') const seedStore = path.join(hostData, 'corestore', 'seeder') const bootStore = path.join(hostData, 'corestore', 'booter') const timeoutMs = clampInt(process.env.BARE_OS_INTEGRATION_TIMEOUT_MS, 120000) const seederReadyTimeoutMs = clampInt( process.env.BARE_OS_INTEGRATION_SEEDER_READY_TIMEOUT_MS, 60000 ) const booterReadyTimeoutMs = clampInt( process.env.BARE_OS_INTEGRATION_BOOTER_READY_TIMEOUT_MS, 90000 ) const testnet = resolveTestnetBootstrapEnv(process.env) /** @type {{ seeder?: import('node:child_process').ChildProcess, booter?: import('node:child_process').ChildProcess }} */ const procs = {} const startedAt = Date.now() const baseEnv = { ...process.env, BARE_OS_HOST_DATA: hostData, BARE_OS_SEED_STORE: seedStore, BARE_OS_BOOT_STORE: bootStore, BARE_OS_NO_SPLASH: '1', BARE_OS_BOOT_TIMEOUT_MS: String(clampInt(process.env.BARE_OS_BOOT_TIMEOUT_MS, 45000)), ...(testnet.envBootstrap ? { HYPERSWARM_BOOTSTRAP: testnet.envBootstrap } : {}) } try { procs.seeder = runNpmScript('os:seeder', { env: baseEnv, name: 'seeder' }) await waitForOutput( procs.seeder, [/seeder active/i, /MBR block 0/i], seederReadyTimeoutMs, 'seeder readiness' ) procs.booter = runNpmScript('os:booter', { env: { ...baseEnv, BARE_OS_SKIP_REPL: '1', BARE_OS_BOOT_TRACE: 'ndjson' }, name: 'booter' }) await waitForOutput( procs.booter, [/booter:kernel_invoke/i, /"ready":\s*true/i, /"type":"boot_ready"/i], booterReadyTimeoutMs, 'booter readiness' ) const booterExited = await waitForExit(procs.booter, 10000) const ok = booterExited.code === 0 || booterExited.code === null if (!ok) { throw new Error(`booter exited with code ${booterExited.code ?? 'signal'}`) } console.log( JSON.stringify({ schema: 2, ok: true, atMs: Date.now(), elapsedMs: Date.now() - startedAt, mode: 'seeder-booter-e2e', testnetMode: testnet.enabled, hyperswarmBootstrap: testnet.redactedBootstrap, tempRoot: tmpBase }) ) } catch (err) { console.error( JSON.stringify({ schema: 2, ok: false, atMs: Date.now(), elapsedMs: Date.now() - startedAt, mode: 'seeder-booter-e2e', testnetMode: testnet.enabled, hyperswarmBootstrap: testnet.redactedBootstrap, error: err instanceof Error ? err.message : String(err) }) ) process.exitCode = 1 } finally { await teardownProc(procs.booter) await teardownProc(procs.seeder) if (process.env.BARE_OS_INTEGRATION_KEEP_TMP !== '1') { await rm(tmpBase, { recursive: true, force: true }) } } /** * @param {string | undefined} raw * @param {number} fallback */ function clampInt(raw, fallback) { const n = Number.parseInt(String(raw ?? ''), 10) if (!Number.isFinite(n) || n <= 0) return fallback return Math.min(Math.max(1000, n), 15 * 60 * 1000) } /** * Local integration testnet mode: * - Enable with BARE_OS_INTEGRATION_TESTNET=1|true|yes. * - Uses BARE_OS_INTEGRATION_TESTNET_BOOTSTRAP when present. * - Otherwise reuses HYPERSWARM_BOOTSTRAP when already configured. * @param {Record} env */ function resolveTestnetBootstrapEnv(env) { const on = String(env.BARE_OS_INTEGRATION_TESTNET ?? '') .trim() .toLowerCase() const enabled = on === '1' || on === 'true' || on === 'yes' const explicit = String(env.BARE_OS_INTEGRATION_TESTNET_BOOTSTRAP ?? '').trim() const inherited = String(env.HYPERSWARM_BOOTSTRAP ?? '').trim() const envBootstrap = enabled ? (explicit || inherited || '') : '' const redactedBootstrap = envBootstrap ? envBootstrap .split(',') .map((s) => s.trim()) .filter(Boolean) .map((s) => (s.length > 20 ? s.slice(0, 20) + '...' : s)) .join(',') : '' return { enabled, envBootstrap, redactedBootstrap } } /** * @param {string} script * @param {{ env: Record, name: string }} opts */ function runNpmScript(script, opts) { const child = spawn('npm', ['run', script], { cwd: root, env: opts.env, stdio: ['ignore', 'pipe', 'pipe'] }) child.stdout?.setEncoding('utf8') child.stderr?.setEncoding('utf8') child.stdout?.on('data', (d) => process.stdout.write(`[${opts.name}] ${d}`)) child.stderr?.on('data', (d) => process.stderr.write(`[${opts.name}] ${d}`)) return child } /** * @param {import('node:child_process').ChildProcess | undefined} child * @param {RegExp[]} patterns * @param {number} ms * @param {string} label */ function waitForOutput(child, patterns, ms, label) { if (!child) return Promise.reject(new Error(`${label}: process missing`)) return new Promise((resolve, reject) => { let buf = '' const to = setTimeout(() => { cleanup() reject(new Error(`${label}: timeout after ${ms}ms`)) }, Math.min(ms, timeoutMs)) const onChunk = (chunk) => { buf += String(chunk) if (buf.length > 120000) buf = buf.slice(-80000) if (patterns.some((re) => re.test(buf))) { cleanup() resolve() } } const onExit = (code) => { cleanup() reject(new Error(`${label}: process exited before match (${code ?? 'signal'})`)) } const cleanup = () => { clearTimeout(to) child.stdout?.off('data', onChunk) child.stderr?.off('data', onChunk) child.off('exit', onExit) } child.stdout?.on('data', onChunk) child.stderr?.on('data', onChunk) child.on('exit', onExit) }) } /** * @param {import('node:child_process').ChildProcess | undefined} child * @param {number} ms */ function waitForExit(child, ms) { if (!child) return Promise.resolve({ code: 0 }) return new Promise((resolve) => { let done = false const finish = (code) => { if (done) return done = true clearTimeout(to) resolve({ code }) } const to = setTimeout(() => finish(null), ms) child.once('exit', finish) }) } /** * @param {import('node:child_process').ChildProcess | undefined} child */ async function teardownProc(child) { if (!child || child.killed || child.exitCode != null) return child.kill('SIGTERM') await waitForExit(child, 6000) if (child.exitCode == null) child.kill('SIGKILL') }