use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::OnceLock;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn sample_plugin() -> &'static PathBuf {
static ARTIFACT: OnceLock<PathBuf> = OnceLock::new();
ARTIFACT.get_or_init(|| {
let root = repo_root();
let status = Command::new(env!("CARGO"))
.args(["build", "--manifest-path"])
.arg(root.join("examples/plugins/rust-lint/Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--release"])
.status()
.expect("running cargo to build the sample plugin");
assert!(status.success(), "failed to build the sample plugin");
root.join("examples/plugins/rust-lint/target/wasm32-wasip2/release/rust_lint.wasm")
})
}
fn watoots(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_watoots"))
.args(args)
.current_dir(repo_root())
.output()
.expect("running watoots")
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn policy() -> String {
repo_root()
.join("examples/policies/rust-lint.toml")
.display()
.to_string()
}
#[test]
fn inspect_without_a_manifest_shows_the_whole_bill() {
let plugin = sample_plugin().display().to_string();
let output = watoots(&["inspect", &plugin]);
let text = stdout(&output);
assert!(text.contains("capabilities"), "{text}");
assert!(text.contains("clock"), "{text}");
assert!(text.contains("DENY"), "{text}");
assert!(text.contains("your application must serve"), "{text}");
assert!(!output.status.success());
}
#[test]
fn inspect_imports_lists_the_interfaces_individually() {
let plugin = sample_plugin().display().to_string();
let output = watoots(&["inspect", &plugin, "--imports"]);
let text = stdout(&output);
assert!(text.contains("wasi:clocks/monotonic-clock"), "{text}");
assert!(text.contains("DENY"), "{text}");
assert!(!output.status.success());
}
#[test]
fn inspect_with_a_policy_and_the_served_interface_is_clean() {
let plugin = sample_plugin().display().to_string();
let output = watoots(&[
"inspect",
&plugin,
"-m",
&policy(),
"--provide",
"watoots:example/log",
]);
let text = stdout(&output);
assert!(text.contains("every import is granted"), "{text}");
assert!(output.status.success());
}
#[test]
fn record_then_replay_round_trips() {
let dir = tempfile::tempdir().unwrap();
let trace = dir.path().join("lint.wave");
let plugin = sample_plugin().display().to_string();
let recorded = watoots(&[
"record",
&plugin,
"-m",
&policy(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"lint",
"-o",
&trace.display().to_string(),
"--",
r#""notes.md""#,
r#""TODO: fix\n""#,
]);
assert!(recorded.status.success(), "{:?}", recorded);
let text = std::fs::read_to_string(&trace).unwrap();
assert!(text.starts_with("watoots-trace 1\n"), "{text}");
assert!(text.contains("export-call lint"), "{text}");
assert!(text.contains("unresolved TODO"), "{text}");
let replayed = watoots(&[
"replay",
&trace.display().to_string(),
"-c",
&plugin,
"--assert",
]);
assert!(replayed.status.success(), "{}", stdout(&replayed));
assert!(stdout(&replayed).contains("matched the trace"));
}
#[test]
fn an_edited_trace_fails_the_assert() {
let dir = tempfile::tempdir().unwrap();
let trace = dir.path().join("lint.wave");
let plugin = sample_plugin().display().to_string();
watoots(&[
"record",
&plugin,
"-m",
&policy(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"lint",
"-o",
&trace.display().to_string(),
"--",
r#""notes.md""#,
r#""TODO\n""#,
]);
let text = std::fs::read_to_string(&trace).unwrap();
std::fs::write(&trace, text.replace("1 diagnostic(s)", "7 diagnostic(s)")).unwrap();
let replayed = watoots(&[
"replay",
&trace.display().to_string(),
"-c",
&plugin,
"--assert",
]);
assert!(!replayed.status.success(), "the edit should have failed CI");
let report = stdout(&replayed);
assert!(report.contains("diverged"), "{report}");
assert!(report.contains("7 diagnostic(s)"), "{report}");
assert!(report.contains("1 diagnostic(s)"), "{report}");
}
#[test]
fn trace_fmt_converts_between_encodings() {
let dir = tempfile::tempdir().unwrap();
let text_trace = dir.path().join("lint.wave");
let binary_trace = dir.path().join("lint.wtr");
let plugin = sample_plugin().display().to_string();
watoots(&[
"record",
&plugin,
"-m",
&policy(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"name",
"-o",
&text_trace.display().to_string(),
]);
let converted = watoots(&[
"trace",
"fmt",
&text_trace.display().to_string(),
"--binary",
"-o",
&binary_trace.display().to_string(),
]);
assert!(converted.status.success());
assert!(std::fs::read(&binary_trace).unwrap().starts_with(b"WTTR"));
let back = watoots(&["trace", "fmt", &binary_trace.display().to_string()]);
assert_eq!(stdout(&back), std::fs::read_to_string(&text_trace).unwrap());
}
#[test]
fn replaying_against_the_wrong_component_is_refused() {
let dir = tempfile::tempdir().unwrap();
let trace = dir.path().join("lint.wave");
let plugin = sample_plugin().display().to_string();
let other = dir.path().join("other.wasm");
std::fs::write(&other, b"(component)").unwrap();
watoots(&[
"record",
&plugin,
"-m",
&policy(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"name",
"-o",
&trace.display().to_string(),
]);
let output = watoots(&[
"replay",
&trace.display().to_string(),
"-c",
&other.display().to_string(),
]);
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr).contains("different component"),
"{:?}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn emit_test_writes_a_runnable_fixture() {
let dir = tempfile::tempdir().unwrap();
let trace = dir.path().join("lint.wave");
let fixture = dir.path().join("replay_test.rs");
let plugin = sample_plugin().display().to_string();
watoots(&[
"record",
&plugin,
"-m",
&policy(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"name",
"-o",
&trace.display().to_string(),
]);
watoots(&[
"replay",
&trace.display().to_string(),
"-c",
&plugin,
"--emit-test",
&fixture.display().to_string(),
]);
let source = std::fs::read_to_string(&fixture).unwrap();
assert!(source.contains("#[test]"), "{source}");
assert!(source.contains("watoots_trace::replay"), "{source}");
assert!(source.contains("is_faithful"), "{source}");
}
#[test]
fn inspect_targets_reports_a_world_the_component_does_not_implement() {
let plugin = sample_plugin().display().to_string();
let wit = tempfile::Builder::new()
.suffix(".wit")
.tempfile()
.expect("creating a WIT file");
std::fs::write(
wit.path(),
"package test:other@0.1.0;\nworld formatter {\n export format: func() -> string;\n}\n",
)
.expect("writing WIT");
let output = watoots(&[
"inspect",
&plugin,
"--targets",
&wit.path().display().to_string(),
]);
let text = stdout(&output);
assert!(text.contains("does not implement world"), "{text}");
assert!(!output.status.success());
}
#[test]
fn wit_semver_check_accepts_a_world_that_demands_less() {
let dir = tempfile::tempdir().expect("temp dir");
let previous = dir.path().join("previous.wit");
let current = dir.path().join("current.wit");
std::fs::write(
&previous,
"package test:api@0.1.0;\nworld api {\n export greet: func() -> string;\n \
export farewell: func() -> string;\n}\n",
)
.unwrap();
std::fs::write(
¤t,
"package test:api@0.2.0;\nworld api {\n export greet: func() -> string;\n}\n",
)
.unwrap();
let output = watoots(&[
"wit",
"semver-check",
"--previous",
&previous.display().to_string(),
"--current",
¤t.display().to_string(),
]);
assert!(output.status.success(), "{}", stdout(&output));
}
#[test]
fn wit_semver_check_rejects_a_changed_signature() {
let dir = tempfile::tempdir().expect("temp dir");
let previous = dir.path().join("previous.wit");
let current = dir.path().join("current.wit");
std::fs::write(
&previous,
"package test:api@0.1.0;\nworld api {\n export greet: func() -> string;\n}\n",
)
.unwrap();
std::fs::write(
¤t,
"package test:api@0.2.0;\nworld api {\n export greet: func(formal: bool) -> string;\n}\n",
)
.unwrap();
let output = watoots(&[
"wit",
"semver-check",
"--previous",
&previous.display().to_string(),
"--current",
¤t.display().to_string(),
]);
assert!(!output.status.success(), "{}", stdout(&output));
assert!(
stdout(&output).contains("incompatible"),
"{}",
stdout(&output)
);
}
const ROLLS: &str = r#"
(component
(type (;0;)
(instance
(type (;0;) (func (result u64)))
(export (;0;) "get-random-u64" (func (type 0)))
)
)
(import "wasi:random/random@0.2.9" (instance $r (type 0)))
(alias export $r "get-random-u64" (func $rand_fn))
(core func $rand_lowered (canon lower (func $rand_fn)))
(core module $m
(import "r" "get-random-u64" (func $rand (result i64)))
(func (export "roll") (param $n i32) (result i64) (call $rand))
)
(core instance $r_i (export "get-random-u64" (func $rand_lowered)))
(core instance $i (instantiate $m (with "r" (instance $r_i))))
(func $roll (param "n" u32) (result u64) (canon lift (core func $i "roll")))
(export "roll" (func $roll))
)
"#;
fn rolling_plugin(determinism: bool) -> (tempfile::TempDir, String, String, String) {
let dir = tempfile::tempdir().expect("temp dir");
let component = dir.path().join("rolls.wasm");
let manifest = dir.path().join("policy.toml");
let out = dir.path().join("findings");
std::fs::write(&component, ROLLS).unwrap();
std::fs::write(
&manifest,
if determinism {
"[permissions]\nrandom = true\n\n[signature]\nrequired = false\n"
} else {
"[permissions]\nrandom = true\n\n[determinism]\nenabled = false\n\n\
[signature]\nrequired = false\n"
},
)
.unwrap();
(
dir,
component.display().to_string(),
manifest.display().to_string(),
out.display().to_string(),
)
}
#[test]
fn fuzz_reports_nothing_when_a_session_reproduces() {
let (_dir, component, manifest, out) = rolling_plugin(true);
let output = watoots(&[
"fuzz", &component, "-m", &manifest, "--cases", "8", "--out", &out,
]);
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
assert!(output.status.success(), "{stderr}{}", stdout(&output));
assert!(stderr.contains("no findings"), "{stderr}");
}
#[test]
fn fuzz_catches_a_session_that_does_not_reproduce() {
let (_dir, component, manifest, out) = rolling_plugin(false);
let output = watoots(&[
"fuzz", &component, "-m", &manifest, "--cases", "8", "--out", &out,
]);
let text = stdout(&output);
assert!(!output.status.success(), "{text}");
assert!(text.contains("crash 0"), "{text}");
assert!(text.contains("diverged"), "{text}");
let crash = std::path::Path::new(&out).join("crash-000.wave");
assert!(crash.is_file(), "{text}");
let recorded = std::fs::read_to_string(&crash).unwrap();
assert!(recorded.starts_with("watoots-trace 1\n"), "{recorded}");
assert!(recorded.contains("export-call roll"), "{recorded}");
assert!(
text.contains("watoots replay") && text.contains("--emit-test"),
"a crash has to say how to become a test: {text}"
);
let replayed = watoots(&[
"replay",
&crash.display().to_string(),
"--component",
&component,
"--assert",
]);
assert!(!replayed.status.success(), "{}", stdout(&replayed));
}
#[test]
fn fuzz_reproduces_a_case_from_its_seed() {
let (_dir, component, manifest, out) = rolling_plugin(true);
let run = || {
let output = watoots(&[
"fuzz", &component, "-m", &manifest, "--cases", "1", "--seed", "9137", "--out", &out,
]);
assert!(output.status.success());
let trace = std::path::Path::new(&out).join("seed.wave");
let recorded = watoots(&[
"record",
&component,
"-m",
&manifest,
"-c",
"roll",
"-o",
&trace.display().to_string(),
"7",
]);
assert!(recorded.status.success());
std::fs::read_to_string(&trace).unwrap()
};
assert_eq!(run(), run());
}
#[test]
fn fuzz_refuses_an_export_the_component_does_not_have() {
let (_dir, component, manifest, out) = rolling_plugin(true);
let output = watoots(&[
"fuzz", &component, "-m", &manifest, "--call", "nope", "--out", &out,
]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("no function \"nope\""), "{stderr}");
assert!(
stderr.contains("roll"),
"a refusal should say what there is: {stderr}"
);
}
#[test]
fn profile_reports_the_three_buckets_and_names_the_functions() {
let plugin = sample_plugin().display().to_string();
let output = watoots(&[
"profile",
&plugin,
"-m",
&policy(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"lint",
"--repeat",
"5",
"--",
r#""notes.md""#,
r#""TODO: fix\n""#,
]);
assert!(output.status.success(), "{output:?}");
let text = stdout(&output);
assert!(text.contains("5 call(s)"), "{text}");
assert!(text.contains("guest"), "{text}");
assert!(text.contains("host call"), "{text}");
assert!(text.contains("marshalling"), "{text}");
assert!(text.contains("marshalling is the remainder"), "{text}");
assert!(text.contains("export lint"), "{text}");
assert!(
text.contains("import watoots:example/log@0.1.0#emit"),
"{text}"
);
}
#[test]
fn profile_writes_a_firefox_profile_when_asked() {
let dir = tempfile::tempdir().unwrap();
let json = dir.path().join("guest.json");
let plugin = sample_plugin().display().to_string();
let output = watoots(&[
"profile",
&plugin,
"-m",
&policy(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"lint",
"--firefox",
&json.display().to_string(),
"--",
r#""notes.md""#,
r#""TODO: fix\n""#,
]);
assert!(output.status.success(), "{output:?}");
let written = std::fs::read_to_string(&json).unwrap();
assert!(written.starts_with('{'), "the processed profile is JSON");
assert!(written.contains("\"threads\""), "and it has threads");
}
#[test]
fn profiling_and_recording_are_separate_subcommands() {
let plugin = sample_plugin().display().to_string();
let output = watoots(&["profile", &plugin, "-c", "lint", "--output", "t.wave"]);
assert!(!output.status.success());
let text = String::from_utf8_lossy(&output.stderr).into_owned();
assert!(text.contains("unexpected argument"), "{text}");
}
fn asset_plugin() -> &'static str {
static ARTIFACT: OnceLock<()> = OnceLock::new();
ARTIFACT.get_or_init(|| {
let root = repo_root();
let crate_dir = root.join("examples/plugins/rust-asset");
let status = Command::new(env!("CARGO"))
.args(["build", "--manifest-path"])
.arg(crate_dir.join("Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--release"])
.status()
.expect("running cargo to build the asset plugin");
assert!(status.success(), "failed to build the asset plugin");
std::fs::copy(
crate_dir.join("target/wasm32-wasip2/release/rust_asset.wasm"),
crate_dir.join("rust_asset.wasm"),
)
.expect("installing the artifact beside its luts/");
});
"examples/plugins/rust-asset/rust_asset.wasm"
}
fn asset_policy() -> &'static str {
"examples/policies/rust-asset.toml"
}
const ASSET_IMAGE: &str =
"{width: 2, height: 2, pixels: [10, 20, 30, 200, 100, 50, 255, 255, 255, 0, 0, 0]}";
#[test]
fn inspect_reports_the_asset_plugin_as_wanting_exactly_one_directory() {
let output = watoots(&[
"inspect",
asset_plugin(),
"-m",
asset_policy(),
"--provide",
"watoots:asset/log",
]);
let text = stdout(&output);
assert!(text.contains("reads ${plugin_dir}/luts"), "{text}");
assert!(text.contains("network -"), "{text}");
assert!(text.contains("every import is granted"), "{text}");
assert!(output.status.success(), "{output:?}");
}
#[test]
fn describe_lists_bare_operation_kinds() {
let output = watoots(&[
"run",
asset_plugin(),
"-m",
asset_policy(),
"--answer",
"watoots:asset/log@0.1.0#emit=",
"-c",
"describe",
]);
assert!(output.status.success(), "{output:?}");
assert_eq!(
stdout(&output).trim(),
r#"{name: "rust-asset", supports: [grayscale, invert, gain, resize, lut]}"#
);
}
#[test]
fn the_asset_plugin_does_not_load_without_the_filesystem_grant() {
let dir = tempfile::tempdir().unwrap();
let policy = dir.path().join("no-fs.toml");
let text = std::fs::read_to_string(repo_root().join(asset_policy())).unwrap();
let stripped: String = text
.lines()
.filter(|line| !line.starts_with("fs.read"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&policy, stripped).unwrap();
let output = watoots(&[
"run",
asset_plugin(),
"-m",
&policy.display().to_string(),
"--answer",
"watoots:asset/log@0.1.0#emit=",
"-c",
"apply",
"--",
ASSET_IMAGE,
"[invert]",
]);
assert!(!output.status.success(), "{output:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("wasi:filesystem"), "{stderr}");
}
#[test]
fn the_asset_plugin_reads_its_lookup_table_through_the_grant() {
let output = watoots(&[
"run",
asset_plugin(),
"-m",
asset_policy(),
"--answer",
"watoots:asset/log@0.1.0#emit=",
"-c",
"apply",
"--",
ASSET_IMAGE,
r#"[grayscale, lut("examples/plugins/rust-asset/luts/sepia.lut")]"#,
]);
assert!(output.status.success(), "{output:?}");
assert_eq!(
stdout(&output).trim(),
"ok({width: 2, height: 2, pixels: [24, 22, 17, 168, 149, 116, 255, 255, 239, 0, 0, 0]})"
);
}
#[test]
fn a_lut_the_grant_does_not_cover_is_an_answer_and_not_a_crash() {
let output = watoots(&[
"run",
asset_plugin(),
"-m",
asset_policy(),
"--answer",
"watoots:asset/log@0.1.0#emit=",
"-c",
"apply",
"--",
ASSET_IMAGE,
r#"[lut("examples/policies/rust-asset.toml")]"#,
]);
assert!(output.status.success(), "{output:?}");
let text = stdout(&output);
assert!(
text.starts_with(r#"err(unreadable({path: "examples/policies/rust-asset.toml", reason: "#),
"{text}"
);
assert!(text.contains("check the manifest"), "{text}");
}
#[test]
fn a_malformed_image_comes_back_as_a_failure_rather_than_a_trap() {
let output = watoots(&[
"run",
asset_plugin(),
"-m",
asset_policy(),
"--answer",
"watoots:asset/log@0.1.0#emit=",
"-c",
"apply",
"--",
"{width: 2, height: 2, pixels: [10, 20, 30, 200, 100, 50, 255, 255, 255, 0, 0]}",
"[invert]",
]);
assert!(output.status.success(), "{output:?}");
let text = stdout(&output);
assert!(text.starts_with("err(malformed("), "{text}");
assert!(text.contains("12 bytes"), "{text}");
assert!(text.contains("got 11"), "{text}");
}
const COUNTER_V1: &str = r#"
(component
(core module $m
(global $n (mut i32) (i32.const 0))
(func (export "get") (result i32) (global.get $n))
(func (export "bump") (param i32)
(global.set $n (i32.add (global.get $n) (local.get 0))))
(func (export "save") (result i32) (global.get $n))
(func (export "restore") (param i32) (global.set $n (local.get 0))))
(core instance $i (instantiate $m))
(func $get (result u32) (canon lift (core func $i "get")))
(func $bump (param "n" u32) (canon lift (core func $i "bump")))
(func $save (result u32) (canon lift (core func $i "save")))
(func $restore (param "state" u32) (canon lift (core func $i "restore")))
(export "get" (func $get))
(export "bump" (func $bump))
(export "save-state" (func $save))
(export "restore-state" (func $restore))
)
"#;
const COUNTER_V2: &str = r#"
(component
(core module $m
(global $n (mut i32) (i32.const 0))
(func (export "bump") (param i32) (result i32)
(global.set $n (i32.add (global.get $n) (local.get 0)))
(i32.add (global.get $n) (i32.const 1000)))
(func (export "save") (result i32) (global.get $n))
(func (export "restore") (param i32) (global.set $n (local.get 0))))
(core instance $i (instantiate $m))
(func $bump (param "n" u32) (result u32) (canon lift (core func $i "bump")))
(func $save (result u32) (canon lift (core func $i "save")))
(func $restore (param "state" u32) (canon lift (core func $i "restore")))
(export "bump" (func $bump))
(export "save-state" (func $save))
(export "restore-state" (func $restore))
)
"#;
const COUNTER_WANTS_NETWORK: &str = r#"
(component
(import "wasi:sockets/tcp@0.2.6" (instance (export "connect" (func))))
(core module $m
(global $n (mut i32) (i32.const 0))
(func (export "bump") (param i32) (result i32)
(global.set $n (i32.add (global.get $n) (local.get 0)))
(global.get $n)))
(core instance $i (instantiate $m))
(func $bump (param "n" u32) (result u32) (canon lift (core func $i "bump")))
(export "bump" (func $bump))
)
"#;
fn counter_dir(replacement: &str) -> (tempfile::TempDir, String, String) {
let dir = tempfile::tempdir().expect("temp dir");
let first = dir.path().join("counter.wat");
let second = dir.path().join("counter-next.wat");
std::fs::write(&first, COUNTER_V1).expect("writing the first build");
std::fs::write(&second, replacement).expect("writing the replacement");
let (a, b) = (first.display().to_string(), second.display().to_string());
(dir, a, b)
}
#[test]
fn reload_calls_swaps_and_calls_again() {
let (_dir, first, second) = counter_dir(COUNTER_V2);
let output = watoots(&["reload", &first, "--to", &second, "-c", "bump", "--", "7"]);
assert!(output.status.success(), "{output:?}");
let text = stdout(&output);
assert_eq!(text.trim(), "1014", "{text}");
let log = String::from_utf8_lossy(&output.stderr).into_owned();
assert!(log.contains("reload 1"), "{log}");
assert!(log.contains("state: carried"), "{log}");
}
#[test]
fn reload_re_runs_the_grant_check_against_the_new_bytes() {
let (_dir, first, second) = counter_dir(COUNTER_WANTS_NETWORK);
let output = watoots(&["reload", &first, "--to", &second, "-c", "bump", "--", "1"]);
assert!(!output.status.success(), "{output:?}");
let log = String::from_utf8_lossy(&output.stderr).into_owned();
assert!(log.contains("wasi:sockets"), "{log}");
assert!(log.contains("not granted"), "{log}");
}
#[test]
fn diff_of_a_build_against_itself_is_quiet_and_succeeds() {
let plugin = sample_plugin().display().to_string();
let output = watoots(&["diff", &plugin, &plugin, "-m", &policy()]);
let text = stdout(&output);
assert!(text.contains("imports\n (unchanged)"), "{text}");
assert!(text.contains("exports\n (unchanged)"), "{text}");
assert!(text.contains("needs no new grants"), "{text}");
assert!(output.status.success(), "{text}");
}
#[test]
fn diff_names_the_manifest_key_a_new_import_would_need() {
let previous = sample_plugin().display().to_string();
let current = asset_plugin().to_string();
let output = watoots(&["diff", &previous, ¤t, "-m", &policy()]);
let text = stdout(&output);
assert!(text.contains("+ wasi:filesystem/types"), "{text}");
assert!(text.contains("permissions.fs.read"), "{text}");
assert!(text.contains("reload would refuse this build"), "{text}");
assert!(!output.status.success(), "{text}");
}
#[test]
fn diff_reports_a_removed_export_as_breaking_callers() {
let previous = sample_plugin().display().to_string();
let current = asset_plugin().to_string();
let output = watoots(&["diff", &previous, ¤t, "-m", &policy()]);
let text = stdout(&output);
assert!(text.contains("- lint"), "{text}");
assert!(text.contains("callers of this break"), "{text}");
assert!(text.contains("export(s) removed"), "{text}");
}
#[test]
fn diff_does_not_file_an_application_interface_as_a_permission_problem() {
let previous = sample_plugin().display().to_string();
let current = asset_plugin().to_string();
let output = watoots(&["diff", &previous, ¤t, "-m", &policy()]);
let text = stdout(&output);
assert!(text.contains("watoots:asset/log"), "{text}");
assert!(
text.contains("your application must serve"),
"an application interface must not be reported as a denial: {text}"
);
let log_line = text
.lines()
.find(|line| line.contains("watoots:asset/log"))
.unwrap_or_default();
assert!(
!log_line.contains("NOT GRANTED"),
"not a permission problem: {log_line}"
);
}
#[test]
fn diff_without_a_manifest_reports_changes_without_judging_them() {
let previous = sample_plugin().display().to_string();
let current = asset_plugin().to_string();
let output = watoots(&["diff", &previous, ¤t]);
let text = stdout(&output);
assert!(text.contains("+ wasi:filesystem/types"), "{text}");
assert!(text.contains("pass -m"), "{text}");
}
#[test]
fn a_lost_import_is_news_and_the_two_failure_kinds_stay_separate() {
let output = watoots(&[
"diff",
asset_plugin(),
&sample_plugin().display().to_string(),
"-m",
asset_policy(),
"--provide",
"watoots:example/log",
]);
let text = stdout(&output);
assert!(text.contains("- wasi:filesystem/types"), "{text}");
assert!(text.contains("no longer needed"), "{text}");
assert!(text.contains("this update needs no new grants"), "{text}");
assert!(text.contains("export(s) removed"), "{text}");
assert!(!output.status.success(), "{text}");
}
#[test]
fn running_an_unverified_plugin_says_so_loudly_and_says_why() {
let output = watoots(&[
"run",
&sample_plugin().display().to_string(),
"-m",
&policy(),
"--answer",
"watoots:example/log#emit=",
"-c",
"name",
]);
let text = String::from_utf8_lossy(&output.stderr);
assert!(text.contains("running unverified"), "{text}");
assert!(text.contains("replace that file"), "{text}");
assert!(text.contains("cosign sign-blob"), "{text}");
let out = stdout(&output);
assert!(!out.contains("running unverified"), "{out}");
}
#[test]
fn inspect_reports_the_signature_posture_alongside_the_capabilities() {
let plugin = sample_plugin().display().to_string();
let output = watoots(&["inspect", &plugin, "-m", &policy()]);
let text = stdout(&output);
assert!(text.contains("publisher"), "{text}");
assert!(text.contains("signature"), "{text}");
assert!(text.contains("any bytes at this path load"), "{text}");
assert!(text.contains("signature WARN"), "{text}");
}
#[test]
fn record_keeps_the_trace_when_the_call_fails_because_that_is_the_bug() {
let dir = tempfile::tempdir().unwrap();
let trace = dir.path().join("bug.wave");
let policy = dir.path().join("tight.toml");
std::fs::write(
&policy,
"[permissions]\nclocks = \"monotonic\"\nenv = {}\n\n\
[limits]\ntransfer = 64\n\n[signature]\nrequired = false\n",
)
.unwrap();
let plugin = sample_plugin().display().to_string();
let source = format!("\"{}\"", "TODO x\\n".repeat(400));
let recorded = watoots(&[
"record",
&plugin,
"-m",
&policy.display().to_string(),
"--answer",
"watoots:example/log@0.1.0#emit=",
"-c",
"lint",
"-o",
&trace.display().to_string(),
"--",
r#""notes.md""#,
&source,
]);
assert!(!recorded.status.success());
let err = String::from_utf8_lossy(&recorded.stderr);
assert!(
err.contains("copied between the host and the guest"),
"{err}"
);
let text = std::fs::read_to_string(&trace).expect("the trace was written");
assert!(text.contains("export-call lint"), "{text}");
assert!(text.contains("WT_ERR_LIMIT_EXCEEDED"), "{text}");
let replayed = watoots(&[
"replay",
&trace.display().to_string(),
"-c",
&plugin,
"--assert",
]);
assert!(
replayed.status.success(),
"{}",
String::from_utf8_lossy(&replayed.stderr)
);
assert!(
stdout(&replayed).contains("matched"),
"{}",
stdout(&replayed)
);
}
#[test]
fn record_writes_nothing_when_the_failure_was_before_the_call() {
let dir = tempfile::tempdir().unwrap();
let trace = dir.path().join("nothing.wave");
let not_a_component = dir.path().join("garbage.wasm");
std::fs::write(¬_a_component, b"this is not a component").unwrap();
let recorded = watoots(&[
"record",
¬_a_component.display().to_string(),
"-c",
"lint",
"-o",
&trace.display().to_string(),
]);
assert!(!recorded.status.success());
assert!(!trace.exists(), "no call happened, so no trace");
}