use super::*;
use std::fs;
use tempfile::TempDir;
struct Fixed(u64);
impl Rng for Fixed {
fn next_u64(&mut self) -> u64 {
self.0
}
}
fn set(names: &[&str]) -> HashSet<String> {
names.iter().map(|s| (*s).to_owned()).collect()
}
const ABC: &[&str] = &["ash", "bay", "cove"];
type Case = (
&'static [&'static str],
u64,
&'static [&'static str],
Result<&'static str, MintError>,
);
#[test]
fn mint_scans_from_the_draw_and_retries_past_collisions() {
let cases: &[Case] = &[
(ABC, 0, &[], Ok("ash")),
(ABC, 2, &[], Ok("cove")),
(ABC, 5, &[], Ok("cove")),
(ABC, 0, &["ash"], Ok("bay")),
(ABC, 2, &["cove", "ash"], Ok("bay")),
(
&["ash", "bay"],
0,
&["ash", "bay"],
Err(MintError::Exhausted(2)),
),
(&[], 0, &[], Err(MintError::Exhausted(0))),
];
for (words, draw, taken, expect) in cases {
let got = mint_from(words, &mut Fixed(*draw), &set(taken));
let want = expect.clone().map(str::to_owned);
assert_eq!(got, want, "words={words:?} draw={draw} taken={taken:?}");
}
}
#[test]
fn exhaustion_error_names_the_pool_size() {
let err = MintError::Exhausted(2);
assert_eq!(
err.to_string(),
"name pool exhausted: all 2 words are occupied"
);
}
#[test]
fn embedded_wordlist_holds_its_invariants() {
let words = wordlist();
assert!(!words.iter().any(|w| w.starts_with('#') || w.is_empty()));
for w in &words {
assert!(
(3..=9).contains(&w.len()) && w.chars().all(|c| c.is_ascii_lowercase()),
"{w:?} violates ^[a-z]{{3,9}}$"
);
}
let unique: HashSet<&&str> = words.iter().collect();
assert_eq!(unique.len(), words.len(), "duplicate word in words.txt");
assert!(!unique.contains(&"unknown"));
assert_eq!(words.len(), 7395);
}
#[test]
fn mint_over_the_embedded_list_avoids_the_occupied_set() {
let name = mint(&mut SplitMix64::from_seed(7), &HashSet::new()).unwrap();
let again = mint(&mut SplitMix64::from_seed(7), &HashSet::new()).unwrap();
assert_eq!(name, again);
assert!(!name.contains('-'));
assert!(wordlist().contains(&name.as_str()));
let next = mint(&mut SplitMix64::from_seed(7), &set(&[&name])).unwrap();
assert_ne!(next, name);
}
#[test]
fn splitmix64_advances_and_seeds_from_entropy() {
let mut rng = SplitMix64::from_seed(0);
let draws: HashSet<u64> = (0..64).map(|_| rng.next_u64()).collect();
assert_eq!(draws.len(), 64, "generator repeated within 64 draws");
assert_ne!(
SplitMix64::from_entropy().next_u64(),
SplitMix64::from_seed(0).next_u64()
);
}
#[test]
fn validate_enforces_the_shape_the_wordlist_used_to_guarantee() {
let cases: &[(&str, Result<&str, NameError>)] = &[
("ops", Ok("ops")),
("dev", Ok("dev")),
("acme-corp", Ok("acme-corp")),
("s3", Ok("s3")),
(" ops ", Ok("ops")),
("", Err(NameError::Shape)),
(" ", Err(NameError::Shape)),
("Ops!", Err(NameError::Shape)),
("Ops", Err(NameError::Shape)),
("two words", Err(NameError::Shape)),
("a/b", Err(NameError::Shape)),
("..", Err(NameError::Shape)),
("-ops", Err(NameError::Shape)),
("ops-", Err(NameError::Shape)),
("a--b", Err(NameError::Shape)),
("unknown", Err(NameError::Reserved)),
];
for (typed, want) in cases {
let got = validate(typed, &[]);
assert_eq!(got, want.clone().map(str::to_owned), "typed={typed:?}");
}
let at_cap = "a".repeat(MAX_BYTES);
assert_eq!(validate(&at_cap, &[]), Ok(at_cap.clone()));
assert_eq!(
validate(&"a".repeat(MAX_BYTES + 1), &[]),
Err(NameError::TooLong)
);
assert_eq!(validate(DEFAULT_NAME, &[]), Ok(DEFAULT_NAME.to_owned()));
}
#[test]
fn validate_refuses_a_leaf_that_exists_under_any_root() {
let (yog, lernie) = (TempDir::new().unwrap(), TempDir::new().unwrap());
let foreign = lernie.path().join("workspaces");
let replay = lernie.path().join("replays");
fs::create_dir_all(yog.path().join("ops")).unwrap();
fs::create_dir_all(foreign.join("acme")).unwrap();
fs::create_dir_all(foreign.join("20260101T-aa")).unwrap();
fs::create_dir_all(&replay).unwrap();
fs::write(replay.join("notes"), "x").unwrap();
let roots = [yog.path().to_path_buf(), foreign, replay];
assert_eq!(
validate("ops", &roots),
Err(NameError::Taken("ops".to_owned()))
);
assert_eq!(
validate("acme", &roots).unwrap_err(),
NameError::Taken("acme".to_owned()),
"a leaf under lernie's own root occupies the name too"
);
assert_eq!(
validate("20260101T-aa", &roots),
Err(NameError::Shape),
"shape is asked first: a lernie auto-id is not a name a human may type"
);
assert_eq!(
validate("notes", &roots),
Err(NameError::Taken("notes".to_owned()))
);
assert_eq!(validate("dev", &roots), Ok("dev".to_owned()));
assert_eq!(
validate("ops", &[yog.path().join("gone")]),
Ok("ops".to_owned())
);
}
#[test]
fn every_refusal_carries_an_operator_facing_reason() {
for err in [
NameError::Shape,
NameError::TooLong,
NameError::Reserved,
NameError::Taken("ops".to_owned()),
] {
assert!(!err.to_string().is_empty());
}
assert_eq!(
NameError::Taken("ops".to_owned()).to_string(),
"`ops` already exists — pick another name"
);
assert_eq!(NameError::TooLong.to_string(), "a name is at most 32 bytes");
assert!(NameError::Reserved.to_string().starts_with("`unknown`"));
}