/** * Electrobun stack adapter — spawns WebKitWebDriver or electrobun binary, * manages PTY daemon lifecycle for terminal tests. */ import { spawn, type ChildProcess } from 'node:child_process'; import { createConnection } from 'node:net'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { existsSync } from 'node:fs'; import { StackAdapter, type StackCapabilities } from './base-adapter.ts'; import type { TestFixture } from '../infra/fixtures.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PROJECT_ROOT = resolve(__dirname, '../../..'); const ELECTROBUN_ROOT = resolve(PROJECT_ROOT, 'ui-electrobun'); export class ElectrobunAdapter extends StackAdapter { readonly name = 'electrobun'; readonly port = 9761; getBinaryPath(): string { return resolve(ELECTROBUN_ROOT, 'build/Agent Orchestrator'); } getDataDir(fixture: TestFixture): string { return fixture.dataDir; } getCapabilities(_fixture: TestFixture): StackCapabilities { return { capabilities: { 'wdio:enforceWebDriverClassic': true, browserName: 'webkit', }, }; } setupDriver(): Promise { return new Promise((res, reject) => { // Check port is free const preCheck = createConnection({ port: this.port, host: 'localhost' }, () => { preCheck.destroy(); reject(new Error( `Port ${this.port} already in use. Kill: lsof -ti:${this.port} | xargs kill` )); }); preCheck.on('error', () => { preCheck.destroy(); // Try WebKitWebDriver first (system-installed), fall back to electrobun binary const driverBin = this.findWebDriver(); const driver = spawn(driverBin, ['--port', String(this.port)], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, AGOR_TEST: '1', }, }); driver.on('error', (err) => { reject(new Error( `Failed to start WebDriver for Electrobun: ${err.message}. ` + 'Ensure WebKitWebDriver or the Electrobun binary is available.' )); }); // TCP readiness probe const deadline = Date.now() + 15_000; const probe = () => { if (Date.now() > deadline) { reject(new Error(`WebDriver not ready on port ${this.port} within 15s`)); return; } const sock = createConnection({ port: this.port, host: 'localhost' }, () => { sock.destroy(); res(driver); }); sock.on('error', () => { sock.destroy(); setTimeout(probe, 300); }); }; setTimeout(probe, 500); }); }); } teardownDriver(driver: ChildProcess): void { driver.kill(); } async startPtyDaemon(): Promise { const daemonPath = resolve(ELECTROBUN_ROOT, 'src/pty-daemon/agor-ptyd'); const altPath = resolve(PROJECT_ROOT, 'target/debug/agor-ptyd'); const bin = existsSync(daemonPath) ? daemonPath : altPath; if (!existsSync(bin)) { throw new Error(`PTY daemon binary not found at ${daemonPath} or ${altPath}`); } const daemon = spawn(bin, [], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, AGOR_TEST: '1' }, }); // Wait for daemon to be ready (simple delay — daemon binds quickly) await new Promise((r) => setTimeout(r, 1000)); return daemon; } stopPtyDaemon(daemon: ChildProcess): void { daemon.kill('SIGTERM'); } verifyBinary(): void { if (!existsSync(this.getBinaryPath())) { throw new Error( `Electrobun binary not found at ${this.getBinaryPath()}. ` + 'Build with: cd ui-electrobun && bun run build:canary' ); } } private findWebDriver(): string { // Check common WebKitWebDriver locations const candidates = [ '/usr/bin/WebKitWebDriver', '/usr/local/bin/WebKitWebDriver', resolve(ELECTROBUN_ROOT, 'node_modules/.bin/webkitwebdriver'), ]; for (const c of candidates) { if (existsSync(c)) return c; } // Fall back to PATH resolution return 'WebKitWebDriver'; } }