This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { mkdirSync, readFileSync, 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));
|
||||
@@ -11,27 +12,72 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, '..');
|
||||
|
||||
// Separate port from the auth-OFF harness (8788) so both configs can coexist.
|
||||
const PORT = Number(process.env.E2E_AUTH_PORT ?? 8790);
|
||||
if (!process.env.E2E_AUTH_PORT) process.env.E2E_AUTH_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_AUTH_PORT);
|
||||
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
||||
|
||||
// Isolated throwaway data + workspace dirs — never touches real ./data/maestro.db.
|
||||
//
|
||||
// DETERMINISTIC path (NOT mkdtemp): Playwright imports this config in the main
|
||||
// runner process AND re-imports it in each spec worker process. A random
|
||||
// mkdtemp would yield a DIFFERENT DB_PATH per import — the webServer (started
|
||||
// from the main-process value) and the spec's seeding code would then open
|
||||
// DIFFERENT databases, so seeded members would never appear. A fixed path makes
|
||||
// every import agree on one DB. The spec computes the SAME path independently
|
||||
// (see DB_PATH in the spec) and seeds into it. We do NOT delete it at import
|
||||
// time (a worker import would nuke the live DB); the server rebuilds its schema
|
||||
// on boot and seeding is idempotent, so a stale file from a prior run is benign.
|
||||
const e2eTmp = join(tmpdir(), 'maestro-e2e-auth');
|
||||
mkdirSync(e2eTmp, { recursive: true });
|
||||
// Mint the run directory once and propagate it through the environment. Config
|
||||
// worker imports inherit the same value, while concurrent runs/worktrees never
|
||||
// share or delete one another's database.
|
||||
// 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_AUTH_TMP) {
|
||||
const ownedData = createOwnedRunDir(cache, 'maestro-e2e-auth-data-');
|
||||
process.env.E2E_AUTH_TMP = ownedData.directory;
|
||||
process.env.E2E_DATA_TOKEN = ownedData.token;
|
||||
}
|
||||
const e2eTmp = process.env.E2E_AUTH_TMP;
|
||||
if (process.env.E2E_RUN_OWNED !== '1') {
|
||||
const ownedRun = createOwnedRunDir(cache, 'maestro-e2e-auth-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-auth-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-auth.db');
|
||||
const WORKTREE_DIR = join(e2eTmp, 'workspaces');
|
||||
|
||||
// Absolute path to the local-auth config YAML (enables POST /auth/local login).
|
||||
const AAO_CONFIG = resolve(repoRoot, 'ui/e2e-auth/config.e2e-auth.yaml');
|
||||
// The checked-in local-auth config YAML (enables POST /auth/local login).
|
||||
const AUTH_CONFIG_SOURCE = resolve(repoRoot, 'ui/e2e-auth/config.e2e-auth.yaml');
|
||||
|
||||
// Assemble a THROWAWAY config for the webServer: the checked-in local-auth YAML
|
||||
// plus one harness-only section appended at run time. Same shape as the auth-OFF
|
||||
// harness (ui/playwright.config.ts, #1394); the extra section is deliberately NOT
|
||||
// written into ui/e2e-auth/config.e2e-auth.yaml, because a checked-in config that
|
||||
// says "outbound network is allowed" reads like part of the production setup.
|
||||
//
|
||||
// What gets appended and why (#1399): WITHOUT `safety.bash_allow_network: true`
|
||||
// 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.
|
||||
//
|
||||
// The write is unconditional on every config evaluation (main process + worker).
|
||||
// That is idempotent: e2eTmp is stable across evaluations in this process and is
|
||||
// inherited by the webServer subprocess via the environment.
|
||||
const AAO_CONFIG = join(e2eTmp, 'e2e-auth-config.yaml');
|
||||
writeFileSync(
|
||||
AAO_CONFIG,
|
||||
`${readFileSync(AUTH_CONFIG_SOURCE, 'utf8')}\nsafety:\n bash_allow_network: true\n`,
|
||||
);
|
||||
|
||||
export default defineConfig({
|
||||
testDir: resolve(__dirname, 'e2e-auth'),
|
||||
@@ -39,14 +85,25 @@ export default defineConfig({
|
||||
// Build+boot can be slow; roomy per-test budget.
|
||||
timeout: 60_000,
|
||||
expect: { timeout: 15_000 },
|
||||
// 走り全体の上限。1 件 60 秒 × 21 件が全部詰まると、サーバー起動の 120 秒と
|
||||
// 合わせて CI のジョブ上限(30 分・ci.yml の timeout-minutes)を食い破り、
|
||||
// ログが途中で切れた赤になって原因が読めなくなる。実測は 21 件で 58.6 秒
|
||||
// (CI run 3212)なので 10 分は十分な余裕がありつつ、暴走はここで止まる。
|
||||
globalTimeout: 600_000,
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
retries: 0,
|
||||
reporter: [['list']],
|
||||
globalTeardown: resolve(__dirname, 'e2e-global-teardown.ts'),
|
||||
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
headless: true,
|
||||
// 表示言語を日本語に固定する。ここが抜けていると UI は英語で描画され、
|
||||
// e2e-auth の spec が持つ日本語の文字列('追加'・'リンクが無効です' 等)は
|
||||
// 一つも当たらない。認証なし側(ui/playwright.config.ts)は最初からこれを
|
||||
// 置いていて、こちら側だけ抜けていた(#1399)。
|
||||
locale: 'ja-JP',
|
||||
// NOTE: the wide viewport that keeps the desktop nav (data-testid="nav-*")
|
||||
// out of the hamburger drawer is set on the chromium project below, AFTER
|
||||
// the Desktop Chrome device spread (which would otherwise pin 1280×720).
|
||||
@@ -72,9 +129,9 @@ export default defineConfig({
|
||||
// Boot the real orchestrator (worker mode) with LOCAL AUTH ON (AAO_CONFIG
|
||||
// points at the local-auth YAML). The bootstrap admin is seeded at startup;
|
||||
// member users are seeded by the spec's beforeAll into the same temp DB.
|
||||
// dist/ must exist (npm run build:all).
|
||||
// The webServer command builds both dist trees before starting Node.
|
||||
webServer: {
|
||||
command: 'node dist/main.js',
|
||||
command: 'node scripts/e2e-web-server.mjs',
|
||||
cwd: repoRoot,
|
||||
url: `${BASE_URL}/ui`,
|
||||
reuseExistingServer: false,
|
||||
@@ -84,6 +141,7 @@ export default defineConfig({
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(PORT),
|
||||
E2E_RUN_TMP: e2eRunTmp,
|
||||
DB_PATH,
|
||||
WORKTREE_DIR,
|
||||
AAO_CONFIG,
|
||||
|
||||
Reference in New Issue
Block a user