Repository Public
1import { expect, type Page } from '@playwright/test';2import { SEED, totp } from './helpers';34/** The Codex instance of the suite: same static export, mock started with MOCK_AGENT=codex (playwright.config.ts). */5export const CODEX_MOCK = `http://127.0.0.1:${process.env.CODEX_MOCK_PORT ?? 8622}/api/v1`;6export const CLAUDE_WEB = `http://127.0.0.1:${process.env.WEB_PORT ?? 3621}`;78export async function resetCodex(page: Page, seed = true): Promise<void> {9 await page.request.post(`${CODEX_MOCK}/__mock/reset`, { data: { seed } });10}11export async function codexLogin(page: Page): Promise<void> {12 const r = await page.request.post('/api/v1/auth/login', { data: { username: SEED.username, password: SEED.password, totp: totp(SEED.totpSecret) } });13 expect(r.ok()).toBeTruthy();14}15async function post<T>(page: Page, path: string, data: unknown): Promise<T> {16 const csrf = (await (await page.request.get('/api/v1/auth/status')).json()).csrfToken as string;17 const r = await page.request.post(`/api/v1${path}`, { data, headers: { 'X-CSRF-Token': csrf } });18 expect(r.ok(), `${path} → ${r.status()}`).toBeTruthy();19 return (await r.json()) as T;20}21/** Same working state as shot-helpers `seed`, on the Codex mock. */22export async function seedCodex(page: Page): Promise<{ shell: string; agent: string; agentTitle: string; pid: string }> {23 await resetCodex(page);24 await codexLogin(page);25 const pid = ((await (await page.request.get('/api/v1/projects')).json()) as { id: string; name: string }[]).find((p) => p.name === 'demo')!.id;26 const shell = await post<{ id: string }>(page, '/sessions', { projectId: pid, kind: 'shell', title: 'api server' });27 const agent = await post<{ id: string; title: string }>(page, '/sessions', { projectId: pid, kind: 'claude' }); // wire kind: "the instance's agent"28 await post(page, '/sessions', { projectId: pid, kind: 'shell', title: 'db migrations' });29 await page.request.post(`${CODEX_MOCK}/__mock/session-state`, { data: { id: agent.id, state: 'WAITING_FOR_INPUT', waitingReason: 'Codex finished its turn and is waiting for you' } });30 await post(page, '/tasks', { projectId: pid, title: 'Unit tests', command: 'echo "42 passed"; exit 0', idempotent: true });31 await post(page, '/tasks', { projectId: pid, title: 'Build release', command: 'for i in 1 2 3 4 5 6 7 8; do echo "compiling module $i"; sleep 20; done' });32 return { shell: shell.id, agent: agent.id, agentTitle: agent.title, pid };33}34/** Computed value of a design token on <html>. */35export const token = (page: Page, name: string) => page.evaluate((n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim().toLowerCase(), name);36/** No user-visible "Claude" anywhere in the rendered page (text, titles, aria-labels, placeholders). */37export async function expectNoClaude(page: Page, where: string): Promise<void> {38 const hits = await page.evaluate(() => {39 const out: string[] = [];40 const re = /claude/i;41 if (re.test(document.title)) out.push(`title: ${document.title}`);42 const visible = document.body.innerText;43 if (re.test(visible)) out.push(`text: ${visible.match(/.{0,40}claude.{0,40}/i)?.[0] ?? ''}`);44 for (const el of document.querySelectorAll('[aria-label],[title],[placeholder]'))45 for (const a of ['aria-label', 'title', 'placeholder']) {46 const v = el.getAttribute(a);47 if (v && re.test(v)) out.push(`${a}: ${v}`);48 }49 return out;50 });51 expect(hits, `user-visible "Claude" on ${where}`).toEqual([]);52}53