open-standards/wai/wai-rs/tests/cli_ops.rs
Transaction Science 09e4fb9a1b
All checks were successful
wai-app and Create / wai-app · does this change touch it (push) Successful in 12s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 41s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 47s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 34s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 19s
wai-audit collector and player integration / does this change touch it (push) Successful in 25s
wai-fleet service / does this change touch it (push) Successful in 16s
wai-quantum for JavaScript / does this change touch it (push) Successful in 19s
WAI operation catalogue / does this change touch it (push) Successful in 26s
wai-key-proxy / does this change touch it (push) Successful in 43s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
wai-quantum studio and desktop / does this change touch it (push) Successful in 29s
wai-ledger service / does this change touch it (push) Successful in 29s
wai-sink-android native sink library / does this change touch it (push) Successful in 32s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m0s
WAI Studio / does this change touch it (push) Successful in 18s
wai-sink-swift native sink package / does this change touch it (push) Successful in 1m11s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m41s
wai-audit collector and player integration / collector · linux (push) Successful in 2m9s
wai-key-proxy / proxy · linux (push) Successful in 2m2s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m45s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m1s
wai-fleet service / service · linux (push) Successful in 4m15s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 6m41s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 7m22s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m18s
wai-ledger service / service · linux (push) Successful in 3m55s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m29s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 6m10s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 7m48s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 47s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 8m31s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m56s
wai-quantum studio and desktop / studio · browser (push) Successful in 6m44s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 8m14s
wai-fleet service / service · macos (push) Successful in 1m1s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 10m58s
wai-ledger service / service · macos (push) Successful in 1m1s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 8m9s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 13m54s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 3m39s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 14m4s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m31s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 7m59s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 8m53s
WAI Studio / Studio in WebKit · linux (push) Successful in 16m57s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 17m59s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 11m32s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m30s
wai deploy: a project's built tree on any deployment target, by its digest, with the target's signed receipts
What a deployment is (wai/extensions/deployment.md §3): dist/ as wai build
writes it, its build record and every file the record lists, named by the
SHA-256 of the record's bytes. A client before it sends a tree, and a target
before it holds one, hold the record to §3.3 and each file to its SHA-256 and
size.

The deployment API (§4, wai/extensions/deployment.openapi.json), which names
no host: GET /v1/deployments, PUT /v1/deployments/files/{sha256},
POST /v1/deployments, POST /v1/deployments/promote and
POST /v1/deployments/rollback, twelve refusal codes, and for each list,
create, promotion and rollback a wai.deployment-receipt/1 signed by the
target's Ed25519 key over its RFC 8785 form (§5): the operation, the
request's nonce, the target's count of changes, the deployment named, the
live deployment after and before, and the digest of what it holds.
wai::deployment (feature ed25519) reads records and signs and verifies
receipts for both sides.

The reference target is wai_deploy_target, a small service of its own in
wai-rs (feature quickstart), not a /_wai/deploy surface of wai dev: wai dev
cannot start without the relay and room service of other crates, and its
/_wai/ routes keep Studio's rules for a page. A target that serves nothing
else is tested alone, in this repository's CI. It keeps files by SHA-256,
written whole, its state written whole in place of the one before and held
to every record and file again on start, and serves the live deployment.

wai deploy (deployment.create), wai deployment list, promote <digest> and
rollback: placed client, preview, in a build with quickstart. wai deploy
builds, or with --prebuilt takes dist/ as it is, holds the tree to its
record before anything is sent, sends only what the target lacks, and with
--promote makes it live. Each takes --target, waits for --yes before it
builds, writes or sends anything, sends nothing with --dry-run, verifies
every receipt and holds it to its request, and keeps the receipts of each
change in .wai/deployments/<target>/ with its own operation receipt naming
them. wai --help's start list gains deploy, before verify. ops.json and
Studio's actions are regenerated (each operation bound to the command line);
mcp.json is unchanged, as no deployment operation is an MCP tool.

tests/cli_deploy.rs, in wai-catalogue.yml, runs the four against the
reference target and plants the faults the text refuses, each refused beside
a control that is not: a file of dist/ changed, added and removed after the
build; bytes put under another SHA-256; a record not in its canonical form;
a promotion of a digest the target does not hold, and of a file's SHA-256;
a rollback with none before; a replayed answer, an edited receipt, a list
that is not its receipt's state; and a kept receipt changed, refused by wai
verify. A rollback makes live exactly the deployment live before (history of
three); --dry-run leaves the target as it was, byte for byte; the OpenAPI
document's routes, operations, codes and receipt members are the
implementation's. tests/cli_ops.rs gains the three --dry-run samples.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 12:43:01 -05:00

840 lines
50 KiB
Rust

//! 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<version>`;
//! - every operation answers `--help` from its catalogue entry, and its mechanical form
//! `wai <noun> <verb>` 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::<Vec<_>>());
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"), "<script src=a.js></script>").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://[email protected]/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<String>, bool)> {
let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
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(&reg).unwrap();
std::fs::copy(wai_dir().join("REGISTRY.md"), reg.join("REGISTRY.md")).unwrap();
let objs: Vec<String> = ["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::<Vec<_>>() }],
});
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"), "<script src=a.js></script>").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<String>, 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<u8> = (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::<String>();
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<Value> = 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::<String>();
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<u8> = (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::<String>()).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"), "<!doctype 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);
}