mod support;
use std::fs;
use kernel::records::{
Capability, JsonValue, Modality, ModelRecord, ModelSource, ModelState, ParamSpec, ParamType,
SourceKind, stable_id,
};
use kernel::registry::{Registry, RegistryError};
use support::TempDir;
fn record(name: &str, path: &str) -> ModelRecord {
ModelRecord::new(
name,
Modality::text(),
vec![Capability::chat()],
ModelSource::new(SourceKind::file(), path),
)
}
#[test]
fn open_empty_directory_is_empty() {
let dir = TempDir::new();
let registry = Registry::open(dir.path()).unwrap();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(registry.list().is_empty());
}
#[test]
fn register_persists_and_survives_reopen() {
let dir = TempDir::new();
let rec = record("Model", "/m.gguf");
let id = rec.id.clone();
{
let mut registry = Registry::open(dir.path()).unwrap();
assert!(registry.register(rec).unwrap());
assert!(registry.contains(&id));
}
let reopened = Registry::open(dir.path()).unwrap();
assert_eq!(reopened.get(&id).unwrap().name, "Model");
assert_eq!(reopened.len(), 1);
}
#[test]
fn registering_an_identical_record_is_a_noop() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
let rec = record("Model", "/m.gguf");
assert!(
registry.register(rec.clone()).unwrap(),
"first insert changes"
);
assert!(
!registry.register(rec).unwrap(),
"identical re-insert is a no-op"
);
}
#[test]
fn register_all_counts_changes_and_is_idempotent() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
let records = vec![record("A", "/a.gguf"), record("B", "/b.gguf")];
assert_eq!(registry.register_all(records.clone()).unwrap(), 2);
assert_eq!(registry.len(), 2);
assert_eq!(
registry.register_all(records).unwrap(),
0,
"no changes second time"
);
}
#[test]
fn unregister_removes_and_persists() {
let dir = TempDir::new();
let rec = record("Model", "/m.gguf");
let id = rec.id.clone();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(rec).unwrap();
let removed = registry.unregister(&id).unwrap();
assert_eq!(removed.unwrap().id, id);
assert!(!registry.contains(&id));
assert!(
registry.unregister(&id).unwrap().is_none(),
"removing again is None"
);
let reopened = Registry::open(dir.path()).unwrap();
assert!(reopened.get(&id).is_none());
}
#[test]
fn list_is_sorted_case_insensitively() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(record("banana", "/1")).unwrap();
registry.register(record("Apple", "/2")).unwrap();
registry.register(record("Cherry", "/3")).unwrap();
let names: Vec<&str> = registry.list().iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, ["Apple", "banana", "Cherry"]);
}
#[test]
fn set_state_updates_present_and_ignores_absent() {
let dir = TempDir::new();
let rec = record("Model", "/m.gguf");
let id = rec.id.clone();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(rec).unwrap();
assert!(
registry
.set_state_if_present(&id, ModelState::Ready)
.unwrap()
);
assert_eq!(registry.get(&id).unwrap().state, ModelState::Ready);
assert!(
!registry
.set_state_if_present("nonexistent", ModelState::Missing)
.unwrap()
);
let reopened = Registry::open(dir.path()).unwrap();
assert_eq!(reopened.get(&id).unwrap().state, ModelState::Ready);
}
#[test]
fn update_applies_transform_and_skips_none() {
let dir = TempDir::new();
let rec = record("Model", "/m.gguf");
let id = rec.id.clone();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(rec).unwrap();
let changed = registry
.update(std::slice::from_ref(&id), |current| {
let mut next = current.clone();
next.alias = Some("Nick".into());
Some(next)
})
.unwrap();
assert_eq!(changed.len(), 1);
assert_eq!(registry.get(&id).unwrap().alias.as_deref(), Some("Nick"));
let unchanged = registry
.update(std::slice::from_ref(&id), |_| None)
.unwrap();
assert!(unchanged.is_empty(), "returning None changes nothing");
let same = registry
.update(&[id], |current| Some(current.clone()))
.unwrap();
assert!(
same.is_empty(),
"an identical transform result changes nothing"
);
let absent = registry
.update(&["missing".to_owned()], |c| Some(c.clone()))
.unwrap();
assert!(absent.is_empty());
}
#[test]
fn corrupt_store_is_quarantined_and_reported() {
let dir = TempDir::new();
fs::write(dir.join("models.json"), b"{ not valid json").unwrap();
match Registry::open(dir.path()) {
Err(RegistryError::CorruptStore(_)) => {}
other => panic!("expected CorruptStore, got {other:?}"),
}
assert!(
!dir.join("models.json").exists(),
"corrupt store should be quarantined"
);
let quarantined = fs::read_dir(dir.path())
.unwrap()
.filter_map(Result::ok)
.any(|e| {
e.file_name()
.to_string_lossy()
.starts_with("models.json.corrupt-")
});
assert!(quarantined);
}
#[test]
fn update_that_changes_id_migrates_the_key() {
let dir = TempDir::new();
let rec = record("Model", "/a.gguf");
let old_id = rec.id.clone();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(rec).unwrap();
let changed = registry
.update(std::slice::from_ref(&old_id), |current| {
let mut next = current.clone();
next.source = ModelSource::new(SourceKind::file(), "/b.gguf");
next.id = stable_id(&next.source);
Some(next)
})
.unwrap();
let new_id = changed[0].id.clone();
assert_ne!(new_id, old_id);
assert!(registry.get(&old_id).is_none(), "old key must be gone");
assert_eq!(registry.get(&new_id).unwrap().name, "Model");
assert_eq!(registry.len(), 1);
let reopened = Registry::open(dir.path()).unwrap();
assert!(reopened.get(&old_id).is_none());
assert_eq!(reopened.get(&new_id).unwrap().id, new_id);
}
#[test]
fn register_replaces_existing_content_and_persists() {
let dir = TempDir::new();
let mut rec = record("Model", "/m.gguf");
let id = rec.id.clone();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(rec.clone()).unwrap();
rec.alias = Some("Renamed".into());
assert!(
registry.register(rec).unwrap(),
"changed content is a change"
);
assert_eq!(registry.get(&id).unwrap().alias.as_deref(), Some("Renamed"));
let reopened = Registry::open(dir.path()).unwrap();
assert_eq!(reopened.get(&id).unwrap().alias.as_deref(), Some("Renamed"));
}
#[test]
fn a_shelf_written_in_mebibytes_keeps_its_sizes_after_the_upgrade() {
let dir = TempDir::new();
std::fs::write(
dir.path().join("models.json"),
r#"{"schema_version":1,"models":[{
"id":"old","name":"Model","modality":"text","capabilities":["chat"],
"source":{"kind":"file","path":"/m.gguf"},
"footprint_mb":4096,"registered_at":1
}]}"#,
)
.unwrap();
let mut registry = Registry::open(dir.path()).expect("open the old store");
let record = registry.get("old").expect("the record survives").clone();
assert_eq!(record.footprint_bytes, Some(4096 * (1 << 20)));
let mut renamed = record;
renamed.alias = Some("nick".to_owned());
registry.register(renamed).unwrap();
let written = std::fs::read_to_string(dir.path().join("models.json")).unwrap();
assert!(!written.contains("footprint_mb"), "{written}");
assert!(written.contains("footprint_bytes"));
}
#[test]
fn an_exact_size_is_not_overwritten_by_a_leftover_mebibyte_figure() {
let dir = TempDir::new();
std::fs::write(
dir.path().join("models.json"),
r#"{"schema_version":1,"models":[{
"id":"both","name":"Model","modality":"text","capabilities":["chat"],
"source":{"kind":"file","path":"/m.gguf"},
"footprint_mb":4096,"footprint_bytes":491400032,"registered_at":1
}]}"#,
)
.unwrap();
let registry = Registry::open(dir.path()).expect("open the store");
assert_eq!(
registry.get("both").expect("the record").footprint_bytes,
Some(491_400_032)
);
}
#[test]
fn reopen_preserves_full_record_fidelity() {
let dir = TempDir::new();
let mut rec = record("Model", "/m.gguf");
rec.alias = Some("Nick".into());
rec.system_prompt = Some("be terse".into());
rec.footprint_bytes = Some(4096 * (1 << 20));
rec.content_fingerprint = Some("abcd".into());
rec.params.push(ParamSpec {
key: "temperature".into(),
param_type: ParamType::Float,
default_value: Some(JsonValue::Double(0.8)),
range: Some(vec![JsonValue::Double(0.0), JsonValue::Double(2.0)]),
values: None,
});
rec.param_values
.insert("temperature".into(), JsonValue::Double(0.5));
let id = rec.id.clone();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(rec.clone()).unwrap();
let reopened = Registry::open(dir.path()).unwrap();
assert_eq!(reopened.get(&id), Some(&rec));
}
#[test]
fn future_schema_is_rejected_not_downgraded() {
let dir = TempDir::new();
let file = dir.join("models.json");
fs::write(&file, br#"{"schema_version":999,"models":[]}"#).unwrap();
match Registry::open(dir.path()) {
Err(RegistryError::FutureSchema {
found: 999,
supported: 1,
}) => {}
other => panic!("expected FutureSchema, got {other:?}"),
}
assert!(
file.exists(),
"a future-schema store must be left in place, not touched"
);
}
#[test]
fn duplicate_ids_in_file_keep_the_last() {
let dir = TempDir::new();
fs::write(
dir.join("models.json"),
br#"{"schema_version":1,"models":[
{"id":"dup","name":"First","modality":"text","capabilities":["chat"],
"source":{"kind":"file","path":"/a"},"registered_at":1},
{"id":"dup","name":"Second","modality":"text","capabilities":["chat"],
"source":{"kind":"file","path":"/b"},"registered_at":2}
]}"#,
)
.unwrap();
let registry = Registry::open(dir.path()).unwrap();
assert_eq!(registry.len(), 1);
assert_eq!(registry.get("dup").unwrap().name, "Second");
}
#[test]
fn register_all_with_internal_duplicate_keeps_last() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
let mut first = record("First", "/x.gguf");
let mut second = first.clone();
second.name = "Second".into();
assert_eq!(second.id, first.id, "same source means same id");
first.name = "First".into();
registry.register_all(vec![first, second]).unwrap();
assert_eq!(registry.len(), 1);
assert_eq!(registry.list()[0].name, "Second");
}
#[test]
fn register_all_counts_only_new_or_changed() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
let a = record("A", "/a.gguf");
let b = record("B", "/b.gguf");
registry.register(a.clone()).unwrap();
let c = record("C", "/c.gguf");
assert_eq!(
registry.register_all(vec![a, b, c]).unwrap(),
2,
"a is unchanged"
);
assert_eq!(registry.len(), 3);
}
#[test]
fn list_tie_breaks_by_id_when_names_match_case_insensitively() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
let lower = record("apple", "/one");
let upper = record("Apple", "/two");
let mut ids = [lower.id.clone(), upper.id.clone()];
ids.sort();
registry.register(lower).unwrap();
registry.register(upper).unwrap();
let listed: Vec<&str> = registry.list().iter().map(|r| r.id.as_str()).collect();
assert_eq!(listed, [ids[0].as_str(), ids[1].as_str()]);
}
#[test]
fn update_over_multiple_ids_changes_only_the_relevant_ones() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
let a = record("A", "/a");
let b = record("B", "/b");
let c = record("C", "/c");
let (ida, idb, idc) = (a.id.clone(), b.id.clone(), c.id.clone());
registry.register_all(vec![a, b, c]).unwrap();
let changed = registry
.update(&[ida.clone(), idb.clone(), idc.clone()], |current| {
if current.id == idb {
let mut next = current.clone();
next.alias = Some("only-b".into());
Some(next)
} else if current.id == idc {
None
} else {
Some(current.clone())
}
})
.unwrap();
assert_eq!(changed.len(), 1);
assert_eq!(changed[0].id, idb);
assert_eq!(registry.get(&idb).unwrap().alias.as_deref(), Some("only-b"));
assert!(registry.get(&ida).unwrap().alias.is_none());
}
#[test]
fn unregister_absent_id_returns_none() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
assert!(registry.unregister("never").unwrap().is_none());
}
#[test]
fn concurrent_writers_do_not_lose_each_others_updates() {
let dir = TempDir::new();
let mut a = Registry::open(dir.path()).unwrap();
let mut b = Registry::open(dir.path()).unwrap();
let x = record("X", "/x.gguf");
let y = record("Y", "/y.gguf");
let (id_x, id_y) = (x.id.clone(), y.id.clone());
assert!(a.register(x).unwrap(), "a registers X");
assert!(
b.register(y).unwrap(),
"b registers Y, reloading a's write first"
);
let c = Registry::open(dir.path()).unwrap();
assert!(c.contains(&id_x), "X must survive both writers");
assert!(c.contains(&id_y), "Y must survive both writers");
assert_eq!(c.len(), 2);
}
#[test]
fn registering_the_same_id_from_two_instances_converges() {
let dir = TempDir::new();
let mut a = Registry::open(dir.path()).unwrap();
let mut b = Registry::open(dir.path()).unwrap();
let mut rec = record("Model", "/m.gguf");
let id = rec.id.clone();
assert!(a.register(rec.clone()).unwrap());
rec.alias = Some("FromB".into());
assert!(
b.register(rec).unwrap(),
"b's differing content is a change"
);
let c = Registry::open(dir.path()).unwrap();
assert_eq!(c.len(), 1);
assert_eq!(c.get(&id).unwrap().alias.as_deref(), Some("FromB"));
}
#[test]
fn a_refresh_picks_up_what_another_process_registered() {
let dir = TempDir::new();
let mut screen = Registry::open(dir.path()).unwrap();
let generation = screen.generation();
let mut worker = Registry::open(dir.path()).unwrap();
worker.register(record("pulled", "/models/pulled")).unwrap();
assert!(screen.is_empty());
assert!(screen.refresh().unwrap());
assert_eq!(screen.len(), 1);
assert!(screen.list().iter().any(|held| held.name == "pulled"));
assert!(screen.generation() > generation);
}
#[test]
fn a_refresh_that_finds_nothing_new_is_not_a_change() {
let dir = TempDir::new();
let mut registry = Registry::open(dir.path()).unwrap();
registry.register(record("held", "/models/held")).unwrap();
let generation = registry.generation();
assert!(!registry.refresh().unwrap());
assert_eq!(registry.generation(), generation);
}