Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 21s
wai-fleet service / does this change touch it (push) Successful in 17s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 26s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 28s
wai-app and Create / wai-app · does this change touch it (push) Successful in 22s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 50s
wai-quantum editors / does this change touch it (push) Successful in 31s
wai-quantum for JavaScript / does this change touch it (push) Successful in 29s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 40s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m23s
wai-quantum studio and desktop / does this change touch it (push) Successful in 30s
wai-fleet service / service · macos (push) Successful in 59s
wai-quantum for Python / does this change touch it (push) Successful in 31s
wai-quantum docs, course and locales / does this change touch it (push) Successful in 1m27s
wai-quantum views in agent hosts / does this change touch it (push) Successful in 21s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 2m4s
wai-key-proxy / does this change touch it (push) Successful in 1m55s
WAI operation catalogue / does this change touch it (push) Successful in 1m56s
wai-quantum command line / does this change touch it (push) Successful in 1m40s
wai-ledger service / does this change touch it (push) Successful in 1m50s
wai-quantum contract / does this change touch it (push) Successful in 1m41s
WAI Studio / does this change touch it (push) Successful in 12s
wai-sink-swift native sink package / does this change touch it (push) Successful in 25s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m43s
wai-sink-android native sink library / does this change touch it (push) Successful in 42s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 2m45s
wai-quantum editors / editors · macos (push) Successful in 2m2s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m21s
wai-fleet service / service · linux (push) Successful in 3m36s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Successful in 12s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 52s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m37s
wai-quantum for Python / python · macos (push) Successful in 2m57s
wai-ledger service / service · macos (push) Successful in 1m16s
wai-quantum editors / editors · linux (push) Successful in 3m46s
wai-audit collector and player integration / collector · linux (push) Successful in 3m39s
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Successful in 3m23s
wai-quantum for Python / python · linux (push) Successful in 4m26s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 6m38s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m7s
wai-quantum command line / command line · macos (push) Successful in 2m33s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m35s
wai-key-proxy / proxy · linux (push) Successful in 1m12s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m3s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 7m40s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 7m39s
wai-ledger service / service · linux (push) Successful in 2m5s
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Successful in 7s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m36s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 8m36s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m10s
wai-quantum views in agent hosts / views · a scripted host (push) Successful in 4m26s
wai-quantum views in agent hosts / views · the page and its engine (push) Successful in 6m33s
wai-quantum contract / API compatibility · linux (push) Successful in 4m44s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 13m4s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m46s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 14m11s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 14m12s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 13m18s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 10m49s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 11m36s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 12m10s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m36s
wai-quantum contract / schemas and the committed tool lists · linux (push) Successful in 9m58s
wai-quantum studio and desktop / studio · browser (push) Successful in 16m35s
WAI Studio / Studio in WebKit · linux (push) Successful in 12m22s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Successful in 22m0s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 24m31s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 28m51s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 28m59s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 29m41s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 11m58s
wai-quantum command line / command line · linux (push) Successful in 48m17s
Folds, each the program it folds run as itself, so what it prints and its exit status are the program's for the same arguments: wai audit report|check|verify (audit.create, audit.check, audit.verify) is wai-audit, --owner its WAI_AUDIT_OWNER; wai serve <role> [--config F] [--check] (service.start) starts wai-<role>: relay, room, ingest and key-release from a file, audit, fleet, ledger and quantum-service from their environment; wai quantum run|plan|lint|verify|methods (quantum.run, .inspect, .check, .verify, .list) is wai-quantum with that verb, and no other verb of it is reached. A program is its flag, WAI_<NAME>_BIN, beside wai, or on PATH; one not there is exit 5, program-not-found. report is not an N3 verb, so wai audit report is the operation audit.create. --check on the daemons: wai-relay, wai-room (its keys too) and wai-ingest --config F --check, and wai-key-release serve F --check, read the configuration as each starts from it and start nothing, exit 1 with the reason when it does not read. The proxy is installed as wai-key-release; wai-key-proxy is the same program under its earlier name. wai cmcd encode|decode|verify is wai_telemetry's tool compiled in (cmcd.rs, one module behind both; telemetry joins full). wai receipt create <file>... --from <input>... [--key] signs the operation receipt a toolchain write leaves, for files made elsewhere. wai verify reads every record by its kind: by name, and a JSON record by what it holds (a delivery receipt or claim as receipt_verify answers it, a room session, an audit bundle as wai-audit verify does with --owner/--channel, a sealed playlist by its mr/<blake3>.json as check-playlist does); a file of no kind is exit 2, unknown-record. wai mcp --toolset quantum starts wai-quantum mcp and answers each quantum_* tool with that server's own answer and schemas. wai media score build|render alias wai score. The quantum operations' SDK names are registered labels. Tests, each fold against what it folds: cli_folds (cmcd over the whole corpus, receipt create against wrap's receipt, verify on the claim, JWP, room and audit corpora and a sealed playlist, audit|serve|quantum against stand-ins); wai-audit tests/fold.rs, wai-quantum-cli tests/wai_fold.rs and joule-moq-room tests/cli_serve.rs against the real programs; cli_mcp's quantum toolset; the daemons' --check (config_check.rs, check.rs). ops.json, mcp.json, the registry export and Studio's actions regenerated. Co-Authored-By: Claude Opus 5.5 <[email protected]>
538 lines
31 KiB
Rust
538 lines
31 KiB
Rust
//! The folds through the `wai` binary (`--features quickstart,telemetry --test cli_folds`, and with
|
|
//! `video_manifest` the sealed playlist too): each folded command against the tool it folds, on the
|
|
//! same input.
|
|
//!
|
|
//! - `wai cmcd encode|decode|verify` prints what `wai_telemetry` prints, byte for byte, with its
|
|
//! exit status, over the whole corpus `telemetry-conformance/` and a copy of it broken;
|
|
//! - `wai receipt create` writes the receipt a toolchain write leaves: the same lineage, step for
|
|
//! step, as `wai wrap`'s for the same input and output, signed by `--key` where it is given, and
|
|
//! `wai verify` holds it to its files;
|
|
//! - `wai verify` reads each record by its kind: a claim and a delivery receipt as `wai mcp`'s
|
|
//! `receipt_verify` answers them (each vector of the claim and JWP corpora), a room session and
|
|
//! an audit bundle as their corpora state, a sealed playlist as `wai http check-playlist` does;
|
|
//! a file that is no record is refused, never read as another kind;
|
|
//! - `wai audit report|check|verify`, `wai serve <role>` and `wai quantum <verb>` hand their program the
|
|
//! arguments given, every flag among them, and return its output and exit status: here against
|
|
//! stand-ins that record what they were given (the programs themselves: wai-audit's
|
|
//! tests/fold.rs, joule-moq-room's tests/cli_serve.rs, wai-quantum-cli's tests/wai_fold.rs); a
|
|
//! program that is not there is exit 5, and `wai quantum` reaches no verb the quantum toolset
|
|
//! does not list.
|
|
#![cfg(all(feature = "quickstart", feature = "telemetry"))]
|
|
|
|
use serde_json::{json, Value};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output, Stdio};
|
|
|
|
// A program a test runs is written by a process of its own (ETXTBSY: support/program.rs).
|
|
#[path = "support/program.rs"]
|
|
mod program;
|
|
|
|
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_folds").join(name);
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
std::fs::create_dir_all(&d).unwrap();
|
|
d
|
|
}
|
|
|
|
/// `bin args` in `dir`, with these variables set, the person's configuration never read.
|
|
fn run(bin: &Path, dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output {
|
|
let mut c = Command::new(bin);
|
|
c.current_dir(dir).env("WAI_CONFIG_DIR", dir.join(".wai-config")).env_remove("WAI_AUDIT_OWNER").args(args).stdin(Stdio::null());
|
|
for (k, v) in envs {
|
|
c.env(k, v);
|
|
}
|
|
c.output().expect("it runs")
|
|
}
|
|
|
|
fn wai(dir: &Path, args: &[&str]) -> Output {
|
|
run(Path::new(env!("CARGO_BIN_EXE_wai")), dir, args, &[])
|
|
}
|
|
|
|
fn text(o: &Output) -> String {
|
|
format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
|
|
}
|
|
|
|
/// That two runs are one: the same standard output, standard error and exit status.
|
|
fn same(a: &Output, b: &Output, what: &str) {
|
|
assert_eq!(a.status.code(), b.status.code(), "{what}: the exit status\n{}\n---\n{}", text(a), text(b));
|
|
assert_eq!(String::from_utf8_lossy(&a.stdout), String::from_utf8_lossy(&b.stdout), "{what}: standard output");
|
|
assert_eq!(String::from_utf8_lossy(&a.stderr), String::from_utf8_lossy(&b.stderr), "{what}: standard error");
|
|
}
|
|
|
|
// ─────────────────────────────── wai cmcd ───────────────────────────────
|
|
|
|
fn telemetry(dir: &Path, args: &[&str]) -> Output {
|
|
run(Path::new(env!("CARGO_BIN_EXE_wai_telemetry")), dir, args, &[])
|
|
}
|
|
|
|
#[test]
|
|
fn cmcd_prints_what_the_corpus_tool_prints() {
|
|
let d = scratch("cmcd");
|
|
let corpus = wai_dir().join("telemetry-conformance");
|
|
let mut n = 0;
|
|
let mut dirs: Vec<PathBuf> = ["observations", "invalid"].iter().flat_map(|s| std::fs::read_dir(corpus.join(s)).unwrap().flatten().map(|e| e.path())).collect();
|
|
dirs.sort();
|
|
for f in &dirs {
|
|
let p = f.to_str().unwrap();
|
|
same(&wai(&d, &["cmcd", "encode", p]), &telemetry(&d, &["encode", p]), p);
|
|
n += 1;
|
|
}
|
|
assert!(n > 30, "{n} observations, valid and invalid");
|
|
// Every report the corpus states, read back.
|
|
let expected: Value = serde_json::from_slice(&std::fs::read(corpus.join("expected/cmcd.json")).unwrap()).unwrap();
|
|
for (name, e) in expected.as_object().unwrap() {
|
|
if let Some(r) = e["report"].as_str() {
|
|
same(&wai(&d, &["cmcd", "decode", r]), &telemetry(&d, &["decode", r]), name);
|
|
}
|
|
}
|
|
same(&wai(&d, &["cmcd", "decode", "v=2,sid=\"unterminated"]), &telemetry(&d, &["decode", "v=2,sid=\"unterminated"]), "a dictionary that does not read");
|
|
// The corpus verified, and a copy with one expected file changed refused by both alike.
|
|
let c = corpus.to_str().unwrap();
|
|
let ok = wai(&d, &["cmcd", "verify", c]);
|
|
assert!(ok.status.success(), "{}", text(&ok));
|
|
same(&ok, &telemetry(&d, &["verify", c]), "the corpus");
|
|
let broken = d.join("broken");
|
|
copy_dir(&corpus, &broken);
|
|
let f = broken.join("expected/cmcd.json");
|
|
let t = std::fs::read_to_string(&f).unwrap().replacen("\"event\"", "\"none\"", 1);
|
|
std::fs::write(&f, t).unwrap();
|
|
let b = broken.to_str().unwrap();
|
|
let bad = wai(&d, &["cmcd", "verify", b]);
|
|
assert_eq!(bad.status.code(), Some(1), "{}", text(&bad));
|
|
same(&bad, &telemetry(&d, &["verify", b]), "a broken copy");
|
|
// With --json, the envelope carries the tool's own JSON.
|
|
let obs = corpus.join("observations/01-request-base.json");
|
|
let o = wai(&d, &["cmcd", "encode", obs.to_str().unwrap(), "--json"]);
|
|
let v: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("{e}: {}", text(&o)));
|
|
let want: Value = serde_json::from_slice(&telemetry(&d, &["encode", obs.to_str().unwrap()]).stdout).unwrap();
|
|
assert_eq!((v["ok"].as_bool(), &v["data"]), (Some(true), &want), "{v}");
|
|
}
|
|
|
|
fn copy_dir(from: &Path, to: &Path) {
|
|
std::fs::create_dir_all(to).unwrap();
|
|
for e in std::fs::read_dir(from).unwrap().flatten() {
|
|
let p = e.path();
|
|
if p.is_dir() {
|
|
copy_dir(&p, &to.join(e.file_name()));
|
|
} else {
|
|
std::fs::copy(&p, to.join(e.file_name())).unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────── wai receipt create ───────────────────────────────
|
|
|
|
/// A receipt's steps, as (op, input, output, work).
|
|
fn steps(receipt: &Path) -> Vec<(String, Value, Value, Value)> {
|
|
let v: Value = serde_json::from_slice(&std::fs::read(receipt).unwrap_or_else(|e| panic!("{}: {e}", receipt.display()))).unwrap();
|
|
v["steps"].as_array().unwrap().iter().map(|s| (s["op"].as_str().unwrap().to_string(), s["input_hash"].clone(), s["output_hash"].clone(), s["work"].clone())).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn receipt_create_writes_the_lineage_a_toolchain_write_leaves() {
|
|
let d = scratch("receipt-create");
|
|
std::fs::write(d.join("in.txt"), b"a payload made elsewhere").unwrap();
|
|
assert!(wai(&d, &["wrap", "wai.text.zstd", "in.txt", "-o", "w.wai"]).status.success());
|
|
let wrap = steps(&d.join("w.wai.wai-receipt.json"));
|
|
// The same input and output, its receipt by this door: the lineage is wrap's, step for step,
|
|
// the write named by this operation.
|
|
std::fs::rename(d.join("w.wai.wai-receipt.json"), d.join("wrap-receipt.json")).unwrap();
|
|
std::fs::write(d.join("k.hex"), "11".repeat(32)).unwrap();
|
|
let o = wai(&d, &["receipt", "create", "w.wai", "--from", "in.txt", "--key", "k.hex"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let made = steps(&d.join("w.wai.wai-receipt.json"));
|
|
assert_eq!(made.len(), wrap.len(), "{made:?}\n{wrap:?}");
|
|
for (m, w) in made.iter().zip(&wrap) {
|
|
assert_eq!((&m.1, &m.2, &m.3), (&w.1, &w.2, &w.3), "{m:?} and {w:?}: the same hashes and work");
|
|
assert_eq!(m.0, w.0.replace("envelope.wrap", "receipt.create"), "{m:?}");
|
|
}
|
|
assert_eq!(made.iter().map(|s| s.0.as_str()).collect::<Vec<_>>(), ["read in.txt", "receipt.create w.wai"], "--from is in the lineage");
|
|
// Signed by the key --key names, which a run's own key is not.
|
|
let r: Value = serde_json::from_slice(&std::fs::read(d.join("w.wai.wai-receipt.json")).unwrap()).unwrap();
|
|
let k = ed25519_dalek::SigningKey::from_bytes(&[0x11; 32]);
|
|
assert_eq!(r["signer_id"], wai::signer_id::did_key(&k.verifying_key().to_bytes()), "--key signs it");
|
|
assert!(text(&o).contains("receipt → w.wai.wai-receipt.json, signed by did:key:"), "{}", text(&o));
|
|
// wai verify reads it, and refuses it once the file has changed.
|
|
let v = wai(&d, &["verify", "w.wai.wai-receipt.json"]);
|
|
assert!(v.status.success(), "{}", text(&v));
|
|
std::fs::write(d.join("w.wai"), b"changed").unwrap();
|
|
let v = wai(&d, &["verify", "w.wai.wai-receipt.json"]);
|
|
assert_eq!(v.status.code(), Some(1), "{}", text(&v));
|
|
// A file that is not there, and no file at all.
|
|
assert_eq!(wai(&d, &["receipt", "create", "gone.bin"]).status.code(), Some(5));
|
|
assert_eq!(wai(&d, &["receipt", "create", "--from", "in.txt"]).status.code(), Some(2));
|
|
assert_eq!(wai(&d, &["receipt", "create", "in.txt", "--frm", "x"]).status.code(), Some(2));
|
|
}
|
|
|
|
// ─────────────────────────────── wai verify, by kind ───────────────────────────────
|
|
|
|
fn corpus(rel: &str) -> Value {
|
|
serde_json::from_slice(&std::fs::read(wai_dir().join(rel)).unwrap()).unwrap()
|
|
}
|
|
|
|
/// `wai verify` on one file: its exit, and the JSON it printed under the file's line (null where
|
|
/// it printed none).
|
|
fn verified(d: &Path, file: &Path, extra: &[&str]) -> (Option<i32>, Value) {
|
|
let mut a = vec!["verify", file.to_str().unwrap()];
|
|
a.extend_from_slice(extra);
|
|
let o = wai(d, &a);
|
|
let t = String::from_utf8_lossy(&o.stdout).into_owned();
|
|
let body: String = t.lines().skip(1).take_while(|l| !l.starts_with("wai verify:")).map(|l| l.strip_prefix(" ").unwrap_or(l)).collect::<Vec<_>>().join("\n");
|
|
(o.status.code(), serde_json::from_str(&body).unwrap_or(Value::Null))
|
|
}
|
|
|
|
/// A record's text, as a corpus gives it: its wire text, or its JSON.
|
|
fn wire(v: &Value) -> String {
|
|
match v {
|
|
Value::String(t) => t.clone(),
|
|
other => other.to_string(),
|
|
}
|
|
}
|
|
|
|
/// The `kind` a record's text names, where it is a JSON object that names one.
|
|
fn named_kind(text: &str) -> Option<String> {
|
|
serde_json::from_str::<Value>(text).ok()?.get("kind")?.as_str().map(String::from)
|
|
}
|
|
|
|
#[test]
|
|
fn verify_reads_each_record_by_its_kind() {
|
|
let d = scratch("verify-kinds");
|
|
// Claims, alone: what receipt_verify answers, and the corpus states. A vector whose text names
|
|
// another kind is read as that kind, and one that names none is no record.
|
|
let doc = corpus("claim-conformance/vectors.json");
|
|
let (mut n, mut other, mut none) = (0, 0, 0);
|
|
for v in doc["vectors"].as_array().unwrap().iter().filter(|v| v["procedure"] == "claim" && v.get("context").is_none()) {
|
|
let f = d.join(format!("{}.json", v["name"].as_str().unwrap()));
|
|
let t = wire(&v["receipt"]);
|
|
std::fs::write(&f, &t).unwrap();
|
|
let (code, got) = verified(&d, &f, &[]);
|
|
match named_kind(&t).as_deref() {
|
|
Some("claim") => {}
|
|
Some("group" | "object") => {
|
|
assert_eq!((code, got["kind"].as_str()), (Some(1), Some("delivery")), "{}: read as the delivery receipt it names itself: {got}", v["name"]);
|
|
other += 1;
|
|
continue;
|
|
}
|
|
_ => {
|
|
assert_eq!(code, Some(2), "{}: no record, refused", v["name"]);
|
|
none += 1;
|
|
continue;
|
|
}
|
|
}
|
|
assert_eq!(got["kind"], "claim", "{}: a claim is read as a claim: {got}", v["name"]);
|
|
let e = &v["expected"];
|
|
if e["outcome"] == "accept" {
|
|
assert_eq!((code, got["verdict"].as_str()), (Some(0), Some("accept")), "{}: {got}", v["name"]);
|
|
if let Some(h) = e["link_hash"].as_str() {
|
|
assert_eq!(got["link_hash"], h, "{}", v["name"]);
|
|
}
|
|
} else {
|
|
assert_eq!((code, got["verdict"].as_str(), got["reason"].as_str()), (Some(1), Some("refuse"), e["reason"].as_str()), "{}: {got}", v["name"]);
|
|
}
|
|
n += 1;
|
|
}
|
|
assert!(n > 280, "{n} claim vectors ({other} naming a receipt's kind, {none} naming none)");
|
|
// Delivery receipts: each group of the JWP corpus with no previous group, alone.
|
|
let doc = corpus("jwp-conformance/vectors.json");
|
|
let mut groups = 0;
|
|
for v in doc["vectors"].as_array().unwrap().iter().filter(|v| v["procedure"] == "group" && v["input"].get("previous").is_none() && v["input"]["group"]["kind"] == "group") {
|
|
let f = d.join(format!("group-{}.json", v["name"].as_str().unwrap()));
|
|
std::fs::write(&f, v["input"]["group"].to_string()).unwrap();
|
|
let (code, got) = verified(&d, &f, &[]);
|
|
assert_eq!((got["kind"].as_str(), got["receipt"].as_str()), (Some("delivery"), Some("group")), "{}: {got}", v["name"]);
|
|
// The corpus says reject where the verifier's answer says refuse.
|
|
let accept = v["expected"]["outcome"] == "accept";
|
|
assert_eq!(got["verdict"], if accept { "accept" } else { "refuse" }, "{}: {got}", v["name"]);
|
|
if let Some(r) = v["expected"]["reason"].as_str() {
|
|
assert_eq!(got["reason"], r, "{}: {got}", v["name"]);
|
|
}
|
|
assert_eq!(code, Some(if accept { 0 } else { 1 }));
|
|
groups += 1;
|
|
}
|
|
assert!(groups > 5, "{groups} group receipts");
|
|
// Room sessions, as `wai session show` gives one.
|
|
let doc = corpus("live-conformance/rooms/vectors.json");
|
|
for v in doc["vectors"].as_array().unwrap() {
|
|
let f = d.join(format!("room-{}.json", v["name"].as_str().unwrap()));
|
|
std::fs::write(&f, json!({"descriptor": v["descriptor"], "grants": v["grants"], "key_log_head": v["key_log_head"], "chains": v["chains"], "development_only": true}).to_string()).unwrap();
|
|
let (code, got) = verified(&d, &f, &[]);
|
|
assert_eq!((got["kind"].as_str(), &got["outcome"]), (Some("room_session"), &v["outcome"]), "{}: {got}", v["name"]);
|
|
assert_eq!(code, Some(if v["outcome"] == "accept" { 0 } else { 1 }), "{}", v["name"]);
|
|
if v["outcome"] == "refuse" {
|
|
assert_eq!((&got["reason"], &got["at"]), (&v["reason"], &v["at"]), "{}", v["name"]);
|
|
}
|
|
}
|
|
// Audit bundles, for their owner and channels: the corpus's outcome; without the owner, refused.
|
|
let doc = corpus("live-conformance/audit/vectors.json");
|
|
let owner = doc["owner"].as_str().unwrap();
|
|
let mut chans = Vec::new();
|
|
for c in doc["channels"].as_array().unwrap() {
|
|
chans.push("--channel");
|
|
chans.push(c.as_str().unwrap());
|
|
}
|
|
let mut with_owner = vec!["--owner", owner];
|
|
with_owner.extend(&chans);
|
|
let mut bundles = 0;
|
|
for s in doc["sessions"].as_array().unwrap() {
|
|
let f = d.join(format!("audit-{}.json", s["name"].as_str().unwrap()));
|
|
let t = s.get("bundle_text").and_then(Value::as_str).map(String::from).unwrap_or_else(|| wire(&s["bundle"]));
|
|
std::fs::write(&f, &t).unwrap();
|
|
let (code, got) = verified(&d, &f, &with_owner);
|
|
let bundle_like = serde_json::from_str::<Value>(&t).ok().is_some_and(|b| b["v"] == 1 && ["session", "acks", "claims", "log"].iter().all(|m| b.get(*m).is_some()));
|
|
if !bundle_like {
|
|
assert_eq!(code, Some(2), "{}: no record, refused: {got}", s["name"]);
|
|
continue;
|
|
}
|
|
bundles += 1;
|
|
assert_eq!(got["outcome"], s["outcome"]["outcome"], "{}: {got}", s["name"]);
|
|
assert_eq!(code, Some(if s["outcome"]["outcome"] == "accept" { 0 } else { 1 }), "{}", s["name"]);
|
|
if s["outcome"]["outcome"] == "refuse" {
|
|
assert_eq!(got["reason"], s["outcome"]["reason"], "{}", s["name"]);
|
|
}
|
|
}
|
|
assert!(bundles > 40, "{bundles} bundles");
|
|
let first = d.join(format!("audit-{}.json", doc["sessions"][0]["name"].as_str().unwrap()));
|
|
let o = wai(&d, &["verify", first.to_str().unwrap()]);
|
|
assert_eq!(o.status.code(), Some(1), "{}", text(&o));
|
|
assert!(text(&o).contains("FAIL an audit bundle is verified for its owner"), "{}", text(&o));
|
|
// A whole directory: each kind counted.
|
|
let o = wai(&d, &["verify", ".", "--owner", owner, "--channel", doc["channels"][0].as_str().unwrap()]);
|
|
let t = text(&o);
|
|
for k in ["claims", "delivery receipts", "room sessions", "audit bundles"] {
|
|
assert!(t.contains(k), "{k}: {t}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn verify_refuses_a_file_that_is_no_record() {
|
|
let d = scratch("verify-unknown");
|
|
std::fs::write(d.join("letter.json"), r#"{"kind": "letter", "to": "someone"}"#).unwrap();
|
|
std::fs::write(d.join("notes.txt"), "not a record").unwrap();
|
|
std::fs::write(d.join("package.json"), r#"{"name": "a-page"}"#).unwrap();
|
|
for f in ["letter.json", "notes.txt", "package.json"] {
|
|
let o = wai(&d, &["verify", f, "--json"]);
|
|
assert_eq!(o.status.code(), Some(2), "{f}: {}", text(&o));
|
|
let v: Value = serde_json::from_slice(&o.stdout).unwrap();
|
|
assert_eq!(v["error"]["code"], "unknown-record", "{f}: {v}");
|
|
}
|
|
// Under a directory, a JSON file that is no record is passed over, and none found is said.
|
|
let o = wai(&d, &["verify", "."]);
|
|
assert_ne!(o.status.code(), Some(0), "{}", text(&o));
|
|
assert!(text(&o).contains("no record under"), "{}", text(&o));
|
|
}
|
|
|
|
/// A WAI media playlist sealed beside its receipt (`wai http seal-playlist`): `wai verify` finds its
|
|
/// receipt by the playlist's BLAKE3 and prints what `wai http check-playlist` prints with it.
|
|
#[cfg(feature = "video_manifest")]
|
|
#[test]
|
|
fn verify_checks_a_sealed_playlist_as_check_playlist_does() {
|
|
let d = scratch("playlist");
|
|
let c = wai_dir().join("http-conformance");
|
|
let listing: Value = serde_json::from_slice(&std::fs::read(c.join("objects.json")).unwrap()).unwrap();
|
|
let objs: Vec<String> = ["O9a", "O9b", "O9c"].iter().map(|id| c.join(listing[id]["path"].as_str().unwrap()).to_str().unwrap().to_string()).collect();
|
|
std::fs::write(d.join("classical.m3u8"), "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:1\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:0.640,\nc0.m4s\n#EXTINF:0.640,\nc1.m4s\n#EXTINF:0.640,\nc2.m4s\n#EXT-X-ENDLIST\n").unwrap();
|
|
let list = json!({"uri_prefix": "objects/", "variants": [{"uri": "wai-180.m3u8", "classical": "classical.m3u8", "resolution": [320, 180], "frame_rate_milli": 25000, "segments": objs.iter().map(|o| json!({"object": o})).collect::<Vec<_>>()}]});
|
|
std::fs::write(d.join("list.json"), list.to_string()).unwrap();
|
|
assert!(wai(&d, &["http", "hls", "--objects", "list.json", "--out", "."]).status.success());
|
|
let key = "07".repeat(32);
|
|
let o = wai(&d, &["http", "seal-playlist", "wai-180.m3u8", "--key", &key]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let receipt = format!("mr/{}.json", blake3::hash(&std::fs::read(d.join("wai-180.m3u8")).unwrap()).to_hex());
|
|
let check = wai(&d, &["http", "check-playlist", "wai-180.m3u8", "--receipt", &receipt]);
|
|
assert!(check.status.success(), "{}", text(&check));
|
|
let v = wai(&d, &["verify", "wai-180.m3u8"]);
|
|
assert!(v.status.success(), "{}", text(&v));
|
|
let lines: Vec<String> = String::from_utf8_lossy(&v.stdout).lines().map(String::from).collect();
|
|
assert_eq!(lines[0], "wai-180.m3u8");
|
|
assert_eq!(format!("{}\n", lines[1].strip_prefix(" ").unwrap()), String::from_utf8_lossy(&check.stdout), "check-playlist's line");
|
|
// Under the directory: the sealed playlist, and neither the classical one nor the master,
|
|
// which no receipt seals.
|
|
let all = wai(&d, &["verify", "."]);
|
|
assert!(all.status.success() && text(&all).contains("1 playlist verified"), "{}", text(&all));
|
|
// A playlist changed after its seal has no receipt of its bytes.
|
|
let mut b = std::fs::read(d.join("wai-180.m3u8")).unwrap();
|
|
let at = b.len() - 20;
|
|
b[at] ^= 1;
|
|
std::fs::write(d.join("wai-180.m3u8"), b).unwrap();
|
|
let v = wai(&d, &["verify", "wai-180.m3u8"]);
|
|
assert_eq!(v.status.code(), Some(1), "{}", text(&v));
|
|
}
|
|
|
|
// ─────────────────────────────── the folds of other programs ───────────────────────────────
|
|
|
|
/// A stand-in program at `path`: it writes the arguments it was given, one a line, and the
|
|
/// variables a fold may set, to `<path>.args`, prints `out`, and exits with `code` (or 1 when an
|
|
/// argument holds the word `bad`).
|
|
#[cfg(unix)]
|
|
fn stand_in(path: &Path, out: &str, code: i32) {
|
|
let script = format!(
|
|
"#!/bin/sh\nfor a in \"$@\"; do echo \"$a\"; done > \"$0.args\"\necho \"owner=$WAI_AUDIT_OWNER\" >> \"$0.args\"\nprintf '%s' '{out}'\nfor a in \"$@\"; do case \"$a\" in *bad*) exit 1;; esac; done\nexit {code}\n"
|
|
);
|
|
program::write_program(path, &script);
|
|
}
|
|
|
|
/// What a stand-in was given: its arguments and the owner it saw; `None` when it did not run.
|
|
fn given(path: &Path) -> Option<Vec<String>> {
|
|
std::fs::read_to_string(format!("{}.args", path.display())).ok().map(|t| t.lines().map(String::from).collect())
|
|
}
|
|
|
|
fn v(s: &[&str]) -> Vec<String> {
|
|
s.iter().map(|x| x.to_string()).collect()
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn audit_hands_wai_audit_every_argument_and_returns_its_exit() {
|
|
let d = scratch("audit");
|
|
let stub = d.join("wai-audit");
|
|
stand_in(&stub, "{\"report\": \"as printed\"}\n", 0);
|
|
let s = stub.to_str().unwrap();
|
|
let owner = "ab".repeat(32);
|
|
let o = wai(&d, &["audit", "check", "served.json", "chan", "b1.json", "b2.json", "--owner", &owner, "--audit", s]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
assert_eq!(given(&stub).unwrap(), v(&["check", "served.json", "chan", "b1.json", "b2.json", &format!("owner={owner}")]), "every argument, and --owner as WAI_AUDIT_OWNER");
|
|
assert_eq!(String::from_utf8_lossy(&o.stdout), "{\"report\": \"as printed\"}\n", "its output, as it printed it");
|
|
let o = wai(&d, &["audit", "verify", "b1.json", "chan", "other", "--owner", &owner, "--audit", s]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
assert_eq!(given(&stub).unwrap(), v(&["verify", "b1.json", "chan", "other", &format!("owner={owner}")]), "verify, and --owner as WAI_AUDIT_OWNER");
|
|
// Its exit status: a refusal (1) and any other, as the program gave them.
|
|
let o = wai(&d, &["audit", "check", "served.json", "chan", "bad.json", "--audit", s]);
|
|
assert_eq!(o.status.code(), Some(1), "{}", text(&o));
|
|
assert_eq!(given(&stub).unwrap().last().unwrap(), "owner=", "no --owner, none set");
|
|
stand_in(&stub, "", 2);
|
|
assert_eq!(wai(&d, &["audit", "report", "data", "chan", "1", "2", "--audit", s]).status.code(), Some(2));
|
|
// --json: the program's own JSON in the envelope, and no --json handed to it.
|
|
stand_in(&stub, "{\"report\": 1}", 0);
|
|
let o = wai(&d, &["audit", "report", "data", "chan", "1", "2", "--audit", s, "--json"]);
|
|
let e: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("{e}: {}", text(&o)));
|
|
assert_eq!((e["ok"].as_bool(), &e["data"]), (Some(true), &json!({"report": 1})), "{e}");
|
|
assert_eq!(given(&stub).unwrap(), v(&["report", "data", "chan", "1", "2", "owner="]));
|
|
// The program found as wai finds it, and not found: exit 5.
|
|
let o = run(Path::new(env!("CARGO_BIN_EXE_wai")), &d, &["audit", "report", "data", "chan", "1", "2"], &[("WAI_AUDIT_BIN", s)]);
|
|
assert!(o.status.success(), "$WAI_AUDIT_BIN: {}", text(&o));
|
|
let o = wai(&d, &["audit", "report", "data", "chan", "1", "2", "--audit", d.join("gone").to_str().unwrap(), "--json"]);
|
|
assert_eq!(o.status.code(), Some(5), "{}", text(&o));
|
|
assert_eq!(serde_json::from_slice::<Value>(&o.stdout).unwrap()["error"]["code"], "program-not-found");
|
|
assert_eq!(wai(&d, &["audit", "report", "d", "c", "1", "2", "--owner", &owner, "--audit", s]).status.code(), Some(2), "report takes no owner");
|
|
}
|
|
|
|
/// `wai` alone in a directory of its own, so that nothing beside it is a program it looks for.
|
|
#[cfg(unix)]
|
|
fn lone_wai(d: &Path) -> PathBuf {
|
|
let dir = d.join("bin");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let w = dir.join("wai");
|
|
program::copy_program(Path::new(env!("CARGO_BIN_EXE_wai")), &w);
|
|
w
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn serve_starts_each_role_by_its_program_and_check_starts_nothing() {
|
|
let d = scratch("serve");
|
|
let relay = d.join("wai-relay");
|
|
stand_in(&relay, "checked\n", 0);
|
|
let r = relay.to_str().unwrap();
|
|
std::fs::write(d.join("relay.toml"), "").unwrap();
|
|
// --check is the program's: it is handed the file and --check, and its verdict is the exit.
|
|
let o = wai(&d, &["serve", "relay", "--config", "relay.toml", "--check", "--bin", r]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
assert_eq!(given(&relay).unwrap(), v(&["--config", "relay.toml", "--check", "owner="]), "--check reaches the program");
|
|
assert_eq!(String::from_utf8_lossy(&o.stdout), "checked\n");
|
|
let o = wai(&d, &["serve", "relay", "--config", "bad.toml", "--check", "--bin", r]);
|
|
assert_eq!(o.status.code(), Some(1), "a configuration the program refuses: {}", text(&o));
|
|
// Without --check, the program started from the file.
|
|
let o = wai(&d, &["serve", "relay", "--config", "relay.toml", "--bin", r]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
assert_eq!(given(&relay).unwrap(), v(&["--config", "relay.toml", "owner="]));
|
|
// Each role's words.
|
|
let key = d.join("wai-key-release");
|
|
stand_in(&key, "", 0);
|
|
assert!(wai(&d, &["serve", "key-release", "--config", "proxy.json", "--check", "--bin", key.to_str().unwrap()]).status.success());
|
|
assert_eq!(given(&key).unwrap(), v(&["serve", "proxy.json", "--check", "owner="]));
|
|
let ledger = d.join("wai-ledger");
|
|
stand_in(&ledger, "", 0);
|
|
assert!(wai(&d, &["serve", "ledger", "--bin", ledger.to_str().unwrap()]).status.success());
|
|
assert_eq!(given(&ledger).unwrap(), v(&["serve", "owner="]));
|
|
// An environment's role takes no file, a file's role needs one, and a role must be one.
|
|
std::fs::remove_file(format!("{}.args", ledger.display())).unwrap();
|
|
for bad in [&["serve", "ledger", "--check", "--bin", ledger.to_str().unwrap()][..], &["serve", "ledger", "--config", "x", "--bin", ledger.to_str().unwrap()], &["serve", "relay", "--bin", r], &["serve", "mailer"], &["serve"], &["serve", "relay", "--config", "x", "--frobnicate"]] {
|
|
let o = wai(&d, bad);
|
|
assert_eq!(o.status.code(), Some(2), "{bad:?}: {}", text(&o));
|
|
}
|
|
assert!(given(&ledger).is_none(), "a refused command line runs nothing");
|
|
// A dry run names the program and runs nothing.
|
|
std::fs::remove_file(format!("{}.args", relay.display())).unwrap();
|
|
let o = wai(&d, &["serve", "relay", "--config", "relay.toml", "--bin", r, "--dry-run"]);
|
|
assert!(o.status.success() && text(&o).contains("wai would run"), "{}", text(&o));
|
|
assert!(given(&relay).is_none(), "a dry run starts nothing");
|
|
// A role whose program is not there: exit 5, naming where it was looked for.
|
|
let w = lone_wai(&d);
|
|
let empty = d.join("empty");
|
|
std::fs::create_dir_all(&empty).unwrap();
|
|
let o = run(&w, &d, &["serve", "ingest", "--config", "g.toml", "--check", "--json"], &[("PATH", empty.to_str().unwrap())]);
|
|
assert_eq!(o.status.code(), Some(5), "{}", text(&o));
|
|
let e: Value = serde_json::from_slice(&o.stdout).unwrap();
|
|
assert_eq!(e["error"]["code"], "program-not-found", "{e}");
|
|
assert!(e["error"]["message"].as_str().unwrap().contains("wai-ingest") && e["error"]["hint"].as_str().unwrap().contains("WAI_INGEST_BIN"), "{e}");
|
|
// Found beside wai, and on PATH.
|
|
program::copy_program(&relay, &w.with_file_name("wai-relay"));
|
|
assert!(run(&w, &d, &["serve", "relay", "--config", "relay.toml", "--check"], &[("PATH", empty.to_str().unwrap())]).status.success());
|
|
let o = run(&w, &d, &["serve", "ledger"], &[("PATH", d.to_str().unwrap())]);
|
|
assert!(o.status.success(), "on PATH: {}", text(&o));
|
|
// wai-key-proxy, the earlier name of wai-key-release, is found when the new one is not.
|
|
program::copy_program(&key, &empty.join("wai-key-proxy"));
|
|
assert!(run(&w, &d, &["serve", "key-release", "--config", "p.json", "--check"], &[("PATH", empty.to_str().unwrap())]).status.success());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn quantum_reaches_the_verbs_of_the_quantum_toolset_and_no_other() {
|
|
let d = scratch("quantum");
|
|
let stub = d.join("wai-quantum");
|
|
stand_in(&stub, "as wai-quantum printed it\n", 0);
|
|
let s = stub.to_str().unwrap();
|
|
for (cmd, want) in [
|
|
(&["quantum", "run", "p.qasm", "--shots", "100"][..], &["run", "p.qasm", "--shots", "100"][..]),
|
|
(&["quantum", "plan", "p.qasm"], &["plan", "p.qasm"]),
|
|
(&["quantum", "inspect", "p.qasm"], &["plan", "p.qasm"]),
|
|
(&["quantum", "lint", "p.qasm"], &["lint", "p.qasm"]),
|
|
(&["quantum", "check", "p.qasm"], &["lint", "p.qasm"]),
|
|
(&["quantum", "verify", "r.json"], &["verify", "r.json"]),
|
|
(&["quantum", "methods"], &["methods"]),
|
|
(&["quantum", "list"], &["methods"]),
|
|
] {
|
|
let mut a = cmd.to_vec();
|
|
a.extend(["--quantum", s]);
|
|
let o = wai(&d, &a);
|
|
assert!(o.status.success(), "{cmd:?}: {}", text(&o));
|
|
assert_eq!(String::from_utf8_lossy(&o.stdout), "as wai-quantum printed it\n", "{cmd:?}");
|
|
let mut w = v(want);
|
|
w.push("owner=".into());
|
|
assert_eq!(given(&stub).unwrap(), w, "{cmd:?}");
|
|
}
|
|
// --json is the program's own: it is handed to it, and its JSON is the envelope's data.
|
|
stand_in(&stub, "{\"verdict\": \"proof\"}", 0);
|
|
let o = wai(&d, &["quantum", "lint", "p.qasm", "--quantum", s, "--json"]);
|
|
let e: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("{e}: {}", text(&o)));
|
|
assert_eq!(e["data"], json!({"verdict": "proof"}), "{e}");
|
|
assert_eq!(given(&stub).unwrap(), v(&["lint", "p.qasm", "--json", "owner="]));
|
|
// Its exit status, as it gave it.
|
|
stand_in(&stub, "", 2);
|
|
assert_eq!(wai(&d, &["quantum", "verify", "r.json", "--quantum", s]).status.code(), Some(2));
|
|
// A verb the toolset does not list is no operation: exit 5, and the program never runs.
|
|
std::fs::remove_file(format!("{}.args", stub.display())).unwrap();
|
|
for verb in ["lab", "submit", "providers", "mcp", "estimate"] {
|
|
let o = wai(&d, &["quantum", verb, "x", "--quantum", s, "--json"]);
|
|
assert_eq!(o.status.code(), Some(5), "wai quantum {verb}: {}", text(&o));
|
|
assert_eq!(serde_json::from_slice::<Value>(&o.stdout).unwrap()["error"]["code"], "operation-not-found", "wai quantum {verb}");
|
|
}
|
|
assert!(given(&stub).is_none(), "no verb outside the toolset reaches wai-quantum");
|
|
// Not there: exit 5.
|
|
let o = run(&lone_wai(&d), &d, &["quantum", "methods"], &[("PATH", d.join("bin").to_str().unwrap())]);
|
|
assert_eq!(o.status.code(), Some(5), "{}", text(&o));
|
|
}
|