// The Studio in the installed browser, as a reader uses it: // // node build.mjs --pkg PATH/TO/ENGINE/PKG && node test/studio.test.mjs // // Serves dist/ (with the engine package in dist/pkg/); the page's index.html names the // live job service, https://wai.transaction.science/quantum-jobs, which the service checks use. // - the first example loads; the program is linted, drawn and planned as it is typed; // - it runs in this browser: the stabilizer tableau, only 00 and 11, ⟨Z0 Z1⟩ = 1, the // receipt checked here and signed by this browser's key; // - an error in the program is marked on its line; // - a rotation runs on the state vector, ⟨Z0⟩ = cos 0.3; // - the same Bell job on the service gives the counts run here, shot for shot, and its // receipt checks here as the service's (left out with STUDIO_SKIP_SERVICE=1); // - Ctrl+Enter in the editor runs; the program and the history survive a reload; // - the critique: its measures as the program changes and their change since the last // run; a smell named with its lines, flagged on the diagram, shown, and fixed by the // engine's rewrite, checked the same circuit; // - Alternatives: Improve offers the engine's rewrite checked equivalent, plotted and // applied; Explore without a model says it needs one; // - the classical-shortcut lens: a Clifford ring with a closing rotation layer priced by // the cut-rank sum, a Clifford circuit easy at any width, a contraction searched only // when thorough, and a check marked stale once the program changes; // - predict first: the form stops a run until the reader predicts, refuses an outcome of // the wrong width, and sets the prediction against the counts; // - the assistant, against a scripted model server: a tool call run on the engine, a // proposal the engine cannot read sent back with its reason, the corrected one checked // and run before it is offered, and Apply; it asks questions first by default and speaks // only when asked; offering a hint after refused runs only when the reader allows it, // and never asking the model itself; Improve and Explore with the model, each candidate // read, measured and compared by the engine, a rewrite that is not the same circuit // left out with its reason; // - no console error and no failed request; at a phone's width nothing scrolls sideways. // Exit 0 iff every check passes. import { createServer } from 'node:http'; import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'; import { dirname, extname, join, normalize, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default; const DIST = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist'); const SHOTS = resolve(dirname(fileURLToPath(import.meta.url)), 'screenshots'); const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.wasm': 'application/wasm', '.json': 'application/json' }; const server = createServer((req, res) => { let file = normalize(join(DIST, decodeURIComponent(new URL(req.url, 'http://x').pathname))); if (!file.startsWith(DIST)) { res.writeHead(403).end(); return; } if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html'); if (!existsSync(file)) { res.writeHead(404).end('not found'); return; } res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' }); res.end(readFileSync(file)); }); await new Promise((r) => server.listen(0, '127.0.0.1', r)); const ORIGIN = `http://127.0.0.1:${server.address().port}`; let fails = 0; const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; }; const LONG = 120_000; const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true }); async function open(viewport = { width: 1400, height: 950 }) { const ctx = await browser.newContext({ viewport, colorScheme: 'dark' }); const page = await ctx.newPage(); const seen = { errors: [], failed: [] }; page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); }); page.on('pageerror', (e) => seen.errors.push(String(e))); page.on('requestfailed', (r) => seen.failed.push(`${r.url()} (${r.failure()?.errorText})`)); page.on('response', (r) => { if (r.status() >= 400) seen.failed.push(`${r.url()} (${r.status()})`); }); await page.goto(ORIGIN + '/', { waitUntil: 'domcontentloaded' }); return { ctx, page, seen }; } const done = (page) => page.waitForFunction(() => !/^Running|^On the service/.test(document.querySelector('.qs-status')?.textContent ?? '') && document.querySelector('#qs-run') && !document.querySelector('#qs-run').disabled, null, { timeout: LONG }); const verified = (page) => page.waitForFunction(() => /verifies|does not verify|not checked/.test(document.querySelector('#qs-verified')?.textContent ?? ''), null, { timeout: LONG }); const counts = (page) => page.$$eval('.qs-bar', (bs) => bs.map((b) => [b.querySelector('.qs-key').textContent, b.querySelector('.qs-n').textContent])); const { ctx, page, seen } = await open(); await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG }); check((await page.textContent('#qs-lint')) === '2 qubits · 2 gates', `the first example is linted as the engine reads it (${await page.textContent('#qs-lint')})`); await page.waitForFunction(() => document.querySelector('#qs-circuit')?.textContent.includes('⊕'), null, { timeout: LONG }); const drawing = await page.textContent('#qs-circuit'); check(/q0: .*H.*●/.test(drawing) && /q1: .*⊕/.test(drawing), 'the circuit is drawn as the program is typed'); const svg = await page.$eval('#qs-diagram svg', (g) => ({ label: g.getAttribute('aria-label'), rings: g.querySelectorAll('.qc-ring').length, dots: g.querySelectorAll('.qc-dot').length, joins: g.querySelectorAll('.qc-join').length, meters: g.querySelectorAll('.qc-measure').length })); check(svg.rings === 1 && svg.dots === 1 && svg.joins === 1 && svg.meters === 2 && /2 qubits/.test(svg.label), `the circuit is drawn as a diagram with a text alternative (${JSON.stringify(svg)})`); await page.click('#qs-astext'); check(await page.isVisible('#qs-circuit') && await page.isHidden('#qs-diagram'), '“Text” shows the circuit as text to copy'); await page.click('#qs-astext'); await page.waitForFunction(() => /Stabilizer tableau/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG }); check(true, `the plan is shown before running (${await page.textContent('#qs-plan')})`); await page.click('#qs-run'); await done(page); await verified(page); check((await page.textContent('.qs-method')) === 'Stabilizer tableau', 'run in this browser: the stabilizer tableau'); const here = await counts(page); check(here.length === 2 && here.every(([k]) => k === '00' || k === '11'), `only 00 and 11 (${JSON.stringify(here)})`); const zz = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent)); check(zz.some((t) => t.startsWith('⟨Z0 Z1⟩') && t.includes('1.000000')), `⟨Z0 Z1⟩ = 1 (${zz.slice(0, 2).join(' | ')})`); check((await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the receipt verifies, checked here (${await page.textContent('#qs-verified')})`); check((await page.textContent('#qs-signed-by')).startsWith('this browser’s key did:key:z6Mk'), `signed by this browser's key (${await page.textContent('#qs-signed-by')})`); check((await page.$$('.qs-hitem')).length === 1, 'the run is in the history'); // What it looks like, for a reader of the change: the dark scheme and the light. mkdirSync(SHOTS, { recursive: true }); await page.screenshot({ path: join(SHOTS, 'studio-dark.png'), fullPage: true }); await page.emulateMedia({ colorScheme: 'light' }); await page.screenshot({ path: join(SHOTS, 'studio-light.png'), fullPage: true }); await page.emulateMedia({ colorScheme: 'dark' }); // An error is marked on its line. await page.click('.qs-editor .cm-content'); await page.keyboard.press('ControlOrMeta+End'); await page.keyboard.type('\nfoo q[0];'); await page.waitForFunction(() => document.querySelector('#qs-lint')?.textContent === 'problem', null, { timeout: LONG }); await page.waitForSelector('.cm-lintRange-error', { timeout: LONG }); check(true, 'an unknown gate is marked in the editor'); for (let i = 0; i < 10; i++) await page.keyboard.press('Backspace'); await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG }); // A rotation: any angle, on the state vector. await page.selectOption('#qs-example', 'rotation'); await page.waitForFunction(() => /State vector/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG }); await page.click('#qs-run'); await done(page); const z = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent)); check((await page.textContent('.qs-method')) === 'State vector, double precision' && z.some((t) => t.startsWith('⟨Z0⟩') && t.includes(Math.cos(0.3).toFixed(6))), `rx(0.3) on the state vector, ⟨Z0⟩ = cos 0.3 (${z[0]})`); // The same Bell job on the service, and here: the same counts. // (STUDIO_SKIP_SERVICE=1 leaves it out, where a build may not reach the service.) if (process.env.STUDIO_SKIP_SERVICE) { console.log('SKIP the service checks (STUDIO_SKIP_SERVICE)'); } else { await page.selectOption('#qs-example', 'bell'); await page.click('#qs-run'); await done(page); await verified(page); const local = await counts(page); await page.click('.qs-seg[data-where=service]'); await page.click('#qs-run'); await done(page); await verified(page); const remote = await counts(page); check(JSON.stringify(remote) === JSON.stringify(local), `the service gives the counts run here, shot for shot (${JSON.stringify(remote)})`); check((await page.textContent('#qs-signed-by')).startsWith('the service’s key') && (await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the service's receipt checks here, as the service's (${await page.textContent('#qs-signed-by')})`); await page.click('.qs-seg[data-where=local]'); } // Ctrl+Enter runs; the program and the history survive a reload. (A job run again // unchanged gives the same signed result, which the history keeps once; so this one // asks for other shots.) const before = (await page.$$('.qs-hitem')).length; await page.fill('#qs-shots', '321'); await page.click('.qs-editor .cm-content'); await page.keyboard.press('ControlOrMeta+Enter'); await done(page); check((await page.$$('.qs-hitem')).length === before + 1, 'Ctrl+Enter in the editor runs the program'); const program = await page.evaluate(() => window.quantumStudio.view.state.doc.toString()); await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG }); check((await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === program, 'the program survives a reload'); await page.waitForFunction((n) => document.querySelectorAll('.qs-hitem').length >= n, before + 1, { timeout: LONG }); check(true, 'the history survives a reload'); // The critique, as the program changes. const doc = () => page.evaluate(() => window.quantumStudio.view.state.doc.toString()); const setProgram = async (q) => { await page.evaluate((q) => window.quantumStudio.fill({ qasm: q }), q); await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, q); }; const chip = (k) => page.$eval(`#qs-measures [data-k=${k}]`, (c) => c.textContent).catch(() => ''); { await page.selectOption('#qs-example', 'bell'); await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG }); const m = await page.textContent('#qs-measures'); check(/2 qubits/.test(m) && /1 two-qubit/.test(m) && await page.isVisible('#qs-clifford') && /fidelity, modelled/.test(m) && (await page.$$('#qs-smells li')).length === 0, `the critique gives the Bell pair's measures, that it is Clifford, a modelled fidelity, and no smells (${m})`); const SMELLY = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n'; await setProgram(SMELLY); await page.waitForSelector('.qs-smell[data-kind=cancels]', { timeout: LONG }); const smell = await page.textContent('.qs-smell[data-kind=cancels] .qs-smell-text'); check(smell === 'x on qubit 1 twice in a row (lines 5 and 6): together they do nothing', `a smell is named with its lines (${smell})`); check((await page.$$('#qs-diagram .qc-item.qc-flag')).length === 2, 'its two gates are flagged on the diagram'); await page.hover('.qs-smell[data-kind=cancels]'); check((await page.$$('#qs-diagram .qc-item.qc-smell')).length === 2 && (await page.$$('#qs-diagram .qc-wire.qc-smell')).length === 0, 'pointing at the smell marks its gates, and not their wires'); await page.click('.qs-smell[data-kind=cancels] button >> text=Show'); const sel = await page.evaluate(() => { const v = window.quantumStudio.view, d = v.state.doc, r = v.state.selection.main; return [r.from === d.line(5).from, r.to === d.line(6).to]; }); check(sel[0] && sel[1], 'Show selects its lines in the editor'); await page.click('#qs-run'); await done(page); await page.click('.qs-smell[data-kind=cancels] .qs-fix'); await page.waitForFunction(() => !/x q\[1\];/.test(window.quantumStudio.view.state.doc.toString()), null, { timeout: LONG }); const shows = await page.textContent('#qs-shows'); check(/^Fixed: x on qubit 1/.test(shows) && /same tableau/.test(shows), `Fix applies the engine's rewrite, checked the same circuit (${shows})`); await page.waitForFunction(() => /−2/.test(document.querySelector('#qs-measures [data-k=gates]')?.textContent ?? ''), null, { timeout: LONG }); check(true, `the measures show their change since the last run (${await chip('gates')})`); await page.click('.qs-editor .cm-content'); await page.keyboard.press('ControlOrMeta+z'); await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, SMELLY, { timeout: LONG }); check(true, 'Undo brings the fixed lines back'); await setProgram('OPENQASM 3.0;\nqubit[3] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\n'); await page.waitForSelector('.qs-smell[data-kind=idle]', { timeout: LONG }); check((await page.$$('#qs-diagram .qc-wire.qc-flag')).length === 2 && !(await page.$('.qs-smell[data-kind=idle] .qs-fix')), 'qubits nothing uses are flagged on their wires, with no Fix'); } // Alternatives: Improve, without a model; Explore says it needs one. { const MERGE = 'OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nrz(0.25) q[0];\nrz(0.5) q[0];\nc[0] = measure q[0];\n'; await setProgram(MERGE); await page.click('#qs-improve'); await page.waitForSelector('.qs-cand', { timeout: LONG }); const card = await page.textContent('.qs-cand'); check(/the engine/.test(card) && /same circuit · on every basis state/.test(card) && /2 gates/.test(card), `Improve offers the engine's rewrite, checked on every basis state (${card.slice(0, 160)})`); check(await page.isVisible('#qs-atlas') && (await page.$$('#qs-atlas .qs-dot')).length === (await page.$$('.qs-cand')).length && (await page.$$('#qs-atlas .qs-ring')).length === 1, 'the atlas plots the draft and each alternative'); check(/Connect a model/.test(await page.textContent('#qs-alt-body')), 'without a model, Improve says the engine’s are the only ones'); check((await doc()) === MERGE, 'nothing changes the program until an alternative is applied'); await page.click('.qs-cand .qs-apply'); check(/rz\(0\.25 \+ 0\.5\) q\[0\];/.test(await doc()), 'Apply puts the alternative in the editor'); await page.click('#qs-explore'); await page.waitForSelector('#qs-alt-nomodel', { timeout: LONG }); check(true, 'Explore without a model says it needs one'); } // The classical-shortcut lens. { let RING = 'OPENQASM 3.0;\nqubit[16] q;\nbit[16] c;\nh q;\n'; for (let i = 0; i < 16; i++) RING += `cz q[${i}], q[${(i + 1) % 16}];\n`; RING += 'rz(0.3) q;\nc = measure q;\n'; await setProgram(RING); await page.click('#qs-shortcuts'); await page.waitForSelector('#qs-shortcuts-summary', { timeout: LONG }); const summary = await page.textContent('#qs-shortcuts-summary'); const cut = await page.textContent('#qs-routes tr[data-route=cutrank]'); check(/cut-rank sum/.test(summary) && /widest cut 2 bits/.test(cut) && await page.$('#qs-routes tr.best[data-route=cutrank]') && /does not apply/.test(await page.textContent('#qs-routes tr[data-route=stabilizer]')), `the lens prices a Clifford ring with a closing layer by the cut-rank sum (${summary})`); check(/not searched/.test(await page.textContent('#qs-routes tr[data-route=tensor_network]')), 'the quick check leaves the contraction search for a thorough one'); await page.click('#qs-shortcuts-thorough'); await page.waitForFunction(() => /2\^/.test(document.querySelector('#qs-routes tr[data-route=tensor_network]')?.textContent ?? ''), null, { timeout: LONG }); check(true, `a thorough check searches a contraction tree (${(await page.textContent('#qs-routes tr[data-route=tensor_network]')).trim()})`); await page.click('.qs-editor .cm-content'); await page.keyboard.press('ControlOrMeta+End'); await page.keyboard.type('\nh q[0];'); await page.waitForSelector('#qs-shortcuts-panel.stale .qs-stale-note', { timeout: LONG }); check(true, 'a check is marked stale once the program changes'); await page.selectOption('#qs-example', 'bell'); await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG }); await page.click('#qs-shortcuts'); await page.waitForFunction(() => /^Classically easy at any width/.test(document.querySelector('#qs-shortcuts-summary')?.textContent ?? '') && !document.querySelector('#qs-shortcuts-panel.stale'), null, { timeout: LONG }); check(true, 'a Clifford circuit is classically easy at any width'); } // Predict first. { await page.selectOption('#qs-example', 'bell'); await page.check('#qs-predict-first'); const ran = (await page.$$('.qs-hitem')).length; await page.click('#qs-run'); await page.waitForSelector('#qs-predict', { timeout: LONG }); check((await page.$$('.qs-hitem')).length === ran && !(await page.$('.qs-method')), 'with Predict first, the run waits for a prediction'); await page.fill('#qs-predict-bits', '000'); await page.fill('#qs-predict-share', '50'); await page.click('#qs-predict-run'); check(/2 classical bits; the outcome has 3/.test(await page.textContent('#qs-predict-note')), 'an outcome of the wrong width is refused, with the width'); await page.fill('#qs-predict-bits', '00'); await page.click('#qs-predict-run'); await done(page); await page.waitForSelector('#qs-predicted', { timeout: LONG }); const pred = await page.textContent('#qs-predicted'); check(/you said00 in 50%/.test(pred) && /the run00 in \d/.test(pred) && /(came out on top|Another outcome)/.test(pred) && /first prediction/.test(pred), `the prediction is set against the counts (${pred})`); await page.uncheck('#qs-predict-first'); } // The assistant, against a scripted model server: it calls a tool, proposes a program // the engine cannot read, gets the engine's reason back, and proposes a corrected one, // which the Studio checks and runs before offering it. { const asked = []; const GOOD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n'; const model = createServer((req, res) => { const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'GET, POST, OPTIONS' }; if (req.method === 'OPTIONS') { res.writeHead(204, cors).end(); return; } if (req.url.endsWith('/models')) { res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ data: [{ id: 'scripted' }] })); return; } let body = ''; req.on('data', (c) => (body += c)); req.on('end', () => { const r = JSON.parse(body); asked.push(r); const msgs = r.messages; // Alternatives' calls carry no tools: candidates, one of them wrong on purpose. if (!r.tools) { const want = msgs[1].content; const content = /^Write \d+ programs that are the same circuit/.test(want) ? 'Idea: the two Hadamards cancel, and the X moves past the CNOT.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nx q[1];\nc = measure q;\n```\nIdea: drop the X.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nc = measure q;\n```' : 'Idea: a GHZ state on three qubits.\n```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n```\n**Idea:** entangle with CZ instead.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[1];\nc = measure q;\n```'; res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message: { role: 'assistant', content }, finish_reason: 'stop' }] })); return; } const told = msgs.some((m) => m.role === 'user' && /could not read the program you proposed/.test(m.content)); const toolDone = msgs.some((m) => m.role === 'tool'); let message; if (!toolDone) message = { role: 'assistant', content: '', tool_calls: [{ id: 'c1', type: 'function', function: { name: 'lint_qasm', arguments: JSON.stringify({ qasm: GOOD }) } }] }; else if (!told) message = { role: 'assistant', content: 'It entangles two qubits. A variant:\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nfoo q[0];\n```' }; else message = { role: 'assistant', content: 'Corrected:\n```qasm\n' + GOOD + '```' }; res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message, finish_reason: message.tool_calls ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } })); }); }); await new Promise((r) => model.listen(0, '127.0.0.1', r)); await page.click('#qs-ai-gear'); await page.fill('#qs-ai-endpoint', `http://127.0.0.1:${model.address().port}/v1`); await page.click('#qs-ai-connect'); await page.waitForFunction(() => /connected · scripted/.test(document.querySelector('#qs-ai-state')?.textContent ?? ''), null, { timeout: LONG }); check(true, 'the assistant connects to a model server and reads its models'); await page.click('.qs-ai-quick button >> text=Explain this circuit'); await page.waitForSelector('.qs-ai-apply', { timeout: LONG }); const log = await page.$$eval('#qs-ai-log > li', (ls) => ls.map((l) => l.textContent)); check(log.some((t) => t.includes('used lint_qasm')), 'the model\'s tool call ran on the engine, and the log shows it'); check(log.some((t) => /could not read the proposed program \(line 3: unknown gate foo\)/.test(t)), `a proposal the engine cannot read goes back with the engine's reason (${log.find((t) => /could not read/.test(t)) ?? 'none'})`); const proposal = await page.textContent('.qs-ai-proposal'); check(/checked: 2 qubits, 2 gates/.test(proposal) && /ran here on Stabilizer tableau/.test(proposal), `the corrected proposal is checked and run before it is offered (${proposal})`); check(asked.length === 3 && asked.every((r) => r.tools?.some((t) => t.function.name === 'run_job') && r.messages[0].role === 'system' && /Never state a probability/.test(r.messages[0].content)), 'every call to the model carries the engine\'s tools and the rule that numbers come from them'); const toolMsg = asked[1].messages.find((m) => m.role === 'tool'); check(toolMsg && JSON.parse(toolMsg.content).qubits === 2, 'the tool\'s answer went back to the model'); await page.click('.qs-ai-apply'); check((await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === GOOD, 'Apply puts the checked program in the editor'); check((await page.inputValue('#qs-ai-stance')) === 'questions' && (await page.inputValue('#qs-ai-initiative')) === 'asked' && /ask one short question/.test(asked[0].messages[0].content), 'by default the assistant asks questions first and speaks only when asked'); const quick = await page.$$eval('.qs-ai-quick button', (bs) => bs.map((b) => b.textContent)); check(quick.includes('Ask me a question') && quick.includes('Hint'), `it can be asked for a question or a hint (${quick.join(', ')})`); check(asked.every((r) => !r.tools || r.tools.some((t) => t.function.name === 'critique_qasm') && r.tools.some((t) => t.function.name === 'check_equivalent')), 'its tools include the critique and the equivalence check'); // Runs refused twice: no hint offered while it speaks only when asked; offered, not sent, when allowed. const calls = asked.length; await setProgram('OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n'); for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); } check(await page.isHidden('#qs-ai-stuck'), 'while it speaks only when asked, refused runs offer no hint'); await page.selectOption('#qs-ai-initiative', 'hint'); for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); } check(await page.isVisible('#qs-ai-stuck') && asked.length === calls, 'allowed to, it offers a hint after refused runs, and asks the model nothing'); await page.selectOption('#qs-ai-initiative', 'asked'); // Improve and Explore with the model: the engine reads, measures and compares each. const DRAFT = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[0];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n'; await setProgram(DRAFT); await page.click('#qs-improve'); await page.waitForFunction(() => document.querySelectorAll('.qs-cand').length && !/looking/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG }); const cards = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent)); const refused = await page.textContent('#qs-alt-refused').catch(() => ''); check(cards.some((t) => /the model/.test(t) && /the two Hadamards cancel/.test(t) && /same circuit · equal tableaus/.test(t)) && /drop the X: not the same circuit/.test(refused), `Improve with the model: its rewrite checked the same circuit, its wrong one left out with the reason (${refused})`); await page.fill('#qs-explore-aim', 'another entangler'); await page.click('#qs-explore'); await page.waitForFunction(() => /a GHZ state/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG }); const ideas = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent)); const muse = asked.filter((r) => !r.tools).map((r) => r.messages[1].content); check(ideas.length === 2 && ideas.some((t) => /3 qubits/.test(t) && /a different circuit/.test(t)) && ideas.some((t) => /entangle with CZ instead/.test(t)) && muse.some((m) => /toward this aim: another entangler/.test(m)), `Explore: the model's ideas, each read and measured by the engine (${ideas.map((t) => t.slice(0, 60)).join(' | ')})`); check((await doc()) === DRAFT, 'neither changes the program until one is applied'); // The candidates come back within the editor's redraw delay: wait for the draft's diagram. await page.waitForFunction(() => /4 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && document.querySelectorAll('#qs-diagram .qc-flag').length === 2, null, { timeout: LONG }); check(!/null/.test(await page.textContent('#qs-alt-body')) && !/null/.test(await page.textContent('#qs-measures')), 'no empty part is written out as “null”'); await page.screenshot({ path: join(SHOTS, 'studio-alternatives.png'), fullPage: true }); model.close(); } check(seen.errors.length === 0, `no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`); check(seen.failed.length === 0, `no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`); await ctx.close(); { const { ctx, page } = await open({ width: 390, height: 844 }); await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG }); const over = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); check(over <= 0, `at 390 px nothing scrolls sideways (overflow ${over} px)`); await ctx.close(); } await browser.close(); server.close(); console.log(`\n${fails === 0 ? 'every check passed' : `${fails} check(s) failed`}`); process.exit(fails === 0 ? 0 : 1);