The apps branch's documents join the version contract. The answer layer's documents (wai.quantum.answer/1: an answer read, carried out, a seal's request and result, a stored answer checked), described as a draft whose form may change before the extension settles; a correctness record and its corpus (wai.quantum.telemetry/1); the views page's manifest (wai.quantum.views-bundle/1); and the command line's harness report, corpus and token counts (wai.quantum.eval-report/1, eval-answers/1, token-cost/1). The engine's test holds every answer of the layer's conformance set (read, carried out, half arrived, and stored and verified), records, the corpus and the committed manifest to them; the eval tests hold a scripted run's report, a model server's, the corpus and the committed token counts; check_schemas.py holds the committed documents too. The tool and diagnostic schemas take the answer layer's outcomes, dropped and defaulted. The result view's own tool, poll_quantum_job, listed only to a host that draws views, answers to the job's schema, and the strict test holds it. The basis rename reaches the views: a compile's words and its view name the gate set by its basis (both read the certificate's old target), and a test in each holds it. The service's job summary now lives in engine::views (the apps branch moved it there); it carries the durable branch's set summary and the processor packages' energy wording with it. The answer set's stored seal was made at 0.6.16; the values it seals name their engine, so it is written again at 0.7.0 (WAI_ANSWER_BUNDLE_WRITE=1). The views crate's lock moves to wai-quantum 0.7.0. Co-Authored-By: Claude Opus 5.5 <[email protected]>
326 lines
23 KiB
JavaScript
326 lines
23 KiB
JavaScript
// 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 = `<!doctype html><html><head><meta charset="utf-8"><title>scripted host</title></head><body><div id="frames"></div><script>
|
||
window.log = {};
|
||
window.mount = (key, html, csp, input, result, appTools, theme) => {
|
||
const log = window.log[key] = { sizes: [], context: [], calls: [], refused: [] };
|
||
const f = document.createElement('iframe');
|
||
f.id = 'f-' + key;
|
||
f.setAttribute('sandbox', 'allow-scripts');
|
||
f.style.cssText = 'width: 720px; height: 900px; border: 1px solid #888';
|
||
f.srcdoc = html.replace('<head>', '<head><meta http-equiv="Content-Security-Policy" content="' + csp + '">');
|
||
const send = (m) => f.contentWindow.postMessage(m, '*');
|
||
window.addEventListener('message', async (ev) => {
|
||
if (ev.source !== f.contentWindow) return;
|
||
const m = ev.data;
|
||
if (!m || m.jsonrpc !== '2.0') return;
|
||
switch (m.method) {
|
||
case 'ui/initialize':
|
||
log.initialize = m.params;
|
||
send({ jsonrpc: '2.0', id: m.id, result: { protocolVersion: '2026-01-26', hostInfo: { name: 'scripted-host', version: '1' }, hostCapabilities: { serverTools: {}, logging: {} }, hostContext: { theme, displayMode: 'inline', availableDisplayModes: ['inline'], containerDimensions: { width: 720, maxHeight: 6000 }, platform: 'web', locale: 'en-GB' } } });
|
||
break;
|
||
case 'ui/notifications/initialized':
|
||
log.initialized = true;
|
||
send({ jsonrpc: '2.0', method: 'ui/notifications/tool-input', params: { arguments: input } });
|
||
send({ jsonrpc: '2.0', method: 'ui/notifications/tool-result', params: result });
|
||
break;
|
||
case 'tools/call':
|
||
log.calls.push(m.params.name);
|
||
if (!appTools.includes(m.params.name)) { log.refused.push(m.params.name); send({ jsonrpc: '2.0', id: m.id, error: { code: -32000, message: 'a view may not call this tool' } }); break; }
|
||
send({ jsonrpc: '2.0', id: m.id, result: await window.callServer(m.params.name, m.params.arguments) });
|
||
break;
|
||
case 'ui/update-model-context':
|
||
log.context.push(m.params);
|
||
send({ jsonrpc: '2.0', id: m.id, result: {} });
|
||
break;
|
||
case 'ui/notifications/size-changed':
|
||
log.sizes.push(m.params);
|
||
break;
|
||
default:
|
||
if (m.id != null) send({ jsonrpc: '2.0', id: m.id, error: { code: -32601, message: 'no ' + m.method } });
|
||
}
|
||
});
|
||
document.getElementById('frames').append(f);
|
||
};
|
||
</script></body></html>`;
|
||
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}"`) && !/<script src|<link /.test(c.text), `${server.name}: the ${name} view is one self-contained page of ${bytes} bytes, within ${LIMIT}`);
|
||
}
|
||
for (const uri of new Set(Object.values(named).map((u) => u.resourceUri))) check(resources.some((r) => r.uri === uri), `${server.name}: ${uri}, which a tool names, is listed`);
|
||
const appTools = tools.filter((t) => (t._meta?.ui?.visibility ?? ['model', 'app']).includes('app')).map((t) => t.name);
|
||
return { tools, named, pages, appTools };
|
||
}
|
||
|
||
const call = async (server, name, args) => (await server.rpc('tools/call', { name, arguments: args })).result;
|
||
const BELL = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
const forged = (res, f) => { const r = structuredClone(res); f(r.structuredContent); return r; };
|
||
|
||
// ---- the command line's server ----
|
||
const cli = stdio(CLI);
|
||
{
|
||
const { named, pages, appTools } = await discover(cli);
|
||
for (const [tool, view] of Object.entries({ run_quantum_circuit: 'result', run_on_target: 'result', draw_circuit: 'circuit', lint_qasm: 'circuit', repair_qasm: 'circuit', compile_qasm: 'circuit', critique_qasm: 'critique', verify_quantum_result: 'receipt', estimate_resources: 'estimate', show_target: 'target' })) {
|
||
check(named[tool]?.resourceUri === `ui://wai-quantum/${view}`, `the command line: ${tool} names the ${view} view`);
|
||
}
|
||
const { ctx, page, seen } = await hostFor(cli);
|
||
const runArgs = { qasm: BELL, shots: 1000, intent: { distribution: { '00': 0.5, '11': 0.5 } } };
|
||
const run = await call(cli, 'run_quantum_circuit', runArgs);
|
||
check(!!run._meta?.['science.transaction.wai/quantum-view']?.layout && run.content.length === 2, 'a run carries what its view draws in _meta, and its answer in words beside its JSON');
|
||
|
||
for (const [csp, form] of [[STRICT, 'JavaScript'], [WASM, 'WebAssembly']]) {
|
||
const under = form === 'JavaScript' ? 'under the default policy' : 'with WebAssembly allowed';
|
||
const v = await draw(page, pages, 'result', runArgs, run, { csp });
|
||
check(v.ok, `the result view draws ${under}`);
|
||
check((await receiptOf(v.frame)) === 'valid', `the run's receipt is checked in the view ${under}`);
|
||
check((await v.frame.$eval('body', (b) => b.dataset.engine)) === form, `by the engine as ${form} (${await text(v.frame, '#wq-checked-by')})`);
|
||
check((await v.frame.$eval('#wq-verdict', (b) => b.dataset.level)) === 'proof', `its verdict, as the receipt seals it: ${await v.frame.$eval('#wq-verdict', (b) => b.title)}`);
|
||
const bars = await v.frame.$$eval('.qs-bar', (bs) => bs.map((b) => [b.dataset.outcome, Number(b.dataset.count)]));
|
||
check(JSON.stringify(Object.fromEntries(bars)) === JSON.stringify(Object.fromEntries(Object.entries(run.structuredContent.result.counts).sort((a, b) => b[1] - a[1]))), `its counts, as the result has them (${bars.map((b) => b.join(': ')).join(', ')})`);
|
||
check((await v.frame.$$('#wq-circuit .qc-item')).length === 4, 'its circuit, drawn from the engine’s layout');
|
||
const log = await v.log();
|
||
check(log.initialize?.protocolVersion === '2026-01-26' && log.initialized, 'the view and the host shook hands at 2026-01-26');
|
||
check(log.sizes.some((s) => s.height > 100), `the view told the host its size (${JSON.stringify(log.sizes.at(-1))})`);
|
||
await v.frame.waitForFunction(() => true);
|
||
await page.waitForFunction((k) => window.log[k].context.length > 0, v.key, { timeout: LONG }).catch(() => {});
|
||
const told = (await v.log()).context.at(-1)?.content?.[0]?.text ?? '';
|
||
check(/checked the receipt .* it verifies, signed by did:key:z/.test(told), `the model is told the check: ${told}`);
|
||
if (form === 'JavaScript') {
|
||
mkdirSync(SHOTS, { recursive: true });
|
||
await (await page.$(`#f-${v.key}`)).screenshot({ path: join(SHOTS, 'view-result.png') });
|
||
}
|
||
}
|
||
|
||
const changed = await draw(page, pages, 'result', runArgs, forged(run, (sc) => { sc.result.counts['00'] += 1; }));
|
||
check((await receiptOf(changed.frame)) === 'invalid', 'a count changed after the seal: the view says the receipt does not seal it');
|
||
|
||
const refused = await call(cli, 'run_quantum_circuit', { qasm: 'OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n' });
|
||
const r = await draw(page, pages, 'result', {}, refused, { state: 'refused' });
|
||
check(r.ok && /Q103/.test(await text(r.frame, '#wq-code')) && (await r.frame.$eval('#wq-verdict', (b) => b.dataset.level)) === 'refused', `a refusal, with its code: ${await text(r.frame, '#wq-code')}`);
|
||
|
||
const vArgs = { job: run.structuredContent };
|
||
const ver = await draw(page, pages, 'receipt', vArgs, await call(cli, 'verify_quantum_result', vArgs));
|
||
await ver.frame.waitForFunction(() => document.querySelector('#wq-agree')?.dataset.agree, null, { timeout: LONG }).catch(() => {});
|
||
check((await receiptOf(ver.frame)) === 'valid' && (await ver.frame.$eval('#wq-agree', (b) => b.dataset.agree).catch(() => null)) === 'yes', 'a receipt checked in the view agrees with the server’s check');
|
||
const badJob = structuredClone(run.structuredContent);
|
||
badJob.result.counts['11'] -= 1;
|
||
const bad = await draw(page, pages, 'receipt', { job: badJob }, await call(cli, 'verify_quantum_result', { job: badJob }));
|
||
await bad.frame.waitForFunction(() => document.querySelector('#wq-agree')?.dataset.agree, null, { timeout: LONG }).catch(() => {});
|
||
check((await receiptOf(bad.frame)) === 'invalid' && /does not verify/.test(await text(bad.frame, '#wq-server-valid')) && (await bad.frame.$eval('#wq-agree', (b) => b.dataset.agree)) === 'yes', 'a changed job: the server and the view both say it does not verify');
|
||
|
||
const cArgs = { qasm: 'OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nrz(0.3) q[0];\ncx q[0], q[1];\n', epsilon: 1e-3 };
|
||
const compiled = await call(cli, 'compile_qasm', cArgs);
|
||
const c = await draw(page, pages, 'circuit', cArgs, compiled);
|
||
check((await receiptOf(c.frame)) === 'valid' && (await c.frame.$$('#wq-passes tbody tr')).length === compiled.structuredContent.result.certificate.passes.length && (await text(c.frame, '#wq-compile')).startsWith(`To ${compiled.structuredContent.result.certificate.basis} at level`), `a compile: each pass with its check, its basis named, and its certificate checked in the view (${await text(c.frame, '#wq-compile')})`);
|
||
check((await c.frame.$$('#wq-circuit .qc-item')).length > 10, 'the compiled circuit drawn');
|
||
|
||
const lintArgs = { qasm: 'OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n' };
|
||
const l = await draw(page, pages, 'circuit', lintArgs, await call(cli, 'lint_qasm', lintArgs), { state: 'refused' });
|
||
check(l.ok && /Q103 on line 3/.test(await text(l.frame, '#wq-code')), 'a lint’s refusal, with its code and line');
|
||
|
||
const repArgs = { qasm: 'OPENQASM 3.0;\nqubit[2] q\nbit[2] c;\nh q[0];\nmeasure q;\n' };
|
||
const rep = await draw(page, pages, 'circuit', repArgs, await call(cli, 'repair_qasm', repArgs));
|
||
check(JSON.stringify(await rep.frame.$$eval('#wq-repairs li', (ls) => ls.map((x) => x.dataset.code))) === '["Q004","Q320"]', 'a repair: each change with its code');
|
||
|
||
const critArgs = { qasm: 'OPENQASM 3.0;\nqubit[3] q;\nh q[0];\nh q[0];\ncx q[0], q[1];\nrz(0.2) q[1];\nrz(0.3) q[1];\n' };
|
||
const crit = await draw(page, pages, 'critique', critArgs, await call(cli, 'critique_qasm', critArgs));
|
||
check((await crit.frame.$$('#wq-smells li')).length === 3, `a critique: its smells (${await text(crit.frame, '#wq-smells')})`);
|
||
await crit.frame.hover('#wq-smells li');
|
||
check((await crit.frame.$$('#wq-circuit .qc-item.qc-smell')).length === 2, 'the smell pointed at is marked on the circuit');
|
||
|
||
const estArgs = { qasm: 'OPENQASM 3.0;\nqubit[3] q;\nh q;\nccx q[0], q[1], q[2];\nrz(0.3) q[0];\n' };
|
||
const est = await call(cli, 'estimate_resources', estArgs);
|
||
const e = await draw(page, pages, 'estimate', estArgs, est);
|
||
check((await e.frame.$$('#wq-frontier .qs-frontier-point')).length === est.structuredContent.result.frontier.length && (await receiptOf(e.frame)) === 'valid', `an estimate: its frontier, and its seal checked in the view (${await text(e.frame, '#wq-headline')})`);
|
||
|
||
const tArgs = { target: 'synthetic-heavy-hex-27' };
|
||
const t = await draw(page, pages, 'target', tArgs, await call(cli, 'show_target', tArgs));
|
||
check((await t.frame.$$('#wq-map .qs-dev-q')).length === 27 && (await t.frame.$$('#wq-map .qs-dev-pair')).length === 28, 'a device target: its coupling map, every qubit and pair');
|
||
|
||
const onArgs = { qasm: BELL, shots: 300, target: 'synthetic-line-5' };
|
||
const on = await draw(page, pages, 'result', onArgs, await call(cli, 'run_on_target', onArgs));
|
||
check((await receiptOf(on.frame)) === 'valid' && /synthetic-line-5/.test(await text(on.frame, '#qs-dev-name')), 'a run on a device target: the device’s account, and the receipt checked');
|
||
|
||
const light = await draw(page, pages, 'result', runArgs, run, { theme: 'light' });
|
||
check((await light.frame.$eval('html', (h) => h.dataset.theme)) === 'light', 'the host’s light theme is the view’s');
|
||
|
||
const away = seen.requests.filter((u) => !u.startsWith(ORIGIN));
|
||
check(away.length === 0, `the views made no request of their own${away.length ? `: ${away.join(', ')}` : ''}`);
|
||
// The default policy refuses WebAssembly, and the browser says so; nothing else may err.
|
||
const errors = seen.errors.filter((m) => !/WebAssembly|wasm-unsafe-eval|unsafe-eval/i.test(m));
|
||
check(errors.length === 0, `no script error in a view${errors.length ? `: ${errors.join(' | ')}` : ''}`);
|
||
await ctx.close();
|
||
}
|
||
cli.close();
|
||
|
||
// ---- the job service's server, over HTTP ----
|
||
if (SERVICE) {
|
||
const dir = mkdtempSync(join(tmpdir(), 'wq-views-'));
|
||
const port = 18300 + Math.floor(Math.random() * 500);
|
||
const base = `http://127.0.0.1:${port}`;
|
||
const svc = spawn(SERVICE, ['serve'], { env: { ...process.env, WAI_QSVC_KEY: join(dir, 'key'), WAI_QSVC_ADDR: `127.0.0.1:${port}`, WAI_QSVC_BUDGET_GB: '4', WAI_QSVC_JOB_GB: '2', WAI_QSVC_WORKERS: '2' }, stdio: ['ignore', 'ignore', 'inherit'] });
|
||
for (let i = 0; i < 100; i++) { if (await fetch(`${base}/v1/health`).then((r) => r.ok, () => false)) break; await new Promise((r) => setTimeout(r, 200)); }
|
||
const s = http(base);
|
||
const { named, pages, appTools, tools } = await discover(s);
|
||
check(/^wq-views-[0-9a-f]{32}$/.test(s.session() ?? ''), `the service's session id says the host draws views (${s.session()})`);
|
||
const poll = tools.find((x) => x.name === 'poll_quantum_job');
|
||
check(poll && JSON.stringify(poll._meta.ui.visibility) === '["app"]' && appTools.includes('poll_quantum_job'), 'the view’s own tool for a running job is kept from the model');
|
||
check(named.run_quantum_circuit?.resourceUri === 'ui://wai-quantum/result' && named.verify_quantum_receipt?.resourceUri === 'ui://wai-quantum/receipt', 'the service’s tools name their views');
|
||
const plain = http(base);
|
||
await plain.rpc('initialize', { protocolVersion: '2025-11-25', capabilities: {} });
|
||
check(plain.session() === null && (await plain.rpc('tools/list')).result.tools.every((x) => !x._meta), 'a host that draws no views gets no session id and no views');
|
||
const { ctx, page, seen } = await hostFor(s);
|
||
const runArgs = { qasm: BELL, shots: 500 };
|
||
const run = await call(s, 'run_quantum_circuit', runArgs);
|
||
const v = await draw(page, pages, 'result', runArgs, run, { appTools });
|
||
check((await receiptOf(v.frame)) === 'valid', `the service's run drawn, its receipt checked in the view (${await text(v.frame, '#wq-signer')})`);
|
||
// A job the service is still running: the view follows it to its end.
|
||
const sub = await (await fetch(`${base}/v1/jobs`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ qasm: BELL, shots: 200, verdict: true }) })).json();
|
||
const running = { content: [{ type: 'text', text: 'still running' }], structuredContent: { id: sub.id, status: 'queued' }, isError: false };
|
||
const f = await draw(page, pages, 'result', { qasm: BELL }, running, { appTools });
|
||
check((await receiptOf(f.frame)) === 'valid' && (await f.log()).calls.includes('poll_quantum_job'), 'a job still running is followed by the view to its end, and its receipt checked');
|
||
const g = await draw(page, pages, 'result', { qasm: BELL }, running, { appTools: [], state: 'running' });
|
||
await g.frame.waitForFunction(() => /would not let the view follow/.test(document.querySelector('#wq-waiting')?.textContent ?? ''), null, { timeout: LONG }).catch(() => {});
|
||
check((await g.log()).refused.includes('poll_quantum_job'), 'a host that refuses the view’s call: the view says so, and names get_quantum_job');
|
||
check(seen.requests.every((u) => u.startsWith(ORIGIN)), 'the service’s views made no request of their own');
|
||
await ctx.close();
|
||
svc.kill();
|
||
rmSync(dir, { recursive: true, force: true });
|
||
}
|
||
|
||
await browser.close();
|
||
host.close();
|
||
console.log(fails ? `${fails} check(s) failed` : 'every check passed');
|
||
process.exit(fails ? 1 : 0);
|