SPEC §3.1 defines the pin a manifest carries (conditioning.prior, and a WAI2 rendition's prior): id, sha256, determinism, and the optional form, tier and supersedes. The digest covers the whole parameter set. A set of several files is pinned through its listing, the new registered prior form wai.prior.set. A sink verifies the pin before it decodes, reports a refusal with one of an ordered set of snake_case reasons, and decodes only from the set it verified. Tier is a closed vocabulary; supersedes is informative. A pin that does not resolve makes its capability unsupported for that envelope: SPEC §4 dispatch continues at the fallback, §7.1 selection at the next rendition, and an inert envelope's error names the reasons. A rendition entry may carry id, sha256 and, on a companion, companion_of_id. SPEC §7 gains a Digests subsection, and §8 says how a MINOR revision adds an optional member. In wai-rs: the manifest types are #[non_exhaustive] with builders; a new pin module verifies pins in memory and on disk; Wai::dispatch_verified, WaiMulti::pick and primary check pins and component digests; every neural decoder reads through ParamSource; a codec-free `ffi` feature carries the C envelope surface with wai_envelope_select and wai_neural_decode_ex. wai-web wraps wai::container. New vectors: the int_mlicpp set-form twin and the pin-conformance corpus with an independent Python verifier, plus sha256.json sidecars for the vectors of capabilities that decode against a parameter set. Squashed from wai/stream-a-parameter-set-identity onto link 12. Conflicts are resolved to keep both sides; the integration commits that follow make the two streams agree. Co-Authored-By: Claude Opus 5.5 <[email protected]>
92 lines
5.4 KiB
Rust
92 lines
5.4 KiB
Rust
//! The manifest types are `#[non_exhaustive]`, so code outside the crate builds
|
|
//! them with constructors and builders. These tests run from outside the crate:
|
|
//! the builders write exactly the JSON the earlier struct literals wrote, a
|
|
//! committed envelope round-trips byte for byte, and members the Rust types model
|
|
//! only as `extra` (or a `security` object) survive a parse and a re-serialization.
|
|
|
|
use wai::container::{
|
|
Conditioning, Manifest, ManifestV2, ModelRequirement, Prior, Rendition, RenditionMeta, Security,
|
|
Supersedes, Wai, WaiMulti,
|
|
};
|
|
|
|
#[test]
|
|
fn builders_write_the_json_a_literal_wrote() {
|
|
let m = Manifest::new(
|
|
"image",
|
|
"replicate",
|
|
ModelRequirement::new("wai.neural.bmshj2018").with_fallback("wai.image.png"),
|
|
Conditioning::new("bmshj2018").with_prior(Prior::new("q3", "ab".repeat(32), "int-rans")),
|
|
)
|
|
.with_target(serde_json::json!({"w": 256, "h": 256}));
|
|
assert_eq!(
|
|
serde_json::to_string(&m).unwrap(),
|
|
format!(
|
|
r#"{{"wai":"1.0","media":"image","intent":"replicate","model_requirement":{{"capability":"wai.neural.bmshj2018","fallback":"wai.image.png"}},"conditioning":{{"kind":"bmshj2018","prior":{{"id":"q3","sha256":"{}","determinism":"int-rans"}}}},"target":{{"w":256,"h":256}}}}"#,
|
|
"ab".repeat(32)
|
|
)
|
|
);
|
|
let plain = Manifest::new("text", "replicate", ModelRequirement::new("wai.text.zstd"), Conditioning::new("zstd"));
|
|
assert_eq!(
|
|
serde_json::to_string(&plain).unwrap(),
|
|
r#"{"wai":"1.0","media":"text","intent":"replicate","model_requirement":{"capability":"wai.text.zstd"},"conditioning":{"kind":"zstd"},"target":null}"#
|
|
);
|
|
let semantic = ModelRequirement::new("wai.semantic.clip").with_min_sim(0.5).with_sim_metric("dreamsim");
|
|
assert_eq!(
|
|
serde_json::to_string(&semantic).unwrap(),
|
|
r#"{"capability":"wai.semantic.clip","min_sim":0.5,"sim_metric":"dreamsim"}"#
|
|
);
|
|
let v2 = ManifestV2::new("audio", "replicate", vec![
|
|
RenditionMeta::new("wai.neural.encodec32", "encodec_tokens").with_prior(Prior::new("e", "cd".repeat(32), "int")),
|
|
RenditionMeta::new("wai.meta.hdr10plus", "descriptor").companion_to("wai.video.av1"),
|
|
])
|
|
.with_target(serde_json::json!({"sr": 48000}));
|
|
assert_eq!(
|
|
serde_json::to_string(&v2).unwrap(),
|
|
format!(
|
|
r#"{{"wai":"1.1","media":"audio","intent":"replicate","renditions":[{{"capability":"wai.neural.encodec32","kind":"encodec_tokens","prior":{{"id":"e","sha256":"{}","determinism":"int"}}}},{{"capability":"wai.meta.hdr10plus","kind":"descriptor","role":"companion","companion_of":"wai.video.av1"}}],"target":{{"sr":48000}}}}"#,
|
|
"cd".repeat(32)
|
|
)
|
|
);
|
|
let sup = Prior::new("b", "bb".repeat(32), "int").with_supersedes(Supersedes::new("aa".repeat(32)).with_form("wai.prior.set"));
|
|
assert!(serde_json::to_string(&sup).unwrap().ends_with(&format!(
|
|
r#""supersedes":{{"sha256":"{}","form":"wai.prior.set"}}}}"#, "aa".repeat(32))));
|
|
let with_sec = plain.with_wai("1.1").with_security(Security::new("mls-sframe", 7));
|
|
assert!(serde_json::to_string(&with_sec).unwrap().contains(r#""security":{"encryption":"mls-sframe","kid":7}"#));
|
|
}
|
|
|
|
#[test]
|
|
fn the_committed_int_mlicpp_envelope_round_trips() {
|
|
let d = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_mlicpp");
|
|
for name in ["stream.wai", "stream_set.wai"] {
|
|
let bytes = std::fs::read(d.join(name)).unwrap();
|
|
assert_eq!(Wai::from_bytes(&bytes).unwrap().to_bytes().unwrap(), bytes, "{name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn security_and_unknown_members_survive() {
|
|
let v1 = r#"{"wai":"1.0","media":"text","intent":"replicate","model_requirement":{"capability":"wai.text.zstd","later_req":1},"conditioning":{"kind":"zstd","later_cond":[1,2]},"security":{"encryption":"mls-sframe","kid":3,"later_sec":"x"},"target":null,"later_top":{"a":true}}"#;
|
|
let mut env = b"WAI1".to_vec();
|
|
env.extend_from_slice(&(v1.len() as u32).to_le_bytes());
|
|
env.extend_from_slice(v1.as_bytes());
|
|
env.extend_from_slice(&2u32.to_le_bytes());
|
|
env.extend_from_slice(b"ct");
|
|
let w = Wai::from_bytes(&env).unwrap();
|
|
assert_eq!(w.manifest.security.as_ref().and_then(|s| s.encryption.as_deref()), Some("mls-sframe"));
|
|
assert!(!w.manifest.security.as_ref().unwrap().is_plaintext());
|
|
assert_eq!(w.to_bytes().unwrap(), env, "nothing is dropped and the key order is kept");
|
|
|
|
let v2 = r#"{"wai":"1.1","media":"video","intent":"replicate","renditions":[{"capability":"wai.video.av1","kind":"av1","id":"main","later_entry":{"n":1}}],"target":null,"later_top":2}"#;
|
|
let manifest: ManifestV2 = serde_json::from_str(v2).unwrap();
|
|
let m = WaiMulti::from_parts(manifest, vec![b"AV1".to_vec()]).unwrap();
|
|
let back = WaiMulti::from_bytes(&m.to_bytes().unwrap()).unwrap();
|
|
assert_eq!(serde_json::to_string(&back.manifest).unwrap(), v2);
|
|
assert_eq!(back.manifest.renditions[0].extra["later_entry"]["n"], 1);
|
|
// The payloads must match the entries.
|
|
let one: ManifestV2 = serde_json::from_str(v2).unwrap();
|
|
assert!(WaiMulti::from_parts(one, vec![]).is_err());
|
|
// Rendition builders carry the identity members.
|
|
let r = Rendition::media("wai.image.png", "png", b"PNG".to_vec()).with_id("p").with_payload_digest();
|
|
assert_eq!(r.meta.id.as_deref(), Some("p"));
|
|
assert_eq!(r.meta.sha256.as_deref(), Some(wai::pin::sha256_hex(b"PNG").as_str()));
|
|
}
|