File size: 11,555 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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';

const url = process.argv[2] ?? 'http://127.0.0.1:4173/';
const result = { url, fixture: 'Real app and workspace worker; controlled model load and prompt replies. GPU failure also tested without fixtures.', checks: [], errors: [] };
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true, args: ['--disable-gpu', '--disable-software-rasterizer'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.setDefaultTimeout(15000);
page.on('pageerror', error => result.errors.push(error.message));
// Control only the model boundary; initialization, files and the terminal are real.
await page.addInitScript(() => {
  const NativeWorker = Worker;
  const fixture = window.loadFixture = { loads: [], prompts: [], cached: false };
  window.Worker = class extends NativeWorker {
    constructor(...args) { super(...args); this.isAgent = String(args[0]).includes('agent.worker'); if (this.isAgent) fixture.worker = this; }
    postMessage(message, ...rest) {
      if (!this.isAgent) return super.postMessage(message, ...rest);
      const { id, action, args } = message;
      if (action === 'cache_status' && fixture.cached) { fixture.emit({ type: 'reply', id, value: true }); return; }
      if (action === 'load') {
        fixture.loads.push(args); fixture.pending = id; fixture.stopped = false;
        fixture.emit({ type: 'busy', action, cachedOnly: args.cachedOnly }); return;
      }
      if (action === 'stop' && fixture.pending) {
        fixture.stopped = true; fixture.emit({ type: 'reply', id, value: true }); return;
      }
      if (action === 'prompt') {
        fixture.prompts.push(args.text);
        const message = { role: 'user', content: [{ type: 'text', text: args.text }], timestamp: Date.now() };
        fixture.emit({ type: 'busy', action });
        fixture.emit({ type: 'agent_event', event: { type: 'message_start', message } });
        fixture.emit({ type: 'reply', id, value: { messages: [message] } });
        fixture.emit({ type: 'idle' }); return;
      }
      return super.postMessage(message, ...rest);
    }
  };
  fixture.emit = data => fixture.worker.dispatchEvent(new MessageEvent('message', { data }));
  fixture.finish = (error, name = 'Error') => {
    const id = fixture.pending; fixture.pending = undefined;
    if (!error) fixture.emit({ type: 'loaded', device: { vendor: 'test fixture' } });
    // Match the worker's error ordering: cleanup/idle before a rejected reply.
    if (error) fixture.emit({ type: 'idle' });
    fixture.emit({ type: 'reply', id, value: {}, error, errorName: name });
    if (!error) fixture.emit({ type: 'idle' });
  };
});
const ready = async () => { await page.waitForFunction(() => window.browserPi?.terminal); await page.evaluate(() => window.browserPi.ready); };
const emit = data => page.evaluate(data => window.loadFixture.emit(data), data);
const finish = (error, name) => page.evaluate(([error, name]) => window.loadFixture.finish(error, name), [error, name]);
const draft = text => page.waitForFunction(text => !window.browserPi.busy && !document.querySelector('#model-dialog').open && window.browserPi.terminal.text === text, text);
const send = async (text = '') => {
  await page.locator('.xterm-helper-textarea').focus();
  if (text) await page.keyboard.type(text);
  await page.locator('#send').click();
  await page.locator('#model-dialog[open]').waitFor();
};
const confirm = async () => {
  await page.locator('#model-confirm').click();
  await page.waitForFunction(() => window.browserPi.busy && !document.querySelector('#model-dialog').open);
};
try {
  await page.goto(url); await ready();
  const text = "What's our total sales revenue?";
  await send(text); await page.locator('#model-cancel').click(); await draft(text);
  assert.equal(await page.evaluate(() => window.loadFixture.loads.length), 0);
  await send(); await confirm();
  assert(await page.locator('#progress').isVisible());
  assert.equal(await page.locator('#progress').getAttribute('value'), null);
  assert.equal(await page.locator('#runtime-meter').getAttribute('data-state'), 'loading');
  assert(await page.locator('#stop').isVisible());
  for (const id of ['#send', '#new-chat', '#load']) assert(await page.locator(id).isDisabled());
  await page.waitForFunction(text => window.browserPi.terminal.readScreen().includes(text) && window.browserPi.terminal.readScreen().includes('Queued'), text);
  assert(await page.locator('.xterm-helper-textarea').evaluate(el => el === document.activeElement));
  await page.evaluate(() => { document.querySelector('#model-confirm').click(); window.browserPi.terminal.submit(); document.querySelector('#send').click(); });
  assert.equal(await page.evaluate(() => window.loadFixture.loads.length), 1);
  assert.equal(await page.evaluate(() => window.loadFixture.prompts.length), 0);
  await emit({ type: 'load_progress', phase: 'download', loaded: 736000000, total: 1840000000 });
  assert.equal(await page.locator('#progress').getAttribute('value'), '40');
  assert.match(await page.locator('#load').getAttribute('title'), /Hugging Face/);
  for (const [width, height] of [[1440,1000],[851,800],[850,800],[390,844],[320,568]]) {
    await page.setViewportSize({ width, height });
    await page.waitForTimeout(250); // Header capsule width transition.
    assert(await page.evaluate(() => {
      const header = document.querySelector('header').getBoundingClientRect();
      return document.documentElement.scrollWidth <= innerWidth && [...document.querySelectorAll('#runtime-meter, .model-panel, #progress')].every(el => {
        const r = el.getBoundingClientRect(); return r.left >= header.left && r.right <= header.right && r.top >= header.top && r.bottom <= header.bottom;
      });
    }), `Header overflow at ${width}`);
    if ([1440,390,320].includes(width)) await page.screenshot({ path: `../validation/load-flow-${width}.png`, fullPage: true });
  }
  await emit({ type: 'load_progress', phase: 'compile' });
  assert(await page.locator('#progress').isVisible()); assert.equal(await page.locator('#progress').getAttribute('value'), null);
  await emit({ type: 'load_progress', phase: 'warmup' });
  assert.equal(await page.locator('#load').textContent(), 'Almost ready…');
  await page.locator('#stop').click();
  assert(await page.evaluate(() => window.loadFixture.stopped && window.browserPi.busy));
  assert(await page.locator('#send').isDisabled());
  await finish('Stopped.', 'AbortError'); await draft(text);
  assert(await page.locator('#progress').isHidden());
  assert.equal(await page.evaluate(() => window.loadFixture.prompts.length), 0);
  result.checks.push('Confirmation closes immediately, queues the visible message, shows all header phases at five widths, rejects duplicate clicks; Stop waits for cleanup and restores the draft');

  await page.setViewportSize({ width: 1440, height: 1000 });
  await send(); await confirm(); await finish('Download interrupted. Please try again.');
  await page.locator('#model-dialog[open]').waitFor();
  assert.equal(await page.locator('#model-preview').textContent(), text);
  assert.match(await page.locator('#model-error').textContent(), /Download interrupted/);
  assert.equal(await page.locator('#model-confirm').textContent(), 'Try again');
  assert(await page.locator('#progress').isHidden());
  await page.keyboard.press('Escape'); await draft(text);
  await send(); await confirm(); await finish('Temporary connection failure.');
  await page.locator('#model-dialog[open]').waitFor();
  await confirm();
  await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type('A follow-up draft');
  await page.locator('#files button').first().focus();
  await finish();
  await page.waitForFunction(() => window.loadFixture.prompts.length === 1 && !window.browserPi.busy);
  assert.deepEqual(await page.evaluate(() => window.loadFixture.prompts), [text]);
  assert.equal(await page.evaluate(() => window.browserPi.terminal.text), 'A follow-up draft');
  assert(await page.locator('#model-dialog').isHidden());
  assert(await page.locator('#progress').isHidden());
  assert(await page.locator('#files button').first().evaluate(el => el === document.activeElement));
  await page.waitForFunction(() => !window.browserPi.terminal.readScreen().includes('Queued'));
  result.checks.push('Failure reopens retry with the original message; Escape keeps the draft; successful retry sends exactly once and retains a follow-up draft and the user’s current focus');

  await page.reload(); await ready();
  await page.evaluate(() => { window.loadFixture.cached = true; });
  await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.type('Cached message'); await page.locator('#send').click();
  await page.waitForFunction(() => window.loadFixture.pending);
  assert(await page.locator('#model-dialog').isHidden());
  assert.equal(await page.evaluate(() => window.loadFixture.loads[0].cachedOnly), true);
  await emit({ type: 'load_progress', phase: 'compile' });
  assert(await page.locator('#progress').isVisible());
  await page.keyboard.press('Escape'); await finish('Stopped.', 'AbortError'); await draft('Cached message');
  result.checks.push('Cached restoration uses the same top bar without confirmation; Escape stops it without opening a retry dialog');

  await page.reload(); await ready();
  await page.locator('#load').click();
  assert(await page.locator('#model-dialog').isHidden());
  assert(await page.locator('#progress').isVisible());
  await finish(); await page.waitForFunction(() => window.browserPi.loaded && !window.browserPi.busy);
  assert.equal(await page.evaluate(() => window.loadFixture.prompts.length), 0);
  result.checks.push('Loading from the header still works without queuing or sending a message');

  // Real unsupported-GPU load, with no worker interception or fake progress.
  const actual = await browser.newPage({ viewport: { width: 390, height: 844 } });
  actual.on('pageerror', error => result.errors.push(error.message));
  await actual.goto(url); await actual.waitForFunction(() => window.browserPi?.terminal); await actual.evaluate(() => window.browserPi.ready);
  await actual.locator('.xterm-helper-textarea').focus(); await actual.keyboard.type('Keep my message'); await actual.locator('#send').click();
  await actual.locator('#model-confirm').click();
  await actual.locator('#model-error').waitFor({ state: 'visible' });
  assert.match(await actual.locator('#model-error').textContent(), /WebGPU/);
  assert(await actual.locator('#progress').isHidden());
  await actual.locator('#model-confirm').click(); await actual.locator('#model-error').waitFor({ state: 'visible' });
  await actual.locator('#model-cancel').click();
  await actual.waitForFunction(() => window.browserPi.terminal.text === 'Keep my message');
  assert.equal((await actual.evaluate(() => window.browserPi.rpc('snapshot'))).messages.length, 0);
  await actual.close(); result.checks.push('Actual unsupported-WebGPU load retries and cancels safely with no message sent');
  assert.deepEqual(result.errors, []); result.passed = true;
} catch (error) {
  result.failure = error.stack; await page.screenshot({ path: '../validation/load-flow-failure.png', fullPage: true }); throw error;
} finally {
  await fs.writeFile('../validation/load-flow.json', JSON.stringify(result, null, 2));
  await browser.close(); console.log(JSON.stringify(result));
}