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/'; const label = process.argv[3] ?? 'agent-shell'; const result = { date: new Date().toISOString(), url, checks: [], errors: [] }; const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true }); const context = await browser.newContext({ viewport: {width:1440,height:1000} }); context.setDefaultTimeout(15_000); const page = await context.newPage(); page.on('pageerror', e => result.errors.push(e.stack)); const input = page.locator('#command'); async function run(command) { await input.fill(command); await input.press('Enter'); await page.waitForFunction(() => !window.browserPi.busy && document.querySelector('#shell-run').textContent === 'Run'); await page.waitForFunction(() => !!document.querySelector('.shell-entry:last-child .shell-status.success, .shell-entry:last-child .shell-status.error')); } async function geometry() { return page.evaluate(() => { const i = document.querySelector('#command'), h=document.querySelector('#command-highlight'); const ir=i.getBoundingClientRect(), hr=h.getBoundingClientRect(); const shell=document.querySelector('.workspace > .terminal').getBoundingClientRect(); const field=document.querySelector('#shell-form').getBoundingClientRect(); return { sameOrigin:ir.x===hr.x && ir.y===hr.y, sameWidth:ir.width===hr.width, sameFont:getComputedStyle(i).font===getComputedStyle(h).font, samePadding:getComputedStyle(i).padding===getComputedStyle(h).padding, sameScroll:i.scrollTop===h.scrollTop, sameText:h.textContent===i.value+'\n', fits:field.left>=shell.left && field.right<=shell.right, overflow:document.documentElement.scrollWidth>innerWidth }; }); } try { await page.goto(url); await page.waitForFunction(()=>window.browserPi?.terminal); await page.evaluate(()=>window.browserPi.ready); assert.equal(await page.locator('header #load').count(),1); assert.equal(await page.locator('.conversation .model-panel').count(),0); assert.equal(await page.locator('header').evaluate(el=>getComputedStyle(el).borderBottomWidth),'0px'); const command='NAME="world"; printf "Hello %s\\n" "$NAME" | cat -n # greeting'; await input.fill(command); assert(await page.locator('#command-highlight .token').count()>4); const colorCount=await page.locator('#command-highlight .token').evaluateAll(items=>new Set(items.map(el=>getComputedStyle(el).color)).size); assert(colorCount>=3); assert((await geometry()).sameText); await input.press('Enter'); await page.waitForFunction(()=>document.querySelector('.shell-stdout')?.textContent.includes('Hello world')); assert.equal(await page.locator('.shell-entry .shell-code').textContent(),command); assert.equal(await page.locator('.shell-status').textContent(),'[exit 0]'); result.checks.push('Top-bar model status replaces the card; Bash highlighting preserves exact command text and real execution'); await input.fill('unsent draft'); await input.press('ArrowUp'); assert.equal(await input.inputValue(),command); await input.press('ArrowDown'); assert.equal(await input.inputValue(),'unsent draft'); await input.fill('printf "one\\n"'); await input.press('Shift+Enter'); await page.keyboard.type('printf "two\\n"'); assert.equal(await input.inputValue(),'printf "one\\n"\nprintf "two\\n"'); assert((await geometry()).sameText); await input.press('Enter'); await page.waitForFunction(()=>document.querySelector('.shell-entry:last-child .shell-stdout')?.textContent==='one\ntwo\n'); await run('printf "problem\\n" >&2; exit 7'); assert.equal(await page.locator('.shell-entry:last-child .shell-stderr').textContent(),'problem\n'); assert.equal(await page.locator('.shell-entry:last-child .shell-status.error').textContent(),'[exit 7]'); result.checks.push('History restores drafts; Shift+Enter runs multiline commands; stderr and nonzero exit status are distinguished'); const hostile=''; await run("printf '%s\\n' '"+hostile+"'"); assert.equal(await page.locator('.shell-entry:last-child .shell-stdout').textContent(),hostile+'\n'); assert.equal(await page.locator('#terminal-output img, #terminal-output script, #command-highlight img').count(),0); await run("printf 'before\\033[2Jafter\\n'"); assert.equal(await page.locator('.shell-entry:last-child .shell-stdout').textContent(),'beforeafter\n'); result.checks.push('Command/file HTML is inert text; terminal escape sequences cannot alter the page'); await page.locator('#editor').fill('An unsaved edit'); await run('printf should-not-run'); assert.match(await page.locator('.shell-entry:last-child .shell-stderr').textContent(),/Save your file edits/); assert.equal(await input.inputValue(),'printf should-not-run'); await page.locator('#save').click(); await page.waitForFunction(()=>!window.browserPi.busy && document.querySelector('#save').disabled); await page.locator('#shell-clear').click(); assert.equal(await page.locator('.shell-entry').count(),0); await run('printf "workspace-ready\\n"'); await input.press('Control+l'); assert.equal(await page.locator('.shell-entry').count(),0); result.checks.push('Unsaved file edits block execution without losing the command; Clear and Ctrl+L clear only output'); await run('ls -la'); await run('printf "Hello from the browser\\n"'); const draft='NAME="δΈ–η•Œ πŸ¦™"\nprintf "%s\\n" "$NAME" | cat -n'; await input.fill(draft); for(const [width,height] of [[1440,1000],[851,640],[850,640],[390,844],[320,568]]) { await page.setViewportSize({width,height}); await page.waitForTimeout(100); const g=await geometry(); assert(g.sameOrigin && g.sameWidth && g.sameFont && g.samePadding && g.sameScroll && g.sameText && g.fits && !g.overflow,JSON.stringify({width,...g})); if([1440,390,320].includes(width)) await page.screenshot({path:`../validation/${label}-${width}x${height}.png`,fullPage:true}); } const long=Array.from({length:16},(_,i)=>`printf 'line ${i} δΈ–η•Œ\\n'`).join('\n'); await input.fill(long); await input.evaluate(el=>{el.scrollTop=el.scrollHeight;el.dispatchEvent(new Event('scroll'));}); assert((await geometry()).sameScroll); await page.locator('.workspace > .terminal summary').click(); await page.locator('.workspace > .terminal summary').click(); assert.equal(await input.inputValue(),long); result.checks.push('Five viewport sizes preserve input/highlight alignment, Unicode, scrolling and drafts through collapse/expand'); const touch = await browser.newContext({ viewport: {width:390,height:844}, isMobile:true, hasTouch:true }); const phone = await touch.newPage(); phone.on('pageerror', e=>result.errors.push(e.stack)); await phone.goto(url); await phone.waitForFunction(()=>window.browserPi?.terminal); await phone.evaluate(()=>window.browserPi.ready); await phone.locator('#command').fill(draft); const touchLayout = await phone.evaluate(()=>{ const load=document.querySelector('#load').getBoundingClientRect(), header=document.querySelector('header').getBoundingClientRect(); const input=getComputedStyle(document.querySelector('#command')), highlight=getComputedStyle(document.querySelector('#command-highlight')); return { fits:load.bottom<=header.bottom && document.documentElement.scrollWidth<=innerWidth, font:input.fontSize, matches:input.font===highlight.font }; }); assert(touchLayout.fits && touchLayout.matches && touchLayout.font==='16px',JSON.stringify(touchLayout)); await phone.screenshot({path:`../validation/${label}-touch.png`,fullPage:true}); await touch.close(); result.checks.push('Touch simulation keeps model status within the top bar and uses aligned 16px command text'); assert.equal(result.errors.length,0); result.passed=true; console.log(JSON.stringify({passed:true,checks:result.checks})); } 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 browser.close();}