File size: 7,636 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
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-cached-start';
const revision = '04a6c49fcba3a65a0351c92644c3a7e9d4343059';
const profile = new URL('../../validation/agent-browser-profile', import.meta.url).pathname;
const gpuArgs = ['--enable-unsafe-webgpu', '--use-angle=vulkan', '--enable-features=Vulkan', '--disable-vulkan-surface'];
const result = { date: new Date().toISOString(), url, checks: [], errors: [] };
let context, page, receipts;
const requests = [];
async function open(args = gpuArgs) {
  context = await chromium.launchPersistentContext(profile, { executablePath: '/usr/bin/chromium', headless: true, viewport: { width: 1440, height: 1000 }, args });
  context.setDefaultTimeout(180_000);
  context.on('request', r => { if (/\/models\/|\/resolve\//.test(r.url())) requests.push(r.url()); });
  page = await context.newPage(); page.on('pageerror', e => result.errors.push(e.stack));
  await page.goto(url); await page.waitForFunction(() => window.browserPi?.terminal); await page.evaluate(() => window.browserPi.ready);
  await page.evaluate(() => document.querySelector('#new-chat').onclick());
  await page.evaluate(() => {
    window.dialogOpens = 0;
    new MutationObserver(() => { if (document.querySelector('#model-dialog').open) window.dialogOpens++; }).observe(document.querySelector('#model-dialog'), { attributes: true, attributeFilter: ['open'] });
  });
}
async function cacheReceipts(value) {
  return page.evaluate(async ({ revision, value }) => {
    const root = await (await navigator.storage.getDirectory()).getDirectoryHandle('pi-minicpm5-' + revision);
    const handle = await root.getFileHandle('verified.json');
    if (value === undefined) return (await handle.getFile()).text();
    const stream = await handle.createWritable(); await stream.write(value); await stream.close();
  }, { revision, value });
}
async function holdCacheLock() {
  await page.evaluate(revision => {
    window.lockHeld = false;
    window.cacheLock = navigator.locks.request('pi-minicpm5-download-' + revision, async () => {
      window.lockHeld = true; await new Promise(resolve => { window.releaseCacheLock = resolve; });
    });
  }, revision);
  await page.waitForFunction(() => window.lockHeld);
}
async function releaseCacheLock() { await page.evaluate(async () => { window.releaseCacheLock?.(); await window.cacheLock; }); }
async function send(text) {
  await page.locator('.xterm-helper-textarea').focus(); await page.keyboard.press('Control+e'); await page.keyboard.press('Control+u');
  await page.keyboard.type(text); await page.locator('#send').click();
}
async function restoredDraft(text) {
  await page.waitForFunction(text => !window.browserPi.busy && !document.querySelector('#model-dialog').open && window.browserPi.terminal.text === text, text);
}
try {
  await open();
  if (!await page.evaluate(() => window.browserPi.rpc('cache_status'))) {
    await page.evaluate(() => window.browserPi.load());
    await context.close(); await open();
  }
  receipts = await cacheReceipts();
  assert.equal(await page.evaluate(() => window.browserPi.loaded), false);
  const task = 'Read cached-start.txt using the read tool and tell me its exact contents.';
  for (const invalid of ['{}', 'null', '{broken']) {
    await cacheReceipts(invalid);
    assert.equal(await page.evaluate(() => window.browserPi.rpc('cache_status')), false);
    const before = requests.length;
    await send(task); await page.locator('#model-dialog[open]').waitFor();
    assert.equal(requests.length, before); await page.locator('#model-cancel').click(); await restoredDraft(task);
  }
  await cacheReceipts(receipts);
  result.checks.push('Incomplete or malformed verification receipts require confirmation without model requests');

  await holdCacheLock();
  const beforeRace = requests.length;
  await send(task); await page.locator('#stop').waitFor({ state: 'visible' });
  assert.equal(await page.locator('#model-dialog').evaluate(el => el.open), false);
  await cacheReceipts('{}'); await releaseCacheLock();
  await page.locator('#model-dialog[open]').waitFor();
  assert.equal(requests.length, beforeRace);
  await page.locator('#model-cancel').click(); await restoredDraft(task); await cacheReceipts(receipts);
  result.checks.push('Cache invalidated after the initial check falls back to confirmation without fetching weights');

  await holdCacheLock();
  const beforeCancel = await page.evaluate(() => window.dialogOpens);
  await send(task); await page.locator('#stop').waitFor({ state: 'visible' });
  await page.locator('#stop').click(); await restoredDraft(task); await releaseCacheLock();
  assert.equal(await page.evaluate(() => window.dialogOpens), beforeCancel);
  assert.equal((await page.evaluate(() => window.browserPi.rpc('snapshot'))).messages.length, 0);
  result.checks.push('Stop cancels a queued cached restore, keeps the draft and opens no modal');

  await context.close(); await open(['--disable-gpu', '--disable-software-rasterizer']);
  await send(task); await page.locator('#model-dialog[open]').waitFor();
  assert.match(await page.locator('#model-error').textContent(), /WebGPU/);
  assert.equal(await page.locator('#model-confirm').textContent(), 'Try again');
  await page.locator('#model-cancel').click(); await restoredDraft(task);
  result.checks.push('A real GPU initialization failure offers retry and preserves the message');
  await context.close(); await open();

  const nonce = 'CACHE-' + crypto.randomUUID().slice(0, 8);
  await page.evaluate(nonce => window.browserPi.rpc('write', { path: '/workspace/cached-start.txt', content: nonce }), nonce);
  assert.equal(await page.evaluate(() => window.browserPi.rpc('cache_status')), true);
  const before = requests.length, start = Date.now();
  // A complete verified cache must suffice even if every model URL is unavailable.
  await context.route('**/models/**', route => route.abort()); await context.route('**/resolve/**', route => route.abort());
  await send(task);
  await page.waitForFunction(() => window.browserPi.loaded && !window.browserPi.busy && window.browserPi.diagnostics.events.includes('agent_end'));
  const state = await page.evaluate(() => window.browserPi.rpc('snapshot'));
  assert.equal(await page.evaluate(() => window.dialogOpens), 0);
  assert.equal(requests.length, before);
  assert.equal(state.messages.filter(m => m.role === 'user').length, 1);
  assert(state.messages.some(m => m.role === 'toolResult' && m.toolName === 'read' && !m.isError));
  assert(state.messages.filter(m => m.role === 'assistant').at(-1).content.some(c => c.text?.includes(nonce)));
  assert.equal(await page.evaluate(() => window.browserPi.terminal.text), '');
  result.elapsedMs = Date.now() - start; result.messages = state.messages;
  result.checks.push('Reloaded app restores the real WebGPU model and sends exactly once, with zero modal openings and zero model requests');
  await page.screenshot({ path: `../validation/${label}.png`, fullPage: true });
  assert.deepEqual(result.errors, []); result.passed = true;
  console.log(JSON.stringify({ passed: true, elapsedMs: result.elapsedMs, checks: result.checks }));
} catch (error) { result.failure = error.stack; throw error; }
finally {
  if (receipts && page && !page.isClosed()) await cacheReceipts(receipts).catch(() => {});
  await releaseCacheLock().catch(() => {});
  await fs.writeFile(`../validation/${label}.json`, JSON.stringify(result, null, 2)); await context?.close();
}