File size: 7,329 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
import { chromium } from 'playwright';
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';

const url = process.argv[2] ?? process.env.APP_URL ?? 'http://127.0.0.1:4173/';
const label = process.argv[3] ?? process.env.TEST_LABEL ?? 'agent-terminal';
const result = { date: new Date().toISOString(), url, errors: [], requests: [], checks: [] };
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', error => { result.errors.push(error.stack); console.log('PAGEERROR', error.stack); });
page.on('console', message => { if (message.type() === 'error') console.log('CONSOLE', message.text().slice(0, 500)); });
context.on('request', request => result.requests.push(request.url()));
const screen = () => page.evaluate(() => window.browserPi.terminal.readScreen());
const text = () => page.evaluate(() => window.browserPi.terminal.text);
async function type(value) { await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type(value); }
async function waitScreen(value) { await page.waitForFunction(value => window.browserPi.terminal.readScreen().includes(value), value); }
try {
  await page.goto(url);
  await page.waitForFunction(() => window.browserPi);
  await page.evaluate(() => window.browserPi.ready);
  await waitScreen('browser workspace');
  result.initial = await screen();
  assert(result.initial.includes('/load'));
  result.checks.push('Original Pi TUI rendered through xterm');

  await type('/help'); await page.keyboard.press('Enter'); await waitScreen('Ctrl+O');
  result.checks.push('Enter submits /help without loading a model');
  await page.keyboard.press('Control+Shift+f'); await type('browser');
  await page.waitForFunction(() => window.browserPi.terminal.text === '' && /\d+\/\d+/.test(window.browserPi.terminal.readScreen()));
  await page.keyboard.press('Escape'); await page.waitForTimeout(30);
  result.checks.push('Pi fullscreen transcript search preserves the editor draft');
  await type('one'); await page.keyboard.press('Shift+Enter'); await type('two');
  assert.equal(await text(), 'one\ntwo');
  await page.keyboard.press('Backspace'); await page.keyboard.press('Control+a'); await type('X');
  assert.equal(await text(), 'one\nXtw');
  await page.keyboard.press('Control+u'); await page.keyboard.press('Control+k');
  await page.keyboard.press('Backspace'); await page.keyboard.press('Control+u');
  assert.equal(await text(), '');
  result.checks.push('Pi multiline editor: Shift+Enter, cursor, deletion, Ctrl+U/K');
  await page.keyboard.press('Control+p'); assert.equal(await text(), '/help');
  await page.keyboard.press('Control+n'); assert.equal(await text(), '');
  result.checks.push('Pi prompt history');
  await type('/he'); await page.keyboard.press('Tab');
  await page.waitForFunction(() => window.browserPi.terminal.text === '/help ');
  await page.keyboard.press('Enter'); await waitScreen('Ctrl+O');
  result.checks.push('Pi autocomplete: /he → /help');

  await type("!printf 'terminal-works\\n' > terminal.txt; cat terminal.txt");
  await page.keyboard.press('Escape'); // Dismiss optional workspace completion.
  await page.waitForTimeout(30); // A lone ESC is delimited by Pi's 10ms input timer.
  await page.keyboard.press('Enter'); await waitScreen('[exit 0]');
  await page.waitForFunction(() => !window.browserPi.busy);
  const state = await page.evaluate(() => window.browserPi.rpc('snapshot'));
  assert.equal(state.files['/workspace/terminal.txt'], 'terminal-works\n');
  assert((await screen()).includes('[exit 0]'));
  result.checks.push('xterm keyboard → Pi editor → real just-bash → workspace file');

  // Model/file text must not be able to clear the terminal or inject OSC URLs.
  await type("!printf 'before\\033[2Jafter\\n'");
  await page.locator('#send').click(); await waitScreen('beforeafter');
  result.checks.push('Shell escape sequences render as inert text');

  // Paste uses xterm's actual bracketed-paste path through Pi's StdinBuffer.
  await context.grantPermissions(['clipboard-read', 'clipboard-write']);
  const largePaste = '!cat > pasted.txt <<\'EOF\'\n' + Array.from({ length: 15 }, (_, i) => 'row-' + i).join('\n') + '\nEOF';
  await page.evaluate(text => navigator.clipboard.writeText(text), largePaste);
  await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+v');
  await page.waitForFunction(text => window.browserPi.terminal.text === text, largePaste);
  await page.locator('#send').click();
  await page.waitForFunction(() => document.querySelector('#files').textContent.includes('pasted.txt'));
  const pastedState = await page.evaluate(() => window.browserPi.rpc('snapshot'));
  assert.equal(pastedState.files['/workspace/pasted.txt'], Array.from({ length: 15 }, (_, i) => 'row-' + i).join('\n') + '\n');
  result.checks.push('Large Pi paste markers expand before Send');
  const pasted = 'hello 世界 🦙\nline two';
  await page.evaluate(text => navigator.clipboard.writeText(text), pasted);
  await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+v');
  await page.waitForFunction(text => window.browserPi.terminal.text === text, pasted);
  result.checks.push('Unicode multiline clipboard paste');
  await page.locator('#newline').click(); assert.equal(await text(), pasted + '\n');
  result.checks.push('Touch-accessible newline button');

  // Resize while preserving a multiline draft and Pi's own viewport renderer.
  const desktop = await page.evaluate(() => ({ cols: window.browserPi.terminal.cols, rows: window.browserPi.terminal.rows }));
  await page.screenshot({ path: `../validation/${label}-desktop.png`, fullPage: true });
  await page.setViewportSize({ width: 390, height: 844 });
  await page.waitForFunction(cols => window.browserPi.terminal.cols < cols, desktop.cols);
  assert.equal(await text(), pasted + '\n');
  assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
  result.sizes = { desktop, mobile: await page.evaluate(() => ({ cols: window.browserPi.terminal.cols, rows: window.browserPi.terminal.rows })) };
  assert(result.sizes.mobile.cols >= 30); assert(result.sizes.mobile.rows >= 15);
  await page.screenshot({ path: `../validation/${label}-mobile.png`, fullPage: true });
  result.checks.push('390px layout and live terminal resize preserve the draft');
  result.mobileLimitation = 'Desktop Chromium viewport simulation; physical mobile keyboards and GPUs not tested.';
  assert.equal(result.errors.length, 0);
  assert.equal(result.requests.filter(url => /^https?:/.test(url) && new URL(url).origin !== new URL(result.url).origin).length, 0);
  result.passed = true; console.log(JSON.stringify({ passed: true, checks: result.checks, sizes: result.sizes }));
} catch (error) {
  result.failure = error.stack;
  result.screen = await screen().catch(() => 'App failed before initialization');
  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(); }