SPB Git

Repository Public

real.spec.ts 23.9 KB 408 lines typescript

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
Files main
Menu

Theme