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();}