A run on one of the person's providers waited inside the desktop app for up to two hours, holding the Studio, and then cancelled the job, giving up its place in a device's queue. The app now writes the job down before it sends it, with the command line's records of jobs on a provider (#581), and asks after it by its id. - wai-quantum-desktop: run_on_provider answers within ten seconds, done or still running with its id; provider_job asks after a job once or cancels it; provider_jobs lists the jobs sent, asking no provider. - Quantum Studio: while a job waits, Cancel cancels it at the provider and "Wait in the background" leaves it there and frees the Studio. A job left waiting, from this window or an earlier one, is followed while the app is open, and its result goes to the history when it finishes. Your jobs lists every job sent. A check that fails during the wait does not end it. - The tests: the app's queued job answered by its id, sealed with the app's key, the same answer after, and cancelled only when asked; the page's wait, cancel, cancel-after-finish, failed check, background and reopened window. The studio workflow names the new app test on every leg. Co-Authored-By: Claude Opus 5.5 <[email protected]>
166 lines
9.7 KiB
JavaScript
166 lines
9.7 KiB
JavaScript
// The "Your hardware" logic, without a browser: a run that is not free asks
|
|
// the person first, a free one does not, a "no" sends nothing, and the
|
|
// dialog's words carry the estimate. A job in the provider's queue is asked
|
|
// after by its id; Cancel cancels it there, "Wait in the background" leaves
|
|
// it, and a job left waiting, by this window or an earlier one, gives its
|
|
// result when it finishes.
|
|
//
|
|
// node test/hardware.test.mjs
|
|
import { HardwareEngine, NOTE, confirmText, costText, jobLine, needsConfirmation } from '../src/hardware.js';
|
|
|
|
let fails = 0;
|
|
const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; };
|
|
|
|
const plan = (free, estimate) => ({ free, estimate, provider: { name: 'lab', device: 'reference:line-5', provenance: 'simulator:x' }, plan: { shots: 100, compile: { native_gates: 12, basis: 'rzsxxcz', swaps: 0 } } });
|
|
const priced = plan(false, { free: false, amount: '0.3', currency: 'USD', unit: 'shot', seconds: 4 });
|
|
|
|
check(needsConfirmation(priced) && !needsConfirmation(plan(true, { free: true })), 'a run not stated free needs a confirmation, a free one does not');
|
|
check(needsConfirmation(plan(undefined, { unknown: 'no estimate' })), 'an unknown cost needs a confirmation');
|
|
check(costText(priced) === '0.3 USD (billed by the shot), about 4 s', `the cost in words (${costText(priced)})`);
|
|
check(costText(plan(false, { unknown: 'the provider gives no estimate' })) === 'cost unknown: the provider gives no estimate', 'an unknown cost says so');
|
|
const words = confirmText(priced).join(' ');
|
|
check(/Run on lab \(reference:line-5/.test(words) && /not free: 0\.3 USD/.test(words) && /12 native gates in rzsxxcz/.test(words) && /spends your account/.test(words), `the dialog's words (${words})`);
|
|
check(/desktop app or the command line/.test(NOTE) && /must not hold the credentials/.test(NOTE), 'the browser note');
|
|
|
|
// The engine against fake desktop commands.
|
|
function fake(p) {
|
|
const calls = [];
|
|
const invoke = async (cmd, args) => {
|
|
calls.push([cmd, args]);
|
|
if (cmd === 'providers_list') return JSON.stringify({ providers: [{ name: 'lab', kind: 'process' }] });
|
|
if (cmd === 'provider_estimate') return JSON.stringify(p);
|
|
if (cmd === 'run_on_provider') return JSON.stringify({ status: 'done', method: 'provider', request: JSON.parse(args.request), result: { counts: { '00': 50, '11': 50 } } });
|
|
return JSON.stringify({ error: `no command ${cmd}` });
|
|
};
|
|
return { calls, invoke };
|
|
}
|
|
{
|
|
const f = fake(plan(true, { free: true }));
|
|
let asked = 0;
|
|
const e = new HardwareEngine(f.invoke, async () => { asked += 1; return true; });
|
|
check((await e.providers()).length === 1, 'the providers are listed');
|
|
e.select('lab');
|
|
const body = await e.run({ qasm: 'OPENQASM 3.0; qubit[2] q;', shots: 100 });
|
|
const run = f.calls.find((c) => c[0] === 'run_on_provider');
|
|
check(asked === 0 && run[1].confirmCost === false && body.request.provider === 'lab', 'a free run is not asked about, and names its provider');
|
|
}
|
|
{
|
|
const f = fake(priced);
|
|
const e = new HardwareEngine(f.invoke, async () => false);
|
|
e.select('lab');
|
|
let err = '';
|
|
try { await e.run({ qasm: 'q', shots: 100 }); } catch (x) { err = x.message; }
|
|
check(err === 'cancelled' && !f.calls.some((c) => c[0] === 'run_on_provider'), 'a run not free, declined, sends nothing');
|
|
}
|
|
{
|
|
const f = fake(priced);
|
|
let shown = null;
|
|
const e = new HardwareEngine(f.invoke, async (p) => { shown = p; return true; });
|
|
e.select('lab');
|
|
await e.run({ qasm: 'q', shots: 100 });
|
|
const run = f.calls.find((c) => c[0] === 'run_on_provider');
|
|
check(shown === null ? false : shown.estimate.amount === '0.3', 'the dialog is shown the estimate');
|
|
check(run && run[1].confirmCost === true, 'a run not free, accepted, is sent confirmed');
|
|
}
|
|
{
|
|
const e = new HardwareEngine(fake(priced).invoke, async () => true);
|
|
let err = '';
|
|
try { await e.run({ qasm: 'q' }); } catch (x) { err = x.message; }
|
|
check(/choose one of your providers/.test(err), 'no provider chosen, no run');
|
|
}
|
|
|
|
// A job that waits in the provider's queue: the run asks after it by its id
|
|
// until it finishes, and the person can cancel it or leave it waiting.
|
|
function queue({ polls = 2, done = { '00': 50, '11': 50 }, failOnce = false } = {}) {
|
|
const calls = [];
|
|
const jobs = new Map(); // id -> {left, cancelled}
|
|
let failed = !failOnce;
|
|
let n = 0;
|
|
const request = { qasm: 'q', shots: 100, provider: 'lab' };
|
|
const body = (id) => ({ id, status: 'done', method: 'provider', request, result: { counts: done } });
|
|
const invoke = async (cmd, args) => {
|
|
calls.push([cmd, args]);
|
|
if (cmd === 'provider_estimate') return JSON.stringify(plan(true, { free: true }));
|
|
if (cmd === 'run_on_provider') {
|
|
const id = `pj-${String(++n).padStart(16, '0')}`;
|
|
jobs.set(id, { left: polls, cancelled: false });
|
|
return JSON.stringify({ id, status: 'running', method: 'provider', request });
|
|
}
|
|
if (cmd === 'provider_jobs') return JSON.stringify({ jobs: [...jobs].map(([id, j]) => ({ id, provider: 'lab', shots: 100, status: j.cancelled ? 'cancelled' : j.left > 0 ? 'running' : 'done', submitted_ms: 0 })) });
|
|
if (cmd === 'provider_job') {
|
|
const j = jobs.get(args.id);
|
|
if (!j) return JSON.stringify({ error: `no job ${args.id}` });
|
|
if (!failed) { failed = true; return JSON.stringify({ error: `could not ask after ${args.id}, and nothing about the job has changed; asking again is safe: the network is down` }); }
|
|
if (j.cancelled) return JSON.stringify({ id: args.id, status: 'cancelled', error: 'cancelled at lab', request });
|
|
if (j.left > 0 && args.cancel) { j.cancelled = true; return JSON.stringify({ id: args.id, status: 'cancelled', error: 'cancelled at lab', request }); }
|
|
if (j.left > 0) { j.left -= 1; return JSON.stringify({ id: args.id, status: 'running', method: 'provider', request }); }
|
|
return JSON.stringify(body(args.id));
|
|
}
|
|
return JSON.stringify({ error: `no command ${cmd}` });
|
|
};
|
|
return { calls, invoke, jobs };
|
|
}
|
|
const fast = { first: 0, most: 0 };
|
|
const asks = (calls) => calls.filter((c) => c[0] === 'provider_job');
|
|
{
|
|
const q = queue({ polls: 2 });
|
|
const e = new HardwareEngine(q.invoke, async () => true, fast);
|
|
e.select('lab');
|
|
const seen = [];
|
|
const body = await e.run({ qasm: 'q', shots: 100 }, (j, asked) => seen.push([j.status, asked, e.progressText(j, asked)]));
|
|
check(body.status === 'done' && body.result.counts['00'] === 50 && asks(q.calls).length === 3 && asks(q.calls).every((c) => c[1].cancel === false && c[1].id === body.id),
|
|
`a queued job is asked after by its id until it finishes (${asks(q.calls).length} asks)`);
|
|
check(seen.length === 3 && /^At lab, queued or running, as pj-0+1; asked 0 times\. Closing the app leaves it there\.$/.test(seen[0][2]), `the status line while it waits (${seen[0]?.[2]})`);
|
|
}
|
|
{
|
|
const q = queue({ polls: 5 });
|
|
const e = new HardwareEngine(q.invoke, async () => true, fast);
|
|
e.select('lab');
|
|
let err = '';
|
|
try { await e.run({ qasm: 'q', shots: 100 }, (j, asked) => { if (asked === 1) e.cancel(); }); } catch (x) { err = x.message; }
|
|
const last = asks(q.calls).at(-1);
|
|
check(err === 'cancelled' && last[1].cancel === true && [...q.jobs.values()][0].cancelled, 'Cancel while a job waits cancels it at the provider');
|
|
}
|
|
{
|
|
const q = queue({ polls: 0 });
|
|
const e = new HardwareEngine(q.invoke, async () => true, fast);
|
|
e.select('lab');
|
|
const body = await e.run({ qasm: 'q', shots: 100 }, () => e.cancel());
|
|
check(body.status === 'done', 'a job that finished before the cancel reached it gives its result: it has been paid for');
|
|
}
|
|
{
|
|
const q = queue({ polls: 2, failOnce: true });
|
|
const e = new HardwareEngine(q.invoke, async () => true, fast);
|
|
e.select('lab');
|
|
const seen = [];
|
|
const body = await e.run({ qasm: 'q', shots: 100 }, (j, asked, failed) => seen.push(e.progressText(j, asked, failed)));
|
|
check(body.status === 'done' && seen.some((s) => /The last check failed, and nothing about the job changed: could not ask after/.test(s)), 'a check that fails does not end the wait, and says so');
|
|
}
|
|
{
|
|
const q = queue({ polls: 4 });
|
|
const e = new HardwareEngine(q.invoke, async () => true, fast);
|
|
e.select('lab');
|
|
const finished = new Promise((resolve) => { e.onFinished = resolve; });
|
|
let err = null;
|
|
try { await e.run({ qasm: 'q', shots: 100 }, (j, asked) => { if (asked === 1) e.later(); }); } catch (x) { err = x; }
|
|
check(err?.later === true && /^pj-0+1 is still at lab\. It is followed from here/.test(err.message) && ![...q.jobs.values()][0].cancelled, `"Wait in the background" leaves the job at the provider, not cancelled (${err?.message})`);
|
|
const body = await finished;
|
|
check(body.status === 'done' && body.id === 'pj-0000000000000001' && asks(q.calls).every((c) => c[1].cancel === false), 'and the job, followed in the background, gives its result when it finishes');
|
|
}
|
|
{
|
|
const q = queue({ polls: 3 });
|
|
const first = new HardwareEngine(q.invoke, async () => true, fast);
|
|
first.select('lab');
|
|
try { await first.run({ qasm: 'q', shots: 100 }, () => first.later()); } catch (x) {}
|
|
// The app opened again: a new engine, the records kept.
|
|
const again = new HardwareEngine(q.invoke, async () => true, fast);
|
|
const finished = new Promise((resolve) => { again.onFinished = resolve; });
|
|
await again.resume();
|
|
check(again.watched.size === 1 && (await finished).status === 'done', 'a job left waiting by an earlier window is followed when the app opens again');
|
|
const listed = await again.jobs();
|
|
check(listed.length === 1 && jobLine(listed[0], () => 'then') === 'pj-0000000000000001 · lab · 100 shots · finished · sent then', `the jobs sent, in words (${jobLine(listed[0], () => 'then')})`);
|
|
}
|
|
|
|
if (fails) { console.error(`${fails} failed`); process.exit(1); }
|
|
console.log('hardware: every check held');
|
|
process.exit(0);
|