// The engine's views inside an agent host, driven by a scripted host in a // headless browser: // // WQ_CLI=PATH/wai-quantum [WQ_SERVICE=PATH/wai-quantum-service] node test/views.test.mjs // // The host speaks the extension for interactive views (io.modelcontextprotocol/ui, // 2026-01-26) as a host does: it says so in initialize, lists the tools and finds each // one's ui:// resource, reads the resource, and draws it in a sandboxed frame under a // content-security policy, answering the view's ui/initialize, giving it the tool's // input and result, carrying its tool calls to the server (only to tools whose // visibility lets a view call them), and keeping what it tells the model. // // For the command line's server (stdio), under the extension's default policy (no // WebAssembly) and under that policy with 'wasm-unsafe-eval': // - every tool with a view names it, and the six views are listed and read, each // one self-contained page within the limit (the sizes are printed); // - a run: its verdict, its counts, its circuit, and its receipt checked in the view, // by the engine as WebAssembly where the policy lets it and as JavaScript where it // does not; the model told the check; a count changed after sealing fails the check; // - a refusal with its code; a receipt checked beside the server's own answer, for a // sealed job and a changed one; a compile's passes and its certificate checked; a // lint's refusal; a repair's changes; a critique's smells marked on the circuit; an // estimate's frontier and its seal; a device target's map; a run on a target; // - a view makes no request of its own, and draws without a script error. // With WQ_SERVICE, the job service started here over HTTP: the session id that says // the host draws views, the view's own tool kept from the model, a run drawn and its // receipt checked as the service's, and a job still running followed by the view // until it is done. // Exit 0 iff every check passes. import { spawn } from 'node:child_process'; import { createServer } from 'node:http'; import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default; const SHOTS = resolve(dirname(fileURLToPath(import.meta.url)), 'screenshots'); const CLI = process.env.WQ_CLI; const SERVICE = process.env.WQ_SERVICE; if (!CLI) { console.error('give WQ_CLI: the wai-quantum binary'); process.exit(2); } const LIMIT = 1 << 20; const UI = { extensions: { 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] } } }; const STRICT = "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self' data:; connect-src 'none';"; const WASM = STRICT.replace("script-src 'self' 'unsafe-inline';", "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval';"); const LONG = 60_000; let fails = 0; const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; }; // ---- the servers ---- function stdio(bin) { const p = spawn(bin, ['mcp'], { stdio: ['pipe', 'pipe', 'inherit'] }); let buf = ''; let next = 1; const waiting = new Map(); p.stdout.setEncoding('utf8'); p.stdout.on('data', (d) => { buf += d; for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) { const line = buf.slice(0, i); buf = buf.slice(i + 1); if (!line.trim()) continue; const m = JSON.parse(line); waiting.get(m.id)?.(m); waiting.delete(m.id); } }); const rpc = (method, params = {}) => new Promise((ok) => { const id = next++; waiting.set(id, ok); p.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); }); return { name: 'the command line’s server', rpc, close: () => p.kill() }; } function http(base) { let session = null; let next = 1; const rpc = async (method, params = {}) => { const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream' }; if (session) headers['mcp-session-id'] = session; const r = await fetch(`${base}/mcp`, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', id: next++, method, params }) }); session = r.headers.get('mcp-session-id') ?? session; return r.json(); }; return { name: 'the job service’s server', rpc, session: () => session }; } // ---- the host page ---- // The host's side of the protocol, in the page: each view in a sandboxed frame of its // own, under `csp`, given its tool's input and result; what it says is kept in `log`. const HOST = `scripted host
`; const host = createServer((req, res) => { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(HOST); }); await new Promise((r) => host.listen(0, '127.0.0.1', r)); const ORIGIN = `http://127.0.0.1:${host.address().port}`; const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true }); // One page for a server, its tool calls carried to it. async function hostFor(server) { const ctx = await browser.newContext({ viewport: { width: 1000, height: 1000 }, colorScheme: 'dark' }); const page = await ctx.newPage(); const seen = { errors: [], requests: [] }; page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); }); page.on('pageerror', (e) => seen.errors.push(String(e))); page.on('request', (r) => seen.requests.push(r.url())); await page.exposeFunction('callServer', async (name, args) => (await server.rpc('tools/call', { name, arguments: args })).result); await page.goto(ORIGIN + '/', { waitUntil: 'domcontentloaded' }); return { ctx, page, seen }; } let keys = 0; // Draw `view` for a tool's `result` (and its `input`) under `csp`; the frame, once the // view says it is drawn. async function draw(page, pages, view, input, result, { csp = STRICT, appTools = [], state = 'shown', theme = 'dark' } = {}) { const key = `v${keys++}`; await page.evaluate(([k, html, c, i, r, a, t]) => window.mount(k, html, c, i, r, a, t), [key, pages[view], csp, input, result, appTools, theme]); const frame = await (await page.waitForSelector(`#f-${key}`)).contentFrame(); const ok = await frame.waitForSelector(`body[data-state="${state}"]`, { timeout: LONG }).then(() => true, () => false); return { key, frame, ok, log: () => page.evaluate((k) => window.log[k], key) }; } const receiptOf = (frame) => frame.waitForFunction(() => { const r = document.querySelector('#wq-receipt'); return r && r.dataset.receipt !== 'checking' && r.dataset.receipt; }, null, { timeout: LONG }).then((h) => h.jsonValue(), () => null); const text = (frame, sel) => frame.$eval(sel, (e) => e.textContent).catch(() => ''); // What the server lists for a host that draws views: each tool's view, the views, and their pages. async function discover(server) { const init = await server.rpc('initialize', { protocolVersion: '2025-11-25', capabilities: UI, clientInfo: { name: 'scripted-host', version: '1' } }); check(/never write HTML/.test(init.result?.instructions ?? ''), `${server.name}: the model is told not to write views itself`); const tools = (await server.rpc('tools/list')).result.tools; const named = Object.fromEntries(tools.filter((t) => t._meta?.ui?.resourceUri).map((t) => [t.name, t._meta.ui])); const resources = (await server.rpc('resources/list')).result.resources.filter((r) => r.uri.startsWith('ui://')); check(resources.length === 6 && resources.every((r) => r.mimeType === 'text/html;profile=mcp-app'), `${server.name}: the six views are listed (${resources.map((r) => r.uri.slice(17)).join(', ')})`); const pages = {}; for (const r of resources) { const c = (await server.rpc('resources/read', { uri: r.uri })).result.contents[0]; const bytes = Buffer.byteLength(c.text, 'utf8'); const name = r.uri.slice('ui://wai-quantum/'.length); pages[name] = c.text; check(c.mimeType === 'text/html;profile=mcp-app' && bytes <= LIMIT && c.text.includes(`content="${name}"`) && !/