469 lines
13 KiB
Rust
469 lines
13 KiB
Rust
mod common;
|
|
use std::process::{Command, Output};
|
|
use weksa::{InventoryStore, PacketStore};
|
|
|
|
fn weksa(dir: &std::path::Path, args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_weksa"))
|
|
.current_dir(dir)
|
|
.args([
|
|
"--corpus",
|
|
"./c.cc",
|
|
"--inventory",
|
|
"./i.cc",
|
|
"--entities",
|
|
"./e.cc",
|
|
])
|
|
.args(args)
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
|
|
/// Admits the cast entities the tests cite.
|
|
fn cast_entities(dir: &std::path::Path, slugs: &[&str]) {
|
|
for slug in slugs {
|
|
let id = format!("entity:{slug}");
|
|
let form = format!("en-US={}", slug.to_uppercase());
|
|
let put = weksa(
|
|
dir,
|
|
&[
|
|
"entity",
|
|
"put",
|
|
&id,
|
|
"--form",
|
|
&form,
|
|
"--origin",
|
|
"cast",
|
|
"--persona",
|
|
slug,
|
|
],
|
|
);
|
|
assert!(put.status.success(), "{}", out(&put));
|
|
}
|
|
}
|
|
|
|
fn out(o: &Output) -> String {
|
|
String::from_utf8_lossy(&o.stdout).into_owned() + &String::from_utf8_lossy(&o.stderr)
|
|
}
|
|
|
|
const ADD: [&str; 14] = [
|
|
"corpus",
|
|
"add",
|
|
"--slug",
|
|
"nibu",
|
|
"--text",
|
|
"Mind the hatch.",
|
|
"--speaker",
|
|
"entity:nibu",
|
|
"--group",
|
|
"scene-1",
|
|
"--admitted-on",
|
|
"2026-10-29",
|
|
"--source",
|
|
"cast-authored",
|
|
];
|
|
|
|
#[test]
|
|
fn seed_add_dump_split_withdraw() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
cast_entities(d.path(), &["nibu"]);
|
|
let seeded = weksa(d.path(), &["inventory", "seed"]);
|
|
assert!(seeded.status.success(), "{}", out(&seeded));
|
|
assert!(out(&seeded).contains("seeded rev:1"));
|
|
let again = weksa(d.path(), &["inventory", "seed"]);
|
|
assert!(
|
|
!again.status.success(),
|
|
"a second seed is refused: {}",
|
|
out(&again)
|
|
);
|
|
let dump = out(&weksa(d.path(), &["inventory", "dump"]));
|
|
assert!(
|
|
dump.contains("rev:1") && dump.contains("speech_act:5") && dump.contains("warn"),
|
|
"{dump}"
|
|
);
|
|
|
|
// A cast-authored item needs its persona and fixture.
|
|
let missing = weksa(d.path(), &ADD);
|
|
assert!(
|
|
!missing.status.success() && out(&missing).contains("--persona"),
|
|
"{}",
|
|
out(&missing)
|
|
);
|
|
let mut add = ADD.to_vec();
|
|
add.extend(["--persona", "nibu", "--fixture", "f.yaml"]);
|
|
let added = weksa(d.path(), &add);
|
|
assert!(
|
|
added.status.success() && out(&added).contains("Added item:nibu:1"),
|
|
"{}",
|
|
out(&added)
|
|
);
|
|
// The same text under a new key is refused (S6); another text is not.
|
|
let copy = weksa(d.path(), &add);
|
|
assert!(
|
|
!copy.status.success() && out(©).contains("S6TextHeld"),
|
|
"{}",
|
|
out(©)
|
|
);
|
|
let mut other = add.clone();
|
|
other[5] = "Mind the other hatch.";
|
|
let second = weksa(d.path(), &other);
|
|
assert!(
|
|
out(&second).contains("Added item:nibu:2"),
|
|
"{}",
|
|
out(&second)
|
|
);
|
|
|
|
let dump = out(&weksa(d.path(), &["corpus", "dump"]));
|
|
assert!(
|
|
dump.contains("item:nibu:1")
|
|
&& dump.contains("Mind the hatch.")
|
|
&& dump.contains("CastAuthored"),
|
|
"{dump}"
|
|
);
|
|
let split = out(&weksa(d.path(), &["split", "item:nibu:1"]));
|
|
assert!(
|
|
["Train", "Validation", "Test"]
|
|
.iter()
|
|
.any(|s| split.trim() == *s),
|
|
"{split}"
|
|
);
|
|
assert!(!weksa(d.path(), &["split", "item:nibu:9"]).status.success());
|
|
|
|
let withdrawn = weksa(d.path(), &["corpus", "withdraw", "item:nibu:1"]);
|
|
assert!(withdrawn.status.success());
|
|
assert!(out(&weksa(d.path(), &["corpus", "dump"])).contains("withdrawn"));
|
|
}
|
|
|
|
#[test]
|
|
fn model_written_and_public_domain_sources_name_their_provenance() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
cast_entities(d.path(), &["s", "player"]);
|
|
let base = [
|
|
"corpus",
|
|
"add",
|
|
"--slug",
|
|
"x",
|
|
"--speaker",
|
|
"entity:s",
|
|
"--group",
|
|
"g",
|
|
"--admitted-on",
|
|
"2026-10-29",
|
|
];
|
|
// S6 refuses one text under two keys, so each run writes its own.
|
|
let runs = std::cell::Cell::new(0);
|
|
let run = |extra: &[&str]| {
|
|
runs.set(runs.get() + 1);
|
|
let text = format!("line {}", runs.get());
|
|
let mut args = base.to_vec();
|
|
args.extend_from_slice(&["--text", &text]);
|
|
args.extend_from_slice(extra);
|
|
weksa(d.path(), &args)
|
|
};
|
|
let no_family = run(&["--source", "model-written"]);
|
|
assert!(
|
|
!no_family.status.success() && out(&no_family).contains("--model-family"),
|
|
"{}",
|
|
out(&no_family)
|
|
);
|
|
let not_an_enum_value = run(&["--source", "model-written", "--model-family", "m"]);
|
|
assert!(
|
|
!not_an_enum_value.status.success(),
|
|
"{}",
|
|
out(¬_an_enum_value)
|
|
);
|
|
let model = run(&[
|
|
"--source",
|
|
"model-written",
|
|
"--model-family",
|
|
"open-ai",
|
|
"--generator",
|
|
"g",
|
|
]);
|
|
assert!(model.status.success(), "{}", out(&model));
|
|
let dump = weksa(d.path(), &["corpus", "dump"]);
|
|
assert!(
|
|
out(&dump).contains("OpenAi") && out(&dump).contains("generator: Some(\"g\")"),
|
|
"{}",
|
|
out(&dump)
|
|
);
|
|
let no_work = run(&["--source", "public-domain", "--author", "a"]);
|
|
assert!(
|
|
!no_work.status.success() && out(&no_work).contains("--work"),
|
|
"{}",
|
|
out(&no_work)
|
|
);
|
|
let pd = run(&[
|
|
"--source",
|
|
"public-domain",
|
|
"--work",
|
|
"w",
|
|
"--author",
|
|
"a",
|
|
"--edition-url",
|
|
"https://example.org/w",
|
|
]);
|
|
assert!(pd.status.success(), "{}", out(&pd));
|
|
let bad_turn = run(&[
|
|
"--source",
|
|
"operator-written",
|
|
"--prior-turn",
|
|
"no equals sign",
|
|
]);
|
|
assert!(
|
|
!bad_turn.status.success() && out(&bad_turn).contains("speaker=text"),
|
|
"{}",
|
|
out(&bad_turn)
|
|
);
|
|
let turn = run(&[
|
|
"--source",
|
|
"operator-written",
|
|
"--prior-turn",
|
|
"entity:player=hello",
|
|
]);
|
|
assert!(turn.status.success(), "{}", out(&turn));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_reports_each_packet() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let w = common::world();
|
|
let full = w.dir.path().join("i.cc");
|
|
let packets = d.path().join("p.cc");
|
|
let packet = common::good();
|
|
PacketStore::open(&packets)
|
|
.unwrap()
|
|
.admit(packet.clone(), &w.inventory, &w.corpus, &w.entities)
|
|
.unwrap();
|
|
|
|
let validate = |inventory: &std::path::Path, extra: &[&str]| {
|
|
Command::new(env!("CARGO_BIN_EXE_weksa"))
|
|
.arg("--inventory")
|
|
.arg(inventory)
|
|
.arg("validate")
|
|
.arg(&packets)
|
|
.args(extra)
|
|
.output()
|
|
.unwrap()
|
|
};
|
|
let ok = validate(&full, &[]);
|
|
assert!(
|
|
ok.status.success() && out(&ok).contains(&format!("ok {}", packet.id)),
|
|
"{}",
|
|
out(&ok)
|
|
);
|
|
assert!(validate(&full, &[packet.id.as_str()]).status.success());
|
|
assert!(!validate(&full, &["packet:missing"]).status.success());
|
|
// Against an inventory that only reaches revision 1 the same packet is refused, by rule.
|
|
let short = d.path().join("short.cc");
|
|
let (rev1, entries1) = weksa::inventory::seed();
|
|
InventoryStore::open(&short)
|
|
.unwrap()
|
|
.admit_revision(rev1, entries1)
|
|
.unwrap();
|
|
let refused = validate(&short, &[]);
|
|
assert!(
|
|
!refused.status.success() && out(&refused).contains("V10"),
|
|
"{}",
|
|
out(&refused)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn schema_writes_the_published_files() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let o = weksa(d.path(), &["schema", "--out", "s"]);
|
|
assert!(o.status.success(), "{}", out(&o));
|
|
let mut names: Vec<String> = std::fs::read_dir(d.path().join("s"))
|
|
.unwrap()
|
|
.map(|e| e.unwrap().file_name().into_string().unwrap())
|
|
.collect();
|
|
names.sort();
|
|
assert_eq!(
|
|
names,
|
|
[
|
|
"weksa.corpus_item.schema.json",
|
|
"weksa.entity.schema.json",
|
|
"weksa.interlingua_packet.schema.json",
|
|
"weksa.inventory_entry.schema.json",
|
|
"weksa.inventory_revision.schema.json"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn entity_put_and_name_spans() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let put = |args: &[&str]| {
|
|
let mut all = vec!["entity", "put", "entity:kay", "--origin", "cast"];
|
|
all.extend_from_slice(args);
|
|
weksa(d.path(), &all)
|
|
};
|
|
let no_persona = put(&["--form", "en-US=Kay"]);
|
|
assert!(
|
|
!no_persona.status.success() && out(&no_persona).contains("--persona"),
|
|
"{}",
|
|
out(&no_persona)
|
|
);
|
|
let first = put(&["--form", "en-US=Kay", "--persona", "kay"]);
|
|
assert!(
|
|
first.status.success() && out(&first).contains("Added entity:kay"),
|
|
"{}",
|
|
out(&first)
|
|
);
|
|
// Forms append; a language's forms keep the order given.
|
|
let grown = put(&[
|
|
"--form",
|
|
"en-US=Kay",
|
|
"--form",
|
|
"en-US=Kay's",
|
|
"--persona",
|
|
"kay",
|
|
]);
|
|
assert!(grown.status.success(), "{}", out(&grown));
|
|
let shrunk = put(&["--form", "en-US=Kay", "--persona", "kay"]);
|
|
assert!(
|
|
!shrunk.status.success() && out(&shrunk).contains("EntityNotAppendOnly"),
|
|
"{}",
|
|
out(&shrunk)
|
|
);
|
|
let dump = out(&weksa(d.path(), &["entity", "dump"]));
|
|
assert!(
|
|
dump.contains("entity:kay") && dump.contains("Kay's"),
|
|
"{dump}"
|
|
);
|
|
|
|
cast_entities(d.path(), &["nibu"]);
|
|
let add = |range: &str, text: &str| {
|
|
weksa(
|
|
d.path(),
|
|
&[
|
|
"corpus",
|
|
"add",
|
|
"--slug",
|
|
"kay",
|
|
"--text",
|
|
text,
|
|
"--speaker",
|
|
"entity:nibu",
|
|
"--group",
|
|
"g",
|
|
"--admitted-on",
|
|
"2026-10-29",
|
|
"--source",
|
|
"operator-written",
|
|
"--name",
|
|
range,
|
|
],
|
|
)
|
|
};
|
|
let named = add("0..3=entity:kay", "Kay left.");
|
|
assert!(named.status.success(), "{}", out(&named));
|
|
let unequal = add("0..3=entity:kay", "Kai left.");
|
|
assert!(
|
|
!unequal.status.success() && out(&unequal).contains("NameSpanNotAForm"),
|
|
"{}",
|
|
out(&unequal)
|
|
);
|
|
let malformed = add("0-3", "Kay again.");
|
|
assert!(
|
|
!malformed.status.success() && out(&malformed).contains("start..end=entity"),
|
|
"{}",
|
|
out(&malformed)
|
|
);
|
|
let dump = out(&weksa(d.path(), &["corpus", "dump"]));
|
|
assert!(
|
|
dump.contains("names:") && dump.contains("entity:kay"),
|
|
"{dump}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn entity_origin_flags_must_match_origin() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
let put = |args: &[&str]| {
|
|
let mut all = vec!["entity", "put", "entity:kay", "--form", "en-US=Kay"];
|
|
all.extend_from_slice(args);
|
|
weksa(d.path(), &all)
|
|
};
|
|
for mismatched in [
|
|
&["--origin", "live", "--persona", "kay"][..],
|
|
&["--origin", "live", "--work", "edda"],
|
|
&["--origin", "cast", "--persona", "kay", "--work", "edda"],
|
|
&[
|
|
"--origin",
|
|
"public-domain",
|
|
"--persona",
|
|
"kay",
|
|
"--work",
|
|
"edda",
|
|
],
|
|
&["--origin", "public-domain", "--persona", "kay"],
|
|
] {
|
|
let o = put(mismatched);
|
|
assert!(!o.status.success(), "{mismatched:?}: {}", out(&o));
|
|
}
|
|
assert!(!out(&weksa(d.path(), &["entity", "dump"])).contains("entity:kay"));
|
|
assert!(put(&["--origin", "live"]).status.success());
|
|
let work = weksa(
|
|
d.path(),
|
|
&[
|
|
"entity",
|
|
"put",
|
|
"entity:brokkr",
|
|
"--form",
|
|
"en-US=Brokkr",
|
|
"--origin",
|
|
"public-domain",
|
|
"--work",
|
|
"edda",
|
|
],
|
|
);
|
|
assert!(work.status.success(), "{}", out(&work));
|
|
assert!(out(&weksa(d.path(), &["entity", "dump"])).contains("PublicDomain"));
|
|
}
|
|
|
|
#[test]
|
|
fn intake_normalizes_to_nfc() {
|
|
let d = tempfile::tempdir().unwrap();
|
|
// A decomposed form and decomposed text meet as the same composed bytes.
|
|
let put = weksa(
|
|
d.path(),
|
|
&[
|
|
"entity",
|
|
"put",
|
|
"entity:rene",
|
|
"--form",
|
|
"fr=Rene\u{301}",
|
|
"--origin",
|
|
"live",
|
|
],
|
|
);
|
|
assert!(put.status.success(), "{}", out(&put));
|
|
cast_entities(d.path(), &["nibu"]);
|
|
let add = weksa(
|
|
d.path(),
|
|
&[
|
|
"corpus",
|
|
"add",
|
|
"--slug",
|
|
"rene",
|
|
"--text",
|
|
"Rene\u{301} part.",
|
|
"--language",
|
|
"fr",
|
|
"--speaker",
|
|
"entity:nibu",
|
|
"--group",
|
|
"g",
|
|
"--admitted-on",
|
|
"2026-10-29",
|
|
"--source",
|
|
"operator-written",
|
|
"--name",
|
|
"0..5=entity:rene",
|
|
],
|
|
);
|
|
assert!(add.status.success(), "{}", out(&add));
|
|
let dump = out(&weksa(d.path(), &["corpus", "dump"]));
|
|
assert!(dump.contains("Ren\u{e9} part."), "{dump}");
|
|
}
|