23 KiB
Sub-plan 5 — Quality & CI (Tasks 10–14)
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
Goal: Integration tests (JSON-RPC roundtrip), property-based tests, Criterion benchmarks, Qiskit cross-validation golden files, and GitHub Actions CI + release workflows.
Starting state (after sub-plan 4):
- Full working MCP server binary
- All tool handlers implemented and unit-tested
cargo testpasses,cargo buildsucceeds
Deliverable: cargo test passes all test suites. cargo bench compiles. CI YAML is valid. Five commits added.
Important — lib target: Tasks 11–12 need use quantum_bridge_mcp::... from tests/. This requires adding src/lib.rs and a [lib] entry in Cargo.toml. Task 11 includes these steps.
Main plan reference: docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md Tasks 10–14.
Task 10: Integration Tests — MCP protocol roundtrip
Files:
-
Create:
tests/integration/mod.rs -
Create:
tests/integration/mcp_protocol.rs -
Step 1: Create test directory
mkdir -p tests/integration
- Step 2: Create tests/integration/mod.rs
// Integration test module — empty file for cargo test discovery.
- Step 3: Create tests/integration/mcp_protocol.rs
//! Roundtrip: spawn binary, send JSON-RPC on stdin, assert stdout.
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, Command, Stdio};
struct McpProcess { child: Child }
impl McpProcess {
fn spawn() -> Self {
let child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp"))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn binary");
Self { child }
}
fn send(&mut self, msg: &str) {
let stdin = self.child.stdin.as_mut().unwrap();
writeln!(stdin, "{}", msg).unwrap();
stdin.flush().unwrap();
}
fn recv_line(&mut self) -> String {
let stdout = self.child.stdout.as_mut().unwrap();
let mut reader = BufReader::new(stdout);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
line.trim().to_string()
}
}
impl Drop for McpProcess {
fn drop(&mut self) { let _ = self.child.kill(); }
}
fn initialize(proc: &mut McpProcess) -> serde_json::Value {
proc.send(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#);
serde_json::from_str(&proc.recv_line()).expect("valid JSON")
}
#[test]
fn initialize_returns_server_info() {
let mut proc = McpProcess::spawn();
let resp = initialize(&mut proc);
assert!(resp["result"]["serverInfo"]["name"].as_str().is_some());
assert_eq!(resp["result"]["protocolVersion"], "2024-11-05");
}
#[test]
fn tools_list_contains_three_tools() {
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let tools = resp["result"]["tools"].as_array().unwrap();
assert_eq!(tools.len(), 3);
let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
assert!(names.contains(&"list_backends"));
assert!(names.contains(&"validate_circuit"));
assert!(names.contains(&"run_circuit"));
}
#[test]
fn call_list_backends_returns_local_simulator() {
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_backends","arguments":{}}}"#);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(payload["backends"][0]["name"], "local_simulator");
}
#[test]
fn call_validate_circuit_with_valid_bell_returns_is_valid_true() {
let bell = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nbit[2] c;\\nh q[0];\\ncx q[0], q[1];\\nc = measure q;";
let call = format!(
r#"{{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{{"name":"validate_circuit","arguments":{{"circuit":"{}"}}}}}}"#,
bell
);
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(&call);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(payload["is_valid"], true);
}
#[test]
fn call_run_circuit_x_gate_returns_100_ones() {
let x_circ = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[1] q;\\nbit[1] c;\\nx q[0];\\nc = measure q;";
let call = format!(
r#"{{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{{"name":"run_circuit","arguments":{{"circuit":"{}","shots":100}}}}}}"#,
x_circ
);
let mut proc = McpProcess::spawn();
initialize(&mut proc);
proc.send(&call);
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(payload["counts"]["1"].as_u64().unwrap(), 100);
}
Note: Uses
env!("CARGO_BIN_EXE_quantum-bridge-mcp")— cargo sets this automatically. Run withcargo test --test integration. If rmcp uses length-prefixed framing instead of newlines, adjustrecv_lineto skip the header bytes.
- Step 4: Run integration tests
cargo test --test integration 2>&1
Expected: all 5 pass.
- Step 5: Commit
git add tests/integration/
git commit -m "test: add MCP protocol integration tests (initialize, tools/list, tools/call)"
Task 11: Property-Based Tests + lib target
Files:
-
Create:
src/lib.rs -
Modify:
Cargo.toml(add[lib]section) -
Modify:
src/main.rs(remove mod declarations now in lib.rs) -
Create:
tests/proptest/mod.rs -
Create:
tests/proptest/invariants.rs -
Step 1: Create src/lib.rs
pub mod error;
pub mod executor;
pub mod types;
pub mod validator;
pub(crate) mod tools;
- Step 2: Add [lib] to Cargo.toml
[lib]
name = "quantum_bridge_mcp"
path = "src/lib.rs"
- Step 3: Update src/main.rs — remove mod declarations, use crate name
use anyhow::Result;
use quantum_bridge_mcp::tools::QuantumBridgeServer;
use rmcp::{ServiceExt, transport::stdio};
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_ansi(false)
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()),
)
.init();
tracing::info!("quantum-bridge-mcp starting");
let service = QuantumBridgeServer::new().serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
- Step 4: Verify cargo test still passes
cargo test 2>&1
Expected: all existing tests still pass.
- Step 5: Create proptest files
mkdir -p tests/proptest
tests/proptest/mod.rs:
// Property-based test module.
tests/proptest/invariants.rs:
//! Quantum mechanics invariants that must hold for any valid circuit.
use proptest::prelude::*;
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator};
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
proptest! {
/// Shot counts always sum to exactly the requested number of shots.
#[test]
fn shot_counts_always_sum_to_requested_shots(shots in 1u32..=1_000u32) {
let circuit = r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;"#;
let sim = LocalSimulator::new();
let result = sim.run(&CircuitSource(circuit.to_string()), ShotCount(shots), false).unwrap();
let total: u64 = result.counts.values().sum();
prop_assert_eq!(total, shots as u64);
}
/// Statevector norm is always 1.0 (±1e-9).
#[test]
fn statevector_norm_is_one(angle in -std::f64::consts::PI..=std::f64::consts::PI) {
let circuit = format!(
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx({angle}) q[0];\nc = measure q;"
);
let sim = LocalSimulator::new();
let result = sim.run(&CircuitSource(circuit), ShotCount(1), true).unwrap();
let sv = result.statevector.unwrap();
let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum();
prop_assert!((norm - 1.0).abs() < 1e-9, "norm={norm}");
}
/// Bitstrings in counts have length equal to the number of qubits.
#[test]
fn count_bitstrings_have_correct_length(
n_qubits in 1usize..=5usize,
shots in 10u32..=100u32,
) {
let gates: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect();
let circuit = format!(
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{gates}c = measure q;\n"
);
let sim = LocalSimulator::new();
let result = sim.run(&CircuitSource(circuit), ShotCount(shots), false).unwrap();
for key in result.counts.keys() {
prop_assert_eq!(key.len(), n_qubits, "wrong length for key '{key}'");
}
}
}
- Step 6: Run proptest
cargo test --test proptest 2>&1
Expected: 3 property tests pass (each runs 256 cases).
- Step 7: Commit
git add src/lib.rs src/main.rs Cargo.toml tests/proptest/
git commit -m "test: add property-based tests for shot normalisation, statevector norm, and bitstring length"
Task 12: Benchmarks
Files:
-
Create:
benches/simulation.rs -
Step 1: Create bench file
mkdir -p benches
benches/simulation.rs:
use criterion::{criterion_group, criterion_main, Criterion};
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator};
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
const BELL: &str = r#"OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;"#;
fn make_h_circuit(n: usize, shots: u32) -> (CircuitSource, ShotCount) {
let gates: String = (0..n).map(|i| format!("h q[{i}];\n")).collect();
(
CircuitSource(format!(
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{n}] c;\n{gates}c = measure q;\n"
)),
ShotCount(shots),
)
}
fn bench_bell_1024_shots(c: &mut Criterion) {
let sim = LocalSimulator::new();
c.bench_function("bell_1024_shots", |b| {
b.iter(|| sim.run(&CircuitSource(BELL.to_string()), ShotCount(1024), false).unwrap())
});
}
fn bench_10_qubits_10k_shots(c: &mut Criterion) {
let sim = LocalSimulator::new();
let (circ, shots) = make_h_circuit(10, 10_000);
c.bench_function("10_qubits_10k_shots", |b| {
b.iter(|| sim.run(&circ, shots, false).unwrap())
});
}
fn bench_20_qubits_10k_shots(c: &mut Criterion) {
let sim = LocalSimulator::new();
let (circ, shots) = make_h_circuit(20, 10_000);
c.bench_function("20_qubits_10k_shots", |b| {
b.iter(|| sim.run(&circ, shots, false).unwrap())
});
}
criterion_group!(benches, bench_bell_1024_shots, bench_10_qubits_10k_shots, bench_20_qubits_10k_shots);
criterion_main!(benches);
- Step 2: Run benchmarks (smoke pass — verifies they compile and run)
cargo bench -- --test 2>&1
Expected: all three benchmark functions report a timing without panicking.
- Step 3: Run full benchmarks to check SLAs
cargo bench 2>&1 | grep -E "bell|qubits"
SLA targets (spec §6):
bell_1024_shots< 5 ms10_qubits_10k_shots< 50 ms20_qubits_10k_shots< 500 ms
If any target is missed, the bottleneck is likely extract_gate_ops allocating on every call. Check if the AST is being re-parsed unnecessarily — consider caching or pre-converting on first call.
- Step 4: Commit
git add benches/simulation.rs
git commit -m "bench: add Criterion benchmarks for Bell, 10-qubit, and 20-qubit circuits"
Task 13: Reference Golden Files + Cross-Validation
Files:
-
Create:
tests/reference/bell.qasm -
Create:
tests/reference/ghz3.qasm -
Create:
tests/reference/invalid/(4 × .qasm + .error.txt pairs) -
Create:
scripts/gen_reference.py -
Create:
tests/reference/cross_validate.rs -
Step 1: Create directory structure and circuit files
mkdir -p tests/reference/invalid scripts
tests/reference/bell.qasm:
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c = measure q;
tests/reference/ghz3.qasm:
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[0];
cx q[0], q[1];
cx q[1], q[2];
c = measure q;
tests/reference/invalid/unsupported_gate.qasm:
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
u3(0.1, 0.2, 0.3) q[0];
tests/reference/invalid/unsupported_gate.error.txt:
gate 'u3' is not supported
tests/reference/invalid/undeclared_qubit.qasm:
OPENQASM 3.0;
include "stdgates.inc";
qubit[1] q;
h q[5];
tests/reference/invalid/undeclared_qubit.error.txt:
out of range
tests/reference/invalid/syntax_error.qasm:
OPENQASM 3.0;
this is not valid;
tests/reference/invalid/syntax_error.error.txt:
error
tests/reference/invalid/qubit_limit.qasm:
OPENQASM 3.0;
qubit[29] q;
tests/reference/invalid/qubit_limit.error.txt:
exceeds
- Step 2: Write scripts/gen_reference.py
#!/usr/bin/env python3
"""Generate reference counts/statevectors via Qiskit Aer. Requires: pip install qiskit qiskit-aer"""
import json, sys
from pathlib import Path
try:
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
except ImportError:
print("ERROR: pip install qiskit qiskit-aer", file=sys.stderr); sys.exit(1)
SHOTS = 10_000
SEED = 42
REF_DIR = Path(__file__).parent.parent / "tests" / "reference"
def run_circuit(qasm_path: Path) -> dict:
qc = QuantumCircuit.from_qasm_file(str(qasm_path))
sim = AerSimulator(method="statevector")
counts = sim.run(qc, shots=SHOTS, seed_simulator=SEED).result().get_counts()
qc_sv = QuantumCircuit.from_qasm_file(str(qasm_path))
qc_sv.save_statevector()
sv = list(sim.run(qc_sv, shots=0).result().get_statevector())
return {
"shots": SHOTS, "seed": SEED,
"counts": {k: int(v) for k, v in counts.items()},
"statevector": [[float(a.real), float(a.imag)] for a in sv],
}
if __name__ == "__main__":
for qasm in REF_DIR.glob("*.qasm"):
out = REF_DIR / qasm.with_suffix(".json").name
data = run_circuit(qasm)
out.write_text(json.dumps(data, indent=2))
print(f"wrote {out}")
- Step 3: Write tests/reference/cross_validate.rs
//! Cross-validates LocalSimulator counts against Qiskit golden files (chi-squared, α=0.01).
use std::collections::HashMap;
use std::path::Path;
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator, MAX_LOCAL_QUBITS};
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
use quantum_bridge_mcp::validator::CircuitValidator;
fn load_circuit(name: &str) -> CircuitSource {
CircuitSource(std::fs::read_to_string(Path::new("tests/reference").join(name)).unwrap())
}
fn load_expected_counts(name: &str) -> HashMap<String, u64> {
let path = Path::new("tests/reference").join(name);
if !path.exists() { return HashMap::new(); }
let data: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
data["counts"].as_object().unwrap().iter()
.map(|(k, v)| (k.clone(), v.as_u64().unwrap()))
.collect()
}
fn chi_squared_passes(observed: &HashMap<String, u64>, expected: &HashMap<String, u64>) -> bool {
if expected.is_empty() { return true; }
let chi_sq: f64 = expected.iter().map(|(k, &exp)| {
let obs = *observed.get(k).unwrap_or(&0) as f64;
let e = exp as f64;
if e == 0.0 { 0.0 } else { (obs - e).powi(2) / e }
}).sum();
let critical = (expected.len() - 1) as f64 * 3.0 + 6.63; // conservative
chi_sq < critical
}
#[test]
fn bell_counts_match_qiskit_golden() {
let expected = load_expected_counts("bell_counts.json");
if expected.is_empty() { return; }
let result = LocalSimulator::new().run(&load_circuit("bell.qasm"), ShotCount(10_000), false).unwrap();
assert!(chi_squared_passes(&result.counts, &expected),
"Bell diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected);
}
#[test]
fn ghz3_counts_match_qiskit_golden() {
let expected = load_expected_counts("ghz3_counts.json");
if expected.is_empty() { return; }
let result = LocalSimulator::new().run(&load_circuit("ghz3.qasm"), ShotCount(10_000), false).unwrap();
assert!(chi_squared_passes(&result.counts, &expected),
"GHZ-3 diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected);
}
#[test]
fn invalid_circuits_produce_validation_errors() {
use std::fs;
let validator = CircuitValidator::new(MAX_LOCAL_QUBITS);
for entry in fs::read_dir("tests/reference/invalid").unwrap() {
let path = entry.unwrap().path();
if path.extension().map(|e| e == "qasm").unwrap_or(false) {
let expected_frag = fs::read_to_string(path.with_extension("error.txt"))
.unwrap().trim().to_lowercase();
let source = fs::read_to_string(&path).unwrap();
let result = validator.validate(&quantum_bridge_mcp::types::CircuitSource(source)).unwrap();
assert!(!result.is_valid, "{} should be invalid", path.display());
let msgs: String = result.diagnostics.iter().map(|d| d.message.to_lowercase()).collect::<Vec<_>>().join(" ");
assert!(msgs.contains(&expected_frag),
"expected '{}' in messages for {}\nGot: {}", expected_frag, path.display(), msgs);
}
}
}
- Step 4: Run cross-validation tests
cargo test --test cross_validate 2>&1
Expected: bell_counts_match_qiskit_golden and ghz3_counts_match_qiskit_golden are skipped (no JSON yet); invalid_circuits_produce_validation_errors passes.
Optional — generate golden files (needs Python + Qiskit):
python3 scripts/gen_reference.py && cargo test --test cross_validate
- Step 5: Commit
git add tests/reference/ scripts/gen_reference.py
git commit -m "test: add cross-validation golden files and gen_reference.py script"
Task 14: GitHub Actions CI + Release
Files:
-
Create:
.github/workflows/ci.yml -
Create:
.github/workflows/release.yml -
Step 1: Create .github/workflows/ci.yml
mkdir -p .github/workflows
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
rust:
name: Rust checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- run: cargo fmt --check
- run: cargo clippy -- -D warnings
- run: cargo test
- run: cargo bench -- --test
cross-validate:
name: Cross-validate with Qiskit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-qiskit
- run: pip install qiskit qiskit-aer
- run: python3 scripts/gen_reference.py
- run: cargo test --test cross_validate
- Step 2: Create .github/workflows/release.yml
name: Release
on:
push:
tags:
- "v*"
jobs:
dist:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { os: ubuntu-latest, target: aarch64-unknown-linux-gnu }
- { os: macos-latest, target: x86_64-apple-darwin }
- { os: macos-latest, target: aarch64-apple-darwin }
- { os: windows-latest, target: x86_64-pc-windows-msvc }
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross (aarch64 Linux)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: cargo install cross
- name: Build
shell: bash
run: |
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
cross build --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
- name: Package (Unix)
if: matrix.os != 'windows-latest'
run: |
cd target/${{ matrix.target }}/release
tar czf quantum-bridge-mcp-${{ matrix.target }}.tar.gz quantum-bridge-mcp
mv quantum-bridge-mcp-${{ matrix.target }}.tar.gz ../../../
- name: Package (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
cd target/${{ matrix.target }}/release
Compress-Archive quantum-bridge-mcp.exe quantum-bridge-mcp-${{ matrix.target }}.zip
mv quantum-bridge-mcp-${{ matrix.target }}.zip ../../../
- uses: actions/upload-artifact@v4
with:
name: quantum-bridge-mcp-${{ matrix.target }}
path: quantum-bridge-mcp-${{ matrix.target }}.*
github-release:
needs: dist
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
merge-multiple: true
- uses: softprops/action-gh-release@v2
with:
files: "quantum-bridge-mcp-*"
generate_release_notes: true
- Step 3: Validate YAML
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo "ci.yml OK"
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo "release.yml OK"
- Step 4: Full local verification pass
cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -10
Expected: all green.
- Step 5: Commit
git add .github/
git commit -m "ci: add GitHub Actions for Rust checks, Qiskit cross-validation, and multi-platform release"
Final verification — all sub-plans complete
cargo fmt --check \
&& cargo clippy -- -D warnings \
&& cargo test \
&& cargo test --test integration \
&& cargo test --test proptest \
&& cargo test --test cross_validate \
&& cargo bench -- --test \
&& echo "ALL CHECKS PASSED"
Expected: ALL CHECKS PASSED. V1 implementation complete.