#![allow(clippy::too_many_lines)]
use super::*;
fn test_state() -> (AppState, tempfile::TempDir) {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let state = AppState::new(root);
state.set_ready();
(state, tmp)
}
fn test_state_warming() -> (AppState, tempfile::TempDir) {
static SKIP_ENFORCEMENT_SET: std::sync::OnceLock<()> = std::sync::OnceLock::new();
SKIP_ENFORCEMENT_SET.get_or_init(|| unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
});
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
(AppState::new(root), tmp)
}
#[tokio::test]
async fn initialize_returns_protocol_version_and_capabilities() {
let (state, _tmp) = test_state();
let req = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test", "version": "0"}
}
});
let resp = handle_message(&state, req).await;
assert_eq!(resp["jsonrpc"], "2.0");
assert_eq!(resp["id"], 1);
assert_eq!(resp["result"]["protocolVersion"], "2024-11-05");
assert!(resp["result"]["capabilities"]["tools"].is_object());
assert_eq!(resp["result"]["serverInfo"]["name"], "trusty-memory");
}
#[tokio::test]
async fn initialized_notification_returns_null() {
let (state, _tmp) = test_state();
let req = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
});
let resp = handle_message(&state, req).await;
assert!(resp.is_null());
}
#[tokio::test]
async fn tools_list_returns_all_tools() {
let (state, _tmp) = test_state();
let req = json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"});
let resp = handle_message(&state, req).await;
let tools = resp["result"]["tools"].as_array().expect("tools array");
assert_eq!(tools.len(), 49);
}
#[tokio::test]
async fn unknown_method_returns_error() {
let (state, _tmp) = test_state();
let req = json!({"jsonrpc": "2.0", "id": 4, "method": "wat"});
let resp = handle_message(&state, req).await;
assert_eq!(resp["error"]["code"], -32601);
}
#[tokio::test]
async fn ping_returns_empty_result() {
let (state, _tmp) = test_state();
let req = json!({"jsonrpc": "2.0", "id": 5, "method": "ping"});
let resp = handle_message(&state, req).await;
assert!(resp["result"].is_object());
}
#[tokio::test]
async fn app_state_default_constructs() {
let (s, _tmp) = test_state();
assert!(!s.version.is_empty());
assert!(s.registry.is_empty());
assert!(s.default_palace.is_none());
}
#[test]
#[cfg(unix)]
fn open_activity_log_with_fallback_returns_discard_when_unwritable() {
if unsafe { libc::geteuid() } == 0 {
eprintln!(
"skipping open_activity_log_with_fallback_returns_discard_when_unwritable: running as root"
);
return;
}
use std::os::unix::fs::PermissionsExt;
let outer = tempfile::tempdir().expect("outer tempdir");
let primary = outer.path().join("primary");
let tmpdir = outer.path().join("fake-tmp");
std::fs::create_dir(&primary).expect("create primary");
std::fs::create_dir(&tmpdir).expect("create tmpdir");
std::fs::set_permissions(&primary, std::fs::Permissions::from_mode(0o000))
.expect("chmod primary");
std::fs::set_permissions(&tmpdir, std::fs::Permissions::from_mode(0o000))
.expect("chmod tmpdir");
let log = open_activity_log_with_fallback_in(&primary, &tmpdir);
let _ = std::fs::set_permissions(&primary, std::fs::Permissions::from_mode(0o700));
let _ = std::fs::set_permissions(&tmpdir, std::fs::Permissions::from_mode(0o700));
assert!(
log.is_discard(),
"expected ActivityLog::Discard when both data root and tempdir are unwritable"
);
let id = log
.append(
ActivitySource::Http,
None,
"drawer_added",
json!({"smoke": true}),
)
.expect("discard append must succeed");
assert_eq!(id, 0);
assert_eq!(log.count().expect("discard count"), 0);
assert!(log
.list(&ActivityFilter::default(), 10, 0)
.expect("discard list")
.is_empty());
}
#[tokio::test]
async fn default_palace_used_when_arg_omitted() {
trusty_common::memory_core::retrieval::seed_shared_embedder_with_mock();
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let registry = trusty_common::memory_core::PalaceRegistry::new();
let palace = trusty_common::memory_core::Palace {
id: trusty_common::memory_core::PalaceId::new("default-pal"),
name: "default-pal".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join("default-pal"),
};
registry
.create_palace(&root, palace)
.expect("create_palace");
let state = AppState::new(root).with_default_palace(Some("default-pal".to_string()));
state.set_ready();
let init = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize"}),
)
.await;
assert_eq!(
init["result"]["serverInfo"]["default_palace"], "default-pal",
"initialize must echo default_palace in serverInfo"
);
let list = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
)
.await;
let tools = list["result"]["tools"].as_array().expect("tools array");
let remember = tools
.iter()
.find(|t| t["name"] == "memory_remember")
.expect("memory_remember tool");
let required: Vec<&str> = remember["inputSchema"]["required"]
.as_array()
.expect("required array")
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(
!required.contains(&"palace"),
"palace must not be required when default is configured; got {required:?}"
);
assert!(required.contains(&"text"));
let call = handle_message(
&state,
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "memory_remember",
"arguments": {"text": "default palace test memory content with several tokens"},
},
}),
)
.await;
let text = call["result"]["content"][0]["text"]
.as_str()
.unwrap_or_else(|| panic!("expected success result, got {call}"));
let parsed: Value = serde_json::from_str(text).expect("parse content json");
assert_eq!(parsed["palace"], "default-pal");
assert_eq!(parsed["status"], "stored");
assert!(parsed["drawer_id"].as_str().is_some());
}
#[tokio::test]
async fn missing_palace_without_default_errors() {
let (state, _tmp) = test_state();
let resp = handle_message(
&state,
json!({
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "memory_recall",
"arguments": {"query": "anything"},
},
}),
)
.await;
assert_eq!(resp["error"]["code"], -32603);
let msg = resp["error"]["message"].as_str().unwrap_or("");
assert!(
msg.contains("missing 'palace'"),
"expected helpful error, got: {msg}"
);
}
#[tokio::test]
async fn load_palaces_from_disk_rehydrates_registry() {
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
{
let writer = PalaceRegistry::new();
for id in ["alpha", "beta"] {
let palace = Palace {
id: PalaceId::new(id),
name: id.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join(id),
};
writer
.create_palace(&root, palace)
.expect("persist palace to disk");
}
}
std::fs::create_dir_all(root.join("not-a-palace")).expect("mkdir");
let state = AppState::new(root);
assert!(
state.registry.is_empty(),
"AppState::new must start with an empty registry"
);
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(count, 2, "both persisted palaces should be loaded");
assert_eq!(state.registry.len(), 2, "registry should hold both palaces");
let ids: Vec<String> = state.registry.list().into_iter().map(|p| p.0).collect();
assert!(ids.contains(&"alpha".to_string()));
assert!(ids.contains(&"beta".to_string()));
}
#[tokio::test]
async fn with_writer_intent_marks_registry_writer() {
use trusty_common::memory_core::store::OpenIntent;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let default_state = AppState::new(root.clone());
assert_eq!(
default_state.registry.open_intent(),
OpenIntent::ReadOnlyClient,
"default AppState registry must be read-only (CLI / test paths)"
);
let writer_state = AppState::new(root).with_writer_intent();
assert_eq!(
writer_state.registry.open_intent(),
OpenIntent::Writer,
"with_writer_intent() must flip the daemon registry to Writer"
);
}
#[tokio::test]
#[should_panic(expected = "is_empty()")]
async fn with_writer_intent_panics_on_hydrated_registry() {
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
{
let writer = PalaceRegistry::new();
let palace = Palace {
id: PalaceId::new("hydrated"),
name: "hydrated".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join("hydrated"),
};
writer
.create_palace(&root, palace)
.expect("persist palace to disk");
}
let state = AppState::new(root);
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(count, 1, "precondition: registry must be hydrated");
let _ = state.with_writer_intent();
}
fn incompatible_palace_fixture() -> (
tempfile::TempDir,
std::path::PathBuf,
trusty_common::memory_core::PalaceId,
std::path::PathBuf,
) {
use trusty_common::memory_core::store::PalaceStore;
use trusty_common::memory_core::{Palace, PalaceId};
const GARBAGE: [u8; 4096] = [0xABu8; 4096];
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let pid = PalaceId::new("stale-format");
let data_dir = root.join(pid.as_str());
std::fs::create_dir_all(&data_dir).expect("mkdir palace data dir");
PalaceStore::save_palace(&Palace {
id: pid.clone(),
name: "stale-format".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: data_dir.clone(),
})
.expect("persist palace metadata");
let store_path = data_dir.join("index.usearch.redb");
std::fs::write(&store_path, GARBAGE).expect("write incompatible-format store");
(tmp, root, pid, store_path)
}
#[tokio::test]
async fn load_palaces_from_disk_honours_registry_open_intent() {
use trusty_common::memory_core::store::redb_open::INCOMPATIBLE_SUFFIX;
use trusty_common::memory_core::store::OpenIntent;
let (_ro_tmp, ro_root, ro_pid, ro_store) = incompatible_palace_fixture();
let before = std::fs::read(&ro_store).expect("read fixture bytes");
let reader_state = AppState::new(ro_root);
assert_eq!(
reader_state.registry.open_intent(),
OpenIntent::ReadOnlyClient,
"precondition: a plain AppState is a read-only client"
);
let reader_count = reader_state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk (read-only client)");
assert_eq!(
reader_count, 0,
"a read-only client must refuse an incompatible-format palace, not recover it"
);
assert!(
reader_state.registry.get(&ro_pid).is_none(),
"a refused palace must not be registered"
);
assert_eq!(
before,
std::fs::read(&ro_store).expect("re-read fixture bytes"),
"the store must be byte-identical after a refused read-only hydration"
);
let (_w_tmp, w_root, w_pid, w_store) = incompatible_palace_fixture();
let writer_state = AppState::new(w_root).with_writer_intent();
assert_eq!(
writer_state.registry.open_intent(),
OpenIntent::Writer,
"precondition: the daemon AppState is a writer"
);
let writer_count = writer_state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk (writer)");
assert_eq!(
writer_count, 1,
"writer-intent hydration must run the #702 recovery and load the palace; \
a count of 0 means load_palaces_from_disk ignored registry.open_intent() \
and opened as a ReadOnlyClient"
);
assert!(
writer_state.registry.get(&w_pid).is_some(),
"the recovered palace must be registered"
);
let backup = w_store.with_file_name(format!(
"{}{INCOMPATIBLE_SUFFIX}",
w_store
.file_name()
.expect("fixture store has a file name")
.to_string_lossy()
));
assert!(
backup.exists(),
"the writer must rename the stale store aside to {}",
backup.display()
);
}
#[tokio::test]
async fn load_palaces_from_disk_records_an_unopenable_palace() {
use trusty_common::memory_core::store::PalaceStore;
use trusty_common::memory_core::{Palace, PalaceId};
let (_tmp, root, pid, _store) = incompatible_palace_fixture();
let good = PalaceId::new("healthy");
let good_dir = root.join(good.as_str());
std::fs::create_dir_all(&good_dir).expect("mkdir healthy palace dir");
PalaceStore::save_palace(&Palace {
id: good.clone(),
name: "healthy".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: good_dir,
})
.expect("persist healthy palace metadata");
let state = AppState::new(root);
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(count, 1, "only the healthy palace hydrates");
let reason = state
.registry
.unopenable_reason(&pid)
.expect("a palace present on disk but unopenable must stay observable");
assert!(
reason.contains("incompatible") || reason.contains("open vector store"),
"the recorded reason must explain the refusal; got: {reason}"
);
assert!(
state.registry.unopenable_reason(&good).is_none(),
"a palace that hydrated must never be reported unopenable"
);
let listed: Vec<String> = state
.registry
.unopenable()
.into_iter()
.map(|(id, _)| id.0)
.collect();
assert_eq!(
listed,
vec!["stale-format".to_string()],
"exactly the unopenable palace is listed"
);
}
#[test]
fn resolve_palace_registry_dir_prefers_palaces_subdir() {
let tmp = tempfile::tempdir().expect("tempdir");
let data_dir = tmp.path().to_path_buf();
std::fs::create_dir_all(data_dir.join("palaces")).expect("mkdir palaces");
let resolved = resolve_palace_registry_dir(data_dir.clone());
assert_eq!(resolved, data_dir.join("palaces"));
}
#[test]
fn resolve_palace_registry_dir_falls_back_to_data_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let data_dir = tmp.path().to_path_buf();
let resolved = resolve_palace_registry_dir(data_dir.clone());
assert_eq!(resolved, data_dir);
}
#[test]
fn resolve_palace_registry_dir_relative_input_is_not_absolute() {
let relative = std::path::PathBuf::from("relative/path");
let result = resolve_palace_registry_dir(relative.clone());
assert!(
!result.is_absolute(),
"a relative input should produce a relative registry dir (caught by main.rs guard)"
);
}
#[test]
fn resolve_palace_registry_dir_root_input_stays_root() {
let root = std::path::PathBuf::from("/");
let result = resolve_palace_registry_dir(root);
assert_eq!(result, std::path::PathBuf::from("/"));
}
#[tokio::test]
async fn load_palaces_from_disk_handles_palaces_subdir() {
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let nested = root.join("palaces");
{
let writer = PalaceRegistry::new();
for id in ["cto", "engineering"] {
let palace = Palace {
id: PalaceId::new(id),
name: id.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: nested.join(id),
};
writer
.create_palace(&nested, palace)
.expect("persist palace under palaces/ subdir");
}
}
let registry_dir = resolve_palace_registry_dir(root);
assert_eq!(registry_dir, nested, "must resolve into palaces/ subdir");
let state = AppState::new(registry_dir);
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(count, 2, "both nested palaces should be loaded");
assert_eq!(state.registry.len(), 2);
let ids: Vec<String> = state.registry.list().into_iter().map(|p| p.0).collect();
assert!(ids.contains(&"cto".to_string()));
assert!(ids.contains(&"engineering".to_string()));
}
#[tokio::test]
async fn load_palaces_from_disk_empty_root_returns_zero() {
let (state, _tmp) = test_state();
let count = state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk on empty root");
assert_eq!(count, 0);
assert!(state.registry.is_empty());
}
#[tokio::test]
async fn palace_name_cache_populated_after_hydration() {
use trusty_common::memory_core::{Palace, PalaceId, PalaceRegistry};
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
{
let writer = PalaceRegistry::new();
for (id, name) in [("alpha", "Alpha Project"), ("beta", "Beta Project")] {
let palace = Palace {
id: PalaceId::new(id),
name: name.to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: root.join(id),
};
writer.create_palace(&root, palace).expect("persist palace");
}
}
let state = AppState::new(root);
assert!(
state.palace_names.is_empty(),
"fresh AppState must start with an empty name cache"
);
state
.load_palaces_from_disk()
.await
.expect("load_palaces_from_disk");
assert_eq!(state.palace_names.len(), 2, "cache must hold both palaces");
assert_eq!(
state.palace_names.get("alpha").map(|e| e.value().clone()),
Some("Alpha Project".to_string()),
);
assert_eq!(
state.palace_names.get("beta").map(|e| e.value().clone()),
Some("Beta Project".to_string()),
);
}
#[tokio::test]
async fn palace_name_cache_updates_on_create() {
use serde_json::json;
let (state, _tmp) = test_state();
let _ = tools::dispatch_tool(&state, "palace_create", json!({"name": "gamma"}))
.await
.expect("palace_create");
assert_eq!(
state.palace_names.get("gamma").map(|e| e.value().clone()),
Some("gamma".to_string()),
"palace_create must populate the in-memory name cache so writes \
can resolve the friendly name without a disk walk"
);
}
#[tokio::test]
async fn initialize_without_default_palace_omits_field() {
let (state, _tmp) = test_state();
let init = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize"}),
)
.await;
assert!(init["result"]["serverInfo"]["default_palace"].is_null());
}
#[tokio::test]
async fn initialize_does_not_advertise_prompts_capability() {
let (state, _tmp) = test_state();
let init = handle_message(
&state,
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize"}),
)
.await;
assert!(
init["result"]["capabilities"]["prompts"].is_null(),
"initialize must NOT advertise the prompts capability; got {init}"
);
for method in ["prompts/list", "prompts/get"] {
let resp =
handle_message(&state, json!({"jsonrpc": "2.0", "id": 2, "method": method})).await;
assert_eq!(
resp["error"]["code"], -32601,
"{method} should return method-not-found; got {resp}"
);
}
}
#[tokio::test]
async fn app_state_starts_with_empty_bound_addr() {
let (state, _tmp) = test_state();
assert!(state.bound_addr.get().is_none());
}
#[test]
fn daemon_event_type_str_matches_sse_tag() {
let cases = [
DaemonEvent::PalaceCreated {
id: "p".into(),
name: "p".into(),
source: ActivitySource::Http,
},
DaemonEvent::DrawerAdded {
palace_id: "p".into(),
palace_name: "p".into(),
drawer_count: 1,
timestamp: chrono::Utc::now(),
content_preview: String::new(),
source: ActivitySource::Mcp,
},
DaemonEvent::DrawerDeleted {
palace_id: "p".into(),
drawer_count: 0,
source: ActivitySource::Http,
},
DaemonEvent::DreamCompleted {
palace_id: None,
merged: 0,
pruned: 0,
compacted: 0,
closets_updated: 0,
duration_ms: 0,
source: ActivitySource::Http,
},
DaemonEvent::StatusChanged {
total_drawers: 0,
total_vectors: 0,
total_kg_triples: 0,
},
DaemonEvent::HookFired {
palace_id: Some("p".into()),
palace_name: Some("p".into()),
hook_type: HookType::UserPromptSubmit,
injection_kind: InjectionKind::PromptContext,
injection_length: 12,
trigger_prompt_excerpt: "hello".into(),
timestamp: chrono::Utc::now(),
duration_ms: 5,
source: ActivitySource::Hook,
},
];
for ev in &cases {
let json = serde_json::to_value(ev).unwrap();
assert_eq!(json["type"].as_str(), Some(ev.type_str()));
}
}
#[test]
fn hook_type_serde_round_trips() {
let cases = [
(HookType::UserPromptSubmit, "\"UserPromptSubmit\""),
(HookType::SessionStart, "\"SessionStart\""),
];
for (ht, expected) in cases {
let s = serde_json::to_string(&ht).unwrap();
assert_eq!(s, expected, "{ht:?} should serialise to {expected}");
let back: HookType = serde_json::from_str(&s).unwrap();
assert_eq!(back, ht);
assert_eq!(ht.as_str(), expected.trim_matches('"'));
}
}
#[test]
fn injection_kind_serde_round_trips() {
let cases = [
(InjectionKind::PromptContext, "\"prompt-context\""),
(InjectionKind::InboxCheck, "\"inbox-check\""),
];
for (ik, expected) in cases {
let s = serde_json::to_string(&ik).unwrap();
assert_eq!(s, expected);
let back: InjectionKind = serde_json::from_str(&s).unwrap();
assert_eq!(back, ik);
assert_eq!(ik.as_str(), expected.trim_matches('"'));
}
}
#[test]
fn hook_excerpt_truncates_long_prompts() {
let long = "x".repeat(200);
let excerpt = hook_prompt_excerpt(&long);
assert!(excerpt.chars().count() <= HOOK_PROMPT_EXCERPT_CHARS);
assert!(excerpt.ends_with('…'));
assert_eq!(hook_prompt_excerpt(""), "");
}
#[test]
fn hook_excerpt_collapses_whitespace() {
let input = "hello\n\nworld\t\tfoo";
let excerpt = hook_prompt_excerpt(input);
assert_eq!(excerpt, "hello world foo");
}
#[test]
fn daemon_event_palace_id_and_source_extraction() {
let ev = DaemonEvent::DrawerAdded {
palace_id: "alpha".into(),
palace_name: "alpha".into(),
drawer_count: 1,
timestamp: chrono::Utc::now(),
content_preview: String::new(),
source: ActivitySource::Mcp,
};
assert_eq!(ev.palace_id(), Some("alpha"));
assert_eq!(ev.source(), Some(ActivitySource::Mcp));
let status = DaemonEvent::StatusChanged {
total_drawers: 1,
total_vectors: 2,
total_kg_triples: 3,
};
assert_eq!(status.palace_id(), None);
assert_eq!(status.source(), None);
let dream = DaemonEvent::DreamCompleted {
palace_id: Some("p1".into()),
merged: 0,
pruned: 0,
compacted: 0,
closets_updated: 0,
duration_ms: 10,
source: ActivitySource::Http,
};
assert_eq!(dream.palace_id(), Some("p1"));
assert_eq!(dream.source(), Some(ActivitySource::Http));
}
#[tokio::test]
async fn emit_persists_mutations_but_skips_status_changed() {
let (state, _tmp) = test_state();
state.emit(DaemonEvent::PalaceCreated {
id: "p".into(),
name: "p".into(),
source: ActivitySource::Http,
});
state.emit(DaemonEvent::StatusChanged {
total_drawers: 1,
total_vectors: 0,
total_kg_triples: 0,
});
state.emit(DaemonEvent::DrawerAdded {
palace_id: "p".into(),
palace_name: "p".into(),
drawer_count: 1,
timestamp: chrono::Utc::now(),
content_preview: "x".into(),
source: ActivitySource::Mcp,
});
state.flush_activity_writes().await;
let count = state.activity_log.count().unwrap();
assert_eq!(count, 2, "only PalaceCreated + DrawerAdded must persist");
}
#[tokio::test]
async fn bm25_lane_disabled_by_default() {
let _guard = crate::commands::env_test_lock().lock().await;
let prev = std::env::var("TRUSTY_BM25_DAEMON").ok();
unsafe {
std::env::remove_var("TRUSTY_BM25_DAEMON");
}
let (state, _tmp) = test_state();
let state = state.with_bm25_lane_from_env();
assert!(
state.bm25.is_none(),
"the bm25 lane must be None when TRUSTY_BM25_DAEMON is unset"
);
if let Some(v) = prev {
unsafe {
std::env::set_var("TRUSTY_BM25_DAEMON", v);
}
}
}
#[tokio::test]
async fn bm25_lane_enabled_when_env_set() {
let _guard = crate::commands::env_test_lock().lock().await;
let prev = std::env::var("TRUSTY_BM25_DAEMON").ok();
unsafe {
std::env::set_var("TRUSTY_BM25_DAEMON", "1");
}
let (state, _tmp) = test_state();
let state = state.with_bm25_lane_from_env();
let lane = state
.bm25
.as_ref()
.expect("the bm25 lane must be Some when TRUSTY_BM25_DAEMON=1");
assert_eq!(
lane.data_dir_for_palace("some-palace"),
state.data_root.join("some-palace").join("bm25"),
);
match prev {
Some(v) => unsafe { std::env::set_var("TRUSTY_BM25_DAEMON", v) },
None => unsafe { std::env::remove_var("TRUSTY_BM25_DAEMON") },
}
}
#[tokio::test]
async fn multi_tenant_mode_disabled_by_default() {
let _guard = crate::commands::env_test_lock().lock().await;
let prev = std::env::var("TRUSTY_MEMORY_MULTI_TENANT").ok();
unsafe {
std::env::remove_var("TRUSTY_MEMORY_MULTI_TENANT");
}
let (state, _tmp) = test_state();
let state = state.with_multi_tenant_mode_from_env();
assert!(
!state.multi_tenant_mode,
"multi_tenant_mode must be false when TRUSTY_MEMORY_MULTI_TENANT is unset"
);
if let Some(v) = prev {
unsafe {
std::env::set_var("TRUSTY_MEMORY_MULTI_TENANT", v);
}
}
}
#[tokio::test]
async fn multi_tenant_mode_enabled_when_env_set() {
let _guard = crate::commands::env_test_lock().lock().await;
let prev = std::env::var("TRUSTY_MEMORY_MULTI_TENANT").ok();
unsafe {
std::env::set_var("TRUSTY_MEMORY_MULTI_TENANT", "1");
}
let (state, _tmp) = test_state();
let state = state.with_multi_tenant_mode_from_env();
assert!(
state.multi_tenant_mode,
"multi_tenant_mode must be true when TRUSTY_MEMORY_MULTI_TENANT=1"
);
match prev {
Some(v) => unsafe { std::env::set_var("TRUSTY_MEMORY_MULTI_TENANT", v) },
None => unsafe { std::env::remove_var("TRUSTY_MEMORY_MULTI_TENANT") },
}
}
#[tokio::test]
async fn daemon_readiness_transitions_warming_to_ready() {
let (state, _tmp) = test_state_warming();
assert_eq!(
state.readiness(),
DaemonReadiness::Warming,
"daemon must start in Warming state"
);
state.set_ready();
assert_eq!(
state.readiness(),
DaemonReadiness::Ready,
"daemon must be Ready after set_ready()"
);
}
#[test]
fn daemon_readiness_from_u8() {
assert_eq!(DaemonReadiness::from_u8(0), DaemonReadiness::Warming);
assert_eq!(DaemonReadiness::from_u8(1), DaemonReadiness::Ready);
assert_eq!(DaemonReadiness::from_u8(255), DaemonReadiness::Ready);
}
#[tokio::test]
async fn open_palace_lazy_reopens_hydration_skipped_palace() {
let (state, _tmp) = test_state();
let pid = trusty_common::memory_core::palace::PalaceId::new("hydration-skip");
let palace = trusty_common::memory_core::Palace {
id: pid.clone(),
name: "hydration-skip".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("hydration-skip"),
};
state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
state.registry.remove(&pid);
assert!(
state.registry.get(&pid).is_none(),
"palace must appear absent (simulating hydration skip) before the lazy-reopen"
);
let handle = state
.registry
.open_palace(&state.data_root, &pid)
.expect("open_palace must lazily reopen a hydration-skipped palace");
assert_eq!(handle.id.as_str(), "hydration-skip");
}