agent-orchestrator/tests/e2e/wdio.conf.js
Hibryda 6459877c89 fix: change dev port from 9700 to 9710 (avoid BridgeCoach conflict)
Port 9700 was occupied by BridgeCoach Docker container, causing the
Tauri debug binary to load the wrong frontend. Changed to 9710:
- vite.config.ts: server.port 9700 → 9710
- tauri.conf.json: devUrl localhost:9700 → localhost:9710
- wdio.conf.js: DEV_URL_PORT check updated
- Binary rebuilt with new port baked in
2026-03-18 04:20:49 +01:00

275 lines
10 KiB
JavaScript

import { spawn, execSync } from 'node:child_process';
import { createConnection } from 'node:net';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { rmSync, existsSync } 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;
// ── Port ──
// Unique port for this project's tauri-driver (avoids conflict with other
// Tauri apps or WebDriver instances on the same machine).
// Range 9000-9999 per project convention. See CLAUDE.md port table.
const TAURI_DRIVER_PORT = 9750;
console.log(`Test fixture created at ${fixture.rootDir}`);
export const config = {
// ── Runner ──
runner: 'local',
maxInstances: 1, // Tauri doesn't support parallel sessions
// ── Connection (tauri-driver on dedicated port) ──
hostname: 'localhost',
port: TAURI_DRIVER_PORT,
path: '/',
// ── Specs ──
// All specs run in a single Tauri app session — state persists between files.
// Stateful describe blocks include reset-to-home-state in their before() hooks.
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/terminal-theme.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-a-structure.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-a-agent.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-a-navigation.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-b-grid.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-b-llm.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'),
resolve(projectRoot, 'tests/e2e/specs/phase-d-settings.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-d-errors.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-e-agents.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-e-health.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-f-search.test.ts'),
resolve(projectRoot, 'tests/e2e/specs/phase-f-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.
* Kills any stale tauri-driver on our port first.
*/
onPrepare() {
// Kill stale tauri-driver on our port to avoid connecting to wrong app
try {
const pids = execSync(`lsof -ti:${TAURI_DRIVER_PORT} 2>/dev/null`, { encoding: 'utf8' }).trim();
if (pids) {
console.log(`Killing stale process(es) on port ${TAURI_DRIVER_PORT}: ${pids}`);
execSync(`kill ${pids} 2>/dev/null`);
}
} catch { /* no process on port — good */ }
// CRITICAL: The debug binary has devUrl (localhost:9700) baked in.
// If another app (e.g., BridgeCoach) is serving on that port, the Tauri
// WebView loads the WRONG frontend. Fail fast if port 9700 is in use.
const DEV_URL_PORT = 9710;
try {
const devPids = execSync(`lsof -ti:${DEV_URL_PORT} 2>/dev/null`, { encoding: 'utf8' }).trim();
if (devPids) {
throw new Error(
`Port ${DEV_URL_PORT} (Tauri devUrl) is in use by another process (PIDs: ${devPids}). ` +
`The debug binary will load that app instead of Agent Orchestrator frontend. ` +
`Either stop the process on port ${DEV_URL_PORT}, or use a release build.`
);
}
} catch (e) {
if (e.message.includes('Port 9700')) throw e;
// lsof returned non-zero = port is free, good
}
// Verify binary exists
if (!existsSync(tauriBinary)) {
if (process.env.SKIP_BUILD) {
throw new Error(`Debug binary not found at ${tauriBinary}. Build first or unset SKIP_BUILD.`);
}
}
if (process.env.SKIP_BUILD) {
// Even with SKIP_BUILD, verify the frontend dist exists
if (!existsSync(resolve(projectRoot, 'dist/index.html'))) {
console.log('Frontend dist/ missing — building frontend only...');
execSync('npm run build', { cwd: projectRoot, stdio: 'inherit' });
}
console.log('SKIP_BUILD set — using existing debug binary.');
return Promise.resolve();
}
return new Promise((resolveHook, reject) => {
// Build frontend first (Tauri --no-bundle skips beforeBuildCommand)
console.log('Building frontend...');
try {
execSync('npm run build', { cwd: projectRoot, stdio: 'inherit' });
} catch (e) {
reject(new Error(`Frontend build failed: ${e.message}`));
return;
}
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.');
resolveHook();
} else {
reject(new Error(`Tauri build failed with exit code ${code}`));
}
});
build.on('error', reject);
});
},
/**
* Spawn tauri-driver on a dedicated port before the session.
* First checks that the port is free (fails fast if another instance is running).
* Uses TCP probe to confirm readiness after spawn.
*/
beforeSession() {
return new Promise((res, reject) => {
// Fail fast if port is already in use (another tauri-driver or stale process)
const preCheck = createConnection({ port: TAURI_DRIVER_PORT, host: 'localhost' }, () => {
preCheck.destroy();
reject(new Error(
`Port ${TAURI_DRIVER_PORT} already in use. Kill the existing process: ` +
`lsof -ti:${TAURI_DRIVER_PORT} | xargs kill`
));
});
preCheck.on('error', () => {
preCheck.destroy();
// Port is free — spawn tauri-driver
tauriDriver = spawn('tauri-driver', ['--port', String(TAURI_DRIVER_PORT)], {
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 until port accepts connections
const deadline = Date.now() + 10_000;
function probe() {
if (Date.now() > deadline) {
reject(new Error(`tauri-driver did not become ready on port ${TAURI_DRIVER_PORT} within 10s`));
return;
}
const sock = createConnection({ port: TAURI_DRIVER_PORT, host: 'localhost' }, () => {
sock.destroy();
console.log(`tauri-driver ready on port ${TAURI_DRIVER_PORT}`);
res();
});
sock.on('error', () => {
sock.destroy();
setTimeout(probe, 200);
});
}
setTimeout(probe, 300);
});
});
},
/**
* Verify the connected app is Agent Orchestrator (not another Tauri/WebKit2GTK app).
* Runs once after the WebDriver session is created, before any spec files.
*/
async before() {
// Wait for app to render, then check for a known element
await browser.waitUntil(
async () => {
const title = await browser.getTitle();
const hasKnownEl = await browser.execute(() =>
document.querySelector('[data-testid="status-bar"]') !== null
|| document.querySelector('.project-grid') !== null
|| document.querySelector('.settings-panel') !== null
);
return hasKnownEl || title.toLowerCase().includes('agor') || title.toLowerCase().includes('orchestrator');
},
{
timeout: 15_000,
interval: 500,
timeoutMsg:
'Connected app is NOT Agent Orchestrator — wrong app detected. ' +
'Check for other Tauri/WebKit2GTK apps running on this machine. ' +
'Kill them or ensure the correct binary is at: ' + tauriBinary,
},
);
console.log('App identity verified: Agent Orchestrator connected.');
},
/**
* 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'),
},
},
};