Files
maestro/ui/playwright.config.ts
T
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

158 lines
7.8 KiB
TypeScript

import { defineConfig, devices } from '@playwright/test';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { mkdirSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import { createOwnedRunDir } from '../scripts/e2e-run-dir.mjs';
// __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.
if (!process.env.E2E_PORT) process.env.E2E_PORT = execFileSync(process.execPath, ['-e', "const s=require('net').createServer();s.listen(0,'127.0.0.1',()=>{console.log(s.address().port);s.close()})"], { encoding: 'utf8' }).trim();
const PORT = Number(process.env.E2E_PORT);
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 specs could open a different DB from the webServer
// (→ "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.
// E2E_RUN_OWNED が立っているのに実行用ディレクトリが無い状態は、手で
// 環境変数を export したときにしか作れない。ここで先に落とす: 一時
// ディレクトリを 1 つでも作った後に投げると、config が完走せず
// globalTeardown も走らないため、作った分が掃除されず残る。
if (process.env.E2E_RUN_OWNED === '1' && !process.env.E2E_RUN_TMP) {
throw new Error('E2E_RUN_OWNED=1 but E2E_RUN_TMP is not set');
}
const cache = join(repoRoot, 'node_modules', '.cache');
mkdirSync(cache, { recursive: true });
if (!process.env.E2E_TMP) {
const ownedData = createOwnedRunDir(cache, 'maestro-e2e-data-');
process.env.E2E_TMP = ownedData.directory;
process.env.E2E_DATA_TOKEN = ownedData.token;
}
const e2eTmp = process.env.E2E_TMP;
if (process.env.E2E_RUN_OWNED !== '1') {
const ownedRun = createOwnedRunDir(cache, 'maestro-e2e-run-');
process.env.E2E_RUN_TMP = ownedRun.directory;
process.env.E2E_RUN_TOKEN = ownedRun.token;
process.env.E2E_RUN_OWNED = '1';
}
process.env.E2E_RUN_PREFIX = 'maestro-e2e-run-';
// 冒頭のガードが「OWNED=1 なのに未設定」を弾き、上の分岐が「未 OWNED」を
// 必ず設定する。したがってここでは必ず値がある(TS はこの 2 段を追えない)。
const e2eRunTmp = process.env.E2E_RUN_TMP!;
process.env.E2E_CACHE_ROOT = cache;
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.
// 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.
// 3) Bash outbound network allowed — WITHOUT this the webServer refuses to boot
// on any host where bwrap cannot create a user namespace. safety.bash_sandbox
// defaults to `auto`, and worker-bootstrap.ts (#1235) treats "network blocking
// is configured but cannot take effect" as a startup error rather than a
// silent downgrade. The dev container hits exactly that (the bwrap binary is
// present, `--unshare-all` is denied), and whether a CI runner does depends on
// its container settings — pinning this makes the boot outcome the same
// everywhere instead of a property of the host. Nothing is actually opened up:
// these specs never reach an LLM, so no agent Bash command ever runs.
const E2E_CONFIG = join(e2eTmp, 'e2e-config.yaml');
writeFileSync(E2E_CONFIG, 'ssh:\n enabled: true\nsafety:\n bash_allow_network: 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']],
globalTeardown: resolve(__dirname, 'e2e-global-teardown.ts'),
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'] },
},
],
// Build before booting so a plain `npm run test:e2e` never serves a stale or
// half-cleaned ui/dist tree. Previously the server could observe ui/dist/
// after Vite emptied it but before index.html was emitted, causing sendFile
// ENOENT failures when E2E and a build ran near each other.
// Boot the real orchestrator (worker mode) with auth OFF (the pinned
// E2E_CONFIG has no auth section => defaults => synthetic 'local' user).
// The webServer command builds both dist trees before starting Node.
webServer: {
command: 'node scripts/e2e-web-server.mjs',
cwd: repoRoot,
url: `${BASE_URL}/ui`,
reuseExistingServer: false,
timeout: 120_000,
stdout: 'pipe',
stderr: 'pipe',
env: {
...process.env,
PORT: String(PORT),
E2E_RUN_TMP: e2eRunTmp,
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',
},
},
});