From 51e89d4e200829b260465f3f1a1d09bc242d505e Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 29 Oct 2026 08:14:24 -0400 Subject: [PATCH] wai-quantum for JavaScript: the engine as the npm package @transaction-science/wai-quantum 0.1.0 wai/wai-quantum-js/ packs the job service's engine for WebAssembly (wai-web with its quantum_engine feature, the engine of wai-quantum 0.7.0) and its bindings as an ES module with its TypeScript declarations and no dependencies, for a page (init() fetches the .wasm beside the module) or for Node (init is given its bytes). The package keeps its own version line, as the Python package does. - build.sh: wai-web built for wasm32 with paths remapped (as the views build does), wasm-bindgen 0.2.127 (refused if it is not the version wai-web's lock file pins), package.json, README and the licence copied, the build refused if a local path is left in the bindings or the binary, then packed. - test/smoke.mjs, on the package unpacked: a job run with a fresh 32-byte key and its receipt verified with this engine's did:key; a forged result that does not verify; a program linted and drawn; a refused program with its diagnostic code (Q310). - test/install.mjs: the README's Node example, run in an empty app that installed the tarball with npm, the package found by name and its .wasm by the subpath it exports. - ci/check.sh runs them all and holds the tarball to the package's allow-list (the seven files and nothing else). - wai-quantum-js.yml runs ci/check.sh on Linux. wai-quantum-js-release.yml runs it on a wai-quantum-cli-v* tag and attaches the tarball and its SHA-256 to the release. Nothing is published to a registry from CI. On the M3 Ultra: the .wasm is 5,928,153 bytes, the tarball 2.26 MB, and every check passes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-js-release.yml | 77 +++++++++++++++++ .github/workflows/wai-quantum-js.yml | 81 ++++++++++++++++++ wai/CHANGELOG.md | 13 +++ wai/wai-quantum-js/README.md | 87 ++++++++++++++++++++ wai/wai-quantum-js/build.sh | 54 ++++++++++++ wai/wai-quantum-js/ci/check.sh | 39 +++++++++ wai/wai-quantum-js/package.json | 40 +++++++++ wai/wai-quantum-js/test/install.mjs | 20 +++++ wai/wai-quantum-js/test/smoke.mjs | 56 +++++++++++++ 9 files changed, 467 insertions(+) create mode 100644 .github/workflows/wai-quantum-js-release.yml create mode 100644 .github/workflows/wai-quantum-js.yml create mode 100644 wai/wai-quantum-js/README.md create mode 100755 wai/wai-quantum-js/build.sh create mode 100755 wai/wai-quantum-js/ci/check.sh create mode 100644 wai/wai-quantum-js/package.json create mode 100644 wai/wai-quantum-js/test/install.mjs create mode 100644 wai/wai-quantum-js/test/smoke.mjs diff --git a/.github/workflows/wai-quantum-js-release.yml b/.github/workflows/wai-quantum-js-release.yml new file mode 100644 index 000000000..6ba3d3e69 --- /dev/null +++ b/.github/workflows/wai-quantum-js-release.yml @@ -0,0 +1,77 @@ +name: wai-quantum for JavaScript, release tarball + +# On a tag wai-quantum-cli-v (the command line's release), builds the npm +# package @transaction-science/wai-quantum (wai/wai-quantum-js) and attaches its +# tarball to the same forge release, with its SHA-256. ci/check.sh builds it, holds +# the tarball to the package's allow-list, runs the smoke test on it unpacked and the +# README's Node example on it installed, before it is attached. Nothing is published +# to a package registry: the tarball is published from the release by hand. +# +# Uploads go to the forge directly over the private network (FORGE_API), never through +# the public tunnel. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + tags: ['wai-quantum-cli-v*'] + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + FORGE_API: http://100.64.224.0:3400/api/v1 + FORGE_REPO: Transaction-Science/open-standards + +jobs: + version: + name: the tag is the command line's version + runs-on: linux + outputs: + version: ${{ steps.v.outputs.version }} + steps: + - uses: actions/checkout@v5 + - id: v + shell: bash + run: | + set -euo pipefail + v="${GITHUB_REF_NAME#wai-quantum-cli-v}" + crate="$(sed -n 's/^version = "\(.*\)"$/\1/p' wai/wai-quantum-cli/Cargo.toml | head -1)" + [ "$v" = "$crate" ] || { echo "::error::the tag says $v, wai-quantum-cli/Cargo.toml says $crate"; exit 1; } + echo "the npm package: $(python3 -I -c 'import json; print(json.load(open("wai/wai-quantum-js/package.json"))["version"])')" + echo "version=$v" >> "$GITHUB_OUTPUT" + + tarball: + name: the npm tarball · linux + needs: version + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 with the wasm32 target + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add wasm32-unknown-unknown + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: wasm-bindgen CLI 0.2.127 + shell: bash + run: cargo install --locked wasm-bindgen-cli --version 0.2.127 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Build, check and hash + shell: bash + run: bash wai/wai-quantum-js/ci/check.sh "$RUNNER_TEMP/dist" + - name: Attach to the release + shell: bash + env: + FORGE_TOKEN: ${{ github.token }} + run: bash wai/wai-quantum-cli/release/publish.sh "$GITHUB_REF_NAME" "wai-quantum ${{ needs.version.outputs.version }}" "$RUNNER_TEMP"/dist/*.tgz "$RUNNER_TEMP"/dist/*.tgz.sha256 diff --git a/.github/workflows/wai-quantum-js.yml b/.github/workflows/wai-quantum-js.yml new file mode 100644 index 000000000..b9e407d69 --- /dev/null +++ b/.github/workflows/wai-quantum-js.yml @@ -0,0 +1,81 @@ +name: wai-quantum for JavaScript + +# The npm package @transaction-science/wai-quantum (wai/wai-quantum-js): the job +# service's engine for WebAssembly (wai-web, feature quantum_engine) with its +# bindings. ci/check.sh builds and packs it; the tarball, unpacked, holds the +# package's allow-list and nothing else; the smoke test runs against what was +# unpacked (a job run with a fresh key and its receipt verified, a forged result +# refused, a program linted and drawn, a refusal with its diagnostic code); and the +# tarball installed into an empty app runs the README's Node example by name. +# Nothing is published. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-quantum-js/**' + - 'wai/wai-web/**' + - 'wai/wai-quantum/**' + - 'wai/wai-rs/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-js.yml' + pull_request: + paths: + - 'wai/wai-quantum-js/**' + - 'wai/wai-web/**' + - 'wai/wai-quantum/**' + - 'wai/wai-rs/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-js.yml' + +jobs: + relevant: + name: does this change touch it + runs-on: linux-arm64 + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-js.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches the npm package: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + js-linux: + name: javascript · linux + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 with the wasm32 target + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add wasm32-unknown-unknown + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # Pinned to the wasm-bindgen version in wai-web/Cargo.lock; build.sh refuses another. + - name: wasm-bindgen CLI 0.2.127 + shell: bash + run: cargo install --locked wasm-bindgen-cli --version 0.2.127 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Build, pack, unpack, smoke-test and install the package + shell: bash + run: bash wai/wai-quantum-js/ci/check.sh "$RUNNER_TEMP/npm" diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 00e233db4..470e412cc 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,19 @@ wire format may change and such changes are recorded here. ## Unreleased +### wai-quantum for JavaScript: the engine as an npm package + +- **`@transaction-science/wai-quantum` 0.1.0** (`wai/wai-quantum-js/`): the job service's engine for WebAssembly + (`wai-web` with its `quantum_engine` feature, carrying the engine of `wai-quantum` 0.7.0) and its bindings, an ES + module with its TypeScript declarations and no dependencies, for a page or for Node. `build.sh` builds it with the + paths remapped and refuses a local path left in what it ships, and packs it. The package keeps its own version line, + as the Python package does. +- **CI:** `wai-quantum-js.yml` builds and packs it, holds the tarball to the package's allow-list, runs + `test/smoke.mjs` on it unpacked (a job run with a fresh key and its receipt verified, a forged result refused, a + program linted and drawn, a refusal with its code) and the README's Node example on it installed by name. + `wai-quantum-js-release.yml` does the same on a `wai-quantum-cli-v*` tag and attaches the tarball and its SHA-256 + to the release. Nothing is published to a registry from CI. + ### wai-quantum-cli 0.2.2: the registry's line shown as text This release changes no code. 0.2.1 put the `mcp-name` line in an HTML comment, but the MCP Registry reads the diff --git a/wai/wai-quantum-js/README.md b/wai/wai-quantum-js/README.md new file mode 100644 index 000000000..59f246c35 --- /dev/null +++ b/wai/wai-quantum-js/README.md @@ -0,0 +1,87 @@ +# @transaction-science/wai-quantum + +The WAI quantum job service's engine in WebAssembly, for a page or for Node. A job is planned, run and sealed here exactly as the service plans, runs and seals it. The answer is the service's JSON, signed with a key you hold, and any job's receipt, the service's included, is checked here. Programs are linted, drawn and critiqued before they run. + +This is version 0.1.0 of the package. It carries the engine of `wai-quantum` 0.7.0, built from `wai-web` with its `quantum_engine` feature. The package keeps its own version line, as the Python package does. The module also holds the rest of `wai-web`'s browser surface (reading WAI envelopes among it); this page is about the quantum engine. + +## Install + +```sh +npm install @transaction-science/wai-quantum +``` + +It is an ES module with its TypeScript declarations, and has no dependencies. + +## In a page + +`init()` fetches `wai_quantum_bg.wasm` from beside the module. With a bundler that does not serve it there, pass its URL: `init({ module_or_path: url })`. + +```js +import init, { QuantumEngine, engine_verify } from '@transaction-science/wai-quantum'; + +await init(); +const engine = new QuantumEngine(crypto.getRandomValues(new Uint8Array(32)), 256); +const job = JSON.parse(engine.run(JSON.stringify({ + qasm: 'OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;', + shots: 1000, +}))); +console.log(job.method, job.result.counts); +console.log(JSON.parse(engine_verify(JSON.stringify(job))).valid); // true +``` + +## In Node + +Node is given the WebAssembly's bytes: + +```js +import init, { QuantumEngine, engine_verify } from '@transaction-science/wai-quantum'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { randomBytes } from 'node:crypto'; + +const wasm = createRequire(import.meta.url).resolve('@transaction-science/wai-quantum/wai_quantum_bg.wasm'); +await init({ module_or_path: await readFile(wasm) }); +const engine = new QuantumEngine(randomBytes(32), 256); +const job = JSON.parse(engine.run(JSON.stringify({ qasm: 'OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c = measure q;', shots: 100 }))); +console.log(job.result.counts, JSON.parse(engine_verify(JSON.stringify(job))).signer); +``` + +## What it offers + +Requests and answers are JSON strings, the job service's own. A call that cannot be answered returns `{"error": …}`; a program the engine refuses also gives its diagnostic `code` (such as `Q310`) and its `meaning`. + +**`new QuantumEngine(seed, memory_mb)`**: an engine that signs with the Ed25519 key whose 32-byte seed is `seed`, and admits a job of at most `memory_mb` MiB. Keep the seed to keep the key. + +- `signer_did()`: the key the receipts name, as a `did:key`. +- `run(request)`: a job run and sealed. The answer holds `status`, `method`, `why`, `request` and `result`, with the result's `receipt`. The request is the service's (`qasm`, `shots`, `seed`, `method`, `observables`, `amplitudes`, `noise`, `intent`, `verdict`, `target`, `compile`, `mitigate` and more). +- `plan(request)`: the method a job would run on and why, its memory and work, without running it. +- `compile(request)`: a program compiled (`{"qasm", "epsilon", "level", "basis"}`), with a certificate of every pass, sealed. +- `estimate(request)`: what a fault-tolerant machine would need to run a program, each estimate sealed. +- `qec(op, args)`: error correction (`code`, `decode` or `threshold`), the answer sealed. +- `methods()`: each method, its accuracy, and the most qubits it holds here. + +**Functions:** + +- `engine_verify(job)`: whether a job's receipt seals its request and result: `{"valid", "key", "signer"}`. Any change to either after sealing makes it false. +- `engine_verdict(job, intent, memory_mb)`: a job's verdict against its intent (or the one given). +- `engine_estimate_verify(answer)`: an estimate's seal, its receipts, and its request re-run to the same figures. +- `engine_lint(src)`: a program read as the engine reads it: `{"ok": true, "qubits", "gates", …}`, or `{"ok": false, "error", "line", "code"}`. +- `engine_draw(src, width)`: the circuit as text (`{"text"}`); `engine_layout(src)` gives it as data for a page to draw. +- `engine_critique(src)`: what the engine can say about a program before it runs: its measures and its smells, each with its lines. +- `engine_equivalent(a, b)`: whether two programs apply the same gates up to a global phase, and how that was decided. +- `engine_repair(src, options)`: a program repaired on the lines its diagnostics flag. +- `engine_shortcuts(src, thorough)`: every exact classical route to simulating a program, with its cost. +- `engine_diagnostic_codes()`: every diagnostic code the reader uses, with its meaning. +- `engine_course(locale)`, and the course's lessons, checks and hints: the circuits course, checked by the engine. + +The declarations (`wai_quantum.d.ts`) list every export. + +A run in a page or in Node does not meter its energy, and its result says so. + +## Elsewhere + +The same engine runs as the command line (`wai-quantum`), as the Python package `wai-quantum`, and as the job service, and a job's receipt verifies in each. The source, its schemas and the versioning policy are at under `wai/`; Quantum Studio, an editor built on this package, is at . + +## Licence + +Apache-2.0. diff --git a/wai/wai-quantum-js/build.sh b/wai/wai-quantum-js/build.sh new file mode 100755 index 000000000..6ae287f94 --- /dev/null +++ b/wai/wai-quantum-js/build.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Build the npm package @transaction-science/wai-quantum: the quantum job service's +# engine for WebAssembly (wai/wai-web with its quantum_engine feature), its JavaScript +# bindings, and this folder's package.json and README, packed as one tarball: +# +# bash wai/wai-quantum-js/build.sh OUT_DIR +# +# Writes OUT_DIR/package/ (the package's files) and OUT_DIR/-.tgz, and +# prints the tarball's SHA-256. It needs Rust with the wasm32-unknown-unknown target, +# wasm-bindgen 0.2.127 (the version wai-web's lock file pins) and npm. Paths are +# remapped out of the binary, and the build is refused if a local path is left in the +# files it ships. +set -euo pipefail +if [ $# -ne 1 ]; then + echo "usage: bash wai/wai-quantum-js/build.sh OUT_DIR" >&2 + exit 2 +fi +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/../.." && pwd) +web="$root/wai/wai-web" +mkdir -p "$1" +out=$(cd "$1" && pwd) +sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1"; else shasum -a 256 "$1"; fi; } + +want=$(sed -n '/^name = "wasm-bindgen"$/{n;s/^version = "\(.*\)"$/\1/p;}' "$web/Cargo.lock") +have=$(wasm-bindgen --version | cut -d' ' -f2) +[ "$have" = "$want" ] || { echo "build.sh: wasm-bindgen is $have; wai-web's lock file pins $want" >&2; exit 1; } + +target=${CARGO_TARGET_DIR:-"$web/target"} +sysroot=$(cd "$web" && rustc --print sysroot) +commit=$(cd "$web" && rustc -vV | sed -n 's/^commit-hash: //p') +export RUSTFLAGS="--remap-path-prefix=$root=/open-standards --remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$sysroot/lib/rustlib/src/rust=/rustc/$commit" +(cd "$web" && cargo build --locked --release --target wasm32-unknown-unknown --features quantum_engine) + +pkg="$out/package" +rm -rf "$pkg" +mkdir -p "$pkg" +wasm-bindgen --target web --out-dir "$pkg" --out-name wai_quantum "$target/wasm32-unknown-unknown/release/wai_web.wasm" +cp "$here/package.json" "$here/README.md" "$pkg/" +cp "$root/wai/LICENSE" "$pkg/LICENSE" + +# Nothing the package ships names the machine that built it. +for leak in "$HOME" /Users/ /home/ /Volumes/ /private/; do + if grep -r -q -a -F "$leak" "$pkg"/wai_quantum*; then + echo "build.sh: '$leak' is in the package's bindings or binary" >&2 + exit 1 + fi +done + +rm -f "$out"/*.tgz +npm pack "$pkg" --pack-destination "$out" --silent > /dev/null +tgz=$(ls "$out"/*.tgz) +echo "wasm: $(wc -c < "$pkg/wai_quantum_bg.wasm" | tr -d ' ') bytes" +sha256 "$tgz" diff --git a/wai/wai-quantum-js/ci/check.sh b/wai/wai-quantum-js/ci/check.sh new file mode 100755 index 000000000..5f9f586a7 --- /dev/null +++ b/wai/wai-quantum-js/ci/check.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# The npm package, built and checked as an installer would meet it: +# +# bash wai/wai-quantum-js/ci/check.sh OUT_DIR +# +# 1. build.sh builds and packs it into OUT_DIR; +# 2. the tarball, unpacked, holds the package's allow-list and nothing else; +# 3. test/smoke.mjs runs against the unpacked package; +# 4. the tarball is installed into an empty app (npm install , nothing else to +# fetch), and test/install.mjs, the README's Node example, runs there by name. +# Leaves the tarball in OUT_DIR, with OUT_DIR/.sha256. +set -euo pipefail +here=$(cd "$(dirname "$0")/.." && pwd) +mkdir -p "$1" +out=$(cd "$1" && pwd) +bash "$here/build.sh" "$out" +tgz=$(ls "$out"/*.tgz) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +mkdir "$work/unpacked" +tar -xzf "$tgz" -C "$work/unpacked" +have=$(cd "$work/unpacked/package" && find . -type f | sed 's|^\./||' | sort) +want=$(printf '%s\n' LICENSE README.md package.json wai_quantum.d.ts wai_quantum.js wai_quantum_bg.wasm wai_quantum_bg.wasm.d.ts | sort) +if [ "$have" != "$want" ]; then + echo "check.sh: the tarball holds other files than the package's allow-list:" >&2 + diff <(echo "$want") <(echo "$have") >&2 || true + exit 1 +fi +echo "the tarball: $(echo "$have" | wc -l | tr -d ' ') files, the allow-list exactly" +node "$here/test/smoke.mjs" "$work/unpacked/package" + +mkdir "$work/app" +printf '{ "name": "app", "private": true, "type": "module" }\n' > "$work/app/package.json" +(cd "$work/app" && npm install "$tgz" --no-audit --no-fund --silent) +cp "$here/test/install.mjs" "$work/app/" +(cd "$work/app" && node install.mjs) + +(cd "$out" && n=$(basename "$tgz") && { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$n"; else shasum -a 256 "$n"; fi; } > "$n.sha256" && cat "$n.sha256") diff --git a/wai/wai-quantum-js/package.json b/wai/wai-quantum-js/package.json new file mode 100644 index 000000000..b0122b645 --- /dev/null +++ b/wai/wai-quantum-js/package.json @@ -0,0 +1,40 @@ +{ + "name": "@transaction-science/wai-quantum", + "version": "0.1.0", + "description": "The WAI quantum engine in WebAssembly: OpenQASM programs planned, run and signed in a page or in Node as the WAI quantum job service runs them, receipts checked, programs linted and drawn.", + "license": "Apache-2.0", + "type": "module", + "main": "./wai_quantum.js", + "module": "./wai_quantum.js", + "types": "./wai_quantum.d.ts", + "exports": { + ".": { + "types": "./wai_quantum.d.ts", + "default": "./wai_quantum.js" + }, + "./wai_quantum_bg.wasm": "./wai_quantum_bg.wasm", + "./package.json": "./package.json" + }, + "files": [ + "wai_quantum.js", + "wai_quantum.d.ts", + "wai_quantum_bg.wasm", + "wai_quantum_bg.wasm.d.ts", + "README.md", + "LICENSE" + ], + "sideEffects": false, + "homepage": "https://wai.transaction.science/quantum-studio/", + "repository": { + "type": "git", + "url": "git+https://git.transaction.science/Transaction-Science/open-standards.git", + "directory": "wai/wai-quantum-js" + }, + "keywords": ["quantum", "openqasm", "webassembly", "simulator", "receipts"], + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/wai/wai-quantum-js/test/install.mjs b/wai/wai-quantum-js/test/install.mjs new file mode 100644 index 000000000..e69a7e8dc --- /dev/null +++ b/wai/wai-quantum-js/test/install.mjs @@ -0,0 +1,20 @@ +// The README's Node example, run from an app that installed the tarball +// (npm install ): the package found by its name, its WebAssembly by the +// subpath it exports, a job run and its receipt verified. +// +// cd APP && node install.mjs +import init, { QuantumEngine, engine_verify } from '@transaction-science/wai-quantum'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { randomBytes } from 'node:crypto'; +import assert from 'node:assert/strict'; + +const wasm = createRequire(import.meta.url).resolve('@transaction-science/wai-quantum/wai_quantum_bg.wasm'); +await init({ module_or_path: await readFile(wasm) }); +const engine = new QuantumEngine(randomBytes(32), 256); +const job = JSON.parse(engine.run(JSON.stringify({ qasm: 'OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c = measure q;', shots: 100 }))); +const v = JSON.parse(engine_verify(JSON.stringify(job))); +assert.equal(v.valid, true, JSON.stringify(v)); +assert.equal(v.signer, engine.signer_did()); +assert.equal(Object.values(job.result.counts).reduce((a, b) => a + b, 0), 100, JSON.stringify(job.result.counts)); +console.log('installed by name: a job run and its receipt verified', JSON.stringify(job.result.counts)); diff --git a/wai/wai-quantum-js/test/smoke.mjs b/wai/wai-quantum-js/test/smoke.mjs new file mode 100644 index 000000000..d2bc908b3 --- /dev/null +++ b/wai/wai-quantum-js/test/smoke.mjs @@ -0,0 +1,56 @@ +// The package as an installer leaves it: its bindings and its WebAssembly, loaded in +// Node with nothing else installed. +// +// node wai/wai-quantum-js/test/smoke.mjs PACKAGE_DIR +// +// Prints one line per check and exits non-zero on the first that fails. +import assert from 'node:assert/strict'; +import { randomBytes } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const dir = resolve(process.argv[2] ?? '.'); +const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8')); +const wq = await import(pathToFileURL(join(dir, pkg.main)).href); +// In Node the module is given its WebAssembly's bytes; a page lets it fetch them. +await wq.default({ module_or_path: await readFile(join(dir, 'wai_quantum_bg.wasm')) }); +console.log('node', process.versions.node, pkg.name, pkg.version); + +const BELL = 'OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;'; + +// A job run with a fresh key, and its receipt checked. +const engine = new wq.QuantumEngine(randomBytes(32), 256); +const signer = engine.signer_did(); +assert.match(signer, /^did:key:z6Mk/, signer); +const job = JSON.parse(engine.run(JSON.stringify({ qasm: BELL, shots: 1000, seed: 7 }))); +assert.equal(job.status, 'done', JSON.stringify(job).slice(0, 400)); +const counts = job.result.counts; +assert.ok(Object.keys(counts).length > 0 && Object.keys(counts).every((k) => k === '00' || k === '11'), JSON.stringify(counts)); +const v = JSON.parse(wq.engine_verify(JSON.stringify(job))); +assert.equal(v.valid, true, JSON.stringify(v)); +assert.equal(v.signer, signer, JSON.stringify(v)); +console.log(`run and verify: ${job.method}, ${Object.values(counts).reduce((a, b) => a + b, 0)} shots, signed by this engine's key`); + +// A result changed after it was sealed does not verify. +const forged = structuredClone(job); +const k = Object.keys(forged.result.counts)[0]; +forged.result.counts[k] += 1; +assert.notEqual(JSON.parse(wq.engine_verify(JSON.stringify(forged))).valid, true); +console.log('a forged result does not verify'); + +// A program read and drawn. +const lint = JSON.parse(wq.engine_lint(BELL)); +assert.equal(lint.ok, true, JSON.stringify(lint)); +assert.equal(lint.qubits, 2, JSON.stringify(lint)); +const drawn = JSON.parse(wq.engine_draw(BELL, 80)); +assert.ok(typeof drawn.text === 'string' && drawn.text.trim().length > 0, JSON.stringify(drawn)); +console.log(`lint and draw: ${lint.qubits} qubits, ${lint.gates} gates; drawn in ${drawn.text.trimEnd().split('\n').length} lines`); + +// A program the engine refuses says why, with its diagnostic code. +const WHILE = 'OPENQASM 3.0; qubit[1] q; bit[1] c; c[0] = measure q[0]; while (c[0]) { x q[0]; c[0] = measure q[0]; }'; +const refused = JSON.parse(engine.run(JSON.stringify({ qasm: WHILE, shots: 10 }))); +assert.ok(typeof refused.error === 'string' && /^Q\d{3}$/.test(refused.code ?? ''), JSON.stringify(refused)); +console.log(`a refused program says why: [${refused.code}] ${refused.meaning}`); + +console.log('smoke: every check passed'); -- 2.54.0 (Apple Git-157)