use super::*;
use crate::memory_core::retrieval::seed_shared_embedder_with_mock;
use crate::memory_core::store::{kg::KnowledgeGraph, vector::UsearchStore};
use tempfile::tempdir;
fn make_handle(id: &str, dir: &std::path::Path) -> PalaceHandle {
let vs = UsearchStore::new(dir.join(format!("{id}.usearch")), 384).unwrap();
let kg = KnowledgeGraph::open(&dir.join(format!("{id}.db"))).unwrap();
PalaceHandle::new(PalaceId::new(id), format!("Identity for {id}"), vs, kg)
}
#[test]
fn register_and_get_roundtrip() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::new();
reg.register(make_handle("alpha", dir.path()));
let h = reg.get(&PalaceId::new("alpha")).expect("registered");
assert_eq!(h.id.as_str(), "alpha");
}
#[test]
fn with_writer_intent_sets_writer_open_intent() {
let default_reg = PalaceRegistry::new();
assert_eq!(
default_reg.open_intent(),
OpenIntent::ReadOnlyClient,
"default registry must open palaces read-only (snapshot fallback)"
);
let writer_reg = PalaceRegistry::new().with_writer_intent();
assert_eq!(
writer_reg.open_intent(),
OpenIntent::Writer,
"with_writer_intent() must mark the registry as a writer"
);
}
#[test]
fn registry_remove_clears_cached_handle() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::new();
let id = PalaceId::new("doomed");
reg.register(make_handle("doomed", dir.path()));
reg.set_gaps(id.clone(), Vec::new());
assert!(reg.get(&id).is_some());
assert!(reg.get_gaps(&id).is_some());
reg.remove(&id);
assert!(reg.get(&id).is_none());
assert!(reg.get_gaps(&id).is_none());
reg.remove(&id);
}
#[test]
fn registry_create_and_open() {
use crate::memory_core::palace::Palace;
use chrono::Utc;
let dir = tempdir().unwrap();
let data_root = dir.path();
let palace = Palace {
id: PalaceId::new("alpha"),
name: "Alpha".to_string(),
description: Some("test".to_string()),
created_at: Utc::now(),
data_dir: data_root.join("alpha"),
};
{
let reg = PalaceRegistry::new();
let handle = reg
.create_palace(data_root, palace.clone())
.expect("create_palace");
assert_eq!(handle.id, PalaceId::new("alpha"));
crate::memory_core::store::palace_store::PalaceStore::save_identity(
&handle.id,
"I am Alpha",
handle.data_dir.as_ref().expect("data_dir set"),
)
.expect("save identity");
}
let reg2 = PalaceRegistry::new();
let handle2 = reg2
.open_palace(data_root, &PalaceId::new("alpha"))
.expect("open_palace");
assert_eq!(handle2.id, PalaceId::new("alpha"));
assert_eq!(handle2.identity, "I am Alpha");
let palaces = PalaceRegistry::list_palaces(data_root).unwrap();
assert_eq!(palaces.len(), 1);
assert_eq!(palaces[0].name, "Alpha");
}
#[tokio::test]
async fn palace_payloads_survive_registry_restart() {
seed_shared_embedder_with_mock();
use crate::memory_core::palace::{Palace, RoomType};
use chrono::Utc;
let dir = tempdir().unwrap();
let data_root = dir.path();
{
let registry = PalaceRegistry::open(data_root).unwrap();
let palace = Palace {
id: PalaceId::new("restart-test"),
name: "Restart".to_string(),
description: None,
created_at: Utc::now(),
data_dir: data_root.join("restart-test"),
};
let handle = registry.create_palace(data_root, palace).unwrap();
handle
.remember(
"the quokka is a small marsupial native to Western Australia".to_string(),
RoomType::Research,
vec!["wildlife".to_string()],
0.7,
)
.await
.expect("remember persists the drawer");
}
let registry = PalaceRegistry::open(data_root).unwrap();
assert_eq!(
registry.len(),
1,
"registry should have hydrated the persisted palace"
);
let handle = registry
.get(&PalaceId::new("restart-test"))
.expect("palace should be registered after open()");
let drawers = handle.drawers.read().clone();
assert!(
drawers
.iter()
.any(|d| d.content.contains("quokka") && d.tags.contains(&"wildlife".to_string())),
"persisted drawer content must survive restart; got {drawers:?}"
);
}
#[test]
fn gaps_cache_round_trip() {
use crate::memory_core::community::KnowledgeGap;
let reg = PalaceRegistry::new();
let pid = PalaceId::new("gap-cache");
assert!(reg.get_gaps(&pid).is_none());
let gaps = vec![KnowledgeGap {
entities: vec!["alpha".to_string(), "beta".to_string()],
internal_density: 0.1,
external_bridges: 1,
suggested_exploration: "Explore connections between alpha and beta".to_string(),
}];
reg.set_gaps(pid.clone(), gaps.clone());
let read = reg.get_gaps(&pid).expect("cached value");
assert_eq!(read.len(), 1);
assert_eq!(read[0].entities, gaps[0].entities);
assert!((read[0].internal_density - 0.1).abs() < 1e-6);
reg.clear_gaps(&pid);
assert!(reg.get_gaps(&pid).is_none());
}
#[test]
fn list_contains_all_registered() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::new();
reg.register(make_handle("a", dir.path()));
reg.register(make_handle("b", dir.path()));
let ids: Vec<_> = reg.list().into_iter().map(|p| p.0).collect();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&"a".to_string()));
assert!(ids.contains(&"b".to_string()));
}
#[test]
fn lru_evicts_least_recently_used() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::with_max_open(2);
reg.register(make_handle("a", dir.path()));
reg.register(make_handle("b", dir.path()));
assert_eq!(reg.len(), 2, "two handles registered");
reg.register(make_handle("c", dir.path()));
assert_eq!(reg.len(), 2, "capacity-2 registry must stay at 2");
assert!(
reg.peek(&PalaceId::new("a")).is_none(),
"LRU handle 'a' must have been evicted"
);
assert!(
reg.peek(&PalaceId::new("b")).is_some(),
"MRU handle 'b' must survive"
);
assert!(
reg.peek(&PalaceId::new("c")).is_some(),
"newly inserted 'c' must be present"
);
}
#[test]
fn open_palace_follows_alias() {
use crate::memory_core::palace::Palace;
use crate::palace_alias::PalaceAliasStore;
use chrono::Utc;
let dir = tempdir().unwrap();
let data_root = dir.path();
let reg = PalaceRegistry::new();
let bare = Palace {
id: PalaceId::new("trusty-tools"),
name: "trusty-tools".to_string(),
description: None,
created_at: Utc::now(),
data_dir: data_root.join("trusty-tools"),
};
reg.create_palace(data_root, bare)
.expect("create bare palace");
PalaceAliasStore::register_alias(data_root, "bobmatnyc-trusty-tools", "trusty-tools")
.expect("register alias");
let handle = reg
.open_palace(data_root, &PalaceId::new("bobmatnyc-trusty-tools"))
.expect("alias must resolve to the existing palace");
assert_eq!(
handle.id,
PalaceId::new("trusty-tools"),
"aliased open must return the canonical target handle, not the alias id"
);
}
#[test]
fn open_palace_ignores_alias_when_target_missing() {
use crate::palace_alias::PalaceAliasStore;
let dir = tempdir().unwrap();
let data_root = dir.path();
let reg = PalaceRegistry::new();
PalaceAliasStore::register_alias(data_root, "ghost", "does-not-exist").expect("register alias");
assert!(
reg.open_palace(data_root, &PalaceId::new("ghost")).is_err(),
"alias to a missing target must not redirect"
);
}
#[test]
fn open_palace_prefers_real_palace_over_alias() {
use crate::memory_core::palace::Palace;
use crate::palace_alias::PalaceAliasStore;
use chrono::Utc;
let dir = tempdir().unwrap();
let data_root = dir.path();
let reg = PalaceRegistry::new();
for id in ["dup", "other"] {
let p = Palace {
id: PalaceId::new(id),
name: id.to_string(),
description: None,
created_at: Utc::now(),
data_dir: data_root.join(id),
};
reg.create_palace(data_root, p).expect("create palace");
}
PalaceAliasStore::register_alias(data_root, "dup", "other").expect("register alias");
let handle = reg
.open_palace(data_root, &PalaceId::new("dup"))
.expect("real palace opens");
assert_eq!(
handle.id,
PalaceId::new("dup"),
"a real palace must win over an alias of the same name"
);
}
#[test]
fn lru_get_promotes_to_mru() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::with_max_open(2);
reg.register(make_handle("a", dir.path()));
reg.register(make_handle("b", dir.path()));
let _ = reg.get(&PalaceId::new("a"));
reg.register(make_handle("c", dir.path()));
assert_eq!(reg.len(), 2);
assert!(
reg.peek(&PalaceId::new("b")).is_none(),
"'b' must be evicted — it was LRU after 'a' was promoted"
);
assert!(
reg.peek(&PalaceId::new("a")).is_some(),
"'a' must survive — it was promoted to MRU by get()"
);
assert!(
reg.peek(&PalaceId::new("c")).is_some(),
"'c' must be present"
);
}
#[test]
fn lru_evicted_handle_reopens() {
use crate::memory_core::palace::Palace;
use chrono::Utc;
let dir = tempdir().unwrap();
let data_root = dir.path();
let reg = PalaceRegistry::with_max_open(1);
let palace_a = Palace {
id: PalaceId::new("alpha"),
name: "Alpha".to_string(),
description: None,
created_at: Utc::now(),
data_dir: data_root.join("alpha"),
};
reg.create_palace(data_root, palace_a)
.expect("create alpha");
assert_eq!(reg.len(), 1, "'alpha' registered");
reg.register(make_handle("beta", data_root));
assert_eq!(reg.len(), 1, "capacity-1: only 'beta' remains");
assert!(
reg.peek(&PalaceId::new("alpha")).is_none(),
"'alpha' must have been evicted"
);
let reopened = reg
.open_palace(data_root, &PalaceId::new("alpha"))
.expect("open_palace after eviction must succeed");
assert_eq!(reopened.id, PalaceId::new("alpha"), "reopened id matches");
assert!(
reg.peek(&PalaceId::new("alpha")).is_some(),
"'alpha' must be back in the cache after reopen"
);
}
#[test]
#[serial_test::serial]
fn from_env_respects_max_open_palaces_env() {
let dir = tempdir().unwrap();
unsafe {
std::env::set_var(MAX_OPEN_PALACES_ENV, "2");
}
let reg = PalaceRegistry::from_env();
unsafe {
std::env::remove_var(MAX_OPEN_PALACES_ENV);
}
reg.register(make_handle("a", dir.path()));
reg.register(make_handle("b", dir.path()));
reg.register(make_handle("c", dir.path()));
assert_eq!(
reg.len(),
2,
"from_env cap=2 must bound the registry to 2 handles"
);
}
#[test]
#[serial_test::serial]
fn max_open_palaces_from_env_defaults_when_unset() {
unsafe {
std::env::remove_var(MAX_OPEN_PALACES_ENV);
}
assert_eq!(max_open_palaces_from_env(), DEFAULT_MAX_OPEN_PALACES);
}
#[test]
fn evict_idle_drops_idle_unreferenced_handle() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::new();
let h = make_handle("idle", dir.path());
h.last_accessed
.store(0, std::sync::atomic::Ordering::Relaxed);
reg.register(h);
assert_eq!(reg.len(), 1);
let evicted = reg.evict_idle(std::time::Duration::from_secs(1));
assert_eq!(evicted, 1, "an idle, unreferenced handle must be evicted");
assert!(
reg.get(&PalaceId::new("idle")).is_none(),
"evicted handle must be gone from the cache"
);
}
#[test]
fn evict_idle_skips_referenced_handle() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::new();
let h = make_handle("busy", dir.path());
h.last_accessed
.store(0, std::sync::atomic::Ordering::Relaxed);
reg.register(h);
let _in_flight = reg.get(&PalaceId::new("busy")).expect("registered");
let evicted = reg.evict_idle(std::time::Duration::from_secs(1));
assert_eq!(evicted, 0, "a referenced handle must be skipped");
assert_eq!(reg.len(), 1);
}
#[test]
fn evict_idle_skips_recent_and_respects_zero_threshold() {
let dir = tempdir().unwrap();
let reg = PalaceRegistry::new();
reg.register(make_handle("fresh", dir.path()));
assert_eq!(
reg.evict_idle(std::time::Duration::from_secs(3600)),
0,
"a recently-accessed handle must not be evicted"
);
let h = make_handle("ancient", dir.path());
h.last_accessed
.store(0, std::sync::atomic::Ordering::Relaxed);
reg.register(h);
assert_eq!(
reg.evict_idle(std::time::Duration::from_secs(0)),
0,
"zero threshold disables eviction"
);
assert_eq!(reg.len(), 2, "no handle should have been evicted");
}
#[test]
fn touch_suppressed_during_compaction() {
use std::sync::atomic::Ordering::Relaxed;
let dir = tempdir().unwrap();
let h = make_handle("dreaming", dir.path());
h.last_accessed.store(0, Relaxed);
h.is_compacting.store(true, Relaxed);
h.touch();
assert_eq!(
h.last_accessed.load(Relaxed),
0,
"touch during compaction must not advance the idle clock"
);
h.is_compacting.store(false, Relaxed);
h.touch();
assert!(
h.last_accessed.load(Relaxed) > 0,
"touch after compaction must advance the idle clock"
);
}
#[tokio::test]
async fn evict_idle_then_reopen_preserves_recall() {
seed_shared_embedder_with_mock();
use crate::memory_core::palace::{Palace, RoomType};
use crate::memory_core::retrieval::recall_with_default_embedder;
use chrono::Utc;
let dir = tempdir().unwrap();
let data_root = dir.path();
let reg = PalaceRegistry::new();
let palace = Palace {
id: PalaceId::new("evict-reopen"),
name: "Evict".to_string(),
description: None,
created_at: Utc::now(),
data_dir: data_root.join("evict-reopen"),
};
let handle = reg.create_palace(data_root, palace).unwrap();
handle
.remember(
"the quokka is a small marsupial native to Western Australia".to_string(),
RoomType::Research,
vec!["wildlife".to_string()],
0.7,
)
.await
.expect("remember persists the drawer");
let before = recall_with_default_embedder(&handle, "quokka", 5)
.await
.expect("pre-evict recall");
let before_contents: Vec<String> = before.iter().map(|r| r.drawer.content.clone()).collect();
assert!(
before_contents.iter().any(|c| c.contains("quokka")),
"sanity: pre-evict recall must find the drawer"
);
handle
.last_accessed
.store(0, std::sync::atomic::Ordering::Relaxed);
let id = handle.id.clone();
drop(handle);
let evicted = reg.evict_idle(std::time::Duration::from_secs(1));
assert_eq!(evicted, 1, "idle palace must be evicted");
assert!(
reg.get(&id).is_none(),
"palace must be cold (absent from the registry) after eviction"
);
let reopened = reg
.open_palace(data_root, &id)
.expect("reopen from disk after idle eviction");
let after = recall_with_default_embedder(&reopened, "quokka", 5)
.await
.expect("post-reopen recall");
let after_contents: Vec<String> = after.iter().map(|r| r.drawer.content.clone()).collect();
assert_eq!(
before_contents, after_contents,
"recall after idle-evict + rehydrate must match pre-eviction results"
);
}