MiniCPM5-2B-WebGPU-Pi-HTTP / app /tests /conversation.mjs
Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw History Blame Contribute Delete
5.53 kB
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(); }