use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use redb::Database;
use serde_json::{json, Value};
use tempfile::TempDir;
use trusty_memory::tools::dispatch_tool;
use trusty_memory::AppState;
struct Fixture {
_tmp: TempDir,
state: AppState,
}
impl Fixture {
fn new() -> Self {
let tmp = tempfile::tempdir().expect("tempdir");
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let state = AppState::new(tmp.path().to_path_buf());
state.set_ready();
Self { _tmp: tmp, state }
}
fn state(&self) -> &AppState {
&self.state
}
fn data_root(&self) -> &Path {
&self.state.data_root
}
}
async fn create_palace(state: &AppState, name: &str) {
dispatch_tool(state, "palace_create", json!({ "name": name }))
.await
.expect("palace_create");
}
async fn remember(state: &AppState, palace: &str, text: &str, tags: &[&str]) -> String {
let tag_values: Vec<Value> = tags.iter().map(|t| json!(t)).collect();
let res = dispatch_tool(
state,
"memory_remember",
json!({
"palace": palace,
"text": text,
"room": "General",
"tags": tag_values,
}),
)
.await
.expect("memory_remember");
res["drawer_id"]
.as_str()
.expect("drawer_id in response")
.to_string()
}
fn lock_palace_files(data_dir: &Path) -> (Database, Database) {
let kg_path = data_dir.join("kg.redb");
let vec_path = data_dir.join("index.usearch.redb");
let kg_lock = Database::create(&kg_path).expect("lock kg.redb");
let vec_lock = Database::create(&vec_path).expect("lock vector redb");
(kg_lock, vec_lock)
}
fn fresh_state(data_root: &Path) -> AppState {
let state = AppState::new(data_root.to_path_buf());
state.set_ready();
state
}
#[tokio::test]
async fn remember_then_recall_returns_drawer() {
let fx = Fixture::new();
create_palace(fx.state(), "round-trip").await;
let drawer_id = remember(
fx.state(),
"round-trip",
"Quokkas are small marsupials native to a few small islands off the coast of Western Australia",
&["wildlife"],
)
.await;
assert!(!drawer_id.is_empty());
let recalled = dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "round-trip", "query": "quokka marsupial Australia", "top_k": 5}),
)
.await
.expect("memory_recall");
let results = recalled["results"].as_array().expect("results array");
assert!(
results
.iter()
.any(|r| r["content"].as_str().unwrap_or("").contains("Quokkas")),
"expected to recall the seeded drawer; got {results:?}"
);
}
#[tokio::test]
async fn recall_ranks_best_match_first() {
let fx = Fixture::new();
create_palace(fx.state(), "rank").await;
remember(
fx.state(),
"rank",
"The Rust borrow checker prevents data races at compile time",
&["rust"],
)
.await;
remember(
fx.state(),
"rank",
"Python uses reference counting combined with a cyclic collector for garbage collection of objects",
&["python"],
)
.await;
remember(
fx.state(),
"rank",
"JavaScript engines use generational garbage collection with separate young and old object generations",
&["js"],
)
.await;
let recalled = dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "rank", "query": "rust ownership and borrow checker", "top_k": 3}),
)
.await
.expect("memory_recall");
let results = recalled["results"].as_array().expect("results array");
let first_l2 = results
.iter()
.find(|r| r["layer"].as_u64().unwrap_or(0) >= 2)
.expect("at least one L2 result");
assert!(
first_l2["content"]
.as_str()
.unwrap_or("")
.contains("borrow checker"),
"best match should be the Rust drawer; got {first_l2:?}"
);
}
#[tokio::test]
async fn recall_deep_returns_at_least_as_many_as_shallow() {
let fx = Fixture::new();
create_palace(fx.state(), "deep").await;
let bodies = [
"Rust enforces ownership and lifetimes at compile time to prevent data races and use-after-free",
"Python is dynamically typed with reference counting and a cyclic garbage collector for heap memory",
"JavaScript engines such as V8 use just-in-time compilation and generational garbage collection",
"Go is a statically typed language with concurrent garbage collection and lightweight goroutines",
"Haskell relies on lazy evaluation, type inference, and pure functional programming abstractions",
];
for body in bodies {
remember(fx.state(), "deep", body, &[]).await;
}
let shallow = dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "deep", "query": "programming languages", "top_k": 10}),
)
.await
.expect("memory_recall");
let deep = dispatch_tool(
fx.state(),
"memory_recall_deep",
json!({"palace": "deep", "query": "programming languages", "top_k": 10}),
)
.await
.expect("memory_recall_deep");
let shallow_n = shallow["results"].as_array().unwrap().len();
let deep_n = deep["results"].as_array().unwrap().len();
assert!(
deep_n >= shallow_n,
"deep ({deep_n}) must surface at least as many results as shallow ({shallow_n})"
);
}
#[tokio::test]
async fn kg_assert_then_query_round_trips() {
let fx = Fixture::new();
create_palace(fx.state(), "kg-rt").await;
dispatch_tool(
fx.state(),
"kg_assert",
json!({
"palace": "kg-rt",
"subject": "alice",
"predicate": "works_at",
"object": "Acme",
"confidence": 0.9,
}),
)
.await
.expect("kg_assert");
let queried = dispatch_tool(
fx.state(),
"kg_query",
json!({"palace": "kg-rt", "subject": "alice"}),
)
.await
.expect("kg_query");
let triples = queried["triples"].as_array().expect("triples array");
assert_eq!(triples.len(), 1, "expected exactly one triple");
assert_eq!(triples[0]["predicate"], "works_at");
assert_eq!(triples[0]["object"], "Acme");
}
#[tokio::test]
async fn kg_query_filters_by_subject() {
let fx = Fixture::new();
create_palace(fx.state(), "kg-filter").await;
dispatch_tool(
fx.state(),
"kg_assert",
json!({
"palace": "kg-filter",
"subject": "alice",
"predicate": "works_at",
"object": "Acme",
}),
)
.await
.expect("kg_assert");
let queried = dispatch_tool(
fx.state(),
"kg_query",
json!({"palace": "kg-filter", "subject": "bob"}),
)
.await
.expect("kg_query");
let triples = queried["triples"].as_array().expect("triples array");
assert!(
triples.is_empty(),
"expected zero triples for unknown subject"
);
}
#[tokio::test]
async fn palace_create_appears_in_list_with_empty_counts() {
let fx = Fixture::new();
create_palace(fx.state(), "fresh").await;
let listed = dispatch_tool(fx.state(), "palace_list", json!({}))
.await
.expect("palace_list");
let ids = listed["palaces"].as_array().expect("palaces array");
assert!(ids.iter().any(|v| v.as_str() == Some("fresh")));
let info = dispatch_tool(fx.state(), "palace_info", json!({"palace": "fresh"}))
.await
.expect("palace_info");
assert_eq!(info["drawer_count"].as_u64(), Some(0));
}
#[tokio::test]
async fn memory_forget_removes_drawer() {
let fx = Fixture::new();
create_palace(fx.state(), "forgetful").await;
let id = remember(
fx.state(),
"forgetful",
"Capybaras are the largest rodents in the world",
&[],
)
.await;
let before = dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "forgetful", "query": "capybara rodent", "top_k": 5}),
)
.await
.expect("recall pre-forget");
assert!(before["results"]
.as_array()
.unwrap()
.iter()
.any(|r| r["content"].as_str().unwrap_or("").contains("Capybaras")));
dispatch_tool(
fx.state(),
"memory_forget",
json!({"palace": "forgetful", "drawer_id": id}),
)
.await
.expect("memory_forget");
let after = dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "forgetful", "query": "capybara rodent", "top_k": 5}),
)
.await
.expect("recall post-forget");
assert!(
!after["results"]
.as_array()
.unwrap()
.iter()
.any(|r| r["content"].as_str().unwrap_or("").contains("Capybaras")),
"drawer must be gone after forget; got {:?}",
after["results"]
);
}
#[tokio::test]
async fn round_trip_remember_recall_forget_recall_empty() {
let fx = Fixture::new();
create_palace(fx.state(), "lifecycle").await;
let id = remember(
fx.state(),
"lifecycle",
"An octopus has three hearts and blue blood",
&[],
)
.await;
let hit = dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "lifecycle", "query": "octopus blood hearts", "top_k": 5}),
)
.await
.unwrap();
assert!(hit["results"]
.as_array()
.unwrap()
.iter()
.any(|r| r["content"].as_str().unwrap_or("").contains("octopus")));
dispatch_tool(
fx.state(),
"memory_forget",
json!({"palace": "lifecycle", "drawer_id": id}),
)
.await
.unwrap();
let miss = dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "lifecycle", "query": "octopus blood hearts", "top_k": 5}),
)
.await
.unwrap();
let l2_hits: Vec<_> = miss["results"]
.as_array()
.unwrap()
.iter()
.filter(|r| r["layer"].as_u64().unwrap_or(0) >= 2)
.collect();
assert!(
!l2_hits
.iter()
.any(|r| r["content"].as_str().unwrap_or("").contains("octopus")),
"forgotten drawer must not appear in L2 recall results; got {l2_hits:?}"
);
}
async fn seed_palace<F, Fut>(data_root: &Path, palace: &str, seed: F)
where
F: FnOnce(AppState, String) -> Fut,
Fut: std::future::Future<Output = ()>,
{
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let state = AppState::new(data_root.to_path_buf());
state.set_ready();
create_palace(&state, palace).await;
seed(state, palace.to_string()).await;
for _ in 0..16 {
tokio::task::yield_now().await;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
#[tokio::test]
async fn read_only_memory_recall_succeeds() {
let tmp = tempfile::tempdir().unwrap();
let data_root = tmp.path().to_path_buf();
seed_palace(&data_root, "ro-recall", |state, palace| async move {
remember(
&state,
&palace,
"Kookaburras are large terrestrial kingfishers native to the woodlands of eastern Australia and southern New Guinea",
&[],
)
.await;
})
.await;
let data_dir = data_root.join("ro-recall");
let _live = lock_palace_files(&data_dir);
let snap_state = fresh_state(&data_root);
let recalled = dispatch_tool(
&snap_state,
"memory_recall",
json!({"palace": "ro-recall", "query": "kookaburra kingfisher", "top_k": 5}),
)
.await
.expect("recall on snapshot must succeed");
let results = recalled["results"].as_array().unwrap();
assert!(
results
.iter()
.any(|r| r["content"].as_str().unwrap_or("").contains("Kookaburras")),
"snapshot recall should surface the seeded drawer; got {results:?}"
);
}
#[tokio::test]
async fn read_only_memory_remember_returns_clear_error() {
let tmp = tempfile::tempdir().unwrap();
let data_root = tmp.path().to_path_buf();
seed_palace(&data_root, "ro-write", |_state, _palace| async move {}).await;
let data_dir = data_root.join("ro-write");
let _live = lock_palace_files(&data_dir);
let snap_state = fresh_state(&data_root);
let res = dispatch_tool(
&snap_state,
"memory_remember",
json!({
"palace": "ro-write",
"text": "this is a long enough write payload to clear the content gate threshold",
"room": "General",
}),
)
.await;
let err = res.expect_err("remember in snapshot mode must error");
let msg = format!("{err:#}");
assert!(
msg.contains("read-only"),
"expected read-only sentinel, got: {msg}"
);
assert!(
msg.contains("daemon"),
"expected daemon guidance, got: {msg}"
);
}
#[tokio::test]
async fn read_only_kg_query_succeeds() {
let tmp = tempfile::tempdir().unwrap();
let data_root = tmp.path().to_path_buf();
seed_palace(&data_root, "ro-kg-r", |state, palace| async move {
dispatch_tool(
&state,
"kg_assert",
json!({
"palace": palace,
"subject": "alice",
"predicate": "knows",
"object": "bob",
}),
)
.await
.expect("kg_assert seed");
})
.await;
let data_dir = data_root.join("ro-kg-r");
let _live = lock_palace_files(&data_dir);
let snap_state = fresh_state(&data_root);
let queried = dispatch_tool(
&snap_state,
"kg_query",
json!({"palace": "ro-kg-r", "subject": "alice"}),
)
.await
.expect("kg_query on snapshot");
let triples = queried["triples"].as_array().unwrap();
assert_eq!(triples.len(), 1);
assert_eq!(triples[0]["object"], "bob");
}
#[tokio::test]
async fn read_only_kg_assert_returns_clear_error() {
let tmp = tempfile::tempdir().unwrap();
let data_root = tmp.path().to_path_buf();
seed_palace(&data_root, "ro-kg-w", |_state, _palace| async move {}).await;
let data_dir = data_root.join("ro-kg-w");
let _live = lock_palace_files(&data_dir);
let snap_state = fresh_state(&data_root);
let res = dispatch_tool(
&snap_state,
"kg_assert",
json!({
"palace": "ro-kg-w",
"subject": "carol",
"predicate": "owns",
"object": "yacht",
}),
)
.await;
let err = res.expect_err("kg_assert in snapshot mode must error");
let msg = format!("{err:#}");
assert!(
msg.contains("read-only"),
"expected read-only sentinel, got: {msg}"
);
}
#[tokio::test]
async fn two_states_can_read_same_palace_simultaneously() {
let fx = Fixture::new();
create_palace(fx.state(), "shared").await;
remember(
fx.state(),
"shared",
"Echidnas are egg-laying mammals known as monotremes, found across Australia and New Guinea",
&[],
)
.await;
let state_b = fresh_state(fx.data_root());
let (a, b) = tokio::join!(
dispatch_tool(fx.state(), "palace_info", json!({"palace": "shared"})),
dispatch_tool(&state_b, "palace_info", json!({"palace": "shared"})),
);
let a = a.expect("info on state A");
let b = b.expect("info on state B");
assert_eq!(a["drawer_count"], b["drawer_count"]);
assert_eq!(a["drawer_count"].as_u64(), Some(1));
}
#[tokio::test]
async fn read_only_open_while_writer_holds_lock_succeeds() {
let tmp = tempfile::tempdir().unwrap();
let data_root = tmp.path().to_path_buf();
seed_palace(&data_root, "concurrent-ro", |state, palace| async move {
remember(
&state,
&palace,
"Wombats produce distinctive cube-shaped droppings due to the unusual elasticity of their intestinal walls",
&[],
)
.await;
})
.await;
let data_dir = data_root.join("concurrent-ro");
let _live = lock_palace_files(&data_dir);
let snap_state = Arc::new(fresh_state(&data_root));
let started = Instant::now();
let info = dispatch_tool(
snap_state.as_ref(),
"palace_info",
json!({"palace": "concurrent-ro"}),
)
.await
.expect("palace_info on snapshot");
assert!(started.elapsed() < Duration::from_secs(2));
assert_eq!(info["drawer_count"].as_u64(), Some(1));
}
#[tokio::test]
#[ignore = "perf budget — requires warm embedder; run with --include-ignored"]
async fn perf_memory_remember_under_500ms() {
let fx = Fixture::new();
create_palace(fx.state(), "perf-remember").await;
remember(fx.state(), "perf-remember", "warm-up drawer", &[]).await;
let started = Instant::now();
remember(
fx.state(),
"perf-remember",
"timed drawer for the perf budget",
&[],
)
.await;
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_millis(500),
"memory_remember took {elapsed:?} (budget: 500ms)"
);
}
#[tokio::test]
#[ignore = "perf budget — 100-drawer seed is slow; run with --include-ignored"]
async fn perf_memory_recall_100_drawers_under_50ms() {
let fx = Fixture::new();
create_palace(fx.state(), "perf-recall").await;
for i in 0..100 {
remember(
fx.state(),
"perf-recall",
&format!("Seed drawer {i} about unique topic alpha-{i}"),
&[],
)
.await;
}
dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "perf-recall", "query": "alpha-50", "top_k": 5}),
)
.await
.unwrap();
let started = Instant::now();
dispatch_tool(
fx.state(),
"memory_recall",
json!({"palace": "perf-recall", "query": "alpha-50", "top_k": 5}),
)
.await
.unwrap();
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_millis(50),
"memory_recall took {elapsed:?} (budget: 50ms)"
);
}
#[tokio::test]
#[ignore = "perf budget — run with --include-ignored"]
async fn perf_kg_assert_under_10ms() {
let fx = Fixture::new();
create_palace(fx.state(), "perf-assert").await;
let started = Instant::now();
dispatch_tool(
fx.state(),
"kg_assert",
json!({
"palace": "perf-assert",
"subject": "alice",
"predicate": "knows",
"object": "bob",
}),
)
.await
.unwrap();
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_millis(10),
"kg_assert took {elapsed:?} (budget: 10ms)"
);
}
#[tokio::test]
#[ignore = "perf budget — 1000-triple seed is slow; run with --include-ignored"]
async fn perf_kg_query_1000_triples_under_20ms() {
let fx = Fixture::new();
create_palace(fx.state(), "perf-query").await;
for i in 0..1000 {
dispatch_tool(
fx.state(),
"kg_assert",
json!({
"palace": "perf-query",
"subject": format!("subject-{i}"),
"predicate": "knows",
"object": format!("object-{i}"),
}),
)
.await
.unwrap();
}
let started = Instant::now();
dispatch_tool(
fx.state(),
"kg_query",
json!({"palace": "perf-query", "subject": "subject-500"}),
)
.await
.unwrap();
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_millis(20),
"kg_query took {elapsed:?} (budget: 20ms)"
);
}
#[tokio::test]
#[ignore = "perf budget — run with --include-ignored"]
async fn perf_palace_cold_open_under_200ms() {
let tmp = tempfile::tempdir().unwrap();
let data_root = tmp.path().to_path_buf();
seed_palace(&data_root, "perf-cold", |_state, _palace| async move {}).await;
let snap = fresh_state(&data_root);
let started = Instant::now();
dispatch_tool(&snap, "palace_info", json!({"palace": "perf-cold"}))
.await
.unwrap();
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_millis(200),
"cold palace_info took {elapsed:?} (budget: 200ms)"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "perf budget — run with --include-ignored"]
async fn perf_ten_concurrent_read_only_opens_under_1s() {
let fx = Fixture::new();
create_palace(fx.state(), "perf-concurrent").await;
remember(fx.state(), "perf-concurrent", "seed", &[]).await;
let data_root = fx.data_root().to_path_buf();
let palace_dir = data_root.join("perf-concurrent");
let _live = lock_palace_files(&palace_dir);
let started = Instant::now();
let mut handles = Vec::with_capacity(10);
for _ in 0..10 {
let root = data_root.clone();
handles.push(tokio::spawn(async move {
let st = AppState::new(root);
dispatch_tool(&st, "palace_info", json!({"palace": "perf-concurrent"})).await
}));
}
for h in handles {
h.await.expect("task join").expect("palace_info ok");
}
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_secs(1),
"10 concurrent snapshot opens took {elapsed:?} (budget: 1s)"
);
}