Form v0 names the model inside the payload by the SHA-256 of one file, and SPEC §5
never said how that relates to §3.1's parameter-set pin, which governs every
GenerativeDeterministic capability: whether a generate envelope carries a pin, or
which wins when a pin and `model` disagree. The library registers the set as
several files, so a form-less pin over the very file `model` names would be
refused form_required. SPEC §5 now states the relation: the model file is the
parameter set and `model` its identity; an envelope that names the generator
carries no `prior` and a sink refuses one that does, whatever it names. §3.1 says
so beside the pin rules, the non-conforming list names it, and prior-carriage §7's
row says the set is never pinned and names the file. No library source changes.
`wai inspect` and `wai unwrap` reported a generate envelope with a corrupt
payload as conforming ("component roles ✓", no payload line). They now report
the payload (form v0, with the reader's reason), the intent and any pin of every
envelope or WAI2 component that names the generator, ✓ or ✗; inspect's verdict
says the envelope does not conform. inspect_and_unwrap_report_the_generators_envelope
covers a prompt byte changed in place, the intent improve, a pin over the model
file itself, and a WAI2 component with and without a pin.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
487 lines
25 KiB
Rust
487 lines
25 KiB
Rust
//! `wai generate` and `wai score build|render`, run as a user runs them: the native
|
||
//! pins a page checks its own output against. Each output is checked against a
|
||
//! committed vector — `tests/vectors/generate/` for pictures, `score-conformance/`
|
||
//! for scores — and every refusal is checked to write nothing.
|
||
//!
|
||
//! Cargo.toml declares this target with required-features = ["generate", "score"], so a
|
||
//! run without them stops with an error instead of reporting an empty binary as passed.
|
||
//! The two larger listed sizes of `instr-v0.json` take seconds in a debug build and run
|
||
//! in the release step.
|
||
|
||
use std::path::{Path, PathBuf};
|
||
use std::process::{Command, Output};
|
||
|
||
use serde_json::Value;
|
||
use wai::container::Wai;
|
||
|
||
fn root() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||
}
|
||
fn gen_dir() -> PathBuf {
|
||
root().join("tests/vectors/generate")
|
||
}
|
||
fn score_dir() -> PathBuf {
|
||
root().join("../score-conformance")
|
||
}
|
||
fn model() -> PathBuf {
|
||
gen_dir().join("generate_spectral_model.json")
|
||
}
|
||
fn jload(p: &Path) -> Value {
|
||
serde_json::from_slice(&std::fs::read(p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))).unwrap()
|
||
}
|
||
fn sha256_hex(b: &[u8]) -> String {
|
||
wai::pin::sha256(b).iter().map(|x| format!("{x:02x}")).collect()
|
||
}
|
||
|
||
fn scratch(name: &str) -> PathBuf {
|
||
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_generate_score");
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
let p = dir.join(name);
|
||
let _ = std::fs::remove_file(&p);
|
||
p
|
||
}
|
||
|
||
fn wai(args: &[&str]) -> Output {
|
||
Command::new(env!("CARGO_BIN_EXE_wai")).args(args).output().expect("run wai")
|
||
}
|
||
|
||
fn ok(run: &Output) -> String {
|
||
assert!(run.status.success(), "wai failed: {}", String::from_utf8_lossy(&run.stderr));
|
||
String::from_utf8(run.stdout.clone()).unwrap()
|
||
}
|
||
|
||
fn refused(run: &Output, out: &Path, what: &str) -> String {
|
||
let stderr = String::from_utf8_lossy(&run.stderr).into_owned();
|
||
assert!(!run.status.success(), "{what}: must be refused; stdout: {}", String::from_utf8_lossy(&run.stdout));
|
||
assert!(!out.exists(), "{what}: a refusal writes nothing");
|
||
stderr
|
||
}
|
||
|
||
/// The `--json` line `wai generate` prints, checked against the file it wrote.
|
||
fn made(stdout: &str, out: &Path, w: u64, h: u64) -> String {
|
||
let v: Value = serde_json::from_str(stdout.trim()).unwrap();
|
||
let rgb = std::fs::read(out).unwrap();
|
||
assert_eq!((v["w"].as_u64(), v["h"].as_u64()), (Some(w), Some(h)));
|
||
assert_eq!(v["bytes"].as_u64(), Some(w * h * 3));
|
||
assert_eq!(rgb.len() as u64, w * h * 3, "RGB8, w·h·3 bytes");
|
||
let sha = v["sha256"].as_str().unwrap().to_string();
|
||
assert_eq!(sha256_hex(&rgb), sha, "the printed SHA-256 is the file's");
|
||
sha
|
||
}
|
||
|
||
fn m() -> String {
|
||
model().to_string_lossy().into_owned()
|
||
}
|
||
fn p(path: &Path) -> &str {
|
||
path.to_str().unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn generate_reproduces_the_committed_vectors() {
|
||
// 96×96: the committed vector and its SHA-256 sidecar.
|
||
let pin = jload(&gen_dir().join("generate.json"));
|
||
let side = jload(&gen_dir().join("sha256.json"));
|
||
let out = scratch("p96.rgb");
|
||
let seed = pin["seed"].to_string();
|
||
let steps = pin["steps"].to_string();
|
||
let run = wai(&["generate", "--model", &m(), "--prompt", pin["prompt"].as_str().unwrap(), "--seed", &seed,
|
||
"--steps", &steps, "--size", "96x96", "-o", p(&out), "--json"]);
|
||
assert_eq!(made(&ok(&run), &out, 96, 96), side["generate"]["sha256"].as_str().unwrap());
|
||
// sizes.json: the same prompt, seed and steps at its non-square sizes.
|
||
let sizes = jload(&gen_dir().join("sizes.json"));
|
||
for s in sizes["sizes"].as_array().unwrap() {
|
||
let (w, h) = (s["w"].as_u64().unwrap(), s["h"].as_u64().unwrap());
|
||
let out = scratch(&format!("p{w}x{h}.rgb"));
|
||
let run = wai(&["generate", "--model", &m(), "--prompt", sizes["prompt"].as_str().unwrap(), "--seed", &seed,
|
||
"--steps", &steps, "--size", &format!("{w}x{h}"), "-o", p(&out), "--json"]);
|
||
assert_eq!(made(&ok(&run), &out, w, h), s["sha256"].as_str().unwrap(), "{w}x{h}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn generate_refuses_flags_outside_form_v0() {
|
||
let base = ["generate", "--model", &m(), "--prompt", "calm ocean", "--seed", "3", "--steps", "4"];
|
||
for (extra, what) in [
|
||
(&["--size", "0x128"][..], "a side of 0"),
|
||
(&["--size", "4097x1"][..], "a side past 4096"),
|
||
(&["--size", "4096x4096"][..], "more than 2^23 pixels"),
|
||
(&["--size", "256"][..], "not WxH"),
|
||
] {
|
||
let out = scratch("flags.rgb");
|
||
let mut args: Vec<&str> = base.to_vec();
|
||
args.extend_from_slice(extra);
|
||
args.extend_from_slice(&["-o", p(&out)]);
|
||
refused(&wai(&args), &out, what);
|
||
}
|
||
for (flag, value, what) in [
|
||
("--seed", "4294967296", "a seed past 2^32 - 1"),
|
||
("--steps", "0", "steps below 1"),
|
||
("--steps", "65", "steps past 64"),
|
||
("--prompt", "a \"quoted\" prompt", "a quotation mark in the prompt"),
|
||
] {
|
||
let out = scratch("flags.rgb");
|
||
let mut args: Vec<String> = base.iter().map(|s| s.to_string()).collect();
|
||
let i = args.iter().position(|a| a == flag).unwrap();
|
||
args[i + 1] = value.into();
|
||
args.extend(["--size", "64x32", "-o", p(&out)].map(String::from));
|
||
let args: Vec<&str> = args.iter().map(String::as_str).collect();
|
||
refused(&wai(&args), &out, what);
|
||
}
|
||
}
|
||
|
||
/// The vector's object, written as `--write-instr` writes it from the same flags.
|
||
#[test]
|
||
fn write_instr_writes_the_vectors_object() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = v["object"].as_str().unwrap();
|
||
let o: Value = serde_json::from_str(object).unwrap();
|
||
let sizes: Vec<String> = o["sizes"].as_array().unwrap().iter().map(|s| format!("{}x{}", s[0], s[1])).collect();
|
||
let out = scratch("written.instr");
|
||
ok(&wai(&["generate", "--model", &m(), "--prompt", o["prompt"].as_str().unwrap(), "--seed", &o["seed"].to_string(),
|
||
"--steps", &o["steps"].to_string(), "--sizes", &sizes.join(","), "--write-instr", p(&out)]));
|
||
assert_eq!(std::fs::read(&out).unwrap(), object.as_bytes());
|
||
}
|
||
|
||
fn instr_sizes(min_pixels: u64, max_pixels: u64) -> usize {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
// A file of its own per caller: the two tests that call this run at once, and each
|
||
// removes and rewrites its object before its `wai` children read it.
|
||
let object = scratch(&format!("object-{min_pixels}.instr"));
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
let mut n = 0;
|
||
for e in v["outputs"].as_array().unwrap() {
|
||
let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap());
|
||
if !(min_pixels..=max_pixels).contains(&(w * h)) {
|
||
continue;
|
||
}
|
||
let out = scratch(&format!("instr{w}x{h}.rgb"));
|
||
let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"),
|
||
"-o", p(&out), "--json"]);
|
||
assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{w}x{h}");
|
||
n += 1;
|
||
}
|
||
n
|
||
}
|
||
|
||
#[test]
|
||
fn instr_makes_the_listed_small_sizes() {
|
||
assert_eq!(instr_sizes(0, 640 * 320), 2);
|
||
}
|
||
|
||
#[test]
|
||
#[cfg_attr(debug_assertions, ignore = "1280x720 and 1920x1080 take seconds in a debug build; the release step runs them")]
|
||
fn instr_makes_the_listed_large_sizes() {
|
||
assert_eq!(instr_sizes(640 * 320 + 1, u64::MAX), 2);
|
||
}
|
||
|
||
/// Every object the vector lists as refused is refused by `wai generate --instr` and
|
||
/// by `wai wrap`, each for the reason the vector states (the first rule the reader
|
||
/// finds broken), and neither writes anything. Holding the reason, not only the
|
||
/// refusal, notices a rule that stops refusing while another rule still refuses the
|
||
/// same object: a quotation mark in the prompt, say, which the canonical-bytes check
|
||
/// would refuse if the prompt rule let it through. The vector's own object is accepted
|
||
/// by both, and the envelope carries it byte for byte with intent `create`.
|
||
#[test]
|
||
fn the_vectors_refused_objects_are_refused() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let cases = v["refused"].as_array().unwrap();
|
||
assert!(cases.len() >= 30, "the vector lists its refusals");
|
||
let object = scratch("refused.instr");
|
||
for c in cases {
|
||
let why = c["why"].as_str().unwrap();
|
||
let reason = c["reason"].as_str().unwrap_or_else(|| panic!("{why}: the vector states no reason"));
|
||
std::fs::write(&object, c["object"].as_str().unwrap()).unwrap();
|
||
let out = scratch("refused.rgb");
|
||
let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "256x128",
|
||
"-o", p(&out)]), &out, why);
|
||
assert!(stderr.contains(&format!("refused: {reason}: ")), "{why}: refused, but not as {reason}: {stderr}");
|
||
let env = scratch("refused.wai");
|
||
let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]),
|
||
&env, why);
|
||
assert!(stderr.contains("instruction object of form v0") && stderr.contains(&format!(": {reason}: ")),
|
||
"{why}: refused, but not as {reason}: {stderr}");
|
||
}
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
let env = scratch("accepted.wai");
|
||
ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]));
|
||
let e = Wai::read(&env).unwrap();
|
||
assert_eq!((e.capability(), e.intent()), ("wai.generate.spectral", "create"));
|
||
assert_eq!(e.payload, v["object"].as_str().unwrap().as_bytes());
|
||
}
|
||
|
||
/// Every object the vector lists as accepted — one at each inclusive bound of form v0 —
|
||
/// is accepted by `wai generate --instr`, which makes its first listed size with the
|
||
/// SHA-256 the vector states, and by `wai wrap`, whose envelope carries it byte for
|
||
/// byte. A reader stricter than form v0 by one refuses one of them.
|
||
#[test]
|
||
fn the_vectors_accepted_objects_are_accepted() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let cases = v["accepted"].as_array().unwrap();
|
||
assert!(cases.len() >= 12, "the vector lists an object at each bound");
|
||
let object = scratch("accepted.instr");
|
||
for c in cases {
|
||
let why = c["why"].as_str().unwrap();
|
||
std::fs::write(&object, c["object"].as_str().unwrap()).unwrap();
|
||
for e in c["outputs"].as_array().unwrap() {
|
||
let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap());
|
||
let out = scratch("accepted.rgb");
|
||
let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"),
|
||
"-o", p(&out), "--json"]);
|
||
assert!(run.status.success(), "{why}: refused: {}", String::from_utf8_lossy(&run.stderr));
|
||
assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{why}: {w}x{h}");
|
||
}
|
||
let env = scratch("accepted-bound.wai");
|
||
let run = wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]);
|
||
assert!(run.status.success(), "{why}: wrap refused it: {}", String::from_utf8_lossy(&run.stderr));
|
||
assert_eq!(Wai::read(&env).unwrap().payload, c["object"].as_str().unwrap().as_bytes(), "{why}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn instr_refuses_another_model_and_an_unlisted_size() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = scratch("model.instr");
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
// The same model with one more byte (a line feed): the same parameters, another digest.
|
||
let other = scratch("other-model.json");
|
||
let mut bytes = std::fs::read(model()).unwrap();
|
||
bytes.push(b'\n');
|
||
std::fs::write(&other, bytes).unwrap();
|
||
let out = scratch("model.rgb");
|
||
let stderr = refused(&wai(&["generate", "--model", p(&other), "--instr", p(&object), "--size", "256x128",
|
||
"-o", p(&out)]), &out, "another model file");
|
||
assert!(stderr.contains("the object's model is sha256:"), "{stderr}");
|
||
let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "96x96", "-o", p(&out)]),
|
||
&out, "an unlisted size");
|
||
assert!(stderr.contains("not a listed size"), "{stderr}");
|
||
}
|
||
|
||
#[test]
|
||
fn wrap_refuses_replicate_over_the_generator() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = scratch("replicate.instr");
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
for extra in [&[][..], &["--intent", "replicate"][..]] {
|
||
let env = scratch("replicate.wai");
|
||
let mut args = vec!["wrap", "wai.generate.spectral", p(&object), "-o", p(&env)];
|
||
args.extend_from_slice(extra);
|
||
let stderr = refused(&wai(&args), &env, "replicate over a generator");
|
||
assert!(stderr.contains("GenerativeDeterministic"), "{stderr}");
|
||
}
|
||
}
|
||
|
||
/// SPEC §5: an envelope that names the generator carries the intent `create`. `wai wrap`
|
||
/// writes no other: not `improve`, not a string the manifest schema leaves open, and not
|
||
/// with the generator as the fallback of another capability.
|
||
#[test]
|
||
fn wrap_writes_the_generator_only_with_intent_create() {
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = scratch("intent.instr");
|
||
std::fs::write(&object, v["object"].as_str().unwrap()).unwrap();
|
||
for intent in ["improve", "banana", "Create", ""] {
|
||
let env = scratch("intent.wai");
|
||
let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", intent, "-o", p(&env)]),
|
||
&env, &format!("intent {intent:?}"));
|
||
assert!(stderr.contains("carries the intent `create`"), "{intent:?}: {stderr}");
|
||
}
|
||
let env = scratch("intent.wai");
|
||
let stderr = refused(&wai(&["wrap", "wai.image.png", p(&object), "--fallback", "wai.generate.spectral",
|
||
"--intent", "improve", "-o", p(&env)]), &env, "the generator as the fallback");
|
||
assert!(stderr.contains("carries the intent `create`"), "{stderr}");
|
||
ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]));
|
||
}
|
||
|
||
/// SPEC §5: `wai inspect` and `wai unwrap` report an envelope that names the generator —
|
||
/// its payload, its intent and any pin — ✓ when it conforms, ✗ with the reason when it
|
||
/// does not. Each bad `WAI1` differs from a good one in one respect: one prompt byte
|
||
/// changed to 0x01 in place (the envelope still parses; the payload is no longer JSON,
|
||
/// since a string holds no raw control character), the intent `improve`, or a pin
|
||
/// over the very model file `model` names. A `WAI2` component of the generator is
|
||
/// reported the same way, with and without a pin.
|
||
#[test]
|
||
fn inspect_and_unwrap_report_the_generators_envelope() {
|
||
use wai::container::{ManifestV2, Prior, RenditionMeta, WaiMulti};
|
||
let v = jload(&gen_dir().join("instr-v0.json"));
|
||
let object = v["object"].as_str().unwrap();
|
||
let input = scratch("report.instr");
|
||
std::fs::write(&input, object).unwrap();
|
||
let good = scratch("report-good.wai");
|
||
ok(&wai(&["wrap", "wai.generate.spectral", p(&input), "--intent", "create", "-o", p(&good)]));
|
||
let report = |env: &Path| (ok(&wai(&["unwrap", p(env)])), ok(&wai(&["inspect", p(env)])));
|
||
let (u, i) = report(&good);
|
||
assert!(u.contains("conformance \u{2713} an instruction object of form v0, intent create, no pin"), "{u}");
|
||
assert!(i.contains("instruction: \u{2713}") && !i.contains("does not conform"), "{i}");
|
||
|
||
let mut bytes = std::fs::read(&good).unwrap();
|
||
let at = bytes.windows(4).position(|w| w == b"calm").unwrap();
|
||
bytes[at] = 0x01;
|
||
let bad_payload = scratch("report-payload.wai");
|
||
std::fs::write(&bad_payload, &bytes).unwrap();
|
||
let mut e = Wai::read(&good).unwrap();
|
||
e.manifest.intent = "improve".into();
|
||
let bad_intent = scratch("report-intent.wai");
|
||
e.write(&bad_intent).unwrap();
|
||
let model_sha = sha256_hex(&std::fs::read(model()).unwrap());
|
||
let pin = Prior::new("the spectral model", model_sha, "integer generator");
|
||
let mut e = Wai::read(&good).unwrap();
|
||
e.manifest.conditioning.prior = Some(pin.clone());
|
||
let bad_pin = scratch("report-pin.wai");
|
||
e.write(&bad_pin).unwrap();
|
||
for (env, said) in [(&bad_payload, "not_json"), (&bad_intent, "the intent is `improve`"), (&bad_pin, "carries a pin")] {
|
||
let (u, i) = report(env);
|
||
assert!(u.contains("conformance \u{2717}") && u.contains(said), "{said}: {u}");
|
||
assert!(i.contains("instruction: \u{2717}") && i.contains(said) && i.contains("does not conform"),
|
||
"{said}: {i}");
|
||
}
|
||
|
||
for (pinned, name) in [(false, "report-two.wai"), (true, "report-two-pin.wai")] {
|
||
let mut meta = RenditionMeta::new("wai.generate.spectral", "bytes");
|
||
if pinned {
|
||
meta = meta.with_prior(pin.clone());
|
||
}
|
||
let two = WaiMulti::from_parts(ManifestV2::new("image", "create", vec![meta]), vec![object.as_bytes().to_vec()]).unwrap();
|
||
let path = scratch(name);
|
||
two.write(&path).unwrap();
|
||
let u = ok(&wai(&["unwrap", p(&path), "--component", "0"]));
|
||
let i = ok(&wai(&["inspect", p(&path)]));
|
||
let mark = if pinned { "\u{2717}" } else { "\u{2713}" };
|
||
assert!(u.contains(&format!("conformance [0] {mark}")), "{name}: {u}");
|
||
assert!(i.contains(&format!("[0] instruction: {mark}")), "{name}: {i}");
|
||
assert_eq!(pinned, u.contains("carries a pin") && i.contains("carries a pin"), "{name}");
|
||
}
|
||
}
|
||
|
||
/// Every score of the corpus: `build` from its authoring form writes the committed
|
||
/// `WSCR` byte for byte, and `render` prints, and `--pcm` writes, the digests
|
||
/// `expected_hashes.json` states.
|
||
#[test]
|
||
fn score_build_and_render_reproduce_the_corpus() {
|
||
let expected = jload(&score_dir().join("expected_hashes.json"));
|
||
let entries = expected.as_object().unwrap();
|
||
assert_eq!(entries.len(), 3);
|
||
for (file, spec) in entries {
|
||
let name = file.strip_suffix(".wscr").unwrap();
|
||
let committed = std::fs::read(score_dir().join("vectors").join(file)).unwrap();
|
||
let built = scratch(&format!("{name}.wscr"));
|
||
let notes = score_dir().join("notes").join(format!("{name}.json"));
|
||
let stdout = ok(&wai(&["score", "build", p(¬es), "-o", p(&built), "--json"]));
|
||
assert_eq!(std::fs::read(&built).unwrap(), committed, "{name}: built byte for byte");
|
||
let b: Value = serde_json::from_str(stdout.trim()).unwrap();
|
||
assert_eq!(b["bytes"].as_u64(), Some(committed.len() as u64));
|
||
assert_eq!(b["score_hash"], spec["score_hash"], "{name}");
|
||
|
||
let pcm = scratch(&format!("{name}.s16"));
|
||
let stdout = ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"]));
|
||
let r: Value = serde_json::from_str(stdout.trim()).unwrap();
|
||
let duration = spec["duration"].as_u64().unwrap();
|
||
assert_eq!(r["samples"].as_u64(), Some(duration), "{name}: the contract's duration, whole");
|
||
assert_eq!(r["sample_rate"].as_u64(), Some(22050));
|
||
assert_eq!(r["notes"], spec["notes"]);
|
||
assert_eq!(r["pcm_sha256"], spec["pcm_sha256"], "{name}");
|
||
assert_eq!(r["mixdown_blake3"], spec["mixdown_hash"], "{name}");
|
||
assert_eq!(r["score_hash"], spec["score_hash"], "{name}");
|
||
let samples = std::fs::read(&pcm).unwrap();
|
||
assert_eq!(samples.len() as u64, duration * 2, "{name}: i16 samples");
|
||
assert_eq!(Some(sha256_hex(&samples).as_str()), spec["pcm_sha256"].as_str(), "{name}: the written PCM");
|
||
|
||
// The score travels under intent `replicate`: its class is StandardDefined.
|
||
let env = scratch(&format!("{name}.wai"));
|
||
ok(&wai(&["wrap", "wai.audio.score", p(&built), "--intent", "replicate", "-o", p(&env)]));
|
||
let e = Wai::read(&env).unwrap();
|
||
assert_eq!((e.capability(), e.intent()), ("wai.audio.score", "replicate"));
|
||
assert_eq!(e.payload, committed);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn score_build_refuses_what_is_not_integer_notes_rendered_whole() {
|
||
let note = r#"{"start":0,"dur":100,"phase_inc":50960238,"gain":1717986918,"waveform":2,"attack":10,"decay":10,"sustain":2147483648,"release":50}"#;
|
||
let score = |note: &str, top: &str| format!(r#"{{"sample_rate":22050,"channels":1,"duration":200{top},"notes":[{note}]}}"#);
|
||
// The base case builds.
|
||
let input = scratch("base.json");
|
||
std::fs::write(&input, score(note, "")).unwrap();
|
||
let out = scratch("base.wscr");
|
||
ok(&wai(&["score", "build", p(&input), "-o", p(&out)]));
|
||
for (doc, what) in [
|
||
(score(¬e.replace("1717986918", "0.4"), ""), "a gain with a fraction"),
|
||
(score(¬e.replace("1717986918", "4294967297"), ""), "a gain past 1.0"),
|
||
(score(¬e.replace("50960238", "4294967296"), ""), "a phase_inc of a whole cycle"),
|
||
(score(¬e.replace("\"waveform\":2", "\"waveform\":4"), ""), "a waveform past 3"),
|
||
(score(¬e.replace("\"start\":0", "\"start\":-1"), ""), "a negative start"),
|
||
(score(¬e.replace("\"release\":50", "\"release\":101"), ""), "a release the duration would cut"),
|
||
(score(¬e.replace("\"dur\":100,", "\"dur\":100,\"pitch\":60,"), ""), "a member that is not a note field"),
|
||
(score(¬e.replace(",\"release\":50", ""), ""), "a missing note field"),
|
||
(score(¬e.replace("\"start\":0", "\"start\":0,\"start\":5"), ""), "a note field given twice"),
|
||
(score(note, ",\"duration\":300"), "a document member given twice"),
|
||
(score(note, ",\"numeric\":\"wai.det.float\""), "another numeric profile"),
|
||
(score(note, "").replace("\"channels\":1", "\"channels\":2"), "two channels"),
|
||
(score(note, "").replace("\"duration\":200", "\"duration\":67108865"), "a duration past 2^26 samples"),
|
||
("[]".to_string(), "not an object"),
|
||
] {
|
||
std::fs::write(&input, &doc).unwrap();
|
||
let out = scratch("refused.wscr");
|
||
let stderr = refused(&wai(&["score", "build", p(&input), "-o", p(&out)]), &out, what);
|
||
assert!(stderr.contains("refused"), "{what}: {stderr}");
|
||
}
|
||
}
|
||
|
||
/// `render` refuses a contract it would misreport: the reference mixdown is one channel
|
||
/// in `wai.det.fixed64`, so a contract stating two channels (a verifier would read the
|
||
/// samples as stereo frames) or another numeric profile is refused, as `build` refuses
|
||
/// to write one. Each case is the committed scale.wscr with its contract patched in place.
|
||
#[test]
|
||
fn score_render_refuses_a_contract_it_does_not_render() {
|
||
let committed = std::fs::read(score_dir().join("vectors/scale.wscr")).unwrap();
|
||
let patch = |from: &str, to: &str| {
|
||
assert_eq!(from.len(), to.len(), "a patch in place keeps every offset");
|
||
let at = committed.windows(from.len()).position(|w| w == from.as_bytes()).unwrap();
|
||
let mut b = committed.clone();
|
||
b[at..at + to.len()].copy_from_slice(to.as_bytes());
|
||
b
|
||
};
|
||
for (bytes, what, said) in [
|
||
(patch("\"channels\":1", "\"channels\":2"), "two channels", "2 channels"),
|
||
(patch("wai.det.fixed64", "wai.det.float64"), "another numeric profile", "wai.det.float64"),
|
||
] {
|
||
let input = scratch("contract.wscr");
|
||
std::fs::write(&input, bytes).unwrap();
|
||
let pcm = scratch("contract.s16");
|
||
let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm), "--json"]), &pcm, what);
|
||
assert!(stderr.contains("refused") && stderr.contains(said), "{what}: {stderr}");
|
||
}
|
||
}
|
||
|
||
/// A `WSCR` that declares more notes than its notes section holds is refused before the
|
||
/// reader reserves room for them: a file of a few bytes can declare 2^32 − 1. The file
|
||
/// carries a well-formed contract (section 0x01) and a notes section (0x02) holding only
|
||
/// the count, so without the check the reader would pass every other rule and reach the
|
||
/// reservation; the check, not a missing section, is what refuses it.
|
||
#[test]
|
||
fn score_render_refuses_a_note_count_its_bytes_cannot_hold() {
|
||
let contract = wai::score::ScoreContract {
|
||
sample_rate: 22050, numeric: wai::world::NUMERIC_FIXED64.into(), channels: 1, duration: 100,
|
||
}
|
||
.to_canonical_json();
|
||
let mut b = b"WSCR".to_vec();
|
||
b.extend_from_slice(&2u16.to_le_bytes());
|
||
for (kind, off, len) in [(0x01u8, 0u32, contract.len() as u32), (0x02, contract.len() as u32, 4)] {
|
||
b.push(kind);
|
||
b.extend_from_slice(&off.to_le_bytes());
|
||
b.extend_from_slice(&len.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(&contract);
|
||
b.extend_from_slice(&u32::MAX.to_le_bytes());
|
||
// The same file declaring no notes renders: the contract and the layout are good.
|
||
let mut empty = b.clone();
|
||
let n = empty.len();
|
||
empty[n - 4..].copy_from_slice(&0u32.to_le_bytes());
|
||
let input = scratch("empty.wscr");
|
||
std::fs::write(&input, empty).unwrap();
|
||
let r: Value = serde_json::from_str(ok(&wai(&["score", "render", p(&input), "--json"])).trim()).unwrap();
|
||
assert_eq!((r["samples"].as_u64(), r["notes"].as_u64()), (Some(100), Some(0)), "{r}");
|
||
let input = scratch("huge.wscr");
|
||
std::fs::write(&input, b).unwrap();
|
||
let pcm = scratch("huge.s16");
|
||
let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm)]), &pcm, "2^32 - 1 notes in 4 bytes");
|
||
assert!(stderr.contains("declares 4294967295 notes"), "{stderr}");
|
||
}
|