803 lines
23 KiB
Markdown
803 lines
23 KiB
Markdown
# 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 test` passes, `cargo build` succeeds
|
||
|
||
**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**
|
||
|
||
```bash
|
||
mkdir -p tests/integration
|
||
```
|
||
|
||
- [ ] **Step 2: Create tests/integration/mod.rs**
|
||
|
||
```rust
|
||
// Integration test module — empty file for cargo test discovery.
|
||
```
|
||
|
||
- [ ] **Step 3: Create tests/integration/mcp_protocol.rs**
|
||
|
||
```rust
|
||
//! 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 with `cargo test --test integration`. If rmcp uses length-prefixed framing instead of newlines, adjust `recv_line` to skip the header bytes.
|
||
|
||
- [ ] **Step 4: Run integration tests**
|
||
|
||
```bash
|
||
cargo test --test integration 2>&1
|
||
```
|
||
|
||
Expected: all 5 pass.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```rust
|
||
pub mod error;
|
||
pub mod executor;
|
||
pub mod types;
|
||
pub mod validator;
|
||
pub(crate) mod tools;
|
||
```
|
||
|
||
- [ ] **Step 2: Add [lib] to Cargo.toml**
|
||
|
||
```toml
|
||
[lib]
|
||
name = "quantum_bridge_mcp"
|
||
path = "src/lib.rs"
|
||
```
|
||
|
||
- [ ] **Step 3: Update src/main.rs — remove mod declarations, use crate name**
|
||
|
||
```rust
|
||
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**
|
||
|
||
```bash
|
||
cargo test 2>&1
|
||
```
|
||
|
||
Expected: all existing tests still pass.
|
||
|
||
- [ ] **Step 5: Create proptest files**
|
||
|
||
```bash
|
||
mkdir -p tests/proptest
|
||
```
|
||
|
||
`tests/proptest/mod.rs`:
|
||
```rust
|
||
// Property-based test module.
|
||
```
|
||
|
||
`tests/proptest/invariants.rs`:
|
||
|
||
```rust
|
||
//! 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**
|
||
|
||
```bash
|
||
cargo test --test proptest 2>&1
|
||
```
|
||
|
||
Expected: 3 property tests pass (each runs 256 cases).
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
mkdir -p benches
|
||
```
|
||
|
||
`benches/simulation.rs`:
|
||
|
||
```rust
|
||
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)**
|
||
|
||
```bash
|
||
cargo bench -- --test 2>&1
|
||
```
|
||
|
||
Expected: all three benchmark functions report a timing without panicking.
|
||
|
||
- [ ] **Step 3: Run full benchmarks to check SLAs**
|
||
|
||
```bash
|
||
cargo bench 2>&1 | grep -E "bell|qubits"
|
||
```
|
||
|
||
SLA targets (spec §6):
|
||
- `bell_1024_shots` < **5 ms**
|
||
- `10_qubits_10k_shots` < **50 ms**
|
||
- `20_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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```python
|
||
#!/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**
|
||
|
||
```rust
|
||
//! 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**
|
||
|
||
```bash
|
||
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):
|
||
```bash
|
||
python3 scripts/gen_reference.py && cargo test --test cross_validate
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
mkdir -p .github/workflows
|
||
```
|
||
|
||
```yaml
|
||
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**
|
||
|
||
```yaml
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: all green.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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.
|