//! The operation catalogue through the `wai` binary, as an agent and a person meet it //! (`--features quickstart --test cli_ops`): //! //! - `wai ops list`, `search`, `schema` and `export` read the catalogue, and the export is //! the committed `wai/catalogue/ops.json` (a drifted copy exits 1); //! - the quickstart installer (`wai/quickstart/install.sh`) installs this crate's version, as //! the release job requires of a tag `wai-v`; //! - every operation answers `--help` from its catalogue entry, and its mechanical form //! `wai ` runs it as its CLI spelling does; //! - the exit codes: 1 a check that failed, 2 usage, 3 permission, 4 confirmation required, //! 5 a file, an operation or a build feature not found, 6 a target that did not come up; //! - `--json`: one envelope (`envelope/1`) on stdout, success and failure, with a command's //! own JSON or its text in `data`, and with a confirmation's proposal and command; //! - `--dry-run`: every operation of this build whose effect is not `reads` writes nothing //! with it, and (for those that write) writes without it, so the dry run is of a command //! that would write; //! - `--yes`: replacing a file the command line did not name waits for it; `--confirm` is its //! deprecated spelling, does the same, and help and the export say so. #![cfg(feature = "quickstart")] use serde_json::Value; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; use wai::catalogue::{self, Confirm, Effect, OPS}; // A program a test runs is written by a process of its own (ETXTBSY: support/program.rs). #[path = "support/program.rs"] mod program; /// `wai` run in `dir`, its configuration directory (targets and kept keys) `dir`'s own: the /// person's is never read or touched here. fn wai_in(dir: &Path, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).env("WAI_CONFIG_DIR", dir.join(".wai-config")).env_remove("WAI_TOKEN").args(args).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)) } fn scratch(name: &str) -> PathBuf { let d = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_ops").join(name); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); d } fn wai_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("..") } /// The one JSON envelope a `--json` run prints. fn envelope(o: &Output) -> Value { let v: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("not one JSON envelope ({e}): {}", text(o))); assert_eq!(v["wai"], catalogue::ENVELOPE, "{v}"); v } #[test] fn ops_list_names_every_operation_and_export_is_the_committed_file() { let d = scratch("list"); let o = wai_in(&d, &["ops", "list", "--json"]); assert!(o.status.success(), "{}", text(&o)); let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["op"].as_str()), (Some(true), Some("ops.list"))); let ids: Vec<&str> = v["data"].as_array().unwrap().iter().map(|r| r["id"].as_str().unwrap()).collect(); assert_eq!(ids, OPS.iter().map(|o| o.id).collect::>()); let plain = wai_in(&d, &["ops", "list"]); for op in OPS { assert!(String::from_utf8_lossy(&plain.stdout).contains(op.id), "{}", op.id); } let committed = wai_dir().join("catalogue/ops.json"); let o = wai_in(&d, &["ops", "export", "--check", committed.to_str().unwrap()]); assert!(o.status.success(), "wai/catalogue/ops.json is the catalogue's export: {}", text(&o)); let o = wai_in(&d, &["ops", "export"]); assert_eq!(o.stdout, std::fs::read(&committed).unwrap(), "`wai ops export` prints the committed export"); // A drifted copy is a check that failed. let drifted = d.join("ops.json"); std::fs::write(&drifted, String::from_utf8_lossy(&o.stdout).replacen("envelope.inspect", "envelope.inspekt", 1)).unwrap(); let o = wai_in(&d, &["ops", "export", "--check", drifted.to_str().unwrap()]); assert_eq!(o.status.code(), Some(1), "{}", text(&o)); assert!(text(&o).contains("regenerate"), "{}", text(&o)); } /// The release job's first check (wai-release.yml) refuses a tag whose installer installs /// another version, after the version bump has merged; this holds the bump to it. #[test] fn the_quickstart_installer_installs_this_version() { let version = env!("CARGO_PKG_VERSION"); let install = std::fs::read_to_string(wai_dir().join("quickstart/install.sh")).unwrap(); let line = format!("VERSION=\"${{WAI_VERSION:-{version}}}\""); assert!( install.lines().any(|l| l == line), "wai/quickstart/install.sh does not install {version}, this crate's version: it needs the line {line}" ); } #[test] fn ops_search_and_schema_answer_offline() { let d = scratch("search"); let o = wai_in(&d, &["ops", "search", "verify", "the", "receipts", "--json"]); let v = envelope(&o); let hits = v["data"].as_array().unwrap(); assert!(!hits.is_empty() && hits.len() <= 5, "{v}"); assert_eq!(hits[0]["id"], "receipt.verify", "{v}"); let o = wai_in(&d, &["ops", "search", "make a new project"]); assert!(String::from_utf8_lossy(&o.stdout).lines().next().unwrap_or("").starts_with("project.create"), "{}", text(&o)); let o = wai_in(&d, &["ops", "schema", "envelope.wrap"]); assert!(o.status.success(), "{}", text(&o)); let s: Value = serde_json::from_slice(&o.stdout).unwrap(); assert_eq!((s["id"].as_str(), s["effect"].as_str(), s["dry_run"].as_bool()), (Some("envelope.wrap"), Some("writes-project"), Some(true))); assert!(s["input"]["required"].as_array().unwrap().iter().any(|r| r == "capability"), "{s}"); let o = wai_in(&d, &["ops", "schema", "envelope.frob", "--json"]); assert_eq!(o.status.code(), Some(5), "{}", text(&o)); assert_eq!(envelope(&o)["error"]["code"], "operation-not-found"); } #[test] fn every_operation_answers_help_from_its_entry_and_its_mechanical_form() { let d = scratch("help"); for op in OPS.iter().filter(|o| o.cli.is_some()) { let cmd = op.command(); let mut words: Vec<&str> = cmd.split_whitespace().skip(1).collect(); words.push("--help"); let o = wai_in(&d, &words); assert!(o.status.success(), "{cmd} --help: {}", text(&o)); let t = String::from_utf8_lossy(&o.stdout); assert!(t.contains(op.id) && t.contains(op.summary), "{cmd} --help: {t}"); let (n, v) = op.parts(); let o = wai_in(&d, &[n, v, "--help"]); assert!(o.status.success() && String::from_utf8_lossy(&o.stdout).contains(op.id), "wai {n} {v} --help: {}", text(&o)); } let o = wai_in(&d, &["--help"]); assert!(o.status.success()); assert!(String::from_utf8_lossy(&o.stdout).contains("sends nothing off your machine unless you name a remote service")); // The mechanical form runs the operation. let o = wai_in(&d, &["registry", "list"]); let p = wai_in(&d, &["capabilities"]); assert!(o.status.success() && o.stdout == p.stdout, "{}", text(&o)); } #[test] fn the_exit_codes_say_what_happened() { let d = scratch("exits"); // 2: usage. assert_eq!(wai_in(&d, &[]).status.code(), Some(2)); assert_eq!(wai_in(&d, &["inspect"]).status.code(), Some(2)); assert_eq!(wai_in(&d, &["create", "x", "--frob"]).status.code(), Some(2)); // 5: a file, an operation, a build that does not hold the operation. assert_eq!(wai_in(&d, &["inspect", "no-such.wai"]).status.code(), Some(5)); let o = wai_in(&d, &["frobnicate", "--json"]); assert_eq!(o.status.code(), Some(5), "{}", text(&o)); let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("operation-not-found")), "{v}"); #[cfg(not(feature = "generate"))] { let o = wai_in(&d, &["generate", "--model", "m.json"]); assert_eq!(o.status.code(), Some(5), "a build without `generate` does not hold media.generate: {}", text(&o)); assert!(text(&o).contains("needs a build with the feature generate"), "{}", text(&o)); } // 1: a check that failed (a receipt whose object changed), with its report in data. assert!(wai_in(&d, &["hello", "h"]).status.success()); let obj = std::fs::read_dir(d.join("h")).unwrap().flatten().map(|e| e.path()).find(|p| p.extension().is_some_and(|x| x == "wai")).unwrap(); let mut b = std::fs::read(&obj).unwrap(); let n = b.len(); b[n - 1] ^= 1; std::fs::write(&obj, &b).unwrap(); let o = wai_in(&d, &["verify", "h", "--json"]); assert_eq!(o.status.code(), Some(1), "{}", text(&o)); let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("check-failed")), "{v}"); assert!(v["data"]["text"].as_str().unwrap().contains("FAIL"), "{v}"); // 3: a file this user may not read (where the user is not root, who reads anything). #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let locked = d.join("locked.wai"); std::fs::write(&locked, b"WAI1").unwrap(); std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap(); if std::fs::read(&locked).is_err() { assert_eq!(wai_in(&d, &["inspect", "locked.wai"]).status.code(), Some(3)); } std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o644)).unwrap(); } } #[cfg(unix)] #[test] fn a_local_stack_that_does_not_come_up_is_unavailable_exit_6() { let d = scratch("unavailable"); // Programs of this wai's toolchain, which answer for it and then fail to start. let fails = |name: &str, toolchain: &str| { let p = d.join(format!("{name}-{toolchain}")); let answer = format!(r#"{{"wai":"envelope/1","op":"toolchain.show","ok":true,"data":{{"program":"{name}","version":"0.1.0","toolchain":"{toolchain}"}}}}"#); program::write_program(&p, &format!("#!/bin/sh\nif [ \"$1\" = --version ]; then echo '{answer}'; exit 0; fi\nexit 1\n")); p.to_str().unwrap().to_string() }; let web = d.join("web"); std::fs::create_dir_all(web.join("room")).unwrap(); std::fs::write(web.join("room/index.html"), "").unwrap(); let w = web.to_str().unwrap(); let v = env!("CARGO_PKG_VERSION"); let o = wai_in(&d, &["dev", "--port", "0", "--relay", &fails("wai-relay", v), "--room", &fails("wai-room", v), "--web", w, "--check", "--json"]); assert_eq!(o.status.code(), Some(6), "{}", text(&o)); let e = envelope(&o); assert_eq!((e["error"]["code"].as_str(), e["error"]["retryable"].as_bool()), (Some("target-unavailable"), Some(true)), "{e}"); // A stale stack: a room service of another toolchain, or a program that does not say, is // refused before anything starts, and starting it again would change nothing. for room in [fails("wai-room", "0.0.1"), d.join("web/room/index.html").to_str().unwrap().to_string(), fails("wai-relay", v)] { let o = wai_in(&d, &["dev", "--port", "0", "--dir", "stale", "--relay", &fails("wai-relay", v), "--room", &room, "--web", w, "--check", "--json"]); assert_eq!(o.status.code(), Some(2), "{room}: {}", text(&o)); let e = envelope(&o); assert_eq!((e["error"]["code"].as_str(), e["error"]["retryable"].as_bool()), (Some("stack-stale"), Some(false)), "{room}: {e}"); assert!(!d.join("stale/relay.toml").exists() && !d.join("stale/room-keys.toml").exists(), "nothing was written: {room}"); } } #[test] fn replacing_a_file_the_command_line_did_not_name_waits_for_yes() { let d = scratch("confirm"); std::fs::write(d.join("p.bin"), b"hello").unwrap(); let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]); assert!(o.status.success(), "{}", text(&o)); let first = std::fs::read(d.join("out.wai")).unwrap(); std::fs::write(d.join("p.bin"), b"hello, again").unwrap(); // Off a terminal, without --yes: exit 4, nothing changed, the proposal and the command. let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--json"]); assert_eq!(o.status.code(), Some(4), "{}", text(&o)); let v = envelope(&o); assert_eq!(v["error"]["code"], "confirmation-required", "{v}"); assert_eq!(v["proposed"][0]["replace"], "out.wai", "{v}"); assert_eq!(v["confirm"], "wai wrap wai.text.zstd p.bin --yes", "{v}"); assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), first, "nothing changed"); let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]); assert_eq!(o.status.code(), Some(4), "{}", text(&o)); assert!(text(&o).contains("to go ahead: wai wrap wai.text.zstd p.bin --yes"), "{}", text(&o)); // With --yes it goes ahead; a named output never waits. assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--yes"]).status.success()); assert_ne!(std::fs::read(d.join("out.wai")).unwrap(), first); assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "-o", "out.wai"]).status.success()); // `wai hello` over an earlier run's files waits too. assert!(wai_in(&d, &["hello", "h"]).status.success()); assert_eq!(wai_in(&d, &["hello", "h"]).status.code(), Some(4)); assert!(wai_in(&d, &["hello", "h", "--yes"]).status.success()); } #[test] fn confirm_is_the_deprecated_spelling_of_yes() { let d = scratch("confirm-alias"); std::fs::write(d.join("p.bin"), b"hello").unwrap(); assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]).status.success()); let first = std::fs::read(d.join("out.wai")).unwrap(); std::fs::write(d.join("p.bin"), b"hello, again").unwrap(); // Without either: exit 4, and the confirming command names --yes. let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--json"]); assert_eq!(o.status.code(), Some(4), "{}", text(&o)); assert_eq!(envelope(&o)["confirm"], "wai wrap wai.text.zstd p.bin --yes"); assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), first, "nothing changed"); // --confirm alone goes ahead as --yes does, wherever it is on the line. let o = wai_in(&d, &["wrap", "--confirm", "wai.text.zstd", "p.bin"]); assert!(o.status.success(), "--confirm is --yes: {}", text(&o)); let replaced = std::fs::read(d.join("out.wai")).unwrap(); assert_ne!(replaced, first); assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--yes"]).status.success()); assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), replaced, "the same change"); // The help names it deprecated, on its own line and in every operation that confirms. let h = String::from_utf8_lossy(&wai_in(&d, &["--help"]).stdout).into_owned(); assert!(h.lines().any(|l| l.trim_start().starts_with("--confirm") && l.contains("deprecated") && l.contains("--yes")), "{h}"); for op in OPS.iter().filter(|o| o.cli.is_some() && o.confirms() != Confirm::Never) { let cmd = op.command(); let words: Vec<&str> = cmd.split_whitespace().skip(1).chain(["--help"]).collect(); let t = String::from_utf8_lossy(&wai_in(&d, &words).stdout).into_owned(); assert!(t.contains("--yes") && t.contains("--confirm, deprecated"), "{} --help: {t}", op.id); } // The export says so, for agents. let e: Value = serde_json::from_slice(&wai_in(&d, &["ops", "export"]).stdout).unwrap(); let yes = e["agent_flags"].as_array().unwrap().iter().find(|f| f["flag"] == "--yes").unwrap().clone(); assert_eq!((yes["aliases"][0]["flag"].as_str(), yes["aliases"][0]["deprecated"]["use_instead"].as_str()), (Some("--confirm"), Some("--yes")), "{yes}"); } /// `--fields` keeps the members of `data` it names, and never the envelope's own: `ok`, `op`, /// `wai`, an error and its exit are there whatever is asked, for an operation that prints its /// envelope itself, for one whose text a child carries, and for a failure. It is taken with /// `--json` alone, and with a value. #[test] fn fields_trim_data_and_never_hide_ok() { let d = scratch("fields"); let o = wai_in(&d, &["ops", "list", "--json", "--fields", "id"]); assert!(o.status.success(), "{}", text(&o)); let v = envelope(&o); let rows = v["data"].as_array().unwrap(); assert_eq!(rows.len(), OPS.len()); assert!(rows.iter().all(|r| r.as_object().is_some_and(|m| m.len() == 1 && m.contains_key("id"))), "{v}"); for asked in ["ok", "wai,op,error", "no.such.member", "data.id"] { for args in [&["ops", "list"][..], &["--version"], &["capabilities"], &["ops", "show", "envelope.wrap"], &["ops", "search", "verify receipts"]] { let mut a = args.to_vec(); a.extend(["--json", "--fields", asked]); let o = wai_in(&d, &a); let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["op"].is_string(), v.get("data").is_some()), (Some(true), true, true), "{a:?}: {v}"); } } // A text operation's envelope, carried from a child: its text kept, or nothing of it. let v = envelope(&wai_in(&d, &["capabilities", "--json", "--fields", "text"])); assert!(v["data"]["text"].as_str().unwrap().contains("wai.image.png"), "{v}"); let v = envelope(&wai_in(&d, &["capabilities", "--json", "--fields", "nothing"])); assert_eq!((v["ok"].as_bool(), &v["data"]), (Some(true), &serde_json::json!({})), "{v}"); // A failure keeps its error and exit. let o = wai_in(&d, &["inspect", "no-such.wai", "--json", "--fields", "id"]); assert_eq!(o.status.code(), Some(5), "{}", text(&o)); let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str(), v["exit"].as_u64()), (Some(false), Some("not-found"), Some(5)), "{v}"); let o = wai_in(&d, &["ops", "schema", "envelope.frob", "--json", "--fields", "id"]); assert_eq!((o.status.code(), envelope(&o)["error"]["code"].as_str()), (Some(5), Some("operation-not-found"))); // Without --json, without a value, twice, or an empty member: usage. for args in [&["ops", "list", "--fields", "id"][..], &["ops", "list", "--json", "--fields"], &["ops", "list", "--json", "--fields", "id", "--fields", "op"], &["ops", "list", "--json", "--fields", "id,,summary"]] { let o = wai_in(&d, args); assert_eq!(o.status.code(), Some(2), "{args:?}: {}", text(&o)); } } /// `wai prior verify` holds a set to its pin: a directory as its listing (the pin `wai prior-set` /// prints), a file as its bytes. A pin of another set, a set with a file changed, and a set read /// under another form are each refused; a digest that is not one, or no path, is usage or not found. #[test] fn prior_verify_holds_a_set_to_its_pin_and_refuses_another() { let d = scratch("prior-verify"); let set = d.join("set"); std::fs::create_dir_all(set.join("w")).unwrap(); std::fs::write(set.join("model.json"), b"{\"layers\": 2}").unwrap(); std::fs::write(set.join("w/weights.bin"), [7u8; 64]).unwrap(); let o = wai_in(&d, &["prior-set", "set"]); assert!(o.status.success(), "{}", text(&o)); let err = String::from_utf8_lossy(&o.stderr).into_owned(); let pin = err.split("sha256 ").nth(1).map(|p| p.trim().to_string()).unwrap_or_else(|| panic!("a pin: {err}")); assert_eq!(pin.len(), 64); let o = wai_in(&d, &["prior", "verify", "set", "--sha256", &pin, "--json"]); assert!(o.status.success(), "{}", text(&o)); let v = envelope(&o); assert_eq!((v["data"]["held"].as_bool(), v["data"]["form"].as_str(), v["data"]["files"].as_u64()), (Some(true), Some("wai.prior.set"), Some(2)), "{v}"); // A bad pin: one digit changed. let last = if pin.ends_with('0') { "1" } else { "0" }; let bad = format!("{}{last}", &pin[..63]); let o = wai_in(&d, &["prior", "verify", "set", "--sha256", &bad, "--json"]); assert_eq!(o.status.code(), Some(1), "a bad pin is refused: {}", text(&o)); let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str(), v["data"]["held"].as_bool(), v["data"]["sha256"].as_str()), (Some(false), Some("check-failed"), Some(false), Some(pin.as_str())), "{v}"); assert_eq!(wai_in(&d, &["prior", "verify", "set", "--sha256", &bad]).status.code(), Some(1)); // The set changed by one byte: refused under its old pin. std::fs::write(set.join("w/weights.bin"), [8u8; 64]).unwrap(); assert_eq!(wai_in(&d, &["prior", "verify", "set", "--sha256", &pin]).status.code(), Some(1)); // A file is its bytes. let one = wai::pin::sha256_hex(b"{\"layers\": 2}"); assert!(wai_in(&d, &["prior", "verify", "set/model.json", "--sha256", &one]).status.success()); // Usage: a digest that is not one, a directory under another form, a file read as a listing. for args in [&["prior", "verify", "set", "--sha256", "abc"][..], &["prior", "verify", "set", "--sha256", &pin.to_uppercase()], &["prior", "verify", "set", "--sha256", &pin, "--form", "wai.other"], &["prior", "verify", "set/model.json", "--sha256", &one, "--form", "wai.prior.set"], &["prior", "verify", "--sha256", &pin]] { assert_eq!(wai_in(&d, args).status.code(), Some(2), "{args:?}"); } assert_eq!(wai_in(&d, &["prior", "verify", "nothing-here", "--sha256", &pin]).status.code(), Some(5)); } /// `wai prior fetch` takes set roots as http-delivery §8.7 defines them, and nothing to ask is /// usage: nothing is sent for any of these. #[cfg(all(feature = "prior_carriage", feature = "cli_tls"))] #[test] fn prior_fetch_refuses_what_is_not_a_set_root() { let d = scratch("prior-fetch"); let pin = "ab".repeat(32); for args in [ &["prior", "fetch", &pin, "--root", "http://sets.example/s/", "--out", "x"][..], &["prior", "fetch", &pin, "--root", "https://sets.example/s", "--out", "x"], &["prior", "fetch", &pin, "--root", "https://user@sets.example/s/", "--out", "x"], &["prior", "fetch", &pin, "--origin", "http://sets.example", "--out", "x"], &["prior", "fetch", &pin, "--out", "x"], &["prior", "fetch", "abc", "--root", "https://sets.example/s/", "--out", "x"], &["prior", "fetch", &pin, "--root", "https://sets.example/s/"], ] { let o = wai_in(&d, args); assert_eq!(o.status.code(), Some(2), "{args:?}: {}", text(&o)); } assert!(!d.join("x").exists()); } /// A build with `--features full` holds every operation of the catalogue (wai-catalogue.yml /// builds it); the catalogue's tests hold the list to the table. #[cfg(feature = "full")] #[test] fn a_full_build_holds_every_operation() { let d = scratch("full"); let v = envelope(&wai_in(&d, &["ops", "list", "--json"])); let missing: Vec<&str> = v["data"].as_array().unwrap().iter().filter(|r| r["available"] != true).map(|r| r["id"].as_str().unwrap()).collect(); assert!(missing.is_empty(), "a build with full does not hold {missing:?}"); } #[test] fn a_text_command_carries_its_output_in_the_envelope() { let d = scratch("envelope"); let o = wai_in(&d, &["capabilities", "--json"]); assert!(o.status.success(), "{}", text(&o)); let v = envelope(&o); assert_eq!((v["op"].as_str(), v["ok"].as_bool()), (Some("registry.list"), Some(true))); assert!(v["data"]["text"].as_str().unwrap().contains("wai.image.png"), "{v}"); let o = wai_in(&d, &["inspect", "no-such.wai", "--json"]); let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("not-found")), "{v}"); assert!(v.get("data").is_none(), "{v}"); } // ─────────────────────────────── --dry-run ─────────────────────────────── /// Every file under `d`, with its length and modification time. fn snapshot(d: &Path) -> Vec<(String, u64, std::time::SystemTime)> { fn walk(root: &Path, p: &Path, out: &mut Vec<(String, u64, std::time::SystemTime)>) { for e in std::fs::read_dir(p).unwrap().flatten() { let q = e.path(); let m = e.metadata().unwrap(); out.push((q.strip_prefix(root).unwrap().display().to_string(), m.len(), m.modified().unwrap())); if m.is_dir() { walk(root, &q, out); } } } let mut v = Vec::new(); walk(d, d, &mut v); v.sort(); v } fn corpus_object(id: &str) -> PathBuf { let c = wai_dir().join("http-conformance"); let listing: Value = serde_json::from_slice(&std::fs::read(c.join("objects.json")).unwrap()).unwrap(); c.join(listing[id]["path"].as_str().unwrap()) } /// A running `wai http origin` over `root`, stopped when dropped. struct Origin(std::process::Child, u16); impl Origin { fn start(root: &Path) -> Origin { let mut child = Command::new(env!("CARGO_BIN_EXE_wai")) .args(["http", "origin", "--root", root.to_str().unwrap(), "--port", "0"]) .stdout(Stdio::piped()) .spawn() .unwrap(); let mut first = String::new(); BufReader::new(child.stdout.take().unwrap()).read_line(&mut first).unwrap(); let port = first.trim().rsplit(':').next().unwrap().trim_end_matches('/').parse().unwrap(); Origin(child, port) } } impl Drop for Origin { fn drop(&mut self) { let _ = self.0.kill(); let _ = self.0.wait(); } } /// A fleet service that answers one request with `body` (status 200), as `wai-fleet` would; /// its `http://` address. fn fleet_service(body: Value) -> String { use std::io::{Read, Write}; let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let addr = format!("http://{}", l.local_addr().unwrap()); std::thread::spawn(move || { if let Ok((mut s, _)) = l.accept() { let mut buf = [0u8; 4096]; let _ = s.read(&mut buf); let b = body.to_string(); let _ = s.write_all(format!("HTTP/1.1 200 X\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{b}", b.len()).as_bytes()); } }); addr } /// A command line that writes, for each operation of this build whose effect is not /// `reads`, with what it needs made in `d`: `(id, args, whether it writes without /// --dry-run)`. An operation that runs something, or changes only a service, writes nothing /// of the project's. fn samples(d: &Path, origin_port: u16) -> Vec<(&'static str, Vec, bool)> { let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::>(); std::fs::write(d.join("in.txt"), b"a payload to carry").unwrap(); assert!(wai_in(d, &["wrap", "wai.text.zstd", "in.txt", "-o", "w.wai"]).status.success()); assert!(wai_in(d, &["encode", "zstd", "in.txt", "-o", "z.wai"]).status.success()); let reg = d.join("regroot"); std::fs::create_dir_all(®).unwrap(); std::fs::copy(wai_dir().join("REGISTRY.md"), reg.join("REGISTRY.md")).unwrap(); let objs: Vec = ["O9a", "O9b", "O9c"].iter().map(|id| corpus_object(id).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 = serde_json::json!({ "uri_prefix": "objects/", "variants": [{ "uri": "wai-180.m3u8", "classical": "classical.m3u8", "resolution": [320, 180], "frame_rate_milli": 25000, "segments": objs.iter().map(|o| serde_json::json!({ "object": o })).collect::>() }], }); std::fs::write(d.join("list.json"), serde_json::to_vec_pretty(&list).unwrap()).unwrap(); let od = corpus_object("OD"); let cid = wai::http_binding::hex(&wai::http_binding::content_hash(&std::fs::read(&od).unwrap())); let web = d.join("web"); std::fs::create_dir_all(web.join("room")).unwrap(); std::fs::write(web.join("room/index.html"), "").unwrap(); let me = env!("CARGO_BIN_EXE_wai"); std::fs::create_dir_all(d.join("agentproj")).unwrap(); // The record a running wai dev keeps: room test and room load read it. std::fs::create_dir_all(d.join("stack-dev")).unwrap(); let record = serde_json::json!({ "format": "wai.dev-stack/1", "toolchain": env!("CARGO_PKG_VERSION"), "pid": 1, "dir": d.join("stack-dev").display().to_string(), "origin": "http://127.0.0.1:9", "service": "http://127.0.0.1:9", "relay": "ws://127.0.0.1:9/moq", "room_bin": me, "relay_quic": null, "relay_cert_sha256": null, "impair": null, }); std::fs::write(d.join("stack-dev/stack.json"), record.to_string()).unwrap(); #[allow(unused_mut)] let mut more: Vec<(&'static str, Vec, bool)> = Vec::new(); #[cfg(feature = "video_manifest")] { let sd = d.join("sealdir"); std::fs::create_dir_all(&sd).unwrap(); std::fs::copy(d.join("classical.m3u8"), sd.join("classical.m3u8")).unwrap(); std::fs::copy(d.join("list.json"), sd.join("list.json")).unwrap(); let o = wai_in(&sd, &["http", "hls", "--objects", "list.json", "--out", "."]); assert!(o.status.success(), "{}", text(&o)); more.push(("playlist.lock", s(&["http", "seal-playlist", "sealdir/wai-180.m3u8", "--key", &"07".repeat(32), "--out", "sealed"]), true)); } // A set fetched by its pin: the dry run names each root's request and the directory, and sends // nothing (a set root is https://, which this test does not serve). #[cfg(all(feature = "prior_carriage", feature = "cli_tls"))] more.push(("prior.fetch", s(&["prior", "fetch", &"ab".repeat(32), "--form", "wai.prior.set", "--root", "https://sets.example/s/", "--out", "fetched"]), false)); #[cfg(feature = "app")] { // A Create runtime as wai-app's runtime.mjs lays one out, and a program made with Create. use sha2::{Digest, Sha256}; let rt = d.join("runtime"); let mut listed = Vec::new(); for (p, text) in [("create/wai-app-app.js", "export {};\n"), ("create/pkg/wai_app_bg.wasm", "\0asm"), ("room/wai-app-room-client.js", "// the room client\n")] { let f = rt.join(p); std::fs::create_dir_all(f.parent().unwrap()).unwrap(); std::fs::write(&f, text).unwrap(); let h: String = Sha256::digest(text.as_bytes()).iter().map(|x| format!("{x:02x}")).collect(); listed.push(serde_json::json!({"path": p, "sha256": h})); } std::fs::write(rt.join("create/wai-app-runtime.json"), serde_json::json!({"files": listed}).to_string()).unwrap(); std::fs::write(d.join("app.oui"), "root = App([show], \"Friday night\")\nshow = Show([player, chat], \"Live\", 2)\nplayer = Player()\nchat = Chat(\"Talk\")\n").unwrap(); more.push(("app.export", s(&["app", "export", "app.oui", "appproj", "--runtime", "runtime"]), true)); } #[cfg(feature = "fleet")] { // A node's enrolment written here; a report's bundle exported from a fleet service (the // corpus's week, which this build verifies before it writes); and a report the service // would sign, which a dry run does not ask for. std::fs::write(d.join("org.key"), "07".repeat(32)).unwrap(); let corpus: Value = serde_json::from_slice(&std::fs::read(wai_dir().join("live-conformance/fleet/vectors.json")).unwrap()).unwrap(); let week = corpus["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "a_week").unwrap().clone(); let digest = week["outcome"]["digest"].as_str().unwrap().to_string(); let url = fleet_service(serde_json::json!({"report": digest, "outcome": week["outcome"], "bundle": week["bundle"]})); more.push(("enrolment.create", s(&["enrolment", "create", &"11".repeat(32), "--role", "relay", "--key", "org.key", "--out", "node.wfe"]), true)); more.push(("report.export", s(&["report", "export", &digest, "--target", &url, "--out", "week.wai-fleet.json"]), true)); more.push(("report.create", s(&["report", "create", "--target", "http://127.0.0.1:9", "--start", "0", "--end", "1"]), false)); } #[cfg(feature = "channel_lineup")] { // The sample channel of extensions/channel-lineup.md §11, imported here; and the guide of // its committed receipt and lineup. let c = wai_dir().join("live-conformance/channels"); let f = |n: &str| c.join(n).to_str().unwrap().to_string(); let corpus: Value = serde_json::from_slice(&std::fs::read(c.join("vectors.json")).unwrap()).unwrap(); std::fs::write(d.join("channel-operator.key"), corpus["operator"]["seed"].as_str().unwrap()).unwrap(); let l = corpus["imports"][0]["outcome"]["lineup"].as_str().unwrap(); let lineup: Vec = (0..l.len()).step_by(2).map(|i| u8::from_str_radix(&l[i..i + 2], 16).unwrap()).collect(); std::fs::write(d.join("sample.wlu"), lineup).unwrap(); let (x, a, b, p, art) = (f("sample.xmltv.xml"), f("sample-a.mrss.xml"), f("sample-b.mrss.xml"), f("sample.m3u8"), f("")); more.push(( "channel.import", s(&["channel", "import", "--channel", "sample-1", "--xmltv", &x, "--mrss", &a, "--mrss-owner", "owner-a", "--mrss", &b, "--mrss-owner", "owner-b", "--owner", "owner-c", "--playlist", &p, "--artwork", &art, "--key", "channel-operator.key", "--out-dir", "channel-out"]), true, )); more.push(("channel.guide", s(&["channel", "guide", "--receipt", &f("sample.channel.json"), "--lineup", "sample.wlu", "--out", "guide.xml"]), true)); } #[cfg(feature = "revenue_share")] { // The revenue share: a contract written from its terms, an owner's countersignature added // to an offered contract, and a statement of the share corpus's October, recomputed here // from its runs and ledger documents. use wai::revenue_share::{corpus, terms}; let hexs = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); let (operator, owner) = (corpus::key("operator"), corpus::key("owner-a")); std::fs::write(d.join("operator.key"), hexs(&operator.to_bytes())).unwrap(); std::fs::write(d.join("owner-a.key"), hexs(&owner.to_bytes())).unwrap(); let a = hexs(&owner.verifying_key().to_bytes()); let t = serde_json::json!({ "start": 0, "end": 10, "currency": "EUR", "channel": "fast-7", "channel_signer": "22".repeat(32), "rule": "preceding", "between": 0, "carriers": [{"room": "11".repeat(32)}], "owners": [a], "splits": [{"operator": 500_000, "owners": {a.clone(): 500_000}}], "assets": {"a/ep-1": 0}, }); std::fs::write(d.join("terms.json"), t.to_string()).unwrap(); std::fs::write(d.join("offered.wrc"), terms(&t).unwrap().offer(&operator).to_bytes()).unwrap(); let share: Value = serde_json::from_slice(&std::fs::read(wai_dir().join("live-conformance/share/vectors.json")).unwrap()).unwrap(); let ledger = |n: &str| { let l = &share["ledgers"][n]; let sources: Vec = l["sources"].as_array().unwrap().iter().map(|s| share["sources"][s.as_str().unwrap()].clone()).collect(); serde_json::json!({"tariff": l["tariff"], "invoice": l["invoice"], "previous": l["previous"], "sources": sources}) }; let inputs = serde_json::json!({ "contract": share["contracts"]["preceding"], "contracts": [], "runs": [share["runs"]["oct_16"], share["runs"]["oct_25"]], "ledgers": [ledger("day_16"), ledger("day_25")], }); std::fs::write(d.join("inputs.json"), inputs.to_string()).unwrap(); more.push(("contract.create", s(&["contract", "create", "terms.json", "--key", "operator.key", "--out", "c.wrc"]), true)); more.push(("contract.update", s(&["contract", "accept", "offered.wrc", "--key", "owner-a.key", "--out", "accepted.wrc"]), true)); more.push(( "statement.create", s(&["statement", "create", "inputs.json", "--party", &a, "--start", "1790812800000000", "--end", "1793491200000000", "--key", "operator.key", "--out", "a.wai-share.json"]), true, )); } #[cfg(feature = "opportunity_report")] { // The opportunity report: owner B's report of the opportunity corpus's day, recomputed // here from its runs and room, and the check of B's reported statement beside it. use wai::opportunity_report::corpus; let hexs = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); let w = corpus::world().unwrap(); let b = &w.reports["owner_b"]; std::fs::write(d.join("owner-b.key"), hexs(&corpus::key("owner-b").to_bytes())).unwrap(); std::fs::write(d.join("opp-inputs.json"), w.pools.report_document(b, false).to_string()).unwrap(); std::fs::write(d.join("opp-report.json"), w.pools.report_document(b, true).to_string()).unwrap(); std::fs::write(d.join("reported.json"), w.pools.reported["b_programmes"]["text"].as_str().unwrap()).unwrap(); let party = hexs(&corpus::pk("owner-b")); let (start, end) = (corpus::DAY16.to_string(), corpus::DAY17.to_string()); more.push(("opportunity.create", s(&["opportunity", "create", "opp-inputs.json", "--party", &party, "--start", &start, "--end", &end, "--key", "owner-b.key", "--out", "b.wai-opportunity.json"]), true)); more.push(("statement.check", s(&["statement", "check", "opp-report.json", "reported.json", "--key", "owner-b.key", "--out", "b.wai-check.json"]), true)); } #[cfg(feature = "generate")] { let model = wai_dir().join("wai-rs/tests/vectors/generate/generate_spectral_model.json"); more.push(("media.generate", s(&["generate", "--model", model.to_str().unwrap(), "--prompt", "a calm spectral horizon", "--seed", "7", "--steps", "1", "--size", "16x16", "-o", "g.rgb"]), true)); } #[cfg(feature = "exact_upscale")] { let prior = wai_dir().join("live-conformance/upscale/priors/generic.wsr"); std::fs::write(d.join("small.png"), wai::png_still::encode(&[9u8; 3 * 8 * 6], 8, 6)).unwrap(); more.push(("media.upscale", s(&["upscale", "run", "small.png", "--prior", prior.to_str().unwrap(), "-o", "up.png"]), true)); } #[cfg(feature = "exact_tonemap")] { use wai::exact_tonemap as tm; let (w, h) = (16, 12); let e = tm::corpus::encode(&tm::corpus::scene(0, w, h), w, h, 4, false, (1, 1), false); let companion = tm::wgm1(&e.metadata.to_bytes(), &wai::png_still::encode_grey(&e.map, e.map_w, e.map_h)); let envelope = wai::container::WaiMulti::new( "image", "replicate", serde_json::Value::Null, vec![ wai::container::Rendition::media(wai::codecs::CAP_IMAGE_PNG, "png", wai::png_still::encode(&e.base, w, h)).with_id("base").with_payload_digest(), wai::container::Rendition::companion(tm::CAP_GAINMAP, companion, wai::codecs::CAP_IMAGE_PNG).with_id("gainmap").with_companion_of_id("base").with_payload_digest(), ], ) .to_bytes() .unwrap(); std::fs::write(d.join("hdr.wai"), envelope).unwrap(); more.push(("media.tonemap", s(&["tonemap", "run", "hdr.wai", "--headrooms", "20000", "--out", "renditions"]), true)); } #[cfg(feature = "score")] { let notes = wai_dir().join("score-conformance/notes/scale.json"); let notes = notes.to_str().unwrap(); assert!(wai_in(d, &["score", "build", notes, "-o", "s0.wscr"]).status.success()); more.push(("score.build", s(&["score", "build", notes, "-o", "s.wscr"]), true)); more.push(("score.decode", s(&["score", "render", "s0.wscr", "--pcm", "s.s16"]), true)); } #[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))] { let frame: Vec = (0..6 * 10 * 3).map(|i| ((i * 13) % 256) as u8).collect(); std::fs::write(d.join("f.rgb"), &frame).unwrap(); for args in [ &["stage", "base", "--frames", "f.rgb", "--dims", "1,6,10", "--q", "40", "-o", "b0.wai"][..], &["stage", "refine", "--base", "b0.wai", "--span", "0:1", "--toward", "f.rgb", "--q", "1", "-o", "l0.wai"], &["stage", "pack", "b0.wai", "l0.wai", "-o", "x0.wai"], ] { let o = wai_in(d, args); assert!(o.status.success(), "{args:?}: {}", text(&o)); } more.push(("stage.build", s(&["stage", "base", "--frames", "f.rgb", "--dims", "1,6,10", "--q", "40", "-o", "b.wai"]), true)); more.push(("stage.encode", s(&["stage", "refine", "--base", "b0.wai", "--span", "0:1", "--toward", "f.rgb", "--q", "1", "-o", "l.wai"]), true)); more.push(("stage.wrap", s(&["stage", "pack", "b0.wai", "l0.wai", "-o", "x.wai"]), true)); more.push(("stage.unwrap", s(&["stage", "explode", "x0.wai", "0", "-o", "e.wai"]), true)); } // A developer's keys, in a keystore of this directory and its files: the person's keystore and // keychain are never touched here. for k in ["turning", "spent"] { let o = wai_in(d, &["key", "create", k, "--for", "signer", "--keystore", "ks", "--store", "file"]); assert!(o.status.success(), "{}", text(&o)); } std::fs::write(d.join("seed.hex"), (100..132u8).map(|b| format!("{b:02x}")).collect::()).unwrap(); more.push(("key.create", s(&["key", "create", "made", "--for", "dev", "--keystore", "ks", "--store", "file"]), true)); more.push(("key.import", s(&["key", "import", "held", "--for", "signer", "--from", "seed.hex", "--keystore", "ks", "--store", "file"]), true)); more.push(("key.rotate", s(&["key", "rotate", "turning", "--keystore", "ks", "--store", "file", "--yes"]), true)); more.push(("key.revoke", s(&["key", "revoke", "spent", "--keystore", "ks", "--yes"]), true)); more.push(("key.export", s(&["key", "export", "turning", "--keystore", "ks", "--out", "turning.pub"]), true)); // The services an operation acts on, in this directory's configuration (wai_in's): a target // added, chosen and removed, and a key kept (read from standard input, so its dry run alone // runs here) and removed. more.push(("target.create", s(&["target", "add", "fleet", "--url", "http://127.0.0.1:9", "--user"]), true)); more.push(("target.update", s(&["target", "use", "fleet"]), true)); more.push(("target.delete", s(&["target", "remove", "fleet", "--user", "--yes"]), true)); { use std::io::Write; let mut c = Command::new(env!("CARGO_BIN_EXE_wai")) .current_dir(d) .env("WAI_CONFIG_DIR", d.join(".wai-config")) .args(["auth", "login", "--url", "http://127.0.0.1:9", "--store", "file"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); c.stdin.take().unwrap().write_all(b"k_a-sample-key\n").unwrap(); let o = c.wait_with_output().unwrap(); assert!(o.status.success(), "{}", text(&o)); } more.push(("credential.create", s(&["auth", "login", "--url", "http://127.0.0.1:8", "--store", "file"]), false)); more.push(("credential.delete", s(&["auth", "logout", "--url", "http://127.0.0.1:9", "--yes"]), true)); // A deployment target's operations: a dry run names the requests it would send and sends none // (no target listens here), the tree it would send the build's plan (tests/cli_deploy.rs runs // each against the reference target). more.push(("deployment.create", s(&["deploy", "buildproj", "--target", "http://127.0.0.1:9", "--promote"]), false)); more.push(("deployment.promote", s(&["deployment", "promote", &"ab".repeat(32), "--target", "http://127.0.0.1:9"]), false)); more.push(("deployment.rollback", s(&["deployment", "rollback", "--target", "http://127.0.0.1:9"]), false)); // Every operation of a service's API that changes it: the request named, and nothing sent. for o in OPS.iter().filter(|o| o.route.is_some() && o.effect != Effect::Reads) { let (n, v) = o.parts(); more.push((o.id, s(&[n, v, "--target", "http://127.0.0.1:9"]), false)); } // A project to lock, and one locked to build. for p in ["lockproj", "buildproj"] { std::fs::create_dir_all(d.join(p)).unwrap(); std::fs::write(d.join(p).join("wai.json"), r#"{"dev":{"site":"."}}"#).unwrap(); std::fs::write(d.join(p).join("index.html"), "\n").unwrap(); } assert!(wai_in(d, &["lock", "buildproj"]).status.success()); // A checkout's wai/ for a corpus's generator to write into, and a stand-in for its tool that // writes a file in the corpus's directory (a real one runs in wai-conformance.yml's corpus // loop, and tests/cli_conform_relay.rs holds the command to its table). let genroot = d.join("genroot"); for sub in ["registry", "wai-rs", "score-conformance"] { std::fs::create_dir_all(genroot.join(sub)).unwrap(); } std::fs::write(genroot.join("SPEC.md"), "").unwrap(); std::fs::write(genroot.join("registry/capabilities.json"), "{}").unwrap(); std::fs::create_dir_all(d.join("genbin")).unwrap(); let generator = "#!/bin/sh\necho written > \"$2/written\"\n"; #[cfg(unix)] program::write_program(&d.join("genbin/wai_score_vectors"), generator); #[cfg(not(unix))] std::fs::write(d.join("genbin/wai_score_vectors"), generator).unwrap(); let mut v = vec![ ("toolchain.test", s(&["hello", "hello-out"]), true), ("project.create", s(&["create", "proj", "--template", "video-room"]), true), ("project.lock", s(&["lock", "lockproj"]), true), ("project.build", s(&["build", "buildproj"]), true), ("envelope.wrap", s(&["wrap", "wai.text.zstd", "in.txt", "-o", "w2.wai"]), true), ("envelope.unwrap", s(&["unwrap", "w.wai", "-o", "payload.out"]), true), ("media.encode", s(&["encode", "zstd", "in.txt", "-o", "e.wai"]), true), ("media.decode", s(&["decode", "z.wai", "-o", "decoded.txt"]), true), ("registry.export", s(&["registry", "export", "--root", reg.to_str().unwrap()]), true), ("ops.export", s(&["ops", "export", "--out", "ops-out.json"]), true), ("http.export", s(&["http", "publish", corpus_object("O3").to_str().unwrap(), "--out", "pub"]), true), ("hls.build", s(&["http", "hls", "--objects", "list.json", "--out", "."]), true), ("dash.build", s(&["http", "mpd", "--objects", "list.json", "--out", "wai.mpd"]), true), ("corpus.generate", s(&["http", "vectors", "diff", ".", "--seed", "1", "--count", "1", "--out", "diff.bin"]), true), ("http.load", s(&["http", "bench", "--url", &format!("http://127.0.0.1:{origin_port}/{cid}.wai#h=1009&n=53794"), "--runs", "1", "--advertise", "wai.video.av1", "--out", "bench.json"]), true), ("dev.start", s(&["dev", "--port", "0", "--relay", me, "--room", me, "--web", web.to_str().unwrap()]), false), // A stack's record, as wai dev writes it: a dry run reads it and sends nothing. ("room.test", s(&["room", "test", "--dir", "stack-dev"]), false), ("room.load", s(&["room", "load", "--dir", "stack-dev", "--viewers", "2"]), false), // The room's own doors on that record: a dry run names the requests and sends none. ("room.join", s(&["room", "join", "--dir", "stack-dev", "--principal", "alice"]), false), ("room.leave", s(&["room", "leave", "--dir", "stack-dev", "--principal", "alice"]), false), ("http.start", s(&["http", "origin", "--root", ".", "--port", "0"]), false), ("conformance.run", s(&["conform", "verifier", "--suite", "jwp", "--impl", &format!("'{}' verdicts {{corpus}}", env!("CARGO_BIN_EXE_wai_jwp_vectors")), "--root", wai_dir().to_str().unwrap(), "--out", "jwp.wai-conform.json"]), true), ("conformance.generate", s(&["conformance", "gen", "--suite", "score", "--root", "genroot", "--bin-dir", d.join("genbin").to_str().unwrap()]), true), ("agent.setup", s(&["setup-agent", "--dir", "agentproj"]), true), ("mcp.start", s(&["mcp", "--project", "."]), false), // The packer given is this binary, which is no packer: the dry run names the package's // directory and runs nothing, and a real run writes nothing here. A pack through this // door with wai-pack itself runs in wai-pack.yml. ("package.build", s(&["package", "build", "--packer", me, "--policy", "p.toml", "--out", "pkg", "--key", "k.hex"]), false), // A receipt for a file made elsewhere, from the envelope above. ("receipt.create", s(&["receipt", "create", "in.txt", "--from", "w.wai"]), true), // A fold's program named, which a dry run names and does not run (tests/cli_folds.rs runs // each against a stand-in, and the program's own crate against the program). ("service.start", s(&["serve", "relay", "--config", "relay.toml", "--bin", me]), false), ("quantum.run", s(&["quantum", "run", "p.qasm", "--quantum", me]), false), ]; v.extend(more); v } #[test] fn every_operation_that_is_not_read_only_writes_nothing_with_dry_run() { let root = scratch("dry-run"); let served = root.join("served"); std::fs::create_dir_all(&served).unwrap(); assert!(wai_in(&served, &["http", "publish", corpus_object("OD").to_str().unwrap(), "--out", "."]).status.success()); let origin = Origin::start(&served); let work = root.join("work"); std::fs::create_dir_all(&work).unwrap(); let table = samples(&work, origin.1); // Every such operation this build holds has a sample: a new one without --dry-run // support cannot join the catalogue unnoticed. for op in OPS.iter().filter(|o| o.cli.is_some() && o.effect != Effect::Reads && o.available()) { assert!(table.iter().any(|(id, _, _)| *id == op.id), "{} is not read-only and has no --dry-run sample here", op.id); } for (id, args, writes) in &table { let before = snapshot(&work); let mut dry: Vec<&str> = args.iter().map(String::as_str).collect(); dry.push("--dry-run"); let o = wai_in(&work, &dry); assert!(o.status.success(), "{id} --dry-run: {}", text(&o)); assert_eq!(snapshot(&work), before, "{id} --dry-run wrote: {}", text(&o)); let t = text(&o); assert!(t.contains("dry run") || t.contains("dry-run"), "{id} --dry-run says so: {t}"); // The same with --json: the envelope says it was a dry run, and lists the plan. dry.push("--json"); let o = wai_in(&work, &dry); assert_eq!(snapshot(&work), before, "{id} --dry-run --json wrote: {}", text(&o)); if *writes { let v = envelope(&o); assert_eq!((v["ok"].as_bool(), v["dry_run"].as_bool()), (Some(true), Some(true)), "{id}: {v}"); assert!(!v["plan"].as_array().unwrap().is_empty(), "{id}: the plan names what it would write: {v}"); // Without --dry-run the same command writes: the dry run was of a write. let refs: Vec<&str> = args.iter().map(String::as_str).collect(); let o = wai_in(&work, &refs); assert!(o.status.success(), "{id}: {}", text(&o)); assert_ne!(snapshot(&work), before, "{id} without --dry-run writes"); } } drop(origin); }