open-standards/wai/wai-rs/tests/cli_toolchain.rs
Transaction Science f6bfd71ca9
Some checks failed
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m7s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m44s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m46s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 2m41s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 19s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 23s
wai-key-proxy / does this change touch it (push) Successful in 1m54s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m51s
WAI operation catalogue / does this change touch it (push) Successful in 3m25s
wai key · Windows / does this change touch it (push) Successful in 1m58s
wai-fleet service / does this change touch it (push) Successful in 2m22s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m56s
wai-audit collector and player integration / does this change touch it (push) Successful in 3m34s
wai-ledger service / does this change touch it (push) Successful in 1m56s
wai-sink-android native sink library / does this change touch it (push) Successful in 1m28s
wai-sink-swift native sink package / does this change touch it (push) Successful in 1m26s
WAI Studio / does this change touch it (push) Successful in 1m32s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m32s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 5m53s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m50s
wai-fleet service / service · macos (push) Successful in 1m21s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m43s
wai-ledger service / service · macos (push) Successful in 1m48s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m59s
WAI Studio / Studio in Chromium and Firefox · macos (push) Failing after 2m51s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Failing after 14m14s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m29s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 31s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
wai key · Windows / wai key and the toolchain's tests · windows (push) Failing after 1m40s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Failing after 1m51s
wai-key-proxy / proxy · linux (push) Successful in 1m25s
wai-ledger service / service · linux (push) Successful in 2m16s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 3m46s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m14s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 6m33s
wai-fleet service / service · linux (push) Successful in 7m5s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 11m35s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m6s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 5m44s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 7m52s
wai target and wai auth: a named service and a kept key, and one --target on every operation that acts on a service
- wai target add|use|list|remove (target.create, .update, .list, .delete): a name for a service's
  address, kept in the project's wai.json (its new targets member, in the schema) or the person's
  own list, never with a key; --read-only refuses every change (exit 3, target-read-only).
- wai auth login|status|logout (credential.create, .list, .delete): the key read from standard
  input, kept for its origin in the keychain on macOS or a file only its owner reads; WAI_TOKEN
  ahead of it; sent as a bearer over https:// or to this machine alone; never in wai.json, a
  list, output, a plan, a log or an error, a service's echo of it redacted.
- --target is catalogue::TARGET wherever it is taken, on operations placed off this machine: the
  fleet's four take a target's name or an address; an unknown name is exit 5, target-not-found.
- The forty operations of the room service's API answer to their mechanical form, sent to their
  route under the target (--query, --body), a change waiting for --yes, a dry run sending nothing.
- wai mcp --target serves them as tools, changes only to a target on this machine or with
  --allow-write; never with --read-only or to a read-only target.
- wai stage refine --toward, --target its deprecated spelling there (wai/DEPRECATIONS.md).
- tests/cli_target.rs, against a stand-in service; ops.json and Studio's actions regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:39:06 -05:00

261 lines
14 KiB
Rust

//! The toolchain's own surface through the `wai` binary (`--features quickstart --test
//! cli_toolchain`):
//!
//! - `wai --version` and `--json` name one version, the digests of the catalogue and the registry
//! as they are committed, the release of the standard and the build's features; the crate, the
//! export and the changelog's newest release say the same version;
//! - `wai docs search` and `wai registry show` give what `wai mcp`'s tools give, with a refusal's
//! exit code and stable code;
//! - a deprecated flag still works and says so in one line, and `wai/DEPRECATIONS.md` is held to
//! the catalogue, both ways.
#![cfg(feature = "quickstart")]
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use wai::catalogue::{self, OPS};
fn wai_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("..").canonicalize().unwrap()
}
fn scratch(name: &str) -> PathBuf {
let d = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_toolchain").join(name);
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
/// `wai`, with no specification root from its environment.
fn wai(d: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(d).args(args).env_remove("WAI_SPEC_ROOT").stdin(Stdio::null()).output().expect("wai runs")
}
fn text(o: &Output) -> String {
format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
}
/// The envelope of a `--json` run.
fn envelope(d: &Path, args: &[&str]) -> (Option<i32>, Value) {
let mut a = args.to_vec();
a.push("--json");
let o = wai(d, &a);
let v: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("{args:?}: not one envelope ({e}): {}", text(&o)));
assert_eq!(v["wai"], catalogue::ENVELOPE, "{v}");
(o.status.code(), v)
}
fn sha256(b: &[u8]) -> String {
Sha256::digest(b).iter().map(|x| format!("{x:02x}")).collect()
}
#[test]
fn version_names_the_toolchain_its_catalogue_its_registry_and_the_standard() {
let d = scratch("version");
let (code, v) = envelope(&d, &["--version"]);
assert_eq!(code, Some(0), "{v}");
assert_eq!((v["ok"].as_bool(), v["op"].as_str()), (Some(true), Some("toolchain.show")), "{v}");
let data = &v["data"];
let version = env!("CARGO_PKG_VERSION");
assert_eq!(data["toolchain"], version);
assert_eq!(data["specification"]["version"], version);
assert_eq!(data["specification"]["formats"], json!(["WAI1", "WAI2"]));
assert_eq!(data["envelope"], catalogue::ENVELOPE);
assert_eq!(data["catalogue"]["format"], catalogue::FORMAT);
assert_eq!(data["catalogue"]["operations"], OPS.len());
// The digests are of the committed files, byte for byte.
let (ops, reg) = (std::fs::read(wai_dir().join("catalogue/ops.json")).unwrap(), std::fs::read(wai_dir().join("registry/capabilities.json")).unwrap());
assert_eq!(data["catalogue"]["sha256"], sha256(&ops));
assert_eq!(data["registry"]["sha256"], sha256(&reg));
let caps: Value = serde_json::from_slice(&reg).unwrap();
assert_eq!(data["registry"]["capabilities"], caps["capabilities"].as_array().unwrap().len());
// The features named are this build's: each answers on, and quickstart is among them here.
let features: Vec<&str> = data["features"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect();
assert!(features.contains(&"quickstart"), "{features:?}");
assert!(features.iter().all(|f| catalogue::feature_on(f) == Some(true)), "{features:?}");
// Text, for a person; the other spellings run the same operation.
let o = wai(&d, &["--version"]);
assert!(o.status.success(), "{}", text(&o));
let t = String::from_utf8_lossy(&o.stdout).into_owned();
assert_eq!(t.lines().next(), Some(format!("wai {version}").as_str()), "{t}");
assert!(t.contains(&sha256(&ops)) && t.contains(&sha256(&reg)), "{t}");
for spelling in [&["version"][..], &["toolchain", "show"]] {
assert_eq!(wai(&d, spelling).stdout, o.stdout, "{spelling:?}");
}
assert_eq!(wai(&d, &["--version", "extra"]).status.code(), Some(2));
let h = wai(&d, &["--version", "--help"]);
assert!(h.status.success() && String::from_utf8_lossy(&h.stdout).contains("toolchain.show"), "{}", text(&h));
}
/// One version source: what the binary says is the crate's, the export's and the newest release of
/// the changelog (WAI toolchain v1, U1).
#[test]
fn the_crate_the_export_and_the_changelog_name_one_version() {
let version = env!("CARGO_PKG_VERSION");
let ops: Value = serde_json::from_slice(&std::fs::read(wai_dir().join("catalogue/ops.json")).unwrap()).unwrap();
assert_eq!(ops["toolchain"], version, "wai/catalogue/ops.json");
let log = std::fs::read_to_string(wai_dir().join("CHANGELOG.md")).unwrap();
let newest = log.lines().find_map(|l| l.strip_prefix("## v")).expect("a release heading");
let newest = newest.split_whitespace().next().unwrap();
assert_eq!(newest, version, "wai/CHANGELOG.md's newest release is not this crate's version");
}
/// A `wai mcp` server's answer to one tool, as `structuredContent`.
fn tool(args: Value, name: &str, project: &Path) -> Value {
let mut c = Command::new(env!("CARGO_BIN_EXE_wai"))
.args(["mcp", "--spec-root", wai_dir().to_str().unwrap(), "--project", project.to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("wai mcp starts");
let mut stdin = c.stdin.take().unwrap();
let mut out = BufReader::new(c.stdout.take().unwrap());
let init = json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}}});
let call = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": name, "arguments": args}});
writeln!(stdin, "{init}\n{}\n{call}", json!({"jsonrpc": "2.0", "method": "notifications/initialized"})).unwrap();
let mut line = String::new();
out.read_line(&mut line).unwrap();
line.clear();
out.read_line(&mut line).unwrap();
drop(stdin);
let _ = c.wait();
let r: Value = serde_json::from_str(&line).unwrap();
r["result"]["structuredContent"].clone()
}
#[test]
fn docs_search_is_the_tool_wai_mcp_serves() {
let d = scratch("docs");
let root = wai_dir();
let root = root.to_str().unwrap();
let args = ["docs", "search", "signer_id", "origin-bound", "--spec-root", root];
let (code, v) = envelope(&d, &args);
assert_eq!(code, Some(0), "{v}");
assert_eq!(v["op"], "docs.search");
let hits = v["data"]["hits"].as_array().unwrap();
assert!(!hits.is_empty(), "{v}");
for h in hits {
let t = h["text"].as_str().unwrap().to_ascii_lowercase();
assert!(t.contains("signer_id") && t.contains("origin-bound"), "{h}");
assert!(h["file"].is_string() && h["line"].is_u64() && h["section"].is_string(), "{h}");
}
// The door gives what the tool gives.
assert_eq!(v["data"], tool(json!({"query": "signer_id origin-bound"}), "docs_search", &d));
// Text names each hit's file and line.
let o = wai(&d, &args);
let t = String::from_utf8_lossy(&o.stdout).into_owned();
assert!(t.contains(&format!("{}:{}", hits[0]["file"].as_str().unwrap(), hits[0]["line"])), "{t}");
let (_, l) = envelope(&d, &["docs", "search", "capability", "--limit", "2", "--spec-root", root]);
assert_eq!((l["data"]["count"].as_u64(), l["data"]["truncated"].as_bool()), (Some(2), Some(true)), "{l}");
let (code, none) = envelope(&d, &["docs", "search", "zzzqqqzzz", "--spec-root", root]);
assert_eq!((code, none["data"]["count"].as_u64()), (Some(0), Some(0)), "no line holds it, and that is an answer");
// The refusals.
assert_eq!(wai(&d, &["docs", "search"]).status.code(), Some(2));
assert_eq!(wai(&d, &["docs", "search", "x", "--nope"]).status.code(), Some(2));
assert_eq!(wai(&d, &["docs", "search", "x", "--limit", "0"]).status.code(), Some(2));
let (code, e) = envelope(&d, &["docs", "search", "x", "--spec-root", d.to_str().unwrap()]);
assert_eq!((code, e["error"]["code"].as_str()), (Some(5), Some("spec-not-found")), "{e}");
assert!(e["error"]["hint"].as_str().unwrap().contains("--spec-root"), "{e}");
}
#[test]
fn registry_show_is_the_tool_wai_mcp_serves() {
let d = scratch("registry");
let root = wai_dir();
let root = root.to_str().unwrap();
let args = ["registry", "show", "wai.image.png", "--spec-root", root];
let (code, v) = envelope(&d, &args);
assert_eq!(code, Some(0), "{v}");
assert_eq!(v["op"], "registry.show");
assert_eq!(v["data"]["registry"]["capability"], "wai.image.png", "{v}");
assert_eq!(v["data"]["decoded_natively_here"], true, "the floor, in this build: {v}");
assert!(!v["data"]["spec_lines"].as_array().unwrap().is_empty(), "{v}");
assert_eq!(v["data"], tool(json!({"capability": "wai.image.png"}), "registry_show", &d));
let t = String::from_utf8_lossy(&wai(&d, &args).stdout).into_owned();
assert!(t.starts_with("wai.image.png\n") && t.contains("decoded natively by this build: yes") && t.contains("in the specification:"), "{t}");
// Without the specification's text it still answers from the registry, and says so.
let (_, no_text) = envelope(&d, &["registry", "show", "wai.image.png", "--spec-root", d.to_str().unwrap()]);
assert!(no_text["data"]["spec_lines"].is_null(), "{no_text}");
assert_eq!(no_text["data"]["registry"]["capability"], "wai.image.png");
let (code, e) = envelope(&d, &["registry", "show", "wai.image.none", "--spec-root", root]);
assert_eq!((code, e["error"]["code"].as_str()), (Some(5), Some("capability-not-found")), "{e}");
assert_eq!(wai(&d, &["registry", "show"]).status.code(), Some(2));
assert_eq!(wai(&d, &["registry", "show", "a", "b"]).status.code(), Some(2));
// The operations beside it still answer as they did.
assert!(wai(&d, &["registry", "list"]).status.success());
}
#[test]
fn a_deprecated_flag_works_and_says_so_in_one_line() {
let d = scratch("deprecated");
std::fs::write(d.join("p.bin"), b"hello").unwrap();
let notice = "wai: deprecated: --confirm, use --yes instead";
let o = wai(&d, &["wrap", "wai.text.zstd", "p.bin", "--confirm"]);
assert!(o.status.success(), "{}", text(&o));
assert_eq!(String::from_utf8_lossy(&o.stderr).matches(notice).count(), 1, "{}", text(&o));
assert!(!String::from_utf8_lossy(&o.stdout).contains("deprecated"), "standard error alone");
// With --json the envelope stays one document, and the notice is said once.
let o = wai(&d, &["wrap", "wai.text.zstd", "p.bin", "--confirm", "--json"]);
assert!(o.status.success(), "{}", text(&o));
assert_eq!(String::from_utf8_lossy(&o.stderr).matches(notice).count(), 1, "{}", text(&o));
let v: Value = serde_json::from_slice(&o.stdout).unwrap();
assert_eq!(v["ok"], true, "{v}");
let o = wai(&d, &["wrap", "wai.text.zstd", "p.bin", "--yes"]);
assert!(o.status.success() && !text(&o).contains("deprecated"), "{}", text(&o));
}
/// `wai/DEPRECATIONS.md` lists what the catalogue marks deprecated, and nothing it does not, unless
/// the row names its removal; `wai/VERSIONING.md` states the window and links the list.
#[test]
fn the_deprecations_page_is_the_catalogues_list() {
let page = std::fs::read_to_string(wai_dir().join("DEPRECATIONS.md")).unwrap();
// (kind, item, deprecated in, replacement, removed in)
let rows: Vec<Vec<String>> = page
.lines()
.filter(|l| l.starts_with('|'))
.skip(2)
.map(|l| l.trim().trim_matches('|').split('|').map(|c| c.trim().trim_matches('`').to_string()).collect())
.collect();
assert!(rows.iter().all(|r| r.len() == 5), "{rows:?}");
let mut marked: Vec<(String, String, String)> = Vec::new();
for f in catalogue::AGENT_FLAGS {
if let Some((alias, since)) = f.deprecated_alias {
marked.push((alias.to_string(), since.to_string(), f.flag.to_string()));
}
}
for o in OPS {
if let Some(d) = o.deprecated {
marked.push((o.id.to_string(), d.since.to_string(), d.use_instead.to_string()));
}
// A flag of one operation renamed: listed as that operation's command and the flag.
for a in o.input {
if let (Some((was, since)), catalogue::Cli::Flag(f)) = (a.was, a.cli) {
let cmd = o.cli.expect("a renamed flag is a command line's");
marked.push((format!("{cmd} {was}"), since.to_string(), format!("{cmd} {f}")));
}
}
}
for (item, since, instead) in &marked {
let r = rows.iter().find(|r| r[1] == *item).unwrap_or_else(|| panic!("the catalogue marks {item} deprecated, and wai/DEPRECATIONS.md does not list it"));
assert_eq!((&r[2], &r[3]), (since, instead), "{item}");
}
for r in &rows {
assert!(marked.iter().any(|(i, _, _)| *i == r[1]) || !r[4].is_empty(), "wai/DEPRECATIONS.md lists {}, which the catalogue does not mark deprecated and no release removed", r[1]);
}
let v = std::fs::read_to_string(wai_dir().join("VERSIONING.md")).unwrap().split_whitespace().collect::<Vec<_>>().join(" ");
for needle in ["(DEPRECATIONS.md)", "at least two minor releases and six months", "wai --version --json", "wai: deprecated: --confirm, use --yes instead"] {
assert!(v.contains(needle), "wai/VERSIONING.md does not state {needle:?}");
}
assert!(page.contains("(VERSIONING.md)"), "wai/DEPRECATIONS.md links the policy");
}