import { defineConfig, devices } from '@playwright/test'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { mkdtempSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; // __dirname under ESM. const __dirname = dirname(fileURLToPath(import.meta.url)); // Repo root is one level up from ui/. const repoRoot = resolve(__dirname, '..'); // Dedicated E2E port — avoids clashing with a dev server on 9876. const PORT = Number(process.env.E2E_PORT ?? 8788); const BASE_URL = `http://127.0.0.1:${PORT}`; // Isolated, throwaway data + workspace dirs so the E2E never touches real data // (real DB lives at ./data/maestro.db). A fresh temp DB also means the personal // space is auto-created clean on first /api/local/spaces hit. // The temp dir MUST be stable across the multiple times Playwright evaluates // this config (main process + worker), otherwise mkdtempSync would mint a new // dir per evaluation and the .e2e-db-path handoff file could point at a // different DB than the one the webServer actually booted with (→ specs open an // empty DB and fail with "no such table: jobs"). Self-seed it into the // environment on the FIRST evaluation: subsequent evals in this process reuse // it, and the webServer subprocess + worker processes inherit it via env. No // caller setup required (plain `npm run test:e2e` works); an explicitly-set // E2E_TMP is still honored. if (!process.env.E2E_TMP) { process.env.E2E_TMP = mkdtempSync(join(tmpdir(), 'maestro-e2e-')); } const e2eTmp = process.env.E2E_TMP; const DB_PATH = join(e2eTmp, 'e2e.db'); const WORKTREE_DIR = join(e2eTmp, 'workspaces'); // Expose the throwaway DB path to specs that need to seed engine state which // can't be produced without a live LLM (e.g. a job parked for tool approval). // Written to a gitignored file next to this config so specs can read it. writeFileSync(join(__dirname, '.e2e-db-path'), DB_PATH); // Dedicated, deterministic config for the E2E webServer, handed over via // AAO_CONFIG. Two reasons: // 1) Isolation — without this, dist/main.js loads the repo-root ./config.yaml // if a developer happens to have one (it's gitignored, so present locally // but absent in CI). That silently changes E2E behavior between machines // (e.g. pointing the worker at a real LLM). Pinning AAO_CONFIG makes the // run identical everywhere; everything unset here falls back to defaults. // 2) SSH enabled — the settings sub-tab specs assert the space-scoped SSH // panel (space-ssh-panel) renders. That testid only mounts once // /api/ssh/connections answers 200; with SSH off the route 404s and the // panel shows its "SSH disabled" view (no testid). In isolation the spec // scraped the brief loading-state render and passed by luck; under full- // suite load that window closed and both SSH specs failed. Enabling SSH // (needs the MCP_ENCRYPTION_KEY set below) makes the panel deterministic. const E2E_CONFIG = join(e2eTmp, 'e2e-config.yaml'); writeFileSync(E2E_CONFIG, 'ssh:\n enabled: true\n'); export default defineConfig({ testDir: resolve(__dirname, 'e2e'), // The build+boot can be slow; keep a roomy per-test budget. timeout: 60_000, expect: { timeout: 15_000 }, fullyParallel: false, workers: 1, retries: 0, reporter: [['list']], use: { baseURL: BASE_URL, headless: true, locale: 'ja-JP', // This sandbox disables the Chromium sandbox (bwrap/userns off). Launch // with --no-sandbox or every browser launch fails to spawn. launchOptions: { args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], }, trace: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], // Boot the real orchestrator (worker mode) with auth OFF (the pinned // E2E_CONFIG has no auth section => defaults => synthetic 'local' user). // dist/ must exist (npm run build:all). webServer: { command: 'node dist/main.js', cwd: repoRoot, url: `${BASE_URL}/ui`, reuseExistingServer: false, timeout: 120_000, stdout: 'pipe', stderr: 'pipe', env: { ...process.env, PORT: String(PORT), DB_PATH, WORKTREE_DIR, // Pin the config to the throwaway E2E file so the run never inherits a // developer's repo-root ./config.yaml (see E2E_CONFIG above). AAO_CONFIG: E2E_CONFIG, AAO_MODE: 'worker', LOG_LEVEL: 'warn', // Mount the MCP subsystem (gated on a 64-hex MCP_ENCRYPTION_KEY) so the // per-space MCP isolation E2E can register/list servers via /api/mcp/*. // A fixed throwaway key keeps the run deterministic; the temp DB it // encrypts is discarded after the run. MCP_ENCRYPTION_KEY: '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff', // Configure a STUB LLM so the first-run SetupWizard never gates the UI // (it shows when no model is configured). The endpoint is never actually // reached by these specs — they assert UI/DB state, not model output — so // a fake host is fine and keeps the E2E self-contained (no caller env). OLLAMA_BASE_URL: process.env.OLLAMA_BASE_URL ?? 'http://127.0.0.1:11434/v1', OLLAMA_MODEL: process.env.OLLAMA_MODEL ?? 'e2e-stub-model', }, }, });