// The Studio's runs on the job service, without a browser or a service: a // job whose answer is lost is sent again under its key to a service that // keeps keys, and sent once, with no key, to one that does not; a poll whose // answer is lost does not end the wait. // // node test/service.test.mjs import { ServiceEngine } from '../src/engine.js'; let fails = 0; const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; }; // A service from a script: each answer is a status and a body, or 'drop' // for a connection that ends before its answer. Every request is kept. function service(script) { const seen = []; globalThis.fetch = async (url, init = {}) => { const path = new URL(url).pathname; seen.push({ method: init.method || 'GET', path, headers: init.headers || {} }); const queue = script[`${init.method || 'GET'} ${path}`] ?? []; const next = queue.length > 1 ? queue.shift() : queue[0]; if (next === undefined) return new Response(JSON.stringify({ error: `no answer for ${path}` }), { status: 500 }); if (next === 'drop') throw new TypeError('Failed to fetch'); return new Response(JSON.stringify(next[1]), { status: next[0], headers: { 'content-type': 'application/json' } }); }; return seen; } const base = 'https://jobs.example/quantum-jobs'; const done = { id: 'j1', status: 'done', result: { counts: { '00': 5, '11': 5 } } }; const keeps = { idempotency: { header: 'Idempotency-Key', on: ['POST /v1/jobs', 'POST /v1/run'] } }; { const seen = service({ 'GET /quantum-jobs/v1': [[200, keeps]], 'POST /quantum-jobs/v1/jobs': ['drop', [202, { id: 'j1', status: 'queued' }]], 'GET /quantum-jobs/v1/jobs/j1': [[200, done]], }); const job = await new ServiceEngine(base).run({ qasm: 'OPENQASM 3.0; qubit q;', shots: 10 }); const posts = seen.filter((r) => r.method === 'POST'); const keys = posts.map((r) => r.headers['idempotency-key']); check(job.status === 'done' && job.id === 'j1', 'the job whose answer was lost ends done'); check(posts.length === 2 && keys[0] && keys[0] === keys[1], `sent again under the same key (${keys.join(', ')})`); check(/^[0-9a-f-]{32,36}$/.test(keys[0]), 'the key is a random UUID'); } { const seen = service({ 'GET /quantum-jobs/v1': [[200, { service: 'wai-quantum-service' }]], 'POST /quantum-jobs/v1/jobs': ['drop'], }); let err = null; try { await new ServiceEngine(base).run({ qasm: 'OPENQASM 3.0; qubit q;' }); } catch (e) { err = e; } const posts = seen.filter((r) => r.method === 'POST'); check(err && posts.length === 1, 'a service that keeps no keys is sent the job once'); check(!('idempotency-key' in posts[0].headers), 'and no key, which its CORS would refuse'); } { const seen = service({ 'GET /quantum-jobs/v1': [[200, keeps]], 'POST /quantum-jobs/v1/jobs': [[202, { id: 'j1', status: 'queued' }]], 'GET /quantum-jobs/v1/jobs/j1': ['drop', [200, { id: 'j1', status: 'running' }], [200, done]], }); const e = new ServiceEngine(base); const job = await e.run({ qasm: 'OPENQASM 3.0; qubit q;' }); check(job.status === 'done', 'a poll whose answer is lost does not end the wait'); await e.run({ qasm: 'OPENQASM 3.0; qubit q;' }); check(seen.filter((r) => r.path === '/quantum-jobs/v1').length === 1, 'the service is asked once whether it keeps keys'); } { service({ 'GET /quantum-jobs/v1': [[200, keeps]], 'POST /quantum-jobs/v1/jobs': [[202, { id: 'j1', status: 'queued' }]], 'GET /quantum-jobs/v1/jobs/j1': ['drop'], }); let err = null; try { await new ServiceEngine(base).run({ qasm: 'OPENQASM 3.0; qubit q;' }); } catch (e) { err = e; } check(err !== null, 'five lost polls in a row end the wait'); } console.log(fails ? `${fails} failed` : 'service: every check passed'); process.exit(fails ? 1 : 0);