use super::*;
use crate::tools::palace_index::PALACE_INDEX_DETAIL_LIMIT;
const READ_TOOLS: &[&str] = &[
"memory_recall",
"memory_recall_deep",
"memory_list",
"room_list",
"wing_list",
"kg_query",
"kg_gaps",
"kg_list_subjects",
"task_list",
"palace_info",
"chat_session_get",
"chat_session_list",
"chat_session_recall",
"palace_verify_embedded",
];
const WRITE_TOOLS: &[&str] = &[
"memory_remember",
"memory_note",
"memory_forget",
"room_create",
"room_rename",
"wing_create",
"kg_assert",
"kg_retract_triple",
"add_alias",
"kg_bootstrap",
"discover_aliases",
"task_add",
"task_complete",
"chat_session_create",
"chat_session_delete",
"palace_compact",
];
fn index_rows(payload: &Value, tool: &str) -> Vec<Value> {
assert_eq!(payload["status"], "no_palace_specified", "tool={tool}");
assert_eq!(payload["tool"], tool, "tool={tool}");
assert!(
payload["hint"].as_str().is_some_and(|h| !h.is_empty()),
"tool={tool} carries no hint: {payload}"
);
payload["palaces"]
.as_array()
.unwrap_or_else(|| panic!("tool={tool} has no palaces array: {payload}"))
.clone()
}
#[tokio::test]
async fn every_read_tool_returns_an_index_with_no_palace() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "indexed"}))
.await
.expect("palace_create");
for tool in READ_TOOLS {
let payload = dispatch_tool(&state, tool, json!({}))
.await
.unwrap_or_else(|e| panic!("{tool} must not error with no palace: {e:#}"));
let rows = index_rows(&payload, tool);
assert_eq!(payload["palace_count"], 1, "tool={tool}");
assert_eq!(rows[0]["palace"], "indexed", "tool={tool}");
}
}
#[tokio::test]
async fn every_write_tool_still_errors_with_no_palace() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "indexed"}))
.await
.expect("palace_create");
for tool in WRITE_TOOLS {
let result = dispatch_tool(&state, tool, json!({})).await;
assert!(
result.is_err(),
"{tool} must still refuse a no-palace call, got: {:?}",
result.ok()
);
}
}
#[tokio::test]
async fn palace_index_reports_counts_and_rooms() {
let (state, _tmp) = test_state();
dispatch_tool(&state, "palace_create", json!({"name": "counted"}))
.await
.expect("palace_create");
dispatch_tool(
&state,
"memory_remember",
json!({
"palace": "counted",
"text": "The palace index reports drawer and room counts so a caller can choose",
"room": "Planning",
}),
)
.await
.expect("memory_remember");
let payload = dispatch_tool(&state, "memory_recall", json!({}))
.await
.expect("memory_recall with no palace returns an index");
let rows = index_rows(&payload, "memory_recall");
let row = &rows[0];
assert_eq!(row["palace"], "counted");
assert_eq!(row["drawer_count"], 1, "row={row}");
assert!(
row["wing_count"].as_u64().is_some_and(|n| n >= 1),
"row={row} must report the default wing"
);
let rooms = row["rooms"].as_array().expect("rooms array");
let planning = rooms
.iter()
.find(|r| r["label"] == "Planning")
.unwrap_or_else(|| panic!("no Planning room in {row}"));
assert_eq!(planning["drawer_count"], 1, "row={row}");
assert_eq!(
row["room_count"].as_u64().map(|n| n as usize),
Some(rooms.len()),
"room_count must match the rooms it lists"
);
}
#[tokio::test]
async fn palace_index_on_an_empty_estate_is_still_a_success() {
let (state, _tmp) = test_state();
let payload = dispatch_tool(&state, "memory_list", json!({}))
.await
.expect("an empty estate is an empty index, not an error");
assert_eq!(index_rows(&payload, "memory_list").len(), 0);
assert_eq!(payload["palace_count"], 0);
}
#[tokio::test]
async fn a_named_palace_that_does_not_exist_still_errors() {
let (state, _tmp) = test_state();
let result = dispatch_tool(
&state,
"memory_recall",
json!({"palace": "no-such-palace", "query": "anything"}),
)
.await;
assert!(
result.is_err(),
"a named-but-missing palace must still error, got: {:?}",
result.ok()
);
}
#[tokio::test]
async fn a_default_palace_still_wins_over_the_index() {
skip_palace_enforcement();
seed_embedder();
let tmp = tempfile::tempdir().expect("tempdir");
let state =
AppState::new(tmp.path().to_path_buf()).with_default_palace(Some("defaulted".to_string()));
state.set_ready();
dispatch_tool(&state, "palace_create", json!({"name": "defaulted"}))
.await
.expect("palace_create");
let payload = dispatch_tool(&state, "memory_list", json!({}))
.await
.expect("memory_list");
assert_eq!(payload["palace"], "defaulted", "payload={payload}");
assert!(
payload["status"].is_null(),
"a resolvable default must not produce an index: {payload}"
);
}
#[tokio::test]
async fn palace_index_details_are_capped_and_the_rest_are_still_listed() {
let (state, _tmp) = test_state();
let total = PALACE_INDEX_DETAIL_LIMIT + 1;
for i in 0..total {
dispatch_tool(&state, "palace_create", json!({"name": format!("p{i:03}")}))
.await
.expect("palace_create");
}
let payload = dispatch_tool(&state, "room_list", json!({}))
.await
.expect("room_list");
let rows = index_rows(&payload, "room_list");
assert_eq!(rows.len(), total, "every palace must be listed");
let detailed = rows.iter().filter(|r| r["drawer_count"].is_u64()).count();
let omitted = rows.iter().filter(|r| r["detail"] == "omitted").count();
assert_eq!(detailed, PALACE_INDEX_DETAIL_LIMIT);
assert_eq!(omitted, total - PALACE_INDEX_DETAIL_LIMIT);
}
#[test]
fn read_tool_schemas_never_require_palace() {
for defs in [tool_definitions_with(false), tool_definitions_with(true)] {
let tools = defs["tools"].as_array().expect("tools array");
for name in READ_TOOLS {
let tool = tools
.iter()
.find(|t| t["name"] == *name)
.unwrap_or_else(|| panic!("{name} missing from tools/list"));
let required: Vec<&str> = tool["inputSchema"]["required"]
.as_array()
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
assert!(
!required.contains(&"palace"),
"{name} still requires palace: {required:?}"
);
}
}
}