File size: 7,851 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 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-runtime-meter';
const result = { date: new Date().toISOString(), url, checks: [], layouts: [], errors: [] };
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true });
let context, page;
try {
for (const [width, height] of [[1440,1000],[1100,800],[1000,800],[851,800],[850,800],[390,844],[320,568],[390,420]]) {
const page = await browser.newPage({ viewport: { width, height }, hasTouch: width < 500, deviceScaleFactor: 2 });
page.on('pageerror', e => result.errors.push(e.message));
await page.goto(url); await page.waitForFunction(() => window.browserPi?.runtimeMeter); await page.evaluate(() => window.browserPi.ready);
assert.equal(await page.locator('.runtime-engine').innerText(), 'WebGPU');
assert.equal(await page.locator('.runtime-value').innerText(), 'Idle');
for (const event of [
{ type: 'busy', action: 'load', cachedOnly: true },
{ type: 'load_progress', phase: 'download', loaded: 184, total: 1840 },
{ type: 'inference_activity', phase: 'prefill' },
...[18,28,23,36,30,38,32].map(rate => ({ type: 'inference_activity', phase: 'decode', rate })),
]) await page.evaluate(event => window.browserPi.runtimeMeter.update(event), event);
assert.equal(await page.locator('.runtime-value').textContent(), '32 tok/s');
assert(await page.locator('.runtime-sparkline').isVisible());
await page.waitForFunction(() => {
const meter = document.querySelector('#runtime-meter');
const content = meter.querySelector('.runtime-content');
return Math.abs(meter.getBoundingClientRect().width - Math.ceil(content.getBoundingClientRect().width) - 2 * parseFloat(getComputedStyle(meter).paddingLeft)) < 1;
});
const layout = await page.evaluate(() => {
const rect = selector => { const r=document.querySelector(selector).getBoundingClientRect();return {x:r.x,y:r.y,right:r.right,bottom:r.bottom,width:r.width,height:r.height}; };
return { header:rect('header'), brand:rect('.brand'), meter:rect('#runtime-meter'), model:rect('.model-panel'), value:rect('.runtime-value'), overflow:document.documentElement.scrollWidth>innerWidth };
});
for (const item of [layout.brand,layout.meter,layout.model]) assert(item.x>=layout.header.x&&item.right<=layout.header.right&&item.y>=layout.header.y&&item.bottom<=layout.header.bottom,JSON.stringify({width,...layout}));
assert(layout.brand.right<=layout.meter.x);
assert(layout.value.right<=layout.meter.right-8);
if(width>850)assert(layout.meter.right<=layout.model.x);
assert(!layout.overflow); result.layouts.push({width,height,...layout});
await page.locator('header').screenshot({path:`../validation/${label}-fixture-${width}x${height}.png`});
await page.evaluate(()=>window.browserPi.runtimeMeter.update({type:'agent_event',event:{type:'tool_execution_start',toolName:'bash'}}));
assert.equal(await page.locator('.runtime-value').textContent(),'Running bash…'); assert(!await page.locator('.runtime-sparkline').isVisible());
assert.equal(await page.locator('.runtime-engine').textContent(),'JavaScript');
await page.evaluate(()=>window.browserPi.runtimeMeter.update({type:'idle'}));
assert.equal(await page.locator('.runtime-value').textContent(),'Idle');
assert.equal(await page.locator('.runtime-engine').textContent(),'WebGPU');
await page.evaluate(()=>window.browserPi.runtimeMeter.update({type:'inference_activity',phase:'decode',rate:32}));
await page.waitForFunction(()=>document.querySelector('.runtime-value').textContent==='Generating…');
assert(!await page.locator('.runtime-sparkline').isVisible());
await page.close();
}
result.checks.push('Eight viewport fixtures fit the header; measured bars, tool/idle states and stale-sample handling render correctly');
await browser.close();
context=await chromium.launchPersistentContext(new URL('../../validation/agent-browser-profile',import.meta.url).pathname,{
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); page=await context.newPage(); page.on('pageerror',e=>result.errors.push(e.message));
await page.goto(url); await page.waitForFunction(()=>window.browserPi?.runtimeMeter); await page.evaluate(()=>window.browserPi.ready);
await page.evaluate(()=>document.querySelector('#new-chat').onclick());
const nonce='METER-'+crypto.randomUUID().slice(0,8);
await page.evaluate(nonce=>window.browserPi.rpc('write',{path:'/workspace/meter-check.txt',content:nonce}),nonce);
if(!await page.evaluate(()=>window.browserPi.rpc('cache_status')))await page.evaluate(()=>window.browserPi.load());
await page.evaluate(()=>{
window.meterStates=[];
new MutationObserver(()=>window.meterStates.push({state:document.querySelector('#runtime-meter').dataset.state,value:document.querySelector('.runtime-value').textContent})).observe(document.querySelector('#runtime-meter'),{attributes:true,attributeFilter:['data-state']});
});
await page.locator('.xterm-helper-textarea').focus();await page.keyboard.type('Read meter-check.txt using the read tool and tell me its exact contents.');await page.locator('#send').click();
await page.waitForFunction(()=>window.browserPi.diagnostics.runtimeSamples.some(s=>s.phase==='decode'&&s.rate>0));
await page.locator('header').screenshot({path:`../validation/${label}-live.png`});
await page.waitForFunction(()=>!window.browserPi.busy&&window.browserPi.diagnostics.events.includes('agent_end'));
const state=await page.evaluate(()=>window.browserPi.rpc('snapshot'));
result.states=await page.evaluate(()=>window.meterStates);
result.samples=await page.evaluate(()=>window.browserPi.diagnostics.runtimeSamples);
assert(result.states.some(s=>s.state==='prefill'));
assert(result.states.some(s=>s.state==='generating'&&/tok\/s/.test(s.value)));
assert(result.states.some(s=>s.state==='tool'));
assert(result.samples.filter(s=>s.rate!==undefined&&s.rate!==null).every(s=>Number.isFinite(s.rate)&&s.rate>=0));
assert(state.messages.filter(m=>m.role==='assistant').at(-1).content.some(c=>c.text?.includes(nonce)));
assert.equal(await page.locator('#filename').textContent(), '/workspace/meter-check.txt');
assert.equal(await page.locator('#editor').inputValue(), nonce);
assert.equal(await page.locator('.runtime-value').textContent(),'Ready'); assert(!await page.locator('.runtime-sparkline').isVisible());
result.checks.push('Actual WebGPU token callbacks drive live rates, with distinct prefill, tool and idle states and a verified file result');
await page.locator('.xterm-helper-textarea').focus();await page.keyboard.type('Count from 1 to 200.');await page.locator('#send').click();
await page.waitForFunction(()=>document.querySelector('#runtime-meter').dataset.state==='generating');await page.locator('#stop').click();
await page.waitForFunction(()=>!window.browserPi.busy);
assert.equal(await page.locator('.runtime-value').textContent(),'Ready'); assert(!await page.locator('.runtime-sparkline').isVisible());
result.checks.push('Stopping generation clears the live rate and chart');
assert.deepEqual(result.errors,[]);result.passed=true;console.log(JSON.stringify({passed:true,checks:result.checks,samples:result.samples.length}));
} catch(error){result.failure=error.stack;throw error;}
finally{await fs.writeFile(`../validation/${label}.json`,JSON.stringify(result,null,2));await context?.close();await browser.close();}
|