open-standards/wai/quantum-studio/test/service.test.mjs
Transaction Science 2e7a49cd7a
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 10s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 8s
WAI energy receipts / does this change touch it (pull_request) Successful in 11s
wai-quantum contract / does this change touch it (pull_request) Successful in 9s
wai-quantum contract / the policy is linked, and the deprecations page reads (pull_request) Successful in 6s
wai-quantum docs, course and locales / does this change touch it (pull_request) Successful in 8s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 33s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 9s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 10s
wai-quantum views in agent hosts / does this change touch it (pull_request) Successful in 8s
WAI Studio / does this change touch it (pull_request) Successful in 8s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 1m23s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m27s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m32s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m41s
wai-quantum views in agent hosts / views · a scripted host (pull_request) Successful in 4m47s
wai-quantum views in agent hosts / views · the page and its engine (pull_request) Successful in 5m9s
wai-quantum contract / schemas and the committed tool lists · linux (pull_request) Successful in 6m38s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m18s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 7m35s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 8m32s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 10m8s
wai-quantum contract / API compatibility · linux (pull_request) Successful in 12m3s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 13m43s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 16m15s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 18m29s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 28m58s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 8m15s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 41m16s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 43m34s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 30m0s
Quantum Studio: a run on the job service whose answer is lost is sent again, safely
The Studio's runs on the job service sent each job once and gave up on
the first lost answer, of the submission or of any poll. A run now goes
under an Idempotency-Key of its own (a random UUID) to a service whose
description says it keeps keys, and is sent again under it, twice at
most, where no answer came back, so a job the service took is answered
rather than taken twice. A service that keeps no keys is sent no key,
which its CORS would refuse, and no job twice. A poll whose answer is
lost no longer ends the wait; five in a row do.

test/service.test.mjs holds each of these against a scripted fetch, and
the studio workflow runs it. The views page is unchanged: engine.js is
not among its sources, and rebuilt in CI's image it is the same bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 06:27:10 -05:00

83 lines
3.8 KiB
JavaScript

// 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);