use super::*;
use crate::AppState;
use serde_json::json;
use trusty_common::memory_core::palace::PalaceId;
use uuid::Uuid;
fn skip_palace_enforcement() {
static SET: std::sync::OnceLock<()> = std::sync::OnceLock::new();
SET.get_or_init(|| unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
});
}
fn seed_embedder() {
trusty_common::memory_core::retrieval::seed_shared_embedder_with_mock();
}
pub(super) fn test_state() -> (AppState, tempfile::TempDir) {
skip_palace_enforcement();
seed_embedder();
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let state = AppState::new(root);
state.set_ready();
(state, tmp)
}
fn test_state_warming() -> (crate::AppState, tempfile::TempDir) {
skip_palace_enforcement();
seed_embedder();
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let state = crate::AppState::new(root);
(state, tmp)
}
#[test]
fn tool_definitions_drops_palace_required_when_default_set() {
let with_default = tool_definitions_with(true);
let without_default = tool_definitions_with(false);
for (name, palace_required_when_no_default) in [
("memory_remember", true),
("memory_recall", false),
("memory_recall_deep", false),
("memory_list", false),
("memory_forget", true),
("palace_info", false),
("palace_compact", true),
("palace_reembed", true),
("palace_unalias", true),
("palace_verify_embedded", false),
("kg_assert", true),
("kg_query", false),
("add_alias", true),
("discover_aliases", true),
] {
for (defs, has_default) in [(&with_default, true), (&without_default, false)] {
let tools = defs["tools"].as_array().unwrap();
let tool = tools.iter().find(|t| t["name"] == name).unwrap();
let required: Vec<&str> = tool["inputSchema"]["required"]
.as_array()
.unwrap()
.iter()
.filter_map(|v| v.as_str())
.collect();
let palace_required = required.contains(&"palace");
let expected = palace_required_when_no_default && !has_default;
assert_eq!(
palace_required, expected,
"tool={name} has_default={has_default} required={required:?}"
);
}
}
}
#[test]
fn tool_definitions_lists_all_tools() {
let defs = tool_definitions();
let tools = defs
.get("tools")
.and_then(|t| t.as_array())
.expect("tools array");
let names: Vec<&str> = tools
.iter()
.filter_map(|t| t.get("name").and_then(|n| n.as_str()))
.collect();
let roster = [
"memory_remember",
"memory_note",
"memory_recall",
"memory_recall_deep",
"memory_list",
"memory_forget",
"palace_create",
"palace_delete",
"palace_update",
"palace_list",
"palace_info",
"palace_compact",
"palace_reembed",
"palace_unalias",
"palace_verify_embedded",
"palace_embed_sweep",
"kg_assert",
"kg_retract_triple",
"kg_query",
"kg_list_subjects",
"memory_recall_all",
"kg_gaps",
"add_alias",
"list_prompt_facts",
"remove_prompt_fact",
"get_prompt_context",
"discover_aliases",
"kg_bootstrap",
"memory_send_message",
"upgrade",
"console_metrics",
"chat_session_create",
"chat_session_add_turn",
"chat_session_get",
"chat_session_recall",
"chat_session_list",
"chat_session_delete",
"chat_turn_append",
"chat_asset_capabilities",
"chat_asset_put",
"chat_asset_get",
"dream_consolidate_room",
"palace_dream",
"task_add",
"task_list",
"task_complete",
"room_list",
"room_create",
"room_rename",
"wing_list",
"wing_create",
"wing_rename",
];
for expected in roster {
assert!(names.contains(&expected), "missing tool: {expected}");
}
for served in &names {
assert!(
roster.contains(served),
"tool not in the roster above: {served}"
);
}
assert_eq!(
names.len(),
roster.len(),
"duplicate tool name in tool_definitions"
);
}
#[tokio::test]
async fn dispatch_palace_create_persists() {
let (state, _tmp) = test_state();
let created = dispatch_tool(&state, "palace_create", json!({"name": "alpha"}))
.await
.expect("palace_create");
assert_eq!(created["palace_id"], "alpha");
let listed = dispatch_tool(&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("alpha")));
}
#[tokio::test]
async fn dispatch_palace_reembed_dry_run_reports_counts() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "reembed-test"}))
.await
.expect("palace_create");
let out = dispatch_tool(&state, "palace_reembed", json!({"palace": "reembed-test"}))
.await
.expect("palace_reembed");
assert_eq!(out["dry_run"], true, "must default to a dry run: {out}");
assert_eq!(out["missing"], 0);
assert_eq!(out["attempted"], 0);
assert_eq!(out["repaired"], 0);
assert!(out["drawer_count"].is_number());
assert!(out["vector_count"].is_number());
}
#[tokio::test]
async fn dispatch_palace_unalias_dry_run_names_ids_and_writes_nothing() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "unalias-test"}))
.await
.expect("palace_create");
let out = dispatch_tool(&state, "palace_unalias", json!({"palace": "unalias-test"}))
.await
.expect("palace_unalias must be dispatchable");
assert_eq!(out["dry_run"], true, "must default to a dry run: {out}");
assert_eq!(
out["outcome"], "clean",
"a palace with no collision is clean, not repaired: {out}"
);
assert_eq!(out["success"], true);
assert!(
out["freed_ids"]
.as_array()
.expect("freed_ids array")
.is_empty(),
"an id SET, empty here — never a bare count: {out}"
);
assert_eq!(
out["reembed_required"], false,
"nothing was freed, so nothing is owed"
);
assert!(out["error"].is_null(), "a clean run has no error: {out}");
}
#[tokio::test]
async fn dispatch_palace_create_force_allowed_in_single_tenant_default() {
let (state, _tmp) = test_state();
let created = dispatch_tool(
&state,
"palace_create",
json!({"name": "forced-slug", "force": true}),
)
.await
.expect("palace_create with force must succeed in default single-tenant mode");
assert_eq!(created["palace_id"], "forced-slug");
}
#[tokio::test]
async fn dispatch_palace_create_force_denied_in_multi_tenant_mode() {
let (mut state, _tmp) = test_state();
state.multi_tenant_mode = true;
let err = dispatch_tool(
&state,
"palace_create",
json!({"name": "forced-slug", "force": true}),
)
.await
.expect_err("force=true must be refused in multi-tenant mode");
assert!(format!("{err:#}").contains("authorization signal"));
}
#[tokio::test]
async fn dispatch_remember_then_recall() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "beta"}))
.await
.expect("palace_create");
let remembered = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "beta",
"text": "Quokkas are the happiest marsupials in Australia by general consensus",
"room": "General",
"tags": ["wildlife"],
}),
)
.await
.expect("memory_remember");
assert!(remembered["drawer_id"].as_str().is_some());
let recalled = dispatch_tool(
&state,
"memory_recall",
json!({"palace": "beta", "query": "Quokkas marsupials Australia", "top_k": 5}),
)
.await
.expect("memory_recall");
let results = recalled["results"].as_array().expect("results");
assert!(
results
.iter()
.any(|r| r["content"].as_str().unwrap_or("").contains("Quokkas")),
"expected to recall the Quokkas drawer; got {results:?}"
);
}
#[tokio::test]
async fn auto_kg_extraction_hooks_into_memory_remember() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "kgauto"}))
.await
.expect("palace_create");
let _ = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "kgauto",
"text": "Rustc is a compiler for the Rust language; tracks #performance",
"room": "Backend",
"tags": ["compiler", "language"],
}),
)
.await
.expect("memory_remember");
let handle = open_palace_handle(&state, "kgauto").expect("open palace");
let triples = handle.kg.list_active(1000, 0).await.expect("list_active");
let auto: Vec<_> = triples
.iter()
.filter(|t| t.provenance.as_deref() == Some(crate::kg_extract::AUTO_PROVENANCE))
.collect();
assert!(
!auto.is_empty(),
"expected at least one auto-extracted triple after memory_remember; got: {triples:?}"
);
assert!(
auto.iter()
.any(|t| t.subject == "tag:compiler" && t.predicate == "tags"),
"expected tag:compiler edge in auto subset: {auto:?}"
);
assert!(
auto.iter()
.any(|t| t.subject == "tag:language" && t.predicate == "tags"),
"expected tag:language edge in auto subset: {auto:?}"
);
assert!(
auto.iter()
.any(|t| t.subject == "room:Backend" && t.predicate == "contains"),
"expected room:Backend edge in auto subset: {auto:?}"
);
assert!(
auto.iter().any(|t| t.predicate == "mentioned-in"),
"expected at least one #hashtag mention triple in auto subset: {auto:?}"
);
}
#[tokio::test]
async fn auto_kg_extraction_no_op_does_not_fail_remember() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "kgnoop"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "kgnoop",
"text": "The quick brown fox jumped over the lazy dog repeatedly",
}),
)
.await
.expect("memory_remember should succeed even when extraction yields nothing");
assert!(res["drawer_id"].as_str().is_some());
}
#[tokio::test]
async fn dispatch_kg_assert_then_query() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "gamma"}))
.await
.expect("palace_create");
let _ = dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "gamma",
"subject": "alice",
"predicate": "works_at",
"object": "Acme",
"confidence": 0.9,
"provenance": "test",
}),
)
.await
.expect("kg_assert");
let queried = dispatch_tool(
&state,
"kg_query",
json!({"palace": "gamma", "subject": "alice"}),
)
.await
.expect("kg_query");
let triples = queried["triples"].as_array().expect("triples array");
assert_eq!(triples.len(), 1);
assert_eq!(triples[0]["object"], "Acme");
assert_eq!(triples[0]["predicate"], "works_at");
}
mod embed_audit_tests;
mod kg_retract_tests;
mod last_used_tests;
mod palace_index_tests;
mod recall_projection_tests;
mod write_budget_tests;
mod write_liveness_tests;
#[tokio::test]
async fn dispatch_kg_list_subjects_returns_distinct_subjects() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "subjects"}))
.await
.expect("palace_create");
for (subject, object) in [("zeta", "Acme"), ("alpha", "Globex")] {
let _ = dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "subjects",
"subject": subject,
"predicate": "works_at",
"object": object,
}),
)
.await
.expect("kg_assert");
}
let listed = dispatch_tool(&state, "kg_list_subjects", json!({"palace": "subjects"}))
.await
.expect("kg_list_subjects");
assert_eq!(listed["palace"], "subjects");
assert_eq!(listed["with_counts"], false);
assert_eq!(listed["truncated"], false);
let subjects: Vec<&str> = listed["subjects"]
.as_array()
.expect("subjects array")
.iter()
.filter_map(|v| v.as_str())
.collect();
assert!(
subjects.contains(&"alpha") && subjects.contains(&"zeta"),
"both asserted subjects should be listed: {subjects:?}"
);
let mut sorted = subjects.clone();
sorted.sort_unstable();
assert_eq!(subjects, sorted, "subjects should be alphabetical");
}
#[tokio::test]
async fn dispatch_kg_list_subjects_with_counts_returns_pairs() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "counts"}))
.await
.expect("palace_create");
for (subject, predicate) in [
("alpha", "works_at"),
("alpha", "lives_in"),
("beta", "owns"),
] {
let _ = dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "counts",
"subject": subject,
"predicate": predicate,
"object": "Acme",
}),
)
.await
.expect("kg_assert");
}
let listed = dispatch_tool(
&state,
"kg_list_subjects",
json!({"palace": "counts", "with_counts": true}),
)
.await
.expect("kg_list_subjects");
assert_eq!(listed["with_counts"], true);
let pairs: Vec<(&str, u64)> = listed["subjects"]
.as_array()
.expect("subjects array")
.iter()
.filter_map(|v| Some((v["subject"].as_str()?, v["count"].as_u64()?)))
.collect();
assert!(
pairs.contains(&("alpha", 2)),
"alpha should carry both of its triples: {pairs:?}"
);
assert!(
pairs.contains(&("beta", 1)),
"beta should carry its single triple: {pairs:?}"
);
}
#[tokio::test]
async fn dispatch_kg_list_subjects_exact_limit_is_not_truncated() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "exactlimit"}))
.await
.expect("palace_create");
for subject in ["alpha", "beta", "gamma"] {
let _ = dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "exactlimit",
"subject": subject,
"predicate": "works_at",
"object": "Acme",
}),
)
.await
.expect("kg_assert");
}
let wide_open = dispatch_tool(
&state,
"kg_list_subjects",
json!({"palace": "exactlimit", "limit": 200}),
)
.await
.expect("kg_list_subjects wide-open baseline");
let total = wide_open["subjects"]
.as_array()
.expect("subjects array")
.len();
assert!(
total >= 3,
"the three asserted subjects should all be present: total={total}"
);
assert_eq!(
wide_open["truncated"], false,
"a limit well above the real total is never truncated"
);
let listed = dispatch_tool(
&state,
"kg_list_subjects",
json!({"palace": "exactlimit", "limit": total}),
)
.await
.expect("kg_list_subjects");
let subjects = listed["subjects"].as_array().expect("subjects array");
assert_eq!(
subjects.len(),
total,
"every subject should come back: {subjects:?}"
);
assert_eq!(
listed["truncated"], false,
"exactly `limit` subjects total is not truncation"
);
let capped = dispatch_tool(
&state,
"kg_list_subjects",
json!({"palace": "exactlimit", "limit": 1}),
)
.await
.expect("kg_list_subjects");
assert_eq!(
capped["truncated"], true,
"limit below the subject count should still report truncated"
);
}
#[tokio::test]
async fn dispatch_kg_gaps_returns_cached() {
use trusty_common::memory_core::community::KnowledgeGap;
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "delta"}))
.await
.expect("palace_create");
let initial = dispatch_tool(&state, "kg_gaps", json!({"palace": "delta"}))
.await
.expect("kg_gaps empty");
let gaps = initial["gaps"].as_array().expect("gaps array");
assert_eq!(gaps.len(), 0);
state.registry.set_gaps(
PalaceId::new("delta"),
vec![KnowledgeGap {
entities: vec!["x".to_string(), "y".to_string()],
internal_density: 0.05,
external_bridges: 0,
suggested_exploration: "Explore connections between x and y".to_string(),
}],
);
let seeded = dispatch_tool(&state, "kg_gaps", json!({"palace": "delta"}))
.await
.expect("kg_gaps seeded");
let gaps = seeded["gaps"].as_array().expect("gaps array");
assert_eq!(gaps.len(), 1);
assert_eq!(gaps[0]["entities"][0], "x");
assert_eq!(gaps[0]["external_bridges"], 0);
assert!(gaps[0]["suggested_exploration"]
.as_str()
.unwrap()
.contains("x"));
}
#[tokio::test]
async fn add_alias_round_trip_through_prompt_cache() {
skip_palace_enforcement();
let _tmp = tempfile::tempdir().expect("tempdir");
let root = _tmp.path().to_path_buf();
let state = AppState::new(root).with_default_palace(Some("ctx".to_string()));
let _ = dispatch_tool(&state, "palace_create", json!({"name": "ctx"}))
.await
.expect("palace_create");
let added = dispatch_tool(
&state,
"add_alias",
json!({"short": "tga", "full": "trusty-git-analytics"}),
)
.await
.expect("add_alias");
assert_eq!(added["asserted"], true);
assert_eq!(added["short"], "tga");
let listed = dispatch_tool(&state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
let facts = listed["facts"].as_array().expect("facts array");
assert!(
facts.iter().any(|f| f["subject"] == "tga"
&& f["predicate"] == "is_alias_for"
&& f["object"] == "trusty-git-analytics"),
"expected tga alias in facts; got {facts:?}"
);
{
let guard = state.prompt_context_cache.read().await;
assert!(
guard.formatted.contains("tga → trusty-git-analytics"),
"prompt cache should contain alias; got: {}",
guard.formatted
);
}
let _ = dispatch_tool(
&state,
"add_alias",
json!({"short": "tm", "full": "trusty-memory", "extra": "the MCP frontend"}),
)
.await
.expect("add_alias with extra");
{
let guard = state.prompt_context_cache.read().await;
assert!(
guard
.formatted
.contains("tm → trusty-memory (the MCP frontend)"),
"alias with extra not formatted; got: {}",
guard.formatted
);
}
let removed = dispatch_tool(
&state,
"remove_prompt_fact",
json!({"subject": "tga", "predicate": "is_alias_for"}),
)
.await
.expect("remove_prompt_fact");
assert_eq!(removed["removed"], true);
{
let guard = state.prompt_context_cache.read().await;
assert!(
!guard.formatted.contains("tga → trusty-git-analytics"),
"retracted alias still in cache: {}",
guard.formatted
);
assert!(
guard.formatted.contains("tm → trusty-memory"),
"non-retracted alias missing from cache: {}",
guard.formatted
);
}
let missing = dispatch_tool(
&state,
"remove_prompt_fact",
json!({"subject": "nope", "predicate": "is_alias_for"}),
)
.await
.expect("remove_prompt_fact missing");
assert_eq!(missing["removed"], false);
}
#[tokio::test]
async fn add_alias_palace_arg_required_without_server_default() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "p"}))
.await
.expect("palace_create");
let added = dispatch_tool(
&state,
"add_alias",
json!({"palace": "p", "short": "tga", "full": "trusty-git-analytics"}),
)
.await
.expect("add_alias with explicit palace");
assert_eq!(added["asserted"], true);
let guard = state.prompt_context_cache.read().await;
assert!(guard.formatted.contains("tga → trusty-git-analytics"));
drop(guard);
let (state2, _tmp2) = test_state();
let err = dispatch_tool(&state2, "add_alias", json!({"short": "x", "full": "y"}))
.await
.expect_err("should fail without palace");
let msg = format!("{err:#}");
assert!(msg.contains("palace"), "error must mention 'palace': {msg}");
assert!(msg.contains("add_alias"), "error must name tool: {msg}");
}
#[tokio::test]
async fn get_prompt_context_serves_cache_and_filters() {
let (state, _tmp) = test_state();
let resp = dispatch_tool(&state, "get_prompt_context", json!({}))
.await
.expect("get_prompt_context empty");
assert_eq!(resp.as_str().unwrap(), "No prompt facts stored yet.");
{
let mut guard = state.prompt_context_cache.write().await;
let triples = vec![
(
"tga".to_string(),
"is_alias_for".to_string(),
"trusty-git-analytics".to_string(),
),
(
"tm".to_string(),
"is_alias_for".to_string(),
"trusty-memory".to_string(),
),
(
"fact-1".to_string(),
"is_fact".to_string(),
"MSRV is 1.88".to_string(),
),
];
let formatted = crate::prompt_facts::build_prompt_context(&triples);
*guard = crate::prompt_facts::PromptFactsCache { triples, formatted };
}
let resp = dispatch_tool(&state, "get_prompt_context", json!({}))
.await
.expect("get_prompt_context populated");
let text = resp.as_str().expect("string body");
assert!(text.contains("tga → trusty-git-analytics"));
assert!(text.contains("tm → trusty-memory"));
assert!(text.contains("MSRV is 1.88"));
let resp = dispatch_tool(&state, "get_prompt_context", json!({"query": "tga"}))
.await
.expect("get_prompt_context filtered");
let text = resp.as_str().expect("string body");
assert!(text.contains("tga → trusty-git-analytics"));
assert!(!text.contains("tm → trusty-memory"));
assert!(!text.contains("MSRV is 1.88"));
let resp = dispatch_tool(&state, "get_prompt_context", json!({"query": "MEMORY"}))
.await
.expect("get_prompt_context case-insensitive");
let text = resp.as_str().expect("string body");
assert!(text.contains("tm → trusty-memory"));
assert!(!text.contains("tga → trusty-git-analytics"));
let resp = dispatch_tool(
&state,
"get_prompt_context",
json!({"query": "zzz-nonexistent"}),
)
.await
.expect("get_prompt_context no-match");
assert_eq!(
resp.as_str().unwrap(),
"No project context found matching your query."
);
let resp = dispatch_tool(&state, "get_prompt_context", json!({"query": " "}))
.await
.expect("get_prompt_context whitespace");
let text = resp.as_str().expect("string body");
assert!(text.contains("tga → trusty-git-analytics"));
assert!(text.contains("tm → trusty-memory"));
}
#[tokio::test]
async fn dispatch_discover_aliases_inserts_new_and_dedupes() {
skip_palace_enforcement();
let _tmp = tempfile::tempdir().expect("tempdir");
let root = _tmp.path().to_path_buf();
let state = AppState::new(root).with_default_palace(Some("disc".to_string()));
let _ = dispatch_tool(&state, "palace_create", json!({"name": "disc"}))
.await
.expect("palace_create");
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root")
.to_path_buf();
let first = dispatch_tool(
&state,
"discover_aliases",
json!({"project_root": workspace_root.to_string_lossy()}),
)
.await
.expect("discover_aliases first");
let new_count = first["new"].as_u64().expect("new is u64");
assert!(new_count > 0, "expected new discoveries on first call");
let discovered = first["discovered"].as_array().expect("discovered array");
assert!(
discovered
.iter()
.any(|d| d["short"] == "tga" && d["full"] == "trusty-git-analytics"),
"expected tga alias in discoveries; got {discovered:?}"
);
{
let guard = state.prompt_context_cache.read().await;
assert!(
guard.formatted.contains("tga → trusty-git-analytics"),
"prompt cache missing tga alias after discover_aliases; got: {}",
guard.formatted
);
}
let second = dispatch_tool(
&state,
"discover_aliases",
json!({"project_root": workspace_root.to_string_lossy()}),
)
.await
.expect("discover_aliases second");
assert_eq!(second["new"].as_u64(), Some(0), "expected 0 new on rerun");
let already_known = second["already_known"].as_u64().expect("already_known");
assert!(
already_known >= new_count,
"expected already_known >= {new_count}, got {already_known}"
);
}
#[tokio::test]
async fn palace_create_auto_seeds_temporal_metadata() {
let (state, _tmp) = test_state();
let created = dispatch_tool(&state, "palace_create", json!({"name": "auto"}))
.await
.expect("palace_create");
assert_eq!(created["palace_id"], "auto");
let summary = &created["bootstrap"];
assert!(summary.is_object(), "expected bootstrap summary object");
assert!(summary["triples_asserted"].as_u64().unwrap_or(0) >= 2);
let queried = dispatch_tool(
&state,
"kg_query",
json!({"palace": "auto", "subject": "auto"}),
)
.await
.expect("kg_query");
let triples = queried["triples"].as_array().expect("triples");
let predicates: Vec<&str> = triples
.iter()
.filter_map(|t| t["predicate"].as_str())
.collect();
assert!(
predicates.contains(&"created_at"),
"expected created_at after palace_create; got {predicates:?}",
);
assert!(
predicates.contains(&"bootstrapped_at"),
"expected bootstrapped_at after palace_create; got {predicates:?}",
);
assert!(
queried.get("hint").is_none(),
"hint should be absent when triples exist"
);
assert!(
queried.get("graph_state").is_none(),
"graph_state should be absent when triples exist"
);
assert!(
queried["kg_triple_count"].as_u64().unwrap_or(0) >= 2,
"kg_triple_count must be present on a hit; got {}",
queried["kg_triple_count"],
);
}
#[tokio::test]
async fn kg_query_reports_subject_not_found_when_graph_has_other_subjects() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "hinted"}))
.await
.expect("palace_create");
let subjects = dispatch_tool(&state, "kg_list_subjects", json!({"palace": "hinted"}))
.await
.expect("kg_list_subjects");
assert!(
!subjects["subjects"]
.as_array()
.expect("subjects")
.is_empty(),
"auto-bootstrap should have seeded at least one subject",
);
let queried = dispatch_tool(
&state,
"kg_query",
json!({"palace": "hinted", "subject": "unrelated-subject"}),
)
.await
.expect("kg_query");
assert_eq!(queried["triples"].as_array().unwrap().len(), 0);
assert_eq!(queried["graph_state"], "subject_not_found");
assert!(
queried["kg_triple_count"].as_u64().unwrap_or(0) >= 2,
"whole-graph count must reflect the bootstrapped triples; got {}",
queried["kg_triple_count"],
);
let hint = queried["hint"].as_str().expect("hint field present");
assert!(
hint.contains("kg_list_subjects"),
"miss hint must name kg_list_subjects; got {hint:?}",
);
assert!(
!hint.contains("Knowledge graph is empty"),
"must not claim emptiness for a non-empty graph; got {hint:?}",
);
}
#[tokio::test]
async fn kg_query_reports_graph_empty_when_graph_has_no_triples() {
use trusty_common::memory_core::palace::Palace;
let (state, _tmp) = test_state();
let palace = Palace {
id: PalaceId::new("barren"),
name: "barren".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join("barren"),
};
let _ = state
.registry
.create_palace(&state.data_root, palace)
.expect("create_palace");
let queried = dispatch_tool(
&state,
"kg_query",
json!({"palace": "barren", "subject": "anything"}),
)
.await
.expect("kg_query");
assert_eq!(queried["triples"].as_array().unwrap().len(), 0);
assert_eq!(queried["kg_triple_count"], 0);
assert_eq!(queried["graph_state"], "graph_empty");
let hint = queried["hint"].as_str().expect("hint field present");
assert!(hint.contains("kg_bootstrap"));
assert!(hint.contains("kg_assert"));
}
#[test]
fn kg_miss_classify_distinguishes_empty_graph_from_missing_subject() {
use crate::bootstrap::KgMiss;
assert_eq!(KgMiss::classify(0, 0), Some(KgMiss::GraphEmpty));
assert_eq!(KgMiss::classify(0, 7), Some(KgMiss::SubjectNotFound));
assert_eq!(KgMiss::classify(3, 7), None);
assert_eq!(KgMiss::classify(3, 0), None);
assert_eq!(KgMiss::GraphEmpty.wire_value(), "graph_empty");
assert_eq!(KgMiss::SubjectNotFound.wire_value(), "subject_not_found");
}
#[tokio::test]
async fn kg_bootstrap_seeds_workspace_facts() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "ws"}))
.await
.expect("palace_create");
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root")
.to_path_buf();
let result = dispatch_tool(
&state,
"kg_bootstrap",
json!({"palace": "ws", "project_path": workspace_root.to_string_lossy()}),
)
.await
.expect("kg_bootstrap");
assert!(result["triples_asserted"].as_u64().unwrap() > 0);
let subject = result["project_subject"]
.as_str()
.expect("project_subject")
.to_string();
let queried = dispatch_tool(
&state,
"kg_query",
json!({"palace": "ws", "subject": subject}),
)
.await
.expect("kg_query");
let triples = queried["triples"].as_array().expect("triples");
let predicates: Vec<&str> = triples
.iter()
.filter_map(|t| t["predicate"].as_str())
.collect();
assert!(
predicates.contains(&"has_workspace_member") || predicates.contains(&"has_language"),
"expected workspace/language fact; got {predicates:?}",
);
assert!(
predicates.contains(&"source_repo"),
"expected source_repo from .git/config; got {predicates:?}",
);
assert!(predicates.contains(&"bootstrapped_at"));
}
#[test]
fn content_gate_blocks_short_no_context() {
assert_eq!(content_gate("yes", None, false), None);
assert_eq!(content_gate("ok", None, false), None);
assert_eq!(
content_gate(" no thanks ", None, false),
None,
"2 words still < 4"
);
assert_eq!(
content_gate("one two three", None, false),
None,
"3 words still < 4"
);
}
#[test]
fn content_gate_force_bypasses_short_content() {
assert_eq!(
content_gate("yes", None, true),
Some("yes".to_string()),
"force=true must bypass the short-content gate"
);
assert_eq!(content_gate("ok", None, true), Some("ok".to_string()));
}
#[test]
fn content_gate_wraps_short_with_context() {
let combined = content_gate(
"yes",
Some("Do you want to enable auto-bootstrap on new palaces?"),
false,
)
.expect("context should unlock the gate");
assert_eq!(
combined,
"Do you want to enable auto-bootstrap on new palaces?\n\n---\n\nyes",
);
let combined = content_gate(
"the quick brown fox jumps over the lazy dog",
Some("Famous typing pangram"),
false,
)
.expect("long content + context still combines");
assert!(combined.starts_with("Famous typing pangram"));
assert!(combined.contains("\n\n---\n\n"));
assert!(combined.ends_with("the quick brown fox jumps over the lazy dog"));
}
#[test]
fn content_gate_keeps_long() {
let body = "User prefers snake_case for python";
let kept = content_gate(body, None, false).expect(">= 4 words passes");
assert_eq!(kept, body, "passing content must round-trip verbatim");
let boundary = "one two three four";
assert_eq!(
content_gate(boundary, None, false).as_deref(),
Some(boundary)
);
}
#[test]
fn content_gate_blank_context_treated_as_none() {
assert_eq!(content_gate("yes", Some(""), false), None);
assert_eq!(content_gate("yes", Some(" "), false), None);
assert_eq!(content_gate("yes", Some("\n\t"), false), None);
}
#[tokio::test]
async fn dispatch_remember_skips_short_no_context() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "gate"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({"palace": "gate", "text": "yes"}),
)
.await
.expect("memory_remember (short)");
assert_eq!(res["status"], "skipped");
assert!(res["reason"]
.as_str()
.unwrap_or("")
.contains("content gate"));
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "gate", "limit": 10}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert!(
drawers.is_empty(),
"no drawer should be written; got {drawers:?}"
);
}
#[tokio::test]
async fn dispatch_remember_with_context_writes_combined() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "ctxgate"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "ctxgate",
"text": "yes",
"context": "Do you want to enable auto-bootstrap on new palaces?",
"force": true,
}),
)
.await
.expect("memory_remember (with context)");
assert_eq!(res["status"], "stored");
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "ctxgate", "limit": 10}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert_eq!(drawers.len(), 1);
let body = drawers[0]["content"].as_str().expect("content");
assert!(body.starts_with("Do you want to enable auto-bootstrap"));
assert!(body.contains("\n\n---\n\n"));
assert!(body.ends_with("yes"));
}
#[tokio::test]
async fn dispatch_note_skips_short_no_context() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "noteg"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_note",
json!({"palace": "noteg", "content": "ok"}),
)
.await
.expect("memory_note (short)");
assert_eq!(res["status"], "skipped");
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "noteg", "limit": 10}),
)
.await
.expect("memory_list");
assert!(listed["drawers"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn dispatch_unknown_tool_errors() {
let (state, _tmp) = test_state();
let err = dispatch_tool(&state, "does_not_exist", json!({}))
.await
.expect_err("should error");
assert!(err.to_string().contains("unknown tool"));
}
#[test]
fn blocklist_gate_blocks_tool_use() {
assert!(blocklist_gate("Tool use: Bash").is_some());
assert!(blocklist_gate("Tool use: Edit File: /Users/me/Projects/foo/bar.rs").is_some());
assert!(blocklist_gate(" Tool use: Read").is_some());
}
#[test]
fn blocklist_gate_blocks_session_ended() {
assert!(
blocklist_gate("Claude Code session ended: 1d2c3b4a-0000-0000-0000-000000000000").is_some()
);
assert!(blocklist_gate("Claude Code session started").is_some());
}
#[test]
fn blocklist_gate_passes_normal_content() {
assert!(blocklist_gate("User prefers snake_case for python").is_none());
assert!(blocklist_gate("Quokkas are the happiest marsupials in Australia").is_none());
assert!(blocklist_gate("Note: refactor the dispatcher next sprint").is_none());
assert!(blocklist_gate("I used Tool use: Bash here").is_none());
assert!(
blocklist_gate("The transcript quoted \"Claude Code session\" lifecycle events twice")
.is_none()
);
}
#[test]
fn blocklist_gate_names_matched_pattern() {
assert_eq!(blocklist_gate("Tool use: Bash"), Some("Tool use: "));
assert_eq!(
blocklist_gate("Claude Code session ended: abc"),
Some("Claude Code session")
);
assert_eq!(blocklist_gate("an ordinary engineering note"), None);
}
#[tokio::test]
async fn dedup_skips_near_duplicate() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "dedup1"}))
.await
.expect("palace_create");
let _ = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "dedup1",
"text": "The quick brown fox jumped over the lazy dog repeatedly today",
}),
)
.await
.expect("memory_remember seed");
let handle = open_palace_handle(&state, "dedup1").expect("open handle");
assert!(
dedup_gate(
&handle,
"The quick brown fox jumped over the lazy dog repeatedly yesterday"
),
"near-duplicate should be detected"
);
assert!(
dedup_gate(
&handle,
"The quick brown fox jumped over the lazy dog repeatedly today"
),
"exact match should be detected"
);
}
#[tokio::test]
async fn dedup_allows_different_content() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "dedup2"}))
.await
.expect("palace_create");
let _ = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "dedup2",
"text": "Quokkas are the happiest marsupials in Australia by general consensus",
}),
)
.await
.expect("memory_remember seed");
let handle = open_palace_handle(&state, "dedup2").expect("open handle");
assert!(
!dedup_gate(
&handle,
"Rust is a systems programming language focused on safety and concurrency"
),
"unrelated content should pass the dedup gate"
);
assert!(!dedup_gate(&handle, " "));
}
#[tokio::test]
async fn dedup_gate_blocks_concurrent_duplicate_writes() {
let (state, _tmp) = test_state();
let state = std::sync::Arc::new(state);
let _ = dispatch_tool(&state, "palace_create", json!({"name": "dedup_race"}))
.await
.expect("palace_create");
let text = "Concurrent identical writes must collapse to a single drawer under the dedup gate";
let s1 = state.clone();
let t1 = tokio::spawn(async move {
dispatch_tool(
&s1,
"memory_remember",
json!({"palace": "dedup_race", "text": text}),
)
.await
});
let s2 = state.clone();
let t2 = tokio::spawn(async move {
dispatch_tool(
&s2,
"memory_remember",
json!({"palace": "dedup_race", "text": text}),
)
.await
});
let r1 = t1.await.expect("join t1").expect("dispatch t1");
let r2 = t2.await.expect("join t2").expect("dispatch t2");
let statuses = [
r1["status"].as_str().unwrap_or(""),
r2["status"].as_str().unwrap_or(""),
];
let stored = statuses.iter().filter(|s| **s == "stored").count();
let skipped = statuses.iter().filter(|s| **s == "skipped").count();
assert_eq!(
stored, 1,
"exactly one concurrent write should be stored; got responses {r1:?} {r2:?}"
);
assert_eq!(
skipped, 1,
"exactly one concurrent write should be skipped; got responses {r1:?} {r2:?}"
);
let skipped_reason = if r1["status"] == "skipped" {
r1["reason"].as_str().unwrap_or("")
} else {
r2["reason"].as_str().unwrap_or("")
};
assert!(
skipped_reason.contains("duplicate within window"),
"skipped envelope should cite dedup reason; got {skipped_reason:?}"
);
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "dedup_race", "limit": 10}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert_eq!(
drawers.len(),
1,
"only one drawer should be persisted after concurrent identical writes; got {drawers:?}"
);
}
#[tokio::test]
async fn dispatch_remember_blocks_blocklist_pattern() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "blk"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({"palace": "blk", "text": "Tool use: Bash"}),
)
.await
.expect("memory_remember (blocked)");
assert_eq!(res["status"], "skipped");
assert!(
res["reason"]
.as_str()
.unwrap_or("")
.contains("blocked pattern"),
"reason should mention blocked pattern; got {res:?}"
);
let listed = dispatch_tool(&state, "memory_list", json!({"palace": "blk", "limit": 10}))
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert!(drawers.is_empty(), "no drawer should be written");
}
#[tokio::test]
async fn dispatch_remember_stores_git_sha_prose() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "shas"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "shas",
"text": "Shipped via PR #1466 squash 0fda534e -> merge 4c536992, CI green.",
}),
)
.await
.expect("memory_remember (git sha prose)");
assert_eq!(
res["status"], "stored",
"git-SHA prose must be stored, not skipped; got {res:?}"
);
assert!(res["drawer_id"].as_str().is_some());
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "shas", "limit": 10}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert_eq!(drawers.len(), 1, "exactly one drawer should land");
assert!(
drawers[0]["content"]
.as_str()
.unwrap_or("")
.contains("4c536992"),
"stored content must preserve the SHA; got {drawers:?}"
);
}
#[tokio::test]
async fn dispatch_remember_blocks_real_secret() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "sec"}))
.await
.expect("palace_create");
let err = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "sec",
"text": "deploy uses token AbCd1234EfGh5678IjKl9012 for the prod webhook auth", }),
)
.await
.expect_err("a real secret must be rejected");
let msg = format!("{err:#}");
assert!(
msg.contains("secret") && msg.contains("AbCd"),
"rejection must name the redacted secret token; got: {msg}"
);
let listed = dispatch_tool(&state, "memory_list", json!({"palace": "sec", "limit": 10}))
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert!(
drawers.is_empty(),
"no drawer should be written for a secret"
);
}
#[tokio::test]
async fn dispatch_remember_force_bypasses_blocklist_gate() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "blk-force"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({"palace": "blk-force", "text": "Tool use: Bash", "force": true}),
)
.await
.expect("memory_remember (forced)");
assert_eq!(
res["status"], "stored",
"force=true must bypass the blocklist gate; got {res:?}"
);
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "blk-force", "limit": 10}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert_eq!(drawers.len(), 1, "the forced write must land");
}
#[tokio::test]
async fn dispatch_remember_force_bypasses_short_content_gate() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "short-force"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({"palace": "short-force", "text": "yes", "force": true}),
)
.await
.expect("memory_remember (forced short content)");
assert_eq!(
res["status"], "stored",
"force=true must bypass the short-content gate; got {res:?}"
);
}
#[tokio::test]
async fn dispatch_remember_force_still_blocks_secret() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "sec-force"}))
.await
.expect("palace_create");
let err = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "sec-force",
"text": "deploy uses token AbCd1234EfGh5678IjKl9012 for the prod webhook auth", "force": true,
}),
)
.await
.expect_err("force=true must still reject secret-shaped content");
let msg = format!("{err:#}");
assert!(
msg.to_lowercase().contains("secret"),
"expected a secret-gate rejection even under force; got: {msg}"
);
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "sec-force", "limit": 10}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert!(
drawers.is_empty(),
"no drawer should be written for a secret, even under force"
);
}
#[tokio::test]
async fn dispatch_remember_allow_secret_like_bypasses_secret_gate() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "sec-allow"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "sec-allow",
"text": "deploy uses token AbCd1234EfGh5678IjKl9012 for the prod webhook auth", "force": true,
"allow_secret_like": true,
}),
)
.await
.expect("force + allow_secret_like must bypass the secret gate too");
assert_eq!(
res["status"], "stored",
"allow_secret_like=true must let secret-shaped content through; got {res:?}"
);
}
#[tokio::test]
async fn dispatch_remember_accepts_live_false_positive_tokens() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "fp2442"}))
.await
.expect("palace_create");
let res = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "fp2442",
"text": "Fixed the retry loop in client/http_client/error.rs::response_or_body_error \
so transient errors no longer abort the batch",
}),
)
.await
.expect("memory_remember (path::fn reference)");
assert_eq!(
res["status"], "stored",
"Rust path::fn reference must be stored, not rejected as a secret; got {res:?}"
);
let res = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "fp2442",
"text": "Milestone: shipped #2486→PR#2491(e993c18a) today, closing the retry-loop regression report finally",
}),
)
.await
.expect("memory_remember (ledger reference)");
assert_eq!(
res["status"], "stored",
"issue/PR/SHA ledger reference must be stored, not rejected as a secret; got {res:?}"
);
}
#[tokio::test]
async fn bm25_index_queue_drops_when_full() {
let (mut state, _tmp) = test_state();
let (tx, _rx_held) = tokio::sync::mpsc::channel::<Bm25IndexRequest>(BM25_INDEX_QUEUE_CAPACITY);
state.bm25_index_tx = tx;
for i in 0..BM25_INDEX_QUEUE_CAPACITY {
bm25_index_enqueue(
&state,
"default",
Uuid::new_v4(),
&format!("filler content {i}"),
);
}
assert_eq!(
state.bm25_index_tx.capacity(),
0,
"after filling, sender capacity must be 0"
);
for i in 0..16 {
bm25_index_enqueue(
&state,
"default",
Uuid::new_v4(),
&format!("overflow content {i}"),
);
}
let probe_req = Bm25IndexRequest {
palace: "default".to_string(),
drawer_id: Uuid::new_v4().to_string(),
content: "probe".to_string(),
};
let probe = state.bm25_index_tx.try_send(probe_req);
match probe {
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {}
other => panic!("expected Full overflow, got {other:?}"),
}
assert_eq!(
crate::bm25_repair::dirty_palaces(&state),
vec!["default".to_string()],
"a dropped index op must queue its palace for coverage repair"
);
}
#[tokio::test]
async fn remember_succeeds_and_defers_embedding_while_state_is_warming() {
use trusty_common::memory_core::store::VectorStore;
let (state, _tmp) = test_state_warming();
let _ = dispatch_tool(
&state,
"palace_create",
serde_json::json!({"name": "warmtest"}),
)
.await
.expect("palace_create");
let content = "Quokkas are famously photogenic marsupials found in Western Australia";
let remembered = dispatch_tool(
&state,
"memory_remember",
serde_json::json!({
"palace": "warmtest",
"text": content,
}),
)
.await
.expect("memory_remember must succeed while Warming (issue #1970)");
assert_eq!(remembered["status"], "stored");
let drawer_id_str = remembered["drawer_id"]
.as_str()
.expect("drawer_id present")
.to_string();
let drawer_id = Uuid::parse_str(&drawer_id_str).expect("valid uuid");
let listed = dispatch_tool(
&state,
"memory_list",
serde_json::json!({"palace": "warmtest"}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers array");
assert!(
drawers.iter().any(|d| d["drawer_id"] == drawer_id_str),
"drawer must be listed immediately even though the embedder is warming"
);
let handle = open_palace_handle(&state, "warmtest").expect("open palace");
let embedder = trusty_common::memory_core::retrieval::shared_embedder()
.await
.expect("shared embedder must initialise");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120);
let mut backfilled = false;
while std::time::Instant::now() < deadline {
let vecs = embedder
.embed_batch(&[content.to_string()])
.await
.expect("embed query");
let hits = handle
.vector_store
.search(&vecs[0], 5)
.await
.expect("vector search");
if hits
.iter()
.any(|h| h.drawer_id.as_bytes()[..8] == drawer_id.as_bytes()[..8])
{
backfilled = true;
break;
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
assert!(
backfilled,
"background embed task must backfill the vector index once the embedder is ready"
);
}
#[tokio::test]
async fn note_succeeds_while_state_is_warming() {
let (state, _tmp) = test_state_warming();
let _ = dispatch_tool(
&state,
"palace_create",
serde_json::json!({"name": "warmtest-note"}),
)
.await
.expect("palace_create");
let result = dispatch_tool(
&state,
"memory_note",
serde_json::json!({
"palace": "warmtest-note",
"content": "short note content here"
}),
)
.await
.expect("memory_note must succeed while Warming (issue #1970)");
assert_eq!(result["status"], "stored");
}
#[test]
fn bm25_hits_hydrate_from_handle_during_warmup() {
use crate::bm25_lane::BM25Hit;
use trusty_common::memory_core::palace::Drawer;
use trusty_common::memory_core::store::kg::KnowledgeGraph;
use trusty_common::memory_core::store::vector::UsearchStore;
let dir = tempfile::tempdir().expect("tempdir");
let vs = UsearchStore::new(dir.path().join("idx.usearch"), 384).expect("vector store");
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).expect("kg");
let handle = trusty_common::memory_core::retrieval::PalaceHandle::new(
PalaceId::new("bm25hydrate"),
String::new(),
vs,
kg,
);
let drawer = Drawer::new(Uuid::new_v4(), "Rustc is a compiler for the Rust language");
let known_id = drawer.id;
handle.add_drawer(drawer);
let hits = vec![
BM25Hit {
doc_id: known_id.to_string(),
score: 4.2,
},
BM25Hit {
doc_id: Uuid::new_v4().to_string(),
score: 1.0,
},
];
let results = bm25_hits_to_recall_results(&handle, &hits);
assert_eq!(
results.len(),
1,
"unknown drawer id must be skipped, not fabricated"
);
assert_eq!(results[0].drawer.id, known_id);
assert_eq!(results[0].score, 4.2);
assert_eq!(results[0].layer, 4, "BM25-hydrated hits use layer 4");
}
#[test]
fn attach_mcp_attribution_dedupes_hand_written_ws_claim_tag() {
let mut tags = vec![
"ws-claim".to_string(),
"ws:feat-x".to_string(),
"area:health-endpoint".to_string(),
];
let args = json!({"workstream": "feat-x"});
helpers::attach_mcp_attribution(&mut tags, &args);
assert_eq!(
tags.iter().filter(|t| *t == "ws:feat-x").count(),
1,
"ws:feat-x must not be duplicated by the auto-stamp; got {tags:?}"
);
assert!(
tags.contains(&"creator:workstream=feat-x".to_string()),
"creator:workstream= must still be stamped; got {tags:?}"
);
assert!(
tags.contains(&"creator:client=trusty-memory-mcp".to_string()),
"non-overlapping creator tags must still be appended; got {tags:?}"
);
}
#[tokio::test]
async fn mcp_room_parse_matches_http() {
use trusty_common::memory_core::palace::RoomType;
assert_eq!(RoomType::parse("backend"), RoomType::Backend);
assert_eq!(RoomType::parse("Backend"), RoomType::Backend);
assert_eq!(RoomType::parse("docs"), RoomType::Documentation);
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "roomparity"}))
.await
.expect("palace_create");
let _ = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "roomparity",
"text": "The scheduler retries a failed job with exponential backoff",
"room": "backend",
"tags": ["scheduler"],
}),
)
.await
.expect("memory_remember");
for spelling in ["Backend", "backend"] {
let listed = dispatch_tool(
&state,
"memory_list",
json!({"palace": "roomparity", "room": spelling, "limit": 10}),
)
.await
.expect("memory_list");
let drawers = listed["drawers"].as_array().expect("drawers");
assert!(
drawers
.iter()
.any(|d| d["content"].as_str().unwrap_or("").contains("scheduler")),
"room={spelling} must find the drawer written as room=backend; got {drawers:?}"
);
}
}
async fn palace_with_rooms(state: &AppState, palace: &str, rooms: &[(&str, &str)]) {
let _ = dispatch_tool(state, "palace_create", json!({"name": palace}))
.await
.expect("palace_create");
for (room, text) in rooms {
let res = dispatch_tool(
state,
"memory_remember",
json!({"palace": palace, "text": text, "room": room, "force": true}),
)
.await
.expect("memory_remember");
assert_eq!(res["status"], "stored", "fixture write was gated: {res}");
}
}
#[tokio::test]
async fn dispatch_note_accepts_an_explicit_room() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "noteroom"}))
.await
.expect("palace_create");
for (room, content) in [
(
Some("decisions"),
"Deploy target is prod-east for this service",
),
(None, "User prefers snake_case in generated identifiers"),
] {
let mut args = json!({"palace": "noteroom", "content": content});
if let Some(r) = room {
args["room"] = json!(r);
}
let res = dispatch_tool(&state, "memory_note", args)
.await
.expect("memory_note");
assert_eq!(res["status"], "stored", "{res}");
}
let scoped = dispatch_tool(
&state,
"memory_list",
json!({"palace": "noteroom", "room": "decisions", "limit": 10}),
)
.await
.expect("memory_list");
let rows = scoped["drawers"].as_array().expect("drawers");
assert_eq!(rows.len(), 1, "exactly the note filed into `decisions`");
assert!(rows[0]["content"].as_str().unwrap().contains("prod-east"));
let general = dispatch_tool(
&state,
"memory_list",
json!({"palace": "noteroom", "room": "General", "limit": 10}),
)
.await
.expect("memory_list");
let rows = general["drawers"].as_array().expect("drawers");
assert_eq!(rows.len(), 1);
assert!(rows[0]["content"].as_str().unwrap().contains("snake_case"));
}
#[tokio::test]
async fn dispatch_room_list_reports_rooms_with_drawer_counts() {
let (state, _tmp) = test_state();
palace_with_rooms(
&state,
"roomlist",
&[
(
"decisions",
"We chose redb over a JSON sidecar for room storage",
),
(
"decisions",
"Room ids are read from the table, never recomputed",
),
(
"Planning",
"The room surface ships before the wing entity does",
),
],
)
.await;
let res = dispatch_tool(&state, "room_list", json!({"palace": "roomlist"}))
.await
.expect("room_list");
let rooms = res["rooms"].as_array().expect("rooms array");
let by_label: std::collections::HashMap<&str, &serde_json::Value> = rooms
.iter()
.map(|r| (r["label"].as_str().unwrap(), r))
.collect();
assert_eq!(by_label["decisions"]["drawer_count"], 2);
assert_eq!(by_label["decisions"]["room_type"], "Custom");
assert_eq!(by_label["Planning"]["drawer_count"], 1);
assert_eq!(by_label["Planning"]["room_type"], "Planning");
for room in rooms {
assert_eq!(room["resolved"], true, "a live write is never unresolved");
assert!(room["room_id"].as_str().is_some());
assert!(room["wing_id"].as_str().is_some());
}
}
#[tokio::test]
async fn dispatch_room_list_rejects_an_unknown_wing() {
let (state, _tmp) = test_state();
palace_with_rooms(&state, "wingcheck", &[("Planning", "A drawer in planning")]).await;
let err = dispatch_tool(
&state,
"room_list",
json!({"palace": "wingcheck", "wing": Uuid::from_u128(7).to_string()}),
)
.await
.expect_err("an unknown wing must be rejected");
assert!(
format!("{err:#}").contains("not implemented yet"),
"{err:#}"
);
let ok = dispatch_tool(
&state,
"room_list",
json!({
"palace": "wingcheck",
"wing": trusty_common::memory_core::room_identity::DEFAULT_WING_ID.to_string(),
}),
)
.await
.expect("default wing accepted");
assert!(!ok["rooms"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn dispatch_room_create_is_idempotent() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "roomcreate"}))
.await
.expect("palace_create");
let first = dispatch_tool(
&state,
"room_create",
json!({"palace": "roomcreate", "label": "Decisions", "description": "why we chose things"}),
)
.await
.expect("room_create");
assert_eq!(first["created"], true);
assert_eq!(
first["label"], "Decisions",
"the caller's spelling survives"
);
let second = dispatch_tool(
&state,
"room_create",
json!({"palace": "roomcreate", "label": "decisions"}),
)
.await
.expect("room_create again");
assert_eq!(second["created"], false);
assert_eq!(second["room_id"], first["room_id"]);
let listed = dispatch_tool(&state, "room_list", json!({"palace": "roomcreate"}))
.await
.expect("room_list");
let rooms = listed["rooms"].as_array().expect("rooms");
assert_eq!(rooms.len(), 1, "one room, not two: {rooms:?}");
assert_eq!(
rooms[0]["drawer_count"], 0,
"creating a room moves no drawer"
);
assert_eq!(rooms[0]["description"], "why we chose things");
}
#[tokio::test]
async fn dispatch_room_rename_leaves_drawers_in_place() {
let (state, _tmp) = test_state();
palace_with_rooms(
&state,
"roomrename",
&[
("checkpoint", "Session checkpoint after the registry landed"),
("General", "Unrelated general memory that must not move"),
],
)
.await;
let before = dispatch_tool(
&state,
"memory_list",
json!({"palace": "roomrename", "limit": 100}),
)
.await
.expect("memory_list before");
let renamed = dispatch_tool(
&state,
"room_rename",
json!({"palace": "roomrename", "room": "checkpoint", "new_label": "Session Checkpoints"}),
)
.await
.expect("room_rename");
assert_eq!(renamed["label"], "Session Checkpoints");
let after = dispatch_tool(
&state,
"memory_list",
json!({"palace": "roomrename", "limit": 100}),
)
.await
.expect("memory_list after");
assert_eq!(before, after, "a rename must not change any drawer");
let by_new = dispatch_tool(
&state,
"memory_list",
json!({"palace": "roomrename", "room": "Session Checkpoints", "limit": 10}),
)
.await
.expect("memory_list new name");
assert_eq!(by_new["drawers"].as_array().unwrap().len(), 1);
let listed = dispatch_tool(&state, "room_list", json!({"palace": "roomrename"}))
.await
.expect("room_list");
let labels: Vec<&str> = listed["rooms"]
.as_array()
.unwrap()
.iter()
.map(|r| r["label"].as_str().unwrap())
.collect();
assert!(labels.contains(&"Session Checkpoints"), "{labels:?}");
assert!(!labels.contains(&"checkpoint"), "{labels:?}");
}
#[tokio::test]
async fn dispatch_room_rename_rejects_a_taken_name() {
let (state, _tmp) = test_state();
palace_with_rooms(
&state,
"roomclash",
&[
("alpha", "A memory that belongs to the alpha room here"),
("beta", "A memory that belongs to the beta room instead"),
],
)
.await;
let err = dispatch_tool(
&state,
"room_rename",
json!({"palace": "roomclash", "room": "alpha", "new_label": "beta"}),
)
.await
.expect_err("must refuse to merge");
assert!(
format!("{err:#}").contains("already belongs to another room"),
"{err:#}"
);
}
#[tokio::test]
async fn dispatch_recall_room_filter_scopes_results() {
let (state, _tmp) = test_state();
palace_with_rooms(
&state,
"recallroom",
&[
(
"Backend",
"Rust is a systems programming language with ownership",
),
(
"Frontend",
"Rust is a systems programming toolkit for the browser",
),
],
)
.await;
for tool in ["memory_recall", "memory_recall_deep"] {
let res = dispatch_tool(
&state,
tool,
json!({
"palace": "recallroom",
"query": "systems programming Rust",
"room": "Backend",
"top_k": 10,
}),
)
.await
.unwrap_or_else(|e| panic!("{tool}: {e:#}"));
let leaked = res["results"]
.as_array()
.expect("results array")
.iter()
.any(|r| {
r["layer"].as_u64().unwrap_or(0) >= 2
&& r["content"].as_str().unwrap_or("").contains("browser")
});
assert!(
!leaked,
"{tool} returned a Frontend drawer under a Backend room filter: {res}"
);
}
}
#[tokio::test]
async fn dispatch_palace_info_reports_room_count() {
let (state, _tmp) = test_state();
palace_with_rooms(
&state,
"infocount",
&[
(
"Planning",
"The room registry lands before the wing entity does",
),
("General", "A second memory that sits in the default room"),
],
)
.await;
let info = dispatch_tool(&state, "palace_info", json!({"palace": "infocount"}))
.await
.expect("palace_info");
assert_eq!(info["room_count"], 2, "{info}");
assert_eq!(info["drawer_count"], 2);
}
#[tokio::test]
async fn dispatch_remember_admits_a_tier_c_slot() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "tierc"}))
.await
.expect("palace_create");
let first = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "tierc",
"text": "PR 4818 is in flight at head d39638482bfe8de462c02c4f40e02b56b16897ff",
"fact_key": "pr:4818/state",
}),
)
.await
.expect("first tier C write");
assert_eq!(first["tier"], "C", "{first}");
assert!(first.get("tier_c_refused").is_none(), "{first}");
let second = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "tierc",
"text": "PR 4818 merged as squash 4c412ae1 at head 59ae50d8 on main",
"fact_key": "pr:4818/state",
"force": true,
}),
)
.await
.expect("second tier C write");
assert_eq!(second["tier"], "C", "{second}");
let handle = open_palace_handle(&state, "tierc").expect("open");
let winner: uuid::Uuid = second["drawer_id"].as_str().unwrap().parse().unwrap();
assert_eq!(
handle.kg.drawer_id_for_fact_key("pr:4818/state").unwrap(),
Some(winner),
"the newer write must hold the slot"
);
assert_eq!(
handle.kg.load_drawers().unwrap().len(),
2,
"the superseded fact is demoted, never deleted (D6)"
);
}
#[tokio::test]
async fn dispatch_remember_reports_a_refused_slot_as_tier_e() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "refused"}))
.await
.expect("palace_create");
let out = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "refused",
"text": "A bare unnamespaced slot name would collide across every workstream",
"fact_key": "state",
}),
)
.await
.expect("the write degrades, it does not fail");
assert_eq!(out["tier"], "E", "{out}");
assert!(
out["tier_c_refused"]
.as_str()
.is_some_and(|s| s.contains("state")),
"{out}"
);
let handle = open_palace_handle(&state, "refused").expect("open");
assert_eq!(handle.kg.drawer_id_for_fact_key("state").unwrap(), None);
assert!(
handle
.kg
.load_drawers()
.unwrap()
.iter()
.all(|d| d.fact_key.is_none() && d.expires_at.is_none()),
"a refused write must not pick up a slot or the Tier C default TTL"
);
}
#[tokio::test]
async fn dispatch_note_admits_a_tier_c_slot() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "tiercnote"}))
.await
.expect("palace_create");
let out = dispatch_tool(
&state,
"memory_note",
json!({
"palace": "tiercnote",
"content": "origin/main is at 2b83d19e right now",
"fact_key": "repo:trusty-tools/main-head",
}),
)
.await
.expect("memory_note");
assert_eq!(out["tier"], "C", "{out}");
let handle = open_palace_handle(&state, "tiercnote").expect("open");
let id: uuid::Uuid = out["drawer_id"].as_str().unwrap().parse().unwrap();
let stored = handle
.kg
.load_drawers()
.unwrap()
.into_iter()
.find(|d| d.id == id)
.expect("stored");
assert_eq!(
stored.fact_key.as_deref(),
Some("repo:trusty-tools/main-head")
);
assert!(
stored.expires_at.is_some(),
"an admitted Tier C fact always carries a retirement condition (D4)"
);
}
#[tokio::test]
async fn dispatch_remember_rejects_an_unparseable_expires_at() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "badttl"}))
.await
.expect("palace_create");
let err = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "badttl",
"text": "A memory whose retirement timestamp cannot be parsed at all",
"fact_key": "pr:1/state",
"expires_at": "tomorrow",
}),
)
.await
.expect_err("an unparseable timestamp must be an error");
assert!(format!("{err:#}").contains("RFC 3339"), "{err:#}");
}
#[test]
fn tool_definitions_expose_the_tier_c_arguments() {
let defs = super::definitions::tool_definitions();
for tool in ["memory_remember", "memory_note"] {
let props = defs["tools"]
.as_array()
.unwrap()
.iter()
.find(|t| t["name"] == tool)
.unwrap_or_else(|| panic!("{tool} missing"))["inputSchema"]["properties"]
.clone();
assert!(props.get("fact_key").is_some(), "{tool} lacks fact_key");
assert!(props.get("expires_at").is_some(), "{tool} lacks expires_at");
}
}
async fn fill_tier_s(state: &AppState, palace: &str, count: usize) {
for i in 0..count {
dispatch_tool(
state,
"kg_assert",
json!({
"palace": palace,
"subject": format!("rule-{i}"),
"predicate": "has_convention",
"object": format!("standing rule number {i}"),
}),
)
.await
.unwrap_or_else(|e| panic!("fact {i} should be admitted below the cap: {e:#}"));
}
}
#[tokio::test]
async fn dispatch_kg_assert_accepts_twenty_facts() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "cap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "cap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
let listed = dispatch_tool(&state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
assert_eq!(
listed["facts"].as_array().expect("facts array").len(),
crate::prompt_facts::TIER_S_MAX_FACTS,
"all 20 facts should be active on the surface",
);
}
#[tokio::test]
async fn dispatch_kg_assert_rejects_twenty_first_fact() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "cap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "cap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
let err = dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "cap",
"subject": "one-too-many",
"predicate": "has_convention",
"object": "this rule arrives when the surface is already full",
}),
)
.await
.expect_err("the 21st fact must be rejected");
let msg = format!("{err:#}");
assert!(msg.contains("Tier S is full"), "{msg}");
let listed = dispatch_tool(&state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
let facts = listed["facts"].as_array().expect("facts array");
assert_eq!(facts.len(), crate::prompt_facts::TIER_S_MAX_FACTS);
assert!(
!facts.iter().any(|f| f["subject"] == "one-too-many"),
"rejected fact must not be in storage: {listed}",
);
}
#[tokio::test]
async fn dispatch_kg_assert_rejection_names_existing_facts() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "cap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "cap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
let err = dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "cap",
"subject": "blocked",
"predicate": "is_fact",
"object": "rejected",
}),
)
.await
.expect_err("must be rejected at the cap");
let msg = format!("{err:#}");
assert!(msg.contains("remove_prompt_fact"), "{msg}");
for i in 0..crate::prompt_facts::TIER_S_MAX_FACTS {
assert!(
msg.contains(&format!("rule-{i} has_convention")),
"rejection must name existing fact rule-{i}: {msg}",
);
assert!(
msg.contains(&format!("standing rule number {i}")),
"rejection must show the object of rule-{i}: {msg}",
);
}
}
#[tokio::test]
async fn dispatch_kg_assert_allows_replacing_existing_fact_at_cap() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "cap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "cap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "cap",
"subject": "rule-3",
"predicate": "has_convention",
"object": "corrected wording for rule three",
}),
)
.await
.expect("replacing an existing fact at the cap must be admitted");
let listed = dispatch_tool(&state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
let facts = listed["facts"].as_array().expect("facts array");
assert_eq!(
facts.len(),
crate::prompt_facts::TIER_S_MAX_FACTS,
"a replacement must not grow the surface",
);
assert!(
facts
.iter()
.any(|f| f["object"] == "corrected wording for rule three"),
"replacement object should be live: {listed}",
);
}
#[tokio::test]
async fn dispatch_kg_assert_retracted_fact_frees_a_slot() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "cap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "cap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "cap",
"subject": "successor",
"predicate": "has_convention",
"object": "waiting for a free slot",
}),
)
.await
.expect_err("cap must be enforced before retirement");
let removed = dispatch_tool(
&state,
"remove_prompt_fact",
json!({"subject": "rule-7", "predicate": "has_convention"}),
)
.await
.expect("remove_prompt_fact");
assert_eq!(removed["removed"], true, "{removed}");
dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "cap",
"subject": "successor",
"predicate": "has_convention",
"object": "admitted into the slot the retraction freed",
}),
)
.await
.expect("a retracted fact must free its slot");
let listed = dispatch_tool(&state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
let facts = listed["facts"].as_array().expect("facts array");
assert_eq!(facts.len(), crate::prompt_facts::TIER_S_MAX_FACTS);
assert!(
facts.iter().any(|f| f["subject"] == "successor"),
"{listed}"
);
assert!(
!facts.iter().any(|f| f["subject"] == "rule-7"),
"retracted fact must not be active: {listed}",
);
}
#[tokio::test]
async fn reasserting_an_identical_fact_refreshes_affirmed_at() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "affirm"}))
.await
.expect("palace_create");
let write = json!({
"palace": "affirm",
"subject": "conv-1",
"predicate": "has_convention",
"object": "Write plainly",
});
dispatch_tool(&state, "kg_assert", write.clone())
.await
.expect("first assert");
let first = affirmed_at_of(&state, "conv-1").await;
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
dispatch_tool(&state, "kg_assert", write)
.await
.expect("re-asserting the identical fact must be admitted");
let second = affirmed_at_of(&state, "conv-1").await;
assert!(
second > first,
"re-asserting a verbatim-identical rule must refresh affirmed_at \
(first={first}, second={second})",
);
let listed = dispatch_tool(&state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
assert_eq!(
listed["facts"].as_array().expect("facts array").len(),
1,
"a re-affirmation supersedes rather than adds: {listed}",
);
}
async fn affirmed_at_of(state: &AppState, subject: &str) -> chrono::DateTime<chrono::Utc> {
let listed = dispatch_tool(state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
let facts = listed["facts"].as_array().expect("facts array").clone();
let row = facts
.iter()
.find(|f| f["subject"] == subject)
.unwrap_or_else(|| panic!("no Tier S fact for {subject}: {listed}"))
.clone();
let raw = row["affirmed_at"]
.as_str()
.unwrap_or_else(|| panic!("affirmed_at missing or not a string: {row}"));
chrono::DateTime::parse_from_rfc3339(raw)
.unwrap_or_else(|e| panic!("affirmed_at {raw:?} is not RFC 3339: {e}"))
.with_timezone(&chrono::Utc)
}
#[tokio::test]
async fn dispatch_kg_assert_accepts_object_at_char_limit() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "form"}))
.await
.expect("palace_create");
let exactly_80 = "x".repeat(crate::prompt_facts::TIER_S_MAX_OBJECT_CHARS);
assert_eq!(exactly_80.chars().count(), 80);
dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "form",
"subject": "boundary",
"predicate": "has_convention",
"object": exactly_80,
}),
)
.await
.expect("an 80-character object is within the form constraint");
}
#[tokio::test]
async fn dispatch_kg_assert_rejects_object_over_char_limit() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "form"}))
.await
.expect("palace_create");
let too_long = "x".repeat(crate::prompt_facts::TIER_S_MAX_OBJECT_CHARS + 1);
let err = dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "form",
"subject": "overlong",
"predicate": "has_convention",
"object": too_long,
}),
)
.await
.expect_err("an 81-character object must be rejected");
let msg = format!("{err:#}");
assert!(
msg.contains("81 characters"),
"actual length missing: {msg}"
);
assert!(msg.contains("limit is 80"), "limit missing: {msg}");
let listed = dispatch_tool(&state, "list_prompt_facts", json!({}))
.await
.expect("list_prompt_facts");
assert!(
listed["facts"].as_array().expect("facts array").is_empty(),
"rejected over-long fact must not be stored: {listed}",
);
}
#[tokio::test]
async fn dispatch_add_alias_enforces_tier_s_cap() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "cap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "cap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
let err = dispatch_tool(
&state,
"add_alias",
json!({"palace": "cap", "short": "tga", "full": "trusty-git-analytics"}),
)
.await
.expect_err("add_alias must respect the Tier S cap");
assert!(format!("{err:#}").contains("Tier S is full"), "{err:#}");
}
#[tokio::test]
async fn dispatch_add_alias_enforces_form_constraint_on_composed_object() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "form"}))
.await
.expect("palace_create");
let err = dispatch_tool(
&state,
"add_alias",
json!({
"palace": "form",
"short": "x",
"full": "y".repeat(60),
"extra": "z".repeat(60),
}),
)
.await
.expect_err("composed object over 80 chars must be rejected");
assert!(format!("{err:#}").contains("limit is 80"), "{err:#}");
}
#[tokio::test]
async fn dispatch_discover_aliases_stops_at_tier_s_cap() {
skip_palace_enforcement();
let _tmp = tempfile::tempdir().expect("tempdir");
let root = _tmp.path().to_path_buf();
let state = AppState::new(root).with_default_palace(Some("disccap".to_string()));
dispatch_tool(&state, "palace_create", json!({"name": "disccap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "disccap", crate::prompt_facts::TIER_S_MAX_FACTS - 1).await;
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root")
.to_path_buf();
let res = dispatch_tool(
&state,
"discover_aliases",
json!({"project_root": workspace_root.to_string_lossy()}),
)
.await
.expect("discover_aliases must not abort when the budget runs out");
assert_eq!(
res["new"].as_u64(),
Some(1),
"exactly the one free slot should be filled: {res}",
);
let rejected = res["rejected"].as_array().expect("rejected array");
assert!(
!rejected.is_empty(),
"refused aliases must be reported, not silently dropped: {res}",
);
let reason = res["rejected_reason"].as_str().expect("rejected_reason");
assert!(reason.contains("Tier S is full"), "{reason}");
assert!(reason.contains("remove_prompt_fact"), "{reason}");
let facts = crate::prompt_facts::gather_hot_triples(&state)
.await
.expect("gather");
assert_eq!(
facts.len(),
crate::prompt_facts::TIER_S_MAX_FACTS,
"auto-discovery must never push the surface past the cap",
);
let refused = rejected[0]["short"].as_str().expect("short");
assert!(
!facts.iter().any(|(s, _, _)| s == refused),
"refused alias {refused} must not be written: {facts:?}",
);
}
#[tokio::test]
async fn dispatch_kg_assert_cap_does_not_apply_to_cold_predicates() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "cap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "cap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "cap",
"subject": "alice",
"predicate": "works_at",
"object": "a description far longer than eighty characters, which is fine \
because this predicate never reaches the always-injected surface",
}),
)
.await
.expect("cold predicates are unaffected by the Tier S budget");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
async fn tier_s_cap_holds_under_concurrent_writes() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "race"}))
.await
.expect("palace_create");
fill_tier_s(&state, "race", crate::prompt_facts::TIER_S_MAX_FACTS - 1).await;
let state = std::sync::Arc::new(state);
let mut handles = Vec::new();
for i in 0..16 {
let state = state.clone();
handles.push(tokio::spawn(async move {
dispatch_tool(
&state,
"kg_assert",
json!({
"palace": "race",
"subject": format!("racer-{i}"),
"predicate": "has_convention",
"object": format!("contender {i}"),
}),
)
.await
.is_ok()
}));
}
let mut admitted = 0usize;
for h in handles {
if h.await.expect("task joined") {
admitted += 1;
}
}
assert_eq!(admitted, 1, "exactly one writer should win the last slot");
let facts = crate::prompt_facts::gather_hot_triples(&state)
.await
.expect("gather");
assert_eq!(
facts.len(),
crate::prompt_facts::TIER_S_MAX_FACTS,
"concurrent writers must never push the surface past the cap",
);
}
#[tokio::test]
async fn chat_kg_assert_tool_enforces_tier_s_cap() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "chatcap"}))
.await
.expect("palace_create");
fill_tier_s(&state, "chatcap", crate::prompt_facts::TIER_S_MAX_FACTS).await;
let args = json!({
"palace_id": "chatcap",
"subject": "chat-rule",
"predicate": "has_convention",
"object": "a rule the assistant tried to add when the surface was full",
})
.to_string();
let res = crate::chat::tools::execute_tool("kg_assert", &args, &state).await;
let err = res["error"].as_str().unwrap_or_default();
assert!(err.contains("Tier S is full"), "{res}");
assert!(err.contains("remove_prompt_fact"), "{res}");
assert!(
res.get("status").is_none(),
"must not report success: {res}"
);
let facts = crate::prompt_facts::gather_hot_triples(&state)
.await
.expect("gather");
assert_eq!(facts.len(), crate::prompt_facts::TIER_S_MAX_FACTS);
assert!(!facts.iter().any(|(s, _, _)| s == "chat-rule"), "{facts:?}");
}
#[tokio::test]
async fn chat_kg_assert_tool_enforces_form_constraint() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "chatform"}))
.await
.expect("palace_create");
let args = json!({
"palace_id": "chatform",
"subject": "verbose",
"predicate": "is_fact",
"object": "q".repeat(crate::prompt_facts::TIER_S_MAX_OBJECT_CHARS + 1),
})
.to_string();
let res = crate::chat::tools::execute_tool("kg_assert", &args, &state).await;
let err = res["error"].as_str().unwrap_or_default();
assert!(err.contains("81 characters"), "{res}");
assert!(err.contains("limit is 80"), "{res}");
let facts = crate::prompt_facts::gather_hot_triples(&state)
.await
.expect("gather");
assert!(facts.is_empty(), "rejected write must not land: {facts:?}");
}
#[tokio::test]
async fn chat_kg_assert_tool_allows_cold_predicates_at_cap() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "chatcold"}))
.await
.expect("palace_create");
fill_tier_s(&state, "chatcold", crate::prompt_facts::TIER_S_MAX_FACTS).await;
let args = json!({
"palace_id": "chatcold",
"subject": "alice",
"predicate": "works_at",
"object": "z".repeat(200),
})
.to_string();
let res = crate::chat::tools::execute_tool("kg_assert", &args, &state).await;
assert_eq!(res["status"], "asserted", "{res}");
}
#[test]
fn kuzu_migrate_refuses_hot_predicates_and_passes_cold_ones() {
use crate::prompt_facts::is_hot_predicate;
for p in crate::prompt_facts::HOT_PREDICATES {
assert!(is_hot_predicate(p), "{p} must be refused by kuzu-migrate");
}
for p in [
"relates_to",
"mentions",
"derived_from",
"part_of",
"alias_of",
] {
assert!(
!is_hot_predicate(p),
"{p} is an ordinary relation type and must still import",
);
}
}
#[tokio::test]
async fn a_failed_index_call_queues_the_palace_for_repair() {
let tmp = tempfile::tempdir().expect("tempdir");
let dirty: crate::bm25_repair::DirtyPalaces = std::sync::Arc::new(dashmap::DashSet::new());
let (tx, rx) = tokio::sync::mpsc::channel::<Bm25IndexRequest>(8);
std::fs::create_dir_all(tmp.path().join("lossy")).expect("palace dir");
std::fs::write(
tmp.path().join("lossy").join("bm25"),
b"a file where the index directory belongs",
)
.expect("block the index dir");
let lane = crate::bm25_lane::Bm25Lane::with_limits(tmp.path().to_path_buf(), 3, None);
spawn_bm25_index_worker(
rx,
Some(std::sync::Arc::clone(&lane)),
std::sync::Arc::clone(&dirty),
);
tx.send(Bm25IndexRequest {
palace: "lossy".to_string(),
drawer_id: Uuid::new_v4().to_string(),
content: "content that will never reach the index".to_string(),
})
.await
.expect("send to worker");
for _ in 0..200 {
if dirty.contains("lossy") {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
assert!(
dirty.contains("lossy"),
"an index call that failed lost the write and must queue the palace"
);
lane.shutdown().await;
}
#[tokio::test]
async fn a_closed_index_queue_queues_the_palace_for_repair() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut state = AppState::new(tmp.path().to_path_buf());
let (tx, rx) = tokio::sync::mpsc::channel::<Bm25IndexRequest>(8);
drop(rx);
state.bm25_index_tx = tx;
assert!(
matches!(
state.bm25_index_tx.try_send(Bm25IndexRequest {
palace: "default".to_string(),
drawer_id: Uuid::new_v4().to_string(),
content: "probe".to_string(),
}),
Err(tokio::sync::mpsc::error::TrySendError::Closed(_))
),
"precondition: the queue must actually be closed, not merely full"
);
bm25_index_enqueue(&state, "default", Uuid::new_v4(), "content that is lost");
assert_eq!(
crate::bm25_repair::dirty_palaces(&state),
vec!["default".to_string()],
"a closed queue loses the write as completely as a full one and must queue repair"
);
}
#[tokio::test]
async fn dispatch_palace_unalias_frees_a_real_collision_and_is_idempotent() {
use redb::{Database, TableDefinition};
const VECTORS: TableDefinition<u64, &[u8]> = TableDefinition::new("vectors");
const VECTOR_KEYS: TableDefinition<&str, u64> = TableDefinition::new("vector_keys");
let (state, tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "collide"}))
.await
.expect("palace_create");
let a = uuid::Uuid::new_v4();
let b = uuid::Uuid::new_v4();
let shared_id: u64 = 7;
{
state.registry.remove(&PalaceId::new("collide"));
let db = Database::create(tmp.path().join("collide/index.usearch.redb"))
.expect("open palace vector redb");
let wtx = db.begin_write().expect("begin");
{
let mut vectors = wtx.open_table(VECTORS).expect("vectors");
let mut keys = wtx.open_table(VECTOR_KEYS).expect("keys");
let encoded = postcard::to_allocvec(&vec![0.05_f32; 384]).expect("encode vector");
vectors.insert(shared_id, encoded.as_slice()).expect("vec");
keys.insert(a.to_string().as_str(), shared_id)
.expect("key a");
keys.insert(b.to_string().as_str(), shared_id)
.expect("key b");
}
wtx.commit().expect("commit");
drop(db); }
let out = dispatch_tool(
&state,
"palace_unalias",
json!({"palace": "collide", "dry_run": false}),
)
.await
.expect("palace_unalias must dispatch on the write path");
assert_eq!(out["dry_run"], false, "explicit write run: {out}");
assert_eq!(
out["outcome"], "repaired",
"a real collision must repair, not report clean: {out}"
);
assert_eq!(out["success"], true, "{out}");
let freed: Vec<String> = out["freed_ids"]
.as_array()
.expect("freed_ids array")
.iter()
.map(|v| v.as_str().expect("uuid string").to_string())
.collect();
assert_eq!(freed.len(), 2, "both members of the group: {out}");
assert!(freed.contains(&a.to_string()), "{a} missing: {out}");
assert!(freed.contains(&b.to_string()), "{b} missing: {out}");
assert_eq!(
out["reembed_required"], true,
"freed drawers are owed a re-embed: {out}"
);
assert!(out["error"].is_null(), "{out}");
let again = dispatch_tool(
&state,
"palace_unalias",
json!({"palace": "collide", "dry_run": false}),
)
.await
.expect("second palace_unalias");
assert_eq!(
again["outcome"], "clean",
"the repair must be durable, not repeatable: {again}"
);
assert!(
again["freed_ids"]
.as_array()
.expect("freed_ids array")
.is_empty(),
"nothing left to free: {again}"
);
}
#[tokio::test]
async fn forget_fails_loudly_when_the_lexical_lane_cannot_confirm_the_delete() {
let (state, _tmp) = test_state();
let palace = "laneblocked";
dispatch_tool(&state, "palace_create", json!({ "name": palace }))
.await
.expect("palace_create");
let remembered = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": palace,
"text": "zqxjunreachable rollback runbook for the staging deployment",
"force": true,
}),
)
.await
.expect("memory_remember");
let drawer_id = remembered["drawer_id"]
.as_str()
.expect("drawer_id")
.to_string();
let root = state.data_root.clone();
let palace_dir = root.join(palace);
std::fs::create_dir_all(&palace_dir).expect("palace dir");
std::fs::write(palace_dir.join("bm25"), b"i am a file, not a directory")
.expect("block the corpus dir");
let state = state.with_bm25_lane(crate::bm25_lane::Bm25Lane::with_limits(root, 3, None));
let err = dispatch_tool(
&state,
"memory_forget",
json!({ "palace": palace, "drawer_id": drawer_id }),
)
.await
.expect_err("a forget that could not delete the lexical copy must not report success");
let msg = format!("{err:#}");
assert!(
msg.contains("lexically searchable"),
"the error must say what may still be findable, got: {msg}"
);
}
#[tokio::test]
async fn forget_succeeds_when_the_lexical_lane_is_disabled() {
let (state, _tmp) = test_state();
assert!(
state.bm25_lane().is_none(),
"precondition: the lane must be off for this test to mean anything"
);
dispatch_tool(&state, "palace_create", json!({"name": "laneoff"}))
.await
.expect("palace_create");
let remembered = dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "laneoff",
"text": "zqxjlaneoff rollback runbook for the staging deployment",
"force": true,
}),
)
.await
.expect("memory_remember");
let drawer_id = remembered["drawer_id"].as_str().expect("drawer_id");
let forgotten = dispatch_tool(
&state,
"memory_forget",
json!({ "palace": "laneoff", "drawer_id": drawer_id }),
)
.await
.expect("memory_forget with the lane off");
assert_eq!(forgotten["status"], "deleted", "{forgotten}");
}
#[test]
fn palace_compact_description_says_vector_index_only() {
let defs = tool_definitions_with(false);
let compact = defs
.get("tools")
.and_then(|t| t.as_array())
.expect("tools array")
.iter()
.find(|d| d["name"] == "palace_compact")
.expect("palace_compact is in the roster");
let description = compact["description"].as_str().expect("a description");
assert!(
description.contains("VECTOR-INDEX ONLY"),
"the vector-only scope must be stated outright: {description}"
);
assert!(
description.contains("does not read, rewrite, prune, or shrink"),
"the description must say what it does NOT do to kg.redb: {description}"
);
assert!(
description.contains("palace compact"),
"it must point at the tool that DOES reclaim kg.redb: {description}"
);
}