use super::*;
use crate::AppState;
use serde_json::json;
use trusty_common::memory_core::palace::PalaceId;
use uuid::Uuid;
fn test_state() -> (AppState, tempfile::TempDir) {
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
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) {
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();
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", true),
("memory_recall_deep", true),
("memory_list", true),
("memory_forget", true),
("palace_info", true),
("palace_compact", true),
("kg_assert", true),
("kg_query", true),
("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");
assert_eq!(tools.len(), 25);
let names: Vec<&str> = tools
.iter()
.filter_map(|t| t.get("name").and_then(|n| n.as_str()))
.collect();
for expected in [
"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",
"kg_assert",
"kg_query",
"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",
] {
assert!(names.contains(&expected), "missing tool: {expected}");
}
}
#[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_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");
}
#[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() {
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() {
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"
);
}
#[tokio::test]
async fn kg_query_emits_hint_when_palace_empty() {
let (state, _tmp) = test_state();
let _ = dispatch_tool(&state, "palace_create", json!({"name": "hinted"}))
.await
.expect("palace_create");
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);
let hint = queried["hint"].as_str().expect("hint field present");
assert!(hint.contains("kg_bootstrap"));
assert!(hint.contains("kg_assert"));
}
#[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), None);
assert_eq!(content_gate("ok", None), None);
assert_eq!(
content_gate(" no thanks ", None),
None,
"2 words still < 4"
);
assert_eq!(
content_gate("one two three", None),
None,
"3 words still < 4"
);
}
#[test]
fn content_gate_wraps_short_with_context() {
let combined = content_gate(
"yes",
Some("Do you want to enable auto-bootstrap on new palaces?"),
)
.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"),
)
.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).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).as_deref(), Some(boundary));
}
#[test]
fn content_gate_blank_context_treated_as_none() {
assert_eq!(content_gate("yes", Some("")), None);
assert_eq!(content_gate("yes", Some(" ")), None);
assert_eq!(content_gate("yes", Some("\n\t")), 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_some());
}
#[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 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(),
data_dir: state.data_root.join("default").join("bm25"),
};
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:?}"),
}
}
#[tokio::test]
async fn remember_returns_warming_error_while_state_is_warming() {
let (state, _tmp) = test_state_warming();
let _ = dispatch_tool(
&state,
"palace_create",
serde_json::json!({"name": "warmtest"}),
)
.await;
let result = dispatch_tool(
&state,
"memory_remember",
serde_json::json!({
"palace": "warmtest",
"text": "test memory that should be rejected while warming up"
}),
)
.await;
let err = result.expect_err("memory_remember must fail while Warming");
let msg = err.to_string();
assert!(
msg.contains("warming up"),
"error must mention 'warming up'; got: {msg}"
);
}
#[tokio::test]
async fn recall_returns_warming_error_while_state_is_warming() {
let (state, _tmp) = test_state_warming();
let _ = dispatch_tool(
&state,
"palace_create",
serde_json::json!({"name": "warmtest-recall"}),
)
.await;
let result = dispatch_tool(
&state,
"memory_recall",
serde_json::json!({
"palace": "warmtest-recall",
"query": "test query"
}),
)
.await;
let err = result.expect_err("memory_recall must fail while Warming");
let msg = err.to_string();
assert!(
msg.contains("warming up"),
"error must mention 'warming up'; got: {msg}"
);
}
#[tokio::test]
async fn note_returns_warming_error_while_state_is_warming() {
let (state, _tmp) = test_state_warming();
let _ = dispatch_tool(
&state,
"palace_create",
serde_json::json!({"name": "warmtest-note"}),
)
.await;
let result = dispatch_tool(
&state,
"memory_note",
serde_json::json!({
"palace": "warmtest-note",
"content": "short note content here"
}),
)
.await;
let err = result.expect_err("memory_note must fail while Warming");
let msg = err.to_string();
assert!(
msg.contains("warming up"),
"error must mention 'warming up'; got: {msg}"
);
}
#[tokio::test]
async fn recall_all_returns_warming_error_while_state_is_warming() {
let (state, _tmp) = test_state_warming();
let result = dispatch_tool(
&state,
"memory_recall_all",
serde_json::json!({
"q": "test query that should be rejected while warming up"
}),
)
.await;
let err = result.expect_err("memory_recall_all must fail while Warming");
let msg = err.to_string();
assert!(
msg.contains("warming up"),
"error must mention 'warming up'; got: {msg}"
);
}