//! 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}; fn wai_in(dir: &Path, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).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() { use std::os::unix::fs::PermissionsExt; let d = scratch("unavailable"); let fail = d.join("fails.sh"); std::fs::write(&fail, "#!/bin/sh\nexit 1\n").unwrap(); std::fs::set_permissions(&fail, std::fs::Permissions::from_mode(0o755)).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 f = fail.to_str().unwrap(); let o = wai_in(&d, &["dev", "--port", "0", "--relay", f, "--room", f, "--web", web.to_str().unwrap(), "--check", "--json"]); assert_eq!(o.status.code(), Some(6), "{}", text(&o)); let v = envelope(&o); assert_eq!((v["error"]["code"].as_str(), v["error"]["retryable"].as_bool()), (Some("target-unavailable"), Some(true)), "{v}"); } #[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}"); } #[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(); #[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)); } #[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 = "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 = "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", "--target", "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", "--target", "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)); } let mut v = vec![ ("toolchain.test", s(&["hello", "hello-out"]), true), ("project.create", s(&["create", "proj", "--template", "video-room"]), 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), ("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), ("agent.setup", s(&["setup-agent", "--dir", "agentproj"]), true), ("mcp.start", s(&["mcp", "--project", "."]), 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); }