File size: 5,533 Bytes
39371ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import { chromium } from 'playwright';
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';

const url = process.argv[2] ?? 'http://127.0.0.1:4173/?source=local';
const label = process.argv[3] ?? 'agent-terminal-dark-conversation';
const profile = new URL('../../validation/agent-browser-profile', import.meta.url).pathname;
const result = { date: new Date().toISOString(), url, kind: 'actual model; ten consecutive user turns, no chat resets', turns: [], errors: [] };
const context = await chromium.launchPersistentContext(profile, {
  executablePath: '/usr/bin/chromium', headless: true, viewport: { width: 1440, height: 1000 },
  args: ['--enable-unsafe-webgpu', '--use-angle=vulkan', '--enable-features=Vulkan', '--disable-vulkan-surface'],
});
context.setDefaultTimeout(180_000);
const page = await context.newPage();
page.on('pageerror', error => result.errors.push(error.stack));
const hasTool = (messages, name) => messages.some(m => m.role === 'toolResult' && m.toolName === name && !m.isError);
try {
  await page.goto(url); await page.waitForFunction(() => window.browserPi?.terminal);
  await page.evaluate(() => window.browserPi.ready);
  await page.evaluate(() => window.browserPi.load());
  await page.waitForFunction(() => !window.browserPi.busy);
  await page.evaluate(() => document.querySelector('#new-chat').onclick());
  await page.evaluate(() => window.browserPi.rpc('shell', { command: 'rm -f /workspace/todo /workspace/numbers.txt /workspace/render-notes.md /workspace/render-missing.txt' }));
  await context.setOffline(true);
  const tasks = [
    { prompt: 'Hi! Reply with one short sentence.' },
    { prompt: 'Create a file called todo containing exactly "# todo" followed by a newline and "- Check rendering". Use the write tool.', check: (s,m) => { assert(hasTool(m,'write')); assert.match(s.files['/workspace/todo'], /Check rendering/); } },
    { prompt: 'Read todo using the read tool and show its contents.', check: (s,m) => assert(hasTool(m,'read')) },
    { prompt: 'Use the edit tool on todo to replace Check rendering with Check dark mode. Then read it to verify.', check: (s,m) => { assert(hasTool(m,'edit')); assert.match(s.files['/workspace/todo'], /Check dark mode/); } },
    { prompt: 'Run this bash command: seq 1 40 > numbers.txt; wc -l numbers.txt', check: (s,m) => { assert(hasTool(m,'bash')); assert.equal(s.files['/workspace/numbers.txt'].trim().split('\n').length,40); } },
    { prompt: 'Read numbers.txt using the read tool. Tell me only its first and last numbers.', check: (s,m) => assert(hasTool(m,'read')) },
    { prompt: 'Write render-notes.md with a Markdown heading Dark mode, a two-item list about padding and contrast, and a fenced bash code block containing cat todo. Then read the file.', check: (s,m) => { assert(hasTool(m,'write')); assert.match(s.files['/workspace/render-notes.md'], /Dark mode/); } },
    { prompt: 'Read render-missing.txt using the read tool. If missing, write exactly Recovered into it, then read it again.', check: (s,m) => { assert(m.some(m=>m.role==='toolResult' && m.isError)); assert.equal(s.files['/workspace/render-missing.txt'].trim(),'Recovered'); } },
    { prompt: 'Run bash to print todo and render-missing.txt, then count the lines in numbers.txt.', check: (s,m) => assert(hasTool(m,'bash')) },
    { prompt: 'Summarize the files we created in three short Markdown bullets. No tools needed.' },
  ];
  let count = 0;
  for (const [index, task] of tasks.entries()) {
    const start = Date.now();
    await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type(task.prompt);
    await page.locator('#send').click(); await page.waitForFunction(() => window.browserPi.busy);
    await page.waitForFunction(() => !window.browserPi.busy);
    const state = await page.evaluate(() => window.browserPi.rpc('snapshot'));
    const messages = state.messages.slice(count); count = state.messages.length;
    const entry = { turn: index+1, elapsedMs: Date.now()-start, prompt: task.prompt, messages };
    result.turns.push(entry);
    assert.equal(state.messages.filter(m=>m.role==='user').length,index+1);
    assert(messages.filter(m=>m.role==='assistant').at(-1)?.content.some(c=>c.type==='text' && c.text.trim()));
    task.check?.(state,messages);
    console.log('PASS turn', index+1, entry.elapsedMs);
  }
  await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+o'); await page.keyboard.press('End');
  await page.screenshot({ path: `../validation/${label}-desktop.png`, fullPage:true });
  await page.keyboard.press('Home'); await page.waitForFunction(()=>window.browserPi.terminal.viewportTop===0);
  await page.keyboard.press('End');
  await page.setViewportSize({width:390,height:844}); await page.waitForTimeout(150);
  assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));
  await page.screenshot({ path: `../validation/${label}-mobile.png`, fullPage:true });
  result.finalState = await page.evaluate(()=>window.browserPi.rpc('snapshot'));
  result.screen = await page.evaluate(()=>window.browserPi.terminal.readScreen());
  result.diagnostics = await page.evaluate(()=>window.browserPi.diagnostics);
  assert.equal(result.errors.length,0); result.passed=true;
} catch(error) { result.failure=error.stack; await page.screenshot({path:`../validation/${label}-failure.png`,fullPage:true}); throw error; }
finally { await fs.writeFile(`../validation/${label}.json`,JSON.stringify(result,null,2)); await context.close(); }