import { spawn } from 'node:child_process'; import { createConnection } from 'node:net'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { rmSync } from 'node:fs'; import { createTestFixture } from './infra/fixtures.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(__dirname, '../..'); // Debug binary path (Cargo workspace target at repo root) const tauriBinary = resolve(projectRoot, 'target/debug/agent-orchestrator'); let tauriDriver; // ── Test Fixture ── // IMPORTANT: Must be created at module top-level (synchronously) because the // capabilities object below references fixtureDataDir/fixtureConfigDir at eval time. // tauri:options.env may not reliably set process-level env vars, so we also // inject into process.env for tauri-driver inheritance. const fixture = createTestFixture('agor-e2e'); process.env.AGOR_TEST = '1'; process.env.AGOR_TEST_DATA_DIR = fixture.dataDir; process.env.AGOR_TEST_CONFIG_DIR = fixture.configDir; console.log(`Test fixture created at ${fixture.rootDir}`); export const config = { // ── Runner ── runner: 'local', maxInstances: 1, // Tauri doesn't support parallel sessions // ── Connection (external tauri-driver on port 4444) ── hostname: 'localhost', port: 4444, path: '/', // ── Specs ── // Single spec file — Tauri launches one app instance per session, // and tauri-driver can't re-create sessions between spec files. specs: [ resolve(projectRoot, 'tests/e2e/specs/smoke.test.ts'), resolve(projectRoot, 'tests/e2e/specs/workspace.test.ts'), resolve(projectRoot, 'tests/e2e/specs/settings.test.ts'), resolve(projectRoot, 'tests/e2e/specs/features.test.ts'), resolve(projectRoot, 'tests/e2e/specs/agent-scenarios.test.ts'), resolve(projectRoot, 'tests/e2e/specs/phase-b.test.ts'), resolve(projectRoot, 'tests/e2e/specs/phase-c-ui.test.ts'), resolve(projectRoot, 'tests/e2e/specs/phase-c-tabs.test.ts'), resolve(projectRoot, 'tests/e2e/specs/phase-c-llm.test.ts'), ], // ── Capabilities ── capabilities: [{ // Disable BiDi negotiation — tauri-driver doesn't support webSocketUrl 'wdio:enforceWebDriverClassic': true, 'tauri:options': { application: tauriBinary, // Test isolation: fixture-created data/config dirs, disable watchers/telemetry env: fixture.env, }, }], // ── Framework ── framework: 'mocha', mochaOpts: { ui: 'bdd', timeout: 180_000, }, // ── Reporter ── reporters: ['spec'], // ── Logging ── logLevel: 'warn', // ── Timeouts ── waitforTimeout: 10_000, connectionRetryTimeout: 30_000, connectionRetryCount: 3, // ── Hooks ── /** * Build the debug binary before the test run. * Uses --debug --no-bundle for fastest build time. */ onPrepare() { if (process.env.SKIP_BUILD) { console.log('SKIP_BUILD set — using existing debug binary.'); return Promise.resolve(); } return new Promise((resolve, reject) => { console.log('Building Tauri debug binary...'); const build = spawn('cargo', ['tauri', 'build', '--debug', '--no-bundle'], { cwd: projectRoot, stdio: 'inherit', }); build.on('close', (code) => { if (code === 0) { console.log('Debug binary ready.'); resolve(); } else { reject(new Error(`Tauri build failed with exit code ${code}`)); } }); build.on('error', reject); }); }, /** * Spawn tauri-driver before the session. * tauri-driver bridges WebDriver protocol to WebKit2GTK's inspector. * Uses TCP probe to confirm port 4444 is accepting connections. */ beforeSession() { return new Promise((res, reject) => { tauriDriver = spawn('tauri-driver', [], { stdio: ['ignore', 'pipe', 'pipe'], }); tauriDriver.on('error', (err) => { reject(new Error( `Failed to start tauri-driver: ${err.message}. ` + 'Install it with: cargo install tauri-driver' )); }); // TCP readiness probe — poll port 4444 until it accepts a connection const maxWaitMs = 10_000; const intervalMs = 200; const deadline = Date.now() + maxWaitMs; function probe() { if (Date.now() > deadline) { reject(new Error('tauri-driver did not become ready within 10s')); return; } const sock = createConnection({ port: 4444, host: 'localhost' }, () => { sock.destroy(); res(); }); sock.on('error', () => { sock.destroy(); setTimeout(probe, intervalMs); }); } // Give it a moment before first probe setTimeout(probe, 300); }); }, /** * Kill tauri-driver after the test run. */ afterSession() { if (tauriDriver) { tauriDriver.kill(); tauriDriver = null; } // Clean up test fixture try { rmSync(fixture.rootDir, { recursive: true, force: true }); console.log('Test fixture cleaned up.'); } catch { /* best-effort cleanup */ } }, // ── TypeScript (auto-compile via tsx) ── autoCompileOpts: { tsNodeOpts: { project: resolve(projectRoot, 'tests/e2e/tsconfig.json'), }, }, };