mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
a1995a186a
* test(e2e): harden UI flake waits with real completion signals Replace synthetic /projects routing, blind boot sleeps, and call-site create retries with observable readiness: real navigation, suite daemon health warmup, expect.poll, clock fast-forward for coalesce TTL, and quiescence for AMR refresh traffic. * test(e2e): raise Playwright fixture timeout for daemon warmup The suite fixture now polls daemon /api/health for up to 60s, which consumed the prior 390s budget's bookkeeping margin. Raise the worker fixture timeout to 450s so start + web/daemon warmup + stop still leave a full minute for teardown bookkeeping on slow CI workers. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * test(e2e): fail closed on health body drain and narrow billing mock Require warmPlaywrightDaemonRuntime to fully consume ok health bodies before declaring readiness, drain non-ok probes, and fulfill only the exact /api/workspace/billing path so nested billing routes fall through. Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build) * test(e2e): reopen projects by id in cross-project file-tab restore Avoid relying on chrome workspace tabs surviving leave+create hard navigation; assert per-project file-tab restore via project routes.
215 lines
6.9 KiB
TypeScript
215 lines
6.9 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
initializePlaywrightRunNamespace,
|
|
PLAYWRIGHT_RUN_NAMESPACE_ENV,
|
|
resolvePlaywrightSlotNamespace,
|
|
} from '@/playwright/runtime-identity';
|
|
import {
|
|
warmPlaywrightDaemonRuntime,
|
|
warmPlaywrightWebRuntime,
|
|
} from '@/playwright/runtime-lifecycle';
|
|
import type { RunToolsDevJsonOptions } from '@/tools-dev/cli';
|
|
import { createToolsDevSuite } from '@/tools-dev/runtime';
|
|
import type { ToolsDevRuntimeDependencies } from '@/tools-dev/runtime';
|
|
import type { ToolsDevSuiteSpec } from '@/tools-dev/types';
|
|
|
|
describe('Playwright tools-dev runtime lifecycle', () => {
|
|
test('keeps a replacement worker on the same logical parallel slot', () => {
|
|
const env: Record<string, string | undefined> = {};
|
|
expect(initializePlaywrightRunNamespace(env, () => 'run-token')).toBe('playwright-run-token');
|
|
expect(env[PLAYWRIGHT_RUN_NAMESPACE_ENV]).toBe('playwright-run-token');
|
|
|
|
expect(resolvePlaywrightSlotNamespace(0, env)).toBe('playwright-run-token-w0');
|
|
expect(resolvePlaywrightSlotNamespace(0, env)).toBe('playwright-run-token-w0');
|
|
expect(resolvePlaywrightSlotNamespace(1, env)).toBe('playwright-run-token-w1');
|
|
});
|
|
|
|
test('uses the explicit namespace as a prefix while keeping concurrent runs isolated', () => {
|
|
const env = {
|
|
OD_E2E_NAMESPACE: 'focused route stress',
|
|
};
|
|
expect(initializePlaywrightRunNamespace(env, () => 'run-token')).toBe(
|
|
'focused-route-stress-run-token',
|
|
);
|
|
expect(resolvePlaywrightSlotNamespace(2, env)).toBe('focused-route-stress-run-token-w2');
|
|
});
|
|
|
|
test('preserves an inherited exact run namespace across Playwright processes', () => {
|
|
const env = {
|
|
OD_E2E_NAMESPACE: 'ignored-prefix',
|
|
[PLAYWRIGHT_RUN_NAMESPACE_ENV]: 'generated-run',
|
|
};
|
|
expect(initializePlaywrightRunNamespace(env, () => 'unused')).toBe('generated-run');
|
|
expect(resolvePlaywrightSlotNamespace(2, env)).toBe('generated-run-w2');
|
|
});
|
|
|
|
test('clears the previous slot incarnation before allocating ports and passes its owner pid', async () => {
|
|
const calls: Array<{ args: string[]; timeoutMs: number | undefined }> = [];
|
|
const order: string[] = [];
|
|
const spec: ToolsDevSuiteSpec = {
|
|
codexHomeDir: '/tmp/codex-home',
|
|
dataDir: '/tmp/data',
|
|
namespace: 'playwright-run-w0',
|
|
ownerPid: 4242,
|
|
root: '/tmp/root',
|
|
toolsDevRoot: '/tmp/tools-dev',
|
|
};
|
|
const runJson: ToolsDevRuntimeDependencies['runJson'] = async <T>(
|
|
_workspaceRoot: string,
|
|
_suite: ToolsDevSuiteSpec,
|
|
args: string[],
|
|
_env: Record<string, string | undefined> = {},
|
|
options?: RunToolsDevJsonOptions,
|
|
): Promise<T> => {
|
|
calls.push({ args, timeoutMs: options?.timeoutMs });
|
|
order.push(args[0] === 'stop' ? 'stop' : 'start');
|
|
if (args[0] === 'start') {
|
|
return {
|
|
daemon: { status: { url: 'http://127.0.0.1:31001' } },
|
|
web: { status: { url: 'http://127.0.0.1:31002' } },
|
|
} as T;
|
|
}
|
|
return {} as T;
|
|
};
|
|
const suite = createToolsDevSuite(spec, {
|
|
allocatePorts: async () => {
|
|
order.push('allocate');
|
|
return {
|
|
daemonPort: 31001,
|
|
webPort: 31002,
|
|
release: async () => {
|
|
order.push('release');
|
|
},
|
|
};
|
|
},
|
|
runJson,
|
|
});
|
|
|
|
await suite.startWeb();
|
|
|
|
expect(order).toEqual(['stop', 'allocate', 'release', 'start']);
|
|
expect(calls[0]?.args.slice(0, 2)).toEqual(['stop', 'web']);
|
|
expect(calls[0]?.timeoutMs).toBe(30_000);
|
|
expect(calls[1]?.args).toContain('--parent-pid');
|
|
expect(calls[1]?.args).toContain('4242');
|
|
expect(calls[1]?.timeoutMs).toBe(180_000);
|
|
});
|
|
|
|
test('aborts a blocked cold warmup before the fixture teardown budget is consumed', async () => {
|
|
const blockedFetch = vi.fn((_url: string, init?: RequestInit) =>
|
|
new Promise<Response>((_resolve, reject) => {
|
|
init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true });
|
|
}));
|
|
|
|
await expect(
|
|
warmPlaywrightWebRuntime('http://127.0.0.1:31002/', {
|
|
fetch: blockedFetch,
|
|
timeoutMs: 10,
|
|
}),
|
|
).rejects.toThrow('Playwright web warmup timed out after 10ms');
|
|
});
|
|
|
|
test('polls daemon health until the first ok response', async () => {
|
|
let calls = 0;
|
|
const fetchRuntime = vi.fn(async () => {
|
|
calls += 1;
|
|
if (calls < 3) {
|
|
return new Response('starting', { status: 503, statusText: 'Service Unavailable' });
|
|
}
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
});
|
|
|
|
await warmPlaywrightDaemonRuntime('http://127.0.0.1:31001/api/health', {
|
|
fetch: fetchRuntime,
|
|
intervalMs: 1,
|
|
timeoutMs: 1_000,
|
|
});
|
|
|
|
expect(calls).toBe(3);
|
|
});
|
|
|
|
test('times out when daemon health never becomes ready', async () => {
|
|
const fetchRuntime = vi.fn(async () => {
|
|
throw new Error('connection refused');
|
|
});
|
|
|
|
await expect(
|
|
warmPlaywrightDaemonRuntime('http://127.0.0.1:31001/api/health', {
|
|
fetch: fetchRuntime,
|
|
intervalMs: 1,
|
|
timeoutMs: 20,
|
|
}),
|
|
).rejects.toThrow('Playwright daemon warmup timed out after 20ms');
|
|
});
|
|
|
|
test('retries when an ok health response body cannot be drained', async () => {
|
|
let calls = 0;
|
|
const fetchRuntime = vi.fn(async () => {
|
|
calls += 1;
|
|
if (calls === 1) {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
arrayBuffer: async () => {
|
|
throw new Error('body truncated');
|
|
},
|
|
} as unknown as Response;
|
|
}
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
});
|
|
|
|
await warmPlaywrightDaemonRuntime('http://127.0.0.1:31001/api/health', {
|
|
fetch: fetchRuntime,
|
|
intervalMs: 1,
|
|
timeoutMs: 1_000,
|
|
});
|
|
|
|
expect(calls).toBe(2);
|
|
});
|
|
|
|
test('drains non-ok health bodies before the next probe', async () => {
|
|
let calls = 0;
|
|
let drained503 = false;
|
|
const fetchRuntime = vi.fn(async () => {
|
|
calls += 1;
|
|
if (calls === 1) {
|
|
return {
|
|
ok: false,
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
arrayBuffer: async () => {
|
|
drained503 = true;
|
|
return new ArrayBuffer(0);
|
|
},
|
|
} as unknown as Response;
|
|
}
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
});
|
|
|
|
await warmPlaywrightDaemonRuntime('http://127.0.0.1:31001/api/health', {
|
|
fetch: fetchRuntime,
|
|
intervalMs: 1,
|
|
timeoutMs: 1_000,
|
|
});
|
|
|
|
expect(calls).toBe(2);
|
|
expect(drained503).toBe(true);
|
|
});
|
|
});
|