diff --git a/crates/didbot/tests/conformance/records.rs b/crates/didbot/tests/conformance/records.rs index 5b0c41a1..994a97ad 100644 --- a/crates/didbot/tests/conformance/records.rs +++ b/crates/didbot/tests/conformance/records.rs @@ -134,3 +134,120 @@ fn the_subject_of_the_vectors_is_a_record() { "the upstream catalog no longer defines {SUBJECT}" ); } + +// --------------------------------------------------------------------------- +// Whole documents, rather than records against one +// --------------------------------------------------------------------------- + +/// One document vector: a name and the lexicon document itself. +#[derive(Debug, Deserialize)] +struct DocumentVector { + /// What the vector is testing, used verbatim in failure reports. + name: String, + /// The document that must be accepted or refused. + lexicon: serde_json::Value, +} + +/// Whether this implementation accepts `document` as a lexicon document. +/// +/// The pipeline a document actually travels: parsed into a [`LexiconDoc`], +/// then compiled by [`Catalog::insert`]. Either step refusing is a refusal — +/// nothing downstream ever sees a document that failed one of them. +fn accepts(document: &serde_json::Value) -> Result<(), String> { + let parsed: LexiconDoc = + serde_json::from_value(document.clone()).map_err(|error| error.to_string())?; + Catalog::new() + .insert(&parsed) + .map_err(|error| error.to_string()) +} + +/// Vectors this implementation does not yet refuse, and what would fix each. +/// +/// Not excusals — upstream is right about every one of them. They are named +/// here so the corpus can run at all, and each is a live claim: a gap that +/// starts being refused fails [`lexicon_document_vectors`] as stale and has +/// to be struck from this list, so it cannot quietly outlive the bug. +/// +/// All four live in the schema compiler, which is `didbot-pds`'s, and in the +/// `id` field's type, which is `didbot-lexicon`'s. +const GAPS: &[(&str, &str)] = &[ + ( + "invalid NSID", + "`LexiconDoc::id` is a `String` and nothing checks it against \ + `didbot::nsid_syntax::validate`, which this crate already runs the \ + NSID vectors through", + ), + ( + "defined unknown", + "`unknown` is a field type, not a definition; the compiler does not \ + distinguish the two positions", + ), + ( + "defined ref", + "a `ref` is a field type for the same reason, and to an NSID this \ + catalog does not hold", + ), + ( + "non-main primary", + "a `record` is a primary definition and may only be named `main`; \ + the compiler does not check where a definition sits", + ), +]; + +/// Every document in `lexicon-valid.json` is accepted and every document in +/// `lexicon-invalid.json` is refused, but for the gaps named in [`GAPS`]. +/// +/// Upstream ships these beside the record vectors and nothing here ran them, +/// so "this implementation reads lexicon documents" was a claim resting on +/// the five documents in `catalog/` all being well-formed. These are the ten +/// that are not, and they found four things. +#[test] +fn lexicon_document_vectors() { + let mut failures = Vec::new(); + let mut checked = 0usize; + let mut seen = vec![false; GAPS.len()]; + + for (file, expect_valid) in [("lexicon-valid.json", true), ("lexicon-invalid.json", false)] { + for vector in read::>(file) { + let outcome = accepts(&vector.lexicon); + if let Some(index) = GAPS.iter().position(|(name, _)| *name == vector.name) { + seen[index] = true; + if outcome.is_err() { + failures.push(format!( + "{file}: {:?} is refused now; strike it from GAPS", + vector.name + )); + } + continue; + } + checked += 1; + match (expect_valid, outcome) { + (true, Err(err)) => failures.push(format!( + "{file}: {:?} should be accepted, but was refused: {err}", + vector.name + )), + (false, Ok(())) => failures.push(format!( + "{file}: {:?} should be refused, but was accepted", + vector.name + )), + _ => {} + } + } + } + + for (index, (name, why)) in GAPS.iter().enumerate() { + if !seen[index] { + failures.push(format!( + "the gap {name:?} ({why}) is no longer in the vectors; drop it" + )); + } + } + + assert!( + failures.is_empty(), + "{} of {checked} lexicon document vectors did not conform:\n {}", + failures.len(), + failures.join("\n ") + ); + assert!(checked > 0, "the lexicon document vectors ran nothing at all"); +}