open-standards/wai/wai-quantum-service/tests/tasks.rs
Transaction Science 037fa0a878
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m43s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 6s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 8s
WAI energy receipts / does this change touch it (pull_request) Successful in 6s
wai-quantum command line / does this change touch it (pull_request) Successful in 8s
wai-quantum docs, course and locales / does this change touch it (pull_request) Successful in 5s
wai-quantum contract / does this change touch it (pull_request) Successful in 13s
wai-quantum editors / does this change touch it (pull_request) Successful in 8s
wai-quantum service / does this change touch it (pull_request) Successful in 6s
wai-quantum for Python / does this change touch it (pull_request) Successful in 9s
wai-quantum views in agent hosts / does this change touch it (pull_request) Successful in 8s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 14s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 39s
wai-quantum service / service energy · linux packages (pull_request) Successful in 1m45s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 9s
wai-quantum command line / command line · macos (pull_request) Successful in 2m25s
wai-quantum command line / command line · linux (pull_request) Successful in 2m58s
wai-quantum contract / the policy is linked, and the deprecations page reads (pull_request) Successful in 4s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m32s
wai-quantum editors / editors · macos (pull_request) Successful in 2m22s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m39s
wai-quantum editors / editors · linux (pull_request) Successful in 2m13s
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 6m32s
wai-quantum contract / API compatibility · linux (pull_request) Successful in 5m19s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 8m56s
wai-quantum service / service · linux (pull_request) Successful in 4m51s
wai-quantum views in agent hosts / views · a scripted host (pull_request) Successful in 3m6s
wai-quantum views in agent hosts / views · the page and its engine (pull_request) Successful in 4m3s
wai-quantum service / service · macos (pull_request) Successful in 4m34s
wai-quantum for Python / python · linux (pull_request) Successful in 5m11s
wai-quantum for Python / python · macos (pull_request) Successful in 4m41s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m57s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 11m50s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 13m17s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 6m20s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 6m41s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 15m8s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 8m34s
wai-quantum contract / schemas and the committed tool lists · linux (pull_request) Successful in 13m25s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 43m58s
wai-quantum-service: the runs are MCP tasks, each task its job
A client that speaks MCP 2025-11-25 may call run_quantum_circuit,
run_quantum_set and run_on_target as tasks. initialize declares `tasks`
(cancel, requests.tools.call) from that version on, and the three declare
execution.taskSupport "optional". The task is the job: tools/call with
`task` admits it as the call does and answers at once, its taskId the
job's id; a program that does not read makes no task, the refusal in the
error's data. tasks/get gives the job as a task (working, completed,
cancelled, failed for a job that failed or was interrupted), its ttl the
service's retention. tasks/result gives get_quantum_job's answer for the
ended job, naming the task in _meta; asked early by a client that takes
text/event-stream it is a stream kept open until the job ends, since the
proxies end a reply held silent, and any other client is held 50 s and
told to poll. tasks/cancel cancels the job and refuses one that has ended.

There is no tasks/list: the public service binds a task to no one, its
id (128 random bits) being what reads it; where the service takes API
keys a task is its key's alone. tools/call's answer moves into
tool_answer, shared with tasks/result, and the run admission into
admit_run and run_refusal, shared with the task.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 12:11:24 -05:00

109 lines
6 KiB
Rust

//! A run as an MCP task, against the service itself: made at once, followed
//! by tasks/get to its end, and its result from tasks/result, its receipt
//! verifying; and tasks/result asked while the task still works, answered as
//! an event stream once it ends.
use serde_json::{Value, json};
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
}
/// One HTTP/1.1 exchange: the status, the headers and the body.
fn http(port: u16, method: &str, path: &str, body: Option<&Value>, accept: &str) -> (u16, String, String) {
let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
s.set_read_timeout(Some(Duration::from_secs(180))).unwrap();
let text = body.map(Value::to_string).unwrap_or_default();
write!(s, "{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nAccept: {accept}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{text}", text.len()).unwrap();
let mut out = String::new();
s.read_to_string(&mut out).unwrap();
let status = out.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0);
let (head, body) = out.split_once("\r\n\r\n").map(|(h, b)| (h.to_string(), b.to_string())).unwrap_or_default();
(status, head, body)
}
fn rpc(port: u16, method: &str, params: Value) -> Value {
let (code, _, body) = http(port, "POST", "/mcp", Some(&json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params })), "application/json, text/event-stream");
assert_eq!(code, 200, "{method}: {body}");
serde_json::from_str(&body).unwrap_or_else(|e| panic!("{method}: {e}: {body}"))
}
fn start(dir: &Path, port: u16) -> Child {
let child = Command::new(env!("CARGO_BIN_EXE_wai-quantum-service"))
.arg("serve")
.env("WAI_QSVC_ADDR", format!("127.0.0.1:{port}"))
.env("WAI_QSVC_STORE", dir.join("store"))
.env("WAI_QSVC_KEY", dir.join("signing.key"))
.env("WAI_QSVC_JOB_GB", "2")
.env("WAI_QSVC_BUDGET_GB", "4")
.env("WAI_QSVC_WORKERS", "1")
.env("WAI_QSVC_ENERGY", "0")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("the service starts");
let t = Instant::now();
while TcpStream::connect(("127.0.0.1", port)).is_err() || http(port, "GET", "/v1/health", None, "application/json").0 != 200 {
assert!(t.elapsed() < Duration::from_secs(60), "the service did not answer");
std::thread::sleep(Duration::from_millis(50));
}
child
}
#[test]
fn a_run_as_a_task_is_followed_to_its_end_and_its_result_streams_when_asked_early() {
let dir = std::env::temp_dir().join(format!("wai-qsvc-tasks-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let port = free_port();
let mut svc = start(&dir, port);
// Made at once, followed by tasks/get to its end.
let bell = json!({ "qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", "shots": 200, "seed": 3 });
let made = rpc(port, "tools/call", json!({ "name": "run_quantum_circuit", "arguments": bell, "task": { "ttl": 60000 } }));
let id = made["result"]["task"]["taskId"].as_str().unwrap_or_else(|| panic!("{made}")).to_string();
assert_eq!(made["result"]["task"]["status"], "working", "{made}");
let t = Instant::now();
let ended = loop {
let got = rpc(port, "tasks/get", json!({ "taskId": id }));
if got["result"]["status"] != "working" {
break got;
}
assert!(t.elapsed() < Duration::from_secs(120), "never ended: {got}");
std::thread::sleep(Duration::from_millis(50));
};
assert_eq!(ended["result"]["status"], "completed", "{ended}");
let r = rpc(port, "tasks/result", json!({ "taskId": id }));
let sc = &r["result"]["structuredContent"];
assert_eq!((sc["id"].as_str(), sc["status"].as_str(), r["result"]["isError"].as_bool()), (Some(id.as_str()), Some("done"), Some(false)), "{r}");
assert_eq!(r["result"]["_meta"]["io.modelcontextprotocol/related-task"]["taskId"], id.as_str());
assert!(wai_quantum::engine::job::verify(&json!({ "request": sc["request"], "result": sc["result"] })).unwrap().0, "{sc}");
assert_eq!(sc["result"]["counts"].as_object().unwrap().values().filter_map(Value::as_u64).sum::<u64>(), 200);
// Asked for its result while it works, the answer is an event stream
// that ends with the result.
// Asked a few milliseconds after it is made, it is still queued or running.
let mut long = String::from("OPENQASM 3.0;\nqubit[16] q;\nh q;\n");
for k in 0..1000 {
long += &format!("rx(0.{}) q[{}];\ncx q[{}], q[{}];\n", k % 9 + 1, k % 16, k % 16, (k + 1) % 16);
}
let made = rpc(port, "tools/call", json!({ "name": "run_quantum_circuit", "arguments": { "qasm": long, "observables": ["Z0"], "method": "statevector" }, "task": {} }));
let id = made["result"]["task"]["taskId"].as_str().unwrap_or_else(|| panic!("{made}")).to_string();
let (code, head, body) = http(port, "POST", "/mcp", Some(&json!({ "jsonrpc": "2.0", "id": 9, "method": "tasks/result", "params": { "taskId": id } })), "application/json, text/event-stream");
assert_eq!(code, 200, "{body}");
assert!(head.to_ascii_lowercase().contains("content-type: text/event-stream"), "{head}");
let data: String = body.lines().filter_map(|l| l.strip_prefix("data:")).map(str::trim_start).collect();
let answer: Value = serde_json::from_str(&data).unwrap_or_else(|e| panic!("{e}: {body}"));
assert_eq!((answer["id"].as_u64(), answer["result"]["structuredContent"]["status"].as_str()), (Some(9), Some("done")), "{answer}");
assert_eq!(answer["result"]["_meta"]["io.modelcontextprotocol/related-task"]["taskId"], id.as_str());
assert_eq!(rpc(port, "tasks/get", json!({ "taskId": id }))["result"]["status"], "completed");
svc.kill().unwrap();
svc.wait().unwrap();
let _ = std::fs::remove_dir_all(&dir);
}