open-standards/wai/wai-rs/tests/cli_generate_score.rs
Transaction Science 7e3b883074 wai generate.spectral: the model is the parameter set, so the envelope carries no pin; inspect and unwrap report the payload
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]>
2026-10-09 17:08:26 -04:00

487 lines
25 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `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(&notes), "-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(&note.replace("1717986918", "0.4"), ""), "a gain with a fraction"),
(score(&note.replace("1717986918", "4294967297"), ""), "a gain past 1.0"),
(score(&note.replace("50960238", "4294967296"), ""), "a phase_inc of a whole cycle"),
(score(&note.replace("\"waveform\":2", "\"waveform\":4"), ""), "a waveform past 3"),
(score(&note.replace("\"start\":0", "\"start\":-1"), ""), "a negative start"),
(score(&note.replace("\"release\":50", "\"release\":101"), ""), "a release the duration would cut"),
(score(&note.replace("\"dur\":100,", "\"dur\":100,\"pitch\":60,"), ""), "a member that is not a note field"),
(score(&note.replace(",\"release\":50", ""), ""), "a missing note field"),
(score(&note.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}");
}