//! 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 ` and `wai quantum ` 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 = ["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::>(), ["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, 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::>().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 { serde_json::from_str::(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::(&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 = ["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::>()}]}); 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::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 `.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. #[cfg(unix)] fn given(path: &Path) -> Option> { std::fs::read_to_string(format!("{}.args", path.display())).ok().map(|t| t.lines().map(String::from).collect()) } #[cfg(unix)] fn v(s: &[&str]) -> Vec { 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::(&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::(&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)); }