Repository Public
1/**2 * Acceptance test of phases 0–2: the real server, the real worker, real PostgreSQL, the production3 * static export under the server's CSP, and REAL models (volume tier), driven through the browser.4 * Serial: one administrator, one shared browser context; every mission is recorded with its model,5 * turns and cost in e2e-real/last-run.json. Total spend stays far below USD 0.50.6 */7import fs from 'node:fs';8import path from 'node:path';9import { devices, expect, request, test, type BrowserContext, type Page } from '@playwright/test';10import { AUTH_FILE, DATA_DIR, HAIKU, KEEP, LUNA, ORIGIN, SCREENSHOTS } from './env';11import {12 Guard, allRowKeys, apiGet, createMission, createProject, gitIn, missionBranch, missionCommits, missionStatus, nextTotpWindow, record, reveal, rowBySeq,13 seedBugProject, startWorker, steps, testsPass, toolRow, totalSpend, totp, workerPid, wsUpgradeStatus, type MissionLite,14} from './helpers';1516const USER = 'owner';17const PASSWORD = 'a-long-passphrase-for-the-real-run-42';18const FIX_OBJECTIVE = 'The test suite fails (`node --test`). Find the bug in sum.js, fix it, run the tests, and commit the fix.';19const TESTS_PASS = '`node --test` exits 0';20const MODEL = process.env.AOS_E2E_MODEL ?? HAIKU; // retry lever: AOS_E2E_MODEL=claude-sonnet-5-52122let context: BrowserContext;23let page: Page;24let guard: Guard;25let secret = '';26const shot = (p: Page, name: string) => p.screenshot({ path: path.join(SCREENSHOTS, `${name}.png`) });27const done = async (p: Page, status = 'done', timeout = 5 * 60_000) => expect(missionStatus(p)).toHaveAttribute('data-status', status, { timeout });2829test.describe.configure({ mode: 'serial' });3031test.beforeAll(async ({ browser }) => {32 fs.mkdirSync(SCREENSHOTS, { recursive: true });33 context = await browser.newContext({ viewport: { width: 1440, height: 900 } });34 guard = new Guard(context);35 page = await context.newPage();36 if (KEEP) {37 // partial re-run: the administrator exists; sign in with the saved second factor38 secret = (JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8')) as { secret: string }).secret;39 await nextTotpWindow();40 const r = await page.request.post('/api/v1/auth/login', { data: { username: USER, password: PASSWORD, totp: totp(secret) }, headers: { origin: ORIGIN } });41 expect(r.ok(), 'sign in on the existing instance').toBeTruthy();42 }43});44/** Project names are unique per run (a partial re-run keeps the instance). */45const RUN_TAG = Date.now().toString(36).slice(-4);46const pname = (base: string): string => `${base}-${RUN_TAG}`;47test.afterAll(async () => {48 await context?.close();49});5051test('unauthenticated REST and WebSocket access is refused', async () => {52 const anon = await request.newContext({ baseURL: ORIGIN });53 for (const p of ['/missions', '/projects', '/waits', '/system/status', '/settings', '/spend', '/policy/rules', '/models']) {54 expect((await anon.get(`/api/v1${p}`)).status(), p).toBe(401);55 }56 expect((await anon.post('/api/v1/missions', { data: {}, headers: { origin: ORIGIN } })).status()).toBe(401);57 expect((await anon.get('/api/v1/health')).status()).toBe(200);58 await anon.dispose();59 const status = await wsUpgradeStatus({ Origin: ORIGIN });60 expect(status).toBe(401);61});6263test('first run: setup token, TOTP enrolment, sign out, sign in with password + TOTP', async () => {64 test.skip(KEEP, 'partial re-run on an existing instance');65 await page.goto('/');66 await expect(page).toHaveURL(/\/setup\//);67 await page.getByTestId('setup-token').fill(fs.readFileSync(path.join(DATA_DIR, 'setup-token'), 'utf8').trim());68 await page.getByTestId('setup-username').fill(USER);69 await page.getByTestId('setup-password').fill(PASSWORD);70 await page.getByTestId('setup-confirm').fill(PASSWORD);71 await page.getByTestId('setup-submit').click();72 await expect(page.getByTestId('second-factor-enroll')).toBeVisible();73 await page.getByTestId('totp-start').click();74 await expect(page.locator('canvas[role="img"]')).toBeVisible();75 secret = (await page.getByTestId('totp-secret').getAttribute('data-secret')) ?? '';76 expect(secret.length).toBeGreaterThan(15);77 fs.writeFileSync(AUTH_FILE, JSON.stringify({ secret }), { mode: 0o600 }); // for AOS_E2E_KEEP re-runs; wiped with the instance78 await page.getByTestId('totp-enroll-code').fill(totp(secret));79 await page.getByRole('button', { name: 'Verify and enable' }).click();80 await expect(page).toHaveURL(/\/home\//);81 await expect(page.getByTestId('needs-you-empty')).toBeVisible();82 await expect(page.getByTestId('today-usd')).toHaveText('$0.00');83 // the setup token is single-use: gone from the host once the administrator exists84 expect(fs.existsSync(path.join(DATA_DIR, 'setup-token'))).toBe(false);85 // the real system status: live worker heartbeat, database, both provider keys present (booleans only)86 await expect(page.getByTestId('health-worker')).toHaveAttribute('data-ok', 'true');87 await expect(page.getByTestId('health-db')).toHaveAttribute('data-ok', 'true');88 await expect(page.getByTestId('health-anthropic')).toHaveAttribute('data-ok', 'true');89 await expect(page.getByTestId('health-openai')).toHaveAttribute('data-ok', 'true');9091 await page.getByRole('button', { name: 'Sign out' }).click();92 await expect(page).toHaveURL(/\/login\//);93 await nextTotpWindow(); // a code cannot be replayed94 await page.getByTestId('login-username').fill(USER);95 await page.getByTestId('login-password').fill(PASSWORD);96 await page.getByTestId('login-submit').click();97 await page.getByLabel('Authentication code').fill(totp(secret));98 await page.getByTestId('totp-submit').click();99 await expect(page).toHaveURL(/\/home\//);100 await guard.expectClean();101});102103test(`scenario 1 (${MODEL}): fix a bug — live timeline, diff, shell exit code, criterion with evidence, done, spend, commit on disk`, async () => {104 const project = await createProject(page, pname('calc-one'));105 const { dir } = seedBugProject(project.path);106 expect(testsPass(dir)).toBe(false);107 const id = await createMission(page, { project: pname('calc-one'), objective: FIX_OBJECTIVE, criteria: [TESTS_PASS], model: MODEL });108 // live: tool rows appear while the mission runs109 await expect(page.locator('[data-testid="tool-row"]').first()).toBeVisible({ timeout: 120_000 });110 await done(page);111 // streamed assistant text was on screen at some point, and nothing is left streaming112 expect(await page.evaluate(() => (window as unknown as { __sawStream?: boolean }).__sawStream === true), 'streamed text appeared').toBe(true);113 await expect(page.locator('[data-streaming]')).toHaveCount(0);114115 const m = await apiGet<MissionLite>(page, `/missions/${id}`);116 const journal = await steps(page, m.rootRunId);117 // the journal the browser received never carries the provider-native transcript118 expect(JSON.stringify(journal)).not.toMatch(/"native"|"signature"|encrypted_content/);119 const read = journal.find((s) => s.name === 'read_file' && s.result?.ok);120 const edit = journal.find((s) => (s.name === 'edit_file' || s.name === 'write_file') && s.result?.ok && s.result.diff);121 const shell = [...journal].reverse().find((s) => s.name === 'shell' && /^\[exit code 0\]/.test(s.result?.content ?? ''));122 expect(read, 'a successful read_file step').toBeTruthy();123 expect(edit, 'a successful edit with a recorded diff').toBeTruthy();124 expect(shell, 'a shell step that exited 0').toBeTruthy();125126 await reveal(page, rowBySeq(page, read!.seq));127 await expect(rowBySeq(page, read!.seq)).toHaveAttribute('data-state', 'ok');128 // the edit is rendered as the diff the tool recorded129 const editRow = rowBySeq(page, edit!.seq);130 await reveal(page, editRow);131 if ((await editRow.getByTestId('diff-view').count()) === 0) await editRow.getByRole('button').first().click();132 await expect(editRow.getByTestId('diff-view')).toBeVisible();133 await expect(editRow.locator('[data-line="add"]').first()).toBeVisible();134 await expect(editRow.locator('[data-line="del"]').first()).toContainText('let i = 1');135 await page.getByTestId('timeline-scroll').evaluate((el) => { el.scrollTop -= 160; });136 await page.waitForTimeout(400);137 await shot(page, 'desktop-mission-diff');138 // shell: terminal block with the exit code reported by the tool139 const shellRow = rowBySeq(page, shell!.seq);140 await reveal(page, shellRow);141 await shellRow.getByRole('button').first().click();142 await expect(shellRow.getByTestId('exit-code')).toHaveText('exit 0');143 await expect(shellRow.getByTestId('shell-output')).not.toContainText('[exit code');144 await page.waitForTimeout(300);145 await shot(page, 'desktop-mission-shell');146147 // criterion flipped with evidence; status done; spend shown and > 0148 const side = page.getByTestId('mission-side');149 await expect(side.locator('[data-testid="criterion"][data-passes="true"]')).toHaveCount(1);150 await expect(side.getByTestId('criterion-evidence')).not.toBeEmpty();151 expect(m.doneCriteria[0]?.evidence?.length ?? 0).toBeGreaterThan(5);152 const spent = Number(await page.getByTestId('spend-of-budget').first().getAttribute('data-spent'));153 expect(spent).toBeGreaterThan(0);154 await expect(page.getByTestId('spend-of-budget').first()).not.toContainText('$0.00 /');155 await expect(page.getByTestId('run-result')).toBeVisible();156 await expect(page.getByTestId('composer-ended')).toBeVisible();157 const keys = await allRowKeys(page);158 expect(new Set(keys).size, 'no duplicated timeline row').toBe(keys.length);159 await page.getByTestId('timeline-scroll').evaluate((el) => { el.scrollTop = el.scrollHeight; });160 await page.waitForTimeout(400);161 await shot(page, 'desktop-mission-result');162163 // on disk: tests pass, the fix is committed on the mission branch164 expect(testsPass(dir)).toBe(true);165 expect(gitIn(dir, 'rev-parse', '--abbrev-ref', 'HEAD')).toBe(missionBranch(id));166 expect(missionCommits(dir, id).length).toBeGreaterThanOrEqual(1);167 expect(gitIn(dir, 'status', '--porcelain', '--untracked-files=no')).toBe('');168 await record(page, 'scenario 1 fix bug', id);169 // the list shows the model without a request per mission170 await page.goto('/missions/');171 await expect(page.getByTestId('missions-list')).toContainText(/Haiku|Sonnet|claude/i);172 await guard.expectClean();173});174175test(`scenario 1 again with ${LUNA}: fix the bug, done, commit on disk`, async () => {176 const project = await createProject(page, pname('calc-luna'));177 const { dir } = seedBugProject(project.path);178 const id = await createMission(page, { project: pname('calc-luna'), objective: FIX_OBJECTIVE, criteria: [TESTS_PASS], model: LUNA });179 await done(page);180 const m = await apiGet<MissionLite>(page, `/missions/${id}`);181 expect(m.model).toBe(LUNA);182 expect(JSON.stringify(await steps(page, m.rootRunId))).not.toMatch(/"native"|encrypted_content/);183 await expect(page.getByTestId('mission-side').locator('[data-testid="criterion"][data-passes="true"]')).toHaveCount(1);184 await expect(toolRow(page, 'finish')).toHaveAttribute('data-state', 'ok');185 expect(testsPass(dir)).toBe(true);186 expect(missionCommits(dir, id).length).toBeGreaterThanOrEqual(1);187 const keys = await allRowKeys(page);188 expect(new Set(keys).size).toBe(keys.length);189 await record(page, 'scenario 1 fix bug (OpenAI)', id);190 await guard.expectClean();191});192193test(`scenario 2 (${MODEL}): git_push needs an approval — shown on Home and in Approvals on a phone, approved, pushed to the remote`, async ({ browser }) => {194 const project = await createProject(page, pname('calc-push'));195 const { dir, remoteDir } = seedBugProject(project.path, { remote: true });196 const id = await createMission(page, {197 project: pname('calc-push'),198 objective: `${FIX_OBJECTIVE} Then push the mission branch to the remote "origin" with the git_push tool.`,199 criteria: [TESTS_PASS, 'The mission branch is pushed to the remote origin'],200 model: MODEL,201 });202 await done(page, 'waiting');203204 const phone = await browser.newContext({ ...devices['iPhone 15'], storageState: await context.storageState() });205 const phoneGuard = new Guard(phone);206 const pp = await phone.newPage();207 await pp.goto('/home/');208 const card = pp.getByTestId('needs-you').getByTestId('wait-card');209 await expect(card).toHaveCount(1);210 await expect(card).toContainText('git push origin');211 await expect(card.getByTestId('risk-badge')).toHaveAttribute('data-risk', 'external');212 await pp.waitForTimeout(600);213 await shot(pp, 'phone-home-approval');214 // nothing wider than the screen215 expect(await pp.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);216217 await pp.goto('/approvals/');218 const inbox = pp.locator('[data-testid="wait-card"][data-kind="approval"]');219 await expect(inbox).toHaveCount(1);220 await expect(inbox.getByTestId('wait-args')).toContainText('"remote"');221 await pp.waitForTimeout(400);222 await shot(pp, 'phone-approvals');223 expect(gitIn(remoteDir!, 'for-each-ref', '--format=%(refname)')).toBe(''); // nothing pushed before the approval224 await inbox.getByTestId('wait-approve').tap();225 await expect(pp.getByTestId('approvals-empty')).toBeVisible();226227 await pp.goto(`/mission/?id=${id}`);228 await done(pp);229 await pp.waitForTimeout(600);230 await shot(pp, 'phone-mission-timeline');231 await expect(pp.getByTestId('timeline-wait').filter({ hasText: 'Approved' })).toBeVisible();232 // a reload still shows the approval, from the journaled wait step (policy.decision events are not replayed)233 await pp.reload();234 await expect(pp.getByTestId('timeline-wait').filter({ hasText: 'Approved' })).toBeVisible();235 await expect(pp.locator('[data-testid="tool-row"][data-tool="git_push"]')).toHaveAttribute('data-state', 'ok');236 await phoneGuard.expectClean();237 await phone.close();238239 // the remote has the mission's commit240 const local = gitIn(dir, 'rev-parse', `refs/heads/${missionBranch(id)}`);241 expect(gitIn(remoteDir!, 'rev-parse', `refs/heads/${missionBranch(id)}`)).toBe(local);242 expect(missionCommits(dir, id).length).toBeGreaterThanOrEqual(1);243 await done(page);244 await record(page, 'scenario 2 push approval', id);245 await guard.expectClean();246});247248test(`scenario 3 (${MODEL}): no done criteria — the agent proposes them, the owner accepts, the mission proceeds`, async () => {249 const project = await createProject(page, pname('hello'));250 const dir = path.join('/tmp/aos-dev-workspace', project.path);251 const id = await createMission(page, { project: pname('hello'), objective: 'Create a file named hello.txt whose content is exactly the word hello followed by a newline, and commit it.', criteria: [], model: MODEL });252 await expect(page.getByTestId('criteria-empty')).toBeVisible();253 const q = page.getByTestId('timeline').getByTestId('timeline-wait').filter({ has: page.getByTestId('wait-option') });254 await expect(q).toBeVisible({ timeout: 120_000 });255 await expect(q).toContainText('The agent proposes these done criteria');256 await expect(q.getByTestId('wait-option')).toHaveText(['Yes — use these criteria']);257 expect(fs.existsSync(path.join(dir, 'hello.txt')), 'no write before the criteria are confirmed').toBe(false);258 await q.getByTestId('wait-option').click();259 // the confirmed criteria appear without a reload260 await expect(page.getByTestId('mission-side').getByTestId('criterion').first()).toBeVisible({ timeout: 60_000 });261 await done(page);262 const m = await apiGet<MissionLite>(page, `/missions/${id}`);263 expect(m.doneCriteria.length).toBeGreaterThanOrEqual(1);264 expect(m.doneCriteria.every((c) => c.passes && (c.evidence ?? '').length > 0)).toBe(true);265 expect(fs.readFileSync(path.join(dir, 'hello.txt'), 'utf8').trim()).toBe('hello');266 await expect(page.getByTestId('timeline').getByTestId('timeline-wait').filter({ hasText: 'Answered' })).toHaveCount(1);267 await record(page, 'scenario 3 criteria proposal', id);268 await guard.expectClean();269});270271test(`scenario 4 (${MODEL}): kill -9 the worker mid-mission — a new worker resumes it, no duplicated commit, coherent timeline after reload`, async () => {272 const project = await createProject(page, pname('calc-crash'));273 const { dir } = seedBugProject(project.path);274 const id = await createMission(page, {275 project: pname('calc-crash'),276 objective: 'First, before anything else, run exactly this shell command once with timeoutSec 120: `sleep 45; echo warmed-up`. Then: the test suite fails (`node --test`); find the bug in sum.js, fix it, run the tests, and commit the fix.',277 criteria: [TESTS_PASS],278 model: MODEL,279 });280 const sleeping = page.locator('[data-testid="tool-row"][data-tool="shell"][data-state="running"]').filter({ hasText: 'sleep 45' });281 await expect(sleeping).toBeVisible({ timeout: 120_000 });282 const m0 = await apiGet<MissionLite>(page, `/missions/${id}`);283 const before = await steps(page, m0.rootRunId);284 const open = before.find((s) => s.status === 'started' && s.name === 'shell');285 expect(open, 'the shell step is journaled as started (intent recorded)').toBeTruthy();286287 const pid = workerPid();288 process.kill(pid, 'SIGKILL');289 await expect.poll(() => { try { process.kill(pid, 0); return true; } catch { return false; } }).toBe(false);290 // with no worker the mission still says "running" (the lease has not expired); the system page tells the truth291 startWorker();292 expect(workerPid()).not.toBe(pid);293294 // reload while the mission is in limbo: the timeline is rebuilt from the journal and keeps following live295 await page.reload();296 await expect(page.locator(`[data-testid="tool-row"][data-seq="${open!.seq}"]`)).toBeVisible({ timeout: 30_000 });297 await done(page, 'done', 7 * 60_000);298299 const journal = await steps(page, m0.rootRunId);300 expect(journal.map((s) => s.seq)).toEqual(Array.from({ length: journal.length }, (_, i) => i)); // contiguous, nothing twice301 // the interrupted step was re-executed IN PLACE (same seq), not journaled a second time302 expect(journal.filter((s) => s.name === 'shell' && String(s.request?.input?.command ?? '').includes('sleep 45'))).toHaveLength(1);303 expect(journal[open!.seq]).toMatchObject({ seq: open!.seq, name: 'shell', status: 'completed' });304 expect(journal[open!.seq]?.result?.content).toContain('warmed-up');305 // steps recorded before the crash are untouched306 for (const s of before.filter((x) => x.status === 'completed')) expect(journal[s.seq]?.result).toEqual(s.result);307 const resumed = rowBySeq(page, open!.seq);308 await reveal(page, resumed);309 await expect(resumed).toHaveAttribute('data-state', 'ok');310 await resumed.getByRole('button').first().click();311 await expect(resumed.getByTestId('shell-output')).toContainText('warmed-up');312 await page.waitForTimeout(300);313 await shot(page, 'desktop-mission-after-crash');314 const keys = await allRowKeys(page);315 expect(new Set(keys).size, 'no duplicated timeline row').toBe(keys.length);316 await expect(page.getByTestId('timeline')).toHaveAttribute('data-steps', String(journal.length));317318 // exactly the commits the agent made once: no duplicate subject, tests pass319 const commits = missionCommits(dir, id);320 expect(commits.length).toBeGreaterThanOrEqual(1);321 expect(new Set(commits).size, 'no duplicated commit').toBe(commits.length);322 expect(journal.filter((s) => s.name === 'git_commit' && /^Committed\./.test(s.result?.content ?? '')).length).toBe(commits.length);323 expect(testsPass(dir)).toBe(true);324 await record(page, 'scenario 4 worker crash', id);325 await guard.expectClean();326});327328test(`scenario 5 (${MODEL}): cancel a running mission from the UI`, async () => {329 const project = await createProject(page, pname('calc-cancel'));330 const { dir } = seedBugProject(project.path);331 const id = await createMission(page, {332 project: pname('calc-cancel'),333 objective: 'First, before anything else, run exactly this shell command once with timeoutSec 300: `sleep 200; echo finished-sleeping`. Then fix the bug in sum.js and commit.',334 criteria: [TESTS_PASS],335 model: MODEL,336 });337 const sleeping = page.locator('[data-testid="tool-row"][data-tool="shell"][data-state="running"]').filter({ hasText: 'sleep 200' });338 await expect(sleeping).toBeVisible({ timeout: 120_000 });339 await page.getByTestId('mission-cancel').click();340 const dialog = page.getByRole('dialog', { name: 'Cancel this mission?' });341 await expect(dialog).toContainText('Spent so far: $0.0'); // the real, small amount — not rounded to $0.00342 await dialog.getByRole('button', { name: 'Cancel mission' }).click();343 await done(page, 'cancelled', 60_000);344 await expect(page.getByTestId('mission-cancel')).toHaveCount(0);345 await expect(page.getByTestId('composer-ended')).toContainText('cancelled');346 // the interrupted command is shown as interrupted, never as still running347 await expect(page.locator('[data-testid="tool-row"][data-tool="shell"]').filter({ hasText: 'sleep 200' })).toHaveAttribute('data-state', 'interrupted');348 await expect(page.getByTestId('thinking')).toHaveCount(0);349 const m = await apiGet<MissionLite>(page, `/missions/${id}`);350 expect(m.status).toBe('cancelled');351 // nothing ran after the cancel: the bug is still there, nothing was committed352 await page.waitForTimeout(4000);353 expect((await steps(page, m.rootRunId)).some((s) => s.name === 'git_commit')).toBe(false);354 expect(missionCommits(dir, id)).toEqual([]);355 await record(page, 'scenario 5 cancel', id);356 await guard.expectClean();357});358359test(`scenario 6 (${MODEL}): a steering message sent mid-run is read by the agent`, async () => {360 const project = await createProject(page, pname('calc-steer'));361 const { dir } = seedBugProject(project.path);362 const id = await createMission(page, { project: pname('calc-steer'), objective: FIX_OBJECTIVE, criteria: [TESTS_PASS], model: MODEL });363 await expect(page.locator('[data-testid="tool-row"]').first()).toBeVisible({ timeout: 120_000 });364 const LINE = '// reviewed by the owner';365 await page.getByTestId('composer-input').fill(`One more requirement: the very first line of sum.js must be exactly this comment line: ${LINE}`);366 await page.getByTestId('composer-send').click();367 await expect(page.getByTestId('timeline').getByTestId('owner-message')).toContainText(LINE);368 await done(page);369 // journaled once (an owner_messages step), no longer pending, shown once370 await expect(page.locator('[data-testid="owner-message"][data-pending]')).toHaveCount(0);371 const m = await apiGet<MissionLite>(page, `/missions/${id}`);372 const journal = await steps(page, m.rootRunId);373 const read = journal.filter((s) => s.kind === 'owner_messages' && JSON.stringify(s.result).includes(LINE));374 expect(read, 'the message was read through an owner_messages step').toHaveLength(1);375 const keys = await allRowKeys(page);376 expect(keys.filter((k) => k.startsWith('o') || k.startsWith('p'))).toHaveLength(1);377 expect(fs.readFileSync(path.join(dir, 'sum.js'), 'utf8').split('\n')[0]).toBe(LINE);378 expect(testsPass(dir)).toBe(true);379 await record(page, 'scenario 6 steering', id);380 await guard.expectClean();381});382383test('after the runs: spend is real and small, every route is clean under the real CSP, sessions end properly', async () => {384 const total = totalSpend();385 console.log(`[real-e2e] total spend of this run: $${total.toFixed(6)}`);386 expect(total).toBeGreaterThan(0);387 expect(total).toBeLessThan(0.5);388 const spend = await apiGet<{ todayUsd: number; byModel: { model: string; usd: number }[] }>(page, '/spend');389 expect(spend.todayUsd).toBeGreaterThan(0);390 await page.goto('/spend/');391 await expect(page.getByTestId('spend-today')).not.toHaveText('$0.00');392 for (const route of ['/home/', '/missions/', '/approvals/', '/projects/', '/policy/', '/system/', '/settings/']) {393 await page.goto(route);394 await page.waitForTimeout(500);395 }396 await page.goto('/system/');397 await expect(page.getByTestId('health-worker')).toHaveAttribute('data-ok', 'true');398 await expect(page.getByTestId('system-counts')).toContainText('Missions running');399 expect(guard.frames, 'WebSocket frames were inspected').toBeGreaterThan(50);400 expect(guard.responses, 'HTTP responses were inspected').toBeGreaterThan(100);401 await guard.expectClean();402 // a WebSocket with the session cookie but a foreign Origin is refused403 const cookie = (await context.cookies()).map((c) => `${c.name}=${c.value}`).join('; ');404 expect(await wsUpgradeStatus({ Origin: ORIGIN, Cookie: cookie }), 'the same cookie with the right Origin is accepted').toBe(101);405 const status = await wsUpgradeStatus({ Origin: 'http://evil.example', Cookie: cookie });406 expect(status).not.toBe(101);407});408