open-standards/wai/wai-rs/tests/cli_dx.rs
Transaction Science aaf0affc43 wai: the command line through the catalogue, and the agent rules on every command
Every command line resolves through the catalogue (wai_cli/door.rs): its CLI spelling, an
alias, or its mechanical form wai <noun> <verb>; an implementation is found by operation
id, and help is written from the catalogue's entries. dx::VERBS is the developer path's view
of it. Every command keeps the agent rules (wai_cli/agent.rs): exit codes 0 to 7, --json
(a versioned envelope, envelope/1, carrying a command's own JSON or its text in data),
--dry-run on every operation that is not read-only (every write goes through
wai_cli/out.rs), and --yes where a change waits for confirmation (exit 4 off a terminal).
wai ops list, search, schema and export. The tests and the quickstart's packaging check read
the JSON forms from the envelope's data.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:52:20 -04:00

140 lines
6.3 KiB
Rust

//! The developer's path through the `wai` binary, as a person runs it: `wai hello` writes an
//! object, its receipt index and its group's receipt file, and `wai verify` checks them
//! offline and refuses each when it is altered; `wai create` writes each example of
//! `wai/examples/` as a project, file for file and byte for byte, and never into a directory
//! that holds anything.
#![cfg(feature = "quickstart")]
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn tmp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("wai-dx-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn wai(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).output().expect("wai runs")
}
fn text(o: &Output) -> String {
format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
}
/// The one file under `dir` whose name ends with `suffix`.
fn one(dir: &Path, suffix: &str) -> PathBuf {
let mut v: Vec<PathBuf> = std::fs::read_dir(dir).unwrap().flatten().map(|e| e.path()).filter(|p| p.to_string_lossy().ends_with(suffix)).collect();
assert_eq!(v.len(), 1, "one {suffix} in {}", dir.display());
v.remove(0)
}
#[test]
fn hello_runs_the_four_steps_and_verify_checks_what_it_wrote() {
let d = tmp("hello");
let o = wai(&d, &["hello", "h"]);
assert!(o.status.success(), "{}", text(&o));
let t = text(&o);
for step in ["1 Name", "2 Dispatch", "3 Rebuild", "4 Receipt", "byte for byte", "delivered-verified"] {
assert!(t.contains(step), "hello says {step}: {t}");
}
let h = d.join("h");
let v = wai(&d, &["verify", "h"]);
assert!(v.status.success(), "{}", text(&v));
assert!(text(&v).contains("delivered-verified") && text(&v).contains("1 receipt index verified"), "{}", text(&v));
// A byte of the object changed: its content hash is not the receipt's.
let obj = one(&h, ".wai");
let good = std::fs::read(&obj).unwrap();
let mut bad = good.clone();
let n = bad.len();
bad[n - 1] ^= 1;
std::fs::write(&obj, &bad).unwrap();
let v = wai(&d, &["verify", "h"]);
assert_eq!(v.status.code(), Some(1), "{}", text(&v));
assert!(text(&v).contains("FAIL"), "{}", text(&v));
std::fs::write(&obj, &good).unwrap();
// A byte of the group's receipt file changed: its name is no longer its link hash, or its
// signature no longer verifies.
let rcpt = one(&h.join("rcpt"), ".json");
let g = std::fs::read_to_string(&rcpt).unwrap();
let i = g.find("\"sig\":\"").expect("a signature") + 7;
let mut b = g.into_bytes();
b[i] = if b[i] == b'0' { b'1' } else { b'0' };
std::fs::write(&rcpt, &b).unwrap();
let v = wai(&d, &["verify", "h"]);
assert_eq!(v.status.code(), Some(1), "{}", text(&v));
// Nothing to verify is an error, not a pass.
std::fs::create_dir_all(d.join("empty")).unwrap();
let v = wai(&d, &["verify", "empty"]);
assert_eq!(v.status.code(), Some(2), "{}", text(&v));
// And hello never writes into a directory of someone else's files.
std::fs::write(d.join("empty/mine.txt"), "mine").unwrap();
assert_eq!(wai(&d, &["hello", "empty"]).status.code(), Some(2));
let _ = std::fs::remove_dir_all(&d);
}
/// Every file of an example but its `test/` directory, relative to it, sorted.
fn example_files(dir: &Path) -> Vec<String> {
fn walk(root: &Path, p: &Path, out: &mut Vec<String>) {
for e in std::fs::read_dir(p).unwrap().flatten() {
let q = e.path();
let rel = q.strip_prefix(root).unwrap().to_string_lossy().replace('\\', "/");
if rel == "test" || rel.starts_with("test/") || rel.ends_with(".DS_Store") || rel == "target" || rel.starts_with("target/") {
continue;
}
if q.is_dir() {
walk(root, &q, out);
} else {
out.push(rel);
}
}
}
let mut v = Vec::new();
walk(dir, dir, &mut v);
v.sort();
v
}
#[test]
fn create_writes_every_example_as_it_is_in_the_repository() {
let d = tmp("create");
let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples");
let o = wai(&d, &["create", "--list", "--json"]);
assert!(o.status.success(), "{}", text(&o));
// The JSON envelope (envelope/1): the list is its data.
let env: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
assert_eq!((env["ok"].as_bool(), env["op"].as_str()), (Some(true), Some("project.create")), "{env}");
let list = env["data"].as_array().unwrap();
let names: Vec<&str> = list.iter().map(|t| t["template"].as_str().unwrap()).collect();
// Every example directory is a template, and every template an example.
let mut dirs: Vec<String> = std::fs::read_dir(&examples).unwrap().flatten().filter(|e| e.path().is_dir()).map(|e| e.file_name().to_string_lossy().into_owned()).collect();
dirs.sort();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(sorted, dirs, "the templates are the examples");
for t in list {
let name = t["template"].as_str().unwrap();
let generated: Vec<&str> = t["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).filter(|f| !examples.join(name).join(f).exists()).collect();
let o = wai(&d, &["create", name, "--template", name]);
assert!(o.status.success(), "{name}: {}", text(&o));
let made = example_files(&d.join(name));
let mut want = example_files(&examples.join(name));
want.extend(generated.iter().map(|g| g.to_string()));
want.sort();
assert_eq!(made, want, "{name}: the project holds the example's files, and those made for it");
for f in example_files(&examples.join(name)) {
assert_eq!(std::fs::read(d.join(name).join(&f)).unwrap(), std::fs::read(examples.join(name).join(&f)).unwrap(), "{name}/{f}: byte for byte");
}
}
// Never into a directory that holds anything.
let o = wai(&d, &["create", "video-room", "--template", "video-room"]);
assert_eq!(o.status.code(), Some(2), "{}", text(&o));
assert!(text(&o).contains("not empty"));
let o = wai(&d, &["create", "x", "--template", "no-such"]);
assert_eq!(o.status.code(), Some(2));
let _ = std::fs::remove_dir_all(&d);
}