File size: 8,247 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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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='<img src=x onerror=alert(1)><script>alert(2)</script>';
  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();}