From 5b36198577a8770a5ec68fc978e31246cdabeb7a Mon Sep 17 00:00:00 2001 From: Weksa Date: Fri, 13 Nov 2026 23:51:11 +0000 Subject: [PATCH 1/4] Mutation pins for context.rs and encode.rs: empty train, uncited speaker, nested mark Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/src/context.rs | 3 ++ crates/weksa/src/context/mutants.rs | 14 ++++++ crates/weksa/src/encode.rs | 3 ++ crates/weksa/src/encode/mutants.rs | 13 ++++++ crates/weksa/tests/mutants_c.rs | 67 +++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+) create mode 100644 crates/weksa/src/context/mutants.rs create mode 100644 crates/weksa/src/encode/mutants.rs create mode 100644 crates/weksa/tests/mutants_c.rs diff --git a/crates/weksa/src/context.rs b/crates/weksa/src/context.rs index 0ecd067..5b64758 100644 --- a/crates/weksa/src/context.rs +++ b/crates/weksa/src/context.rs @@ -759,6 +759,9 @@ impl Prompt { } } +#[cfg(test)] +mod mutants; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/weksa/src/context/mutants.rs b/crates/weksa/src/context/mutants.rs new file mode 100644 index 0000000..876816f --- /dev/null +++ b/crates/weksa/src/context/mutants.rs @@ -0,0 +1,14 @@ +//! Pins for private reach of `context.rs` that no run can produce. + +use super::*; + +/// A corpus with no live Train item of the panel's languages holds an empty +/// train. (A run refuses a panel without Train items at open, so only the +/// builder shows it.) +#[test] +fn no_train_item_of_the_panel_is_an_empty_train() { + let dir = tempfile::tempdir().unwrap(); + let corpus = CorpusStore::open(&dir.path().join("c.cc")).unwrap(); + let train = TrainItems::from_corpus(&corpus, &["en-US".to_string()]).unwrap(); + assert_eq!((train.len(), train.is_empty()), (0, true)); +} diff --git a/crates/weksa/src/encode.rs b/crates/weksa/src/encode.rs index ed3f496..500d575 100644 --- a/crates/weksa/src/encode.rs +++ b/crates/weksa/src/encode.rs @@ -566,6 +566,9 @@ pub fn packet_id(item: &CorpusItemId, rev: u32, encoder: &EncoderId) -> PacketId PacketId::parse(&format!("packet:{}", hex16(digest))).expect("a packet id is well formed") } +#[cfg(test)] +mod mutants; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/weksa/src/encode/mutants.rs b/crates/weksa/src/encode/mutants.rs new file mode 100644 index 0000000..344c2a9 --- /dev/null +++ b/crates/weksa/src/encode/mutants.rs @@ -0,0 +1,13 @@ +//! Pins for `unmark` that the import door only reaches through a whole work. + +use super::*; + +/// A form that opens another markup is malformed, though its entity id is +/// sound: the model wrote a nested or unclosed mark, not a name. +#[test] +fn a_form_that_opens_a_mark_is_malformed() { + assert_eq!(unmark("[[entity:orsa|Or[[sa]] came"), None); + // The same line without the nested opener unmarks. + let (plain, names) = unmark("[[entity:orsa|Orsa]] came").unwrap(); + assert_eq!((plain.as_str(), names.len()), ("Orsa came", 1)); +} diff --git a/crates/weksa/tests/mutants_c.rs b/crates/weksa/tests/mutants_c.rs new file mode 100644 index 0000000..dac7ca3 --- /dev/null +++ b/crates/weksa/tests/mutants_c.rs @@ -0,0 +1,67 @@ +//! Pins for behaviour of `context.rs`, `encode.rs` and `run.rs` found unpinned +//! by mutation testing. Each test states what the code does in domain terms. + +mod common; + +use std::path::{Path, PathBuf}; + +use weksa::context::Prompt; +use weksa::entity::{Entity, EntityOrigin, LanguageForms}; +use weksa::packet::{EntityId, proof_packet}; +use weksa::{CorpusItemId, CorpusStore, EntityForms, EntityStore, InventoryStore, PacketStore}; + +fn data(sub: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data").join(sub) +} + +struct Held(Vec); + +impl EntityForms for Held { + fn entity(&self, id: &EntityId) -> anyhow::Result> { + Ok(self.0.iter().find(|e| &e.id == id).cloned()) + } +} + +fn spoken(slug: &str, form: &str) -> Entity { + Entity { + id: EntityId::parse(&format!("entity:{slug}")).unwrap(), + forms: vec![LanguageForms { + language: "en-US".into(), + forms: vec![form.into()], + pronunciation: None, + }], + origin: EntityOrigin::Cast { persona: slug.into(), fixture: "f.yaml".into() }, + } +} + +/// A speaker whom no referent cites contributes no entry to the lowering +/// prompt, even when another entity is cited: only the cited entity is listed. +#[test] +fn an_uncited_speaker_is_absent_from_the_lowering_prompt() { + let cast_inv = InventoryStore::open(&data("interlingua/inventory.cc")).unwrap(); + let entities = EntityStore::open(&data("entity/entity.cc")).unwrap(); + let corpus = CorpusStore::open(&data("corpus/corpus.cc")).unwrap(); + let dir = tempfile::tempdir().unwrap(); + let view = PacketStore::open(&dir.path().join("p.cc")) + .unwrap() + .admit(proof_packet(), &cast_inv, &corpus, &entities) + .unwrap(); + let state = weksa::SpeakerStateStore::open(&dir.path().join("s.cc")).unwrap(); + let view = view.with_scene(&cast_inv.inventory().unwrap(), &state, &corpus).unwrap(); + let held = Held(vec![spoken("nibu", "Nibu"), spoken("other", "SPEAKERCANARY")]); + let item = CorpusItemId::parse("item:nibu:1").unwrap(); + let speaker = EntityId::parse("entity:other").unwrap(); + let prompt = Prompt::lower( + &view, + &item, + &speaker, + "en-US", + &cast_inv.inventory().unwrap(), + &held, + &weksa::Ontology::empty(), + ) + .unwrap(); + let text = prompt.text(); + assert!(text.contains("\n\nentities: {\"entity:nibu\":[\"Nibu\"]}\n\n"), "{text}"); + assert!(!text.contains("SPEAKERCANARY"), "{text}"); +} -- 2.47.3 From 48d444245303509c469591ab190852baed20e26a Mon Sep 17 00:00:00 2001 From: Weksa Date: Sat, 14 Nov 2026 00:13:27 +0000 Subject: [PATCH 2/4] Mutation pin for run panel refusal Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/tests/mutants_c.rs | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/weksa/tests/mutants_c.rs b/crates/weksa/tests/mutants_c.rs index dac7ca3..5aebcc7 100644 --- a/crates/weksa/tests/mutants_c.rs +++ b/crates/weksa/tests/mutants_c.rs @@ -65,3 +65,43 @@ fn an_uncited_speaker_is_absent_from_the_lowering_prompt() { assert!(text.contains("\n\nentities: {\"entity:nibu\":[\"Nibu\"]}\n\n"), "{text}"); assert!(!text.contains("SPEAKERCANARY"), "{text}"); } + +/// A run refuses a panel language that lacks a live Train or a live Validation +/// item: one no item carries, one with only a Test item, one with only Train. +#[test] +fn a_panel_language_no_item_carries_is_refused_at_open() { + let dir = tempfile::tempdir().unwrap(); + let d = dir.path(); + // Builds the corpus, entity and ontology stores the run reads. + drop(common::run_over(d)); + let entities = common::entities_in(d); + let mut corpus = CorpusStore::open(&d.join("c.cc")).unwrap(); + for (key, language, bucket) in [("item:jp:1", "ja-JP", 90), ("item:pt:1", "pt-BR", 10)] { + let mut item = common::item_in(key, &format!("line of {key}"), &common::group_in_bucket(bucket), common::cast()); + item.language = language.into(); + corpus.add(item, &entities).unwrap(); + } + drop(corpus); + let create = |panel: &[&str]| { + weksa::run::RunStores::create( + &d.join(format!("run-{}", panel.join("-"))), + "t", + weksa::run::RunPaths { + corpus: &d.join("c.cc"), + inventory: &d.join("i.cc"), + entities: &d.join("entities.cc"), + ontology: &d.join("ontology.cc"), + speaker_state: &common::speaker_state_beside(&d.join("ontology.cc")), + }, + "fake-model", + "bag", + weksa::run::LowererReach::ToolLessRelay, + weksa::corpus::ModelFamily::Unrecorded, + &panel.iter().map(|t| (*t).to_string()).collect::>(), + ) + }; + assert!(create(&["en-US"]).is_ok()); + for panel in [&["ja-JP"][..], &["pt-BR"], &["xx-XX"], &["en-US", "ja-JP"]] { + assert!(matches!(create(panel), Err(weksa::run::RunError::Panel)), "{panel:?}"); + } +} -- 2.47.3 From 9e68d2d978e6d30b155b67d45d082db4958c37a1 Mon Sep 17 00:00:00 2001 From: Weksa Date: Sat, 14 Nov 2026 00:34:13 +0000 Subject: [PATCH 3/4] Mutation pins for run recall: split rule and directory Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/src/run.rs | 3 +++ crates/weksa/src/run/mutants.rs | 21 +++++++++++++++++++++ crates/weksa/tests/mutants_c.rs | 8 ++++++++ 3 files changed, 32 insertions(+) create mode 100644 crates/weksa/src/run/mutants.rs diff --git a/crates/weksa/src/run.rs b/crates/weksa/src/run.rs index 7339a6e..15216d6 100644 --- a/crates/weksa/src/run.rs +++ b/crates/weksa/src/run.rs @@ -3112,6 +3112,9 @@ fn discard_inventory(dir: &Path) { } } +#[cfg(test)] +mod mutants; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/weksa/src/run/mutants.rs b/crates/weksa/src/run/mutants.rs new file mode 100644 index 0000000..75b1e63 --- /dev/null +++ b/crates/weksa/src/run/mutants.rs @@ -0,0 +1,21 @@ +//! Pins for private functions of `run.rs` that the doors reach only through a +//! whole run. + +use super::*; + +/// A line is probed only when it has at least four words, and then it is +/// split in two near its byte middle, the pieces trimmed. +#[test] +fn a_recall_line_splits_from_four_words_and_not_before() { + assert_eq!(recall_split("one two three"), None); + assert_eq!(recall_split(""), None); + assert_eq!( + recall_split("one two three four"), + Some(("one two".to_string(), "three four".to_string())) + ); + // No spaces: each ideograph is a word, the split falls at the middle. + assert_eq!( + recall_split("天地不仁以万物为刍狗"), + Some(("天地不仁以".to_string(), "万物为刍狗".to_string())) + ); +} diff --git a/crates/weksa/tests/mutants_c.rs b/crates/weksa/tests/mutants_c.rs index 5aebcc7..92d55f9 100644 --- a/crates/weksa/tests/mutants_c.rs +++ b/crates/weksa/tests/mutants_c.rs @@ -105,3 +105,11 @@ fn a_panel_language_no_item_carries_is_refused_at_open() { assert!(matches!(create(panel), Err(weksa::run::RunError::Panel)), "{panel:?}"); } } + +/// A run keeps its recall probes in `recall` under its own root. +#[test] +fn a_run_keeps_its_recall_under_its_root() { + let dir = tempfile::tempdir().unwrap(); + let run = common::run_over(dir.path()); + assert_eq!(run.recall_dir(), dir.path().join("run").join("recall")); +} -- 2.47.3 From aaf4eecbbf0684502ca6e3c83f64107cd2fdc41c Mon Sep 17 00:00:00 2001 From: Weksa Date: Sat, 14 Nov 2026 01:06:07 +0000 Subject: [PATCH 4/4] Format the mutation pins Co-Authored-By: Claude Sonnet 5.5 --- crates/weksa/tests/mutants_c.rs | 35 ++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/weksa/tests/mutants_c.rs b/crates/weksa/tests/mutants_c.rs index 92d55f9..70c499d 100644 --- a/crates/weksa/tests/mutants_c.rs +++ b/crates/weksa/tests/mutants_c.rs @@ -11,7 +11,9 @@ use weksa::packet::{EntityId, proof_packet}; use weksa::{CorpusItemId, CorpusStore, EntityForms, EntityStore, InventoryStore, PacketStore}; fn data(sub: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data").join(sub) + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../data") + .join(sub) } struct Held(Vec); @@ -30,7 +32,10 @@ fn spoken(slug: &str, form: &str) -> Entity { forms: vec![form.into()], pronunciation: None, }], - origin: EntityOrigin::Cast { persona: slug.into(), fixture: "f.yaml".into() }, + origin: EntityOrigin::Cast { + persona: slug.into(), + fixture: "f.yaml".into(), + }, } } @@ -47,8 +52,13 @@ fn an_uncited_speaker_is_absent_from_the_lowering_prompt() { .admit(proof_packet(), &cast_inv, &corpus, &entities) .unwrap(); let state = weksa::SpeakerStateStore::open(&dir.path().join("s.cc")).unwrap(); - let view = view.with_scene(&cast_inv.inventory().unwrap(), &state, &corpus).unwrap(); - let held = Held(vec![spoken("nibu", "Nibu"), spoken("other", "SPEAKERCANARY")]); + let view = view + .with_scene(&cast_inv.inventory().unwrap(), &state, &corpus) + .unwrap(); + let held = Held(vec![ + spoken("nibu", "Nibu"), + spoken("other", "SPEAKERCANARY"), + ]); let item = CorpusItemId::parse("item:nibu:1").unwrap(); let speaker = EntityId::parse("entity:other").unwrap(); let prompt = Prompt::lower( @@ -62,7 +72,10 @@ fn an_uncited_speaker_is_absent_from_the_lowering_prompt() { ) .unwrap(); let text = prompt.text(); - assert!(text.contains("\n\nentities: {\"entity:nibu\":[\"Nibu\"]}\n\n"), "{text}"); + assert!( + text.contains("\n\nentities: {\"entity:nibu\":[\"Nibu\"]}\n\n"), + "{text}" + ); assert!(!text.contains("SPEAKERCANARY"), "{text}"); } @@ -77,7 +90,12 @@ fn a_panel_language_no_item_carries_is_refused_at_open() { let entities = common::entities_in(d); let mut corpus = CorpusStore::open(&d.join("c.cc")).unwrap(); for (key, language, bucket) in [("item:jp:1", "ja-JP", 90), ("item:pt:1", "pt-BR", 10)] { - let mut item = common::item_in(key, &format!("line of {key}"), &common::group_in_bucket(bucket), common::cast()); + let mut item = common::item_in( + key, + &format!("line of {key}"), + &common::group_in_bucket(bucket), + common::cast(), + ); item.language = language.into(); corpus.add(item, &entities).unwrap(); } @@ -102,7 +120,10 @@ fn a_panel_language_no_item_carries_is_refused_at_open() { }; assert!(create(&["en-US"]).is_ok()); for panel in [&["ja-JP"][..], &["pt-BR"], &["xx-XX"], &["en-US", "ja-JP"]] { - assert!(matches!(create(panel), Err(weksa::run::RunError::Panel)), "{panel:?}"); + assert!( + matches!(create(panel), Err(weksa::run::RunError::Panel)), + "{panel:?}" + ); } } -- 2.47.3