use palace::db;
use palace::general_extractor::extract_memories;
use palace::mcp_server::dispatch_tool;
use palace::palace::Palace;
use palace::ranker::hybrid_search;
use palace::searcher::search_memories;
use palace::store::{add_drawer, preference_search_filtered, DrawerFilter};
fn test_db() -> rusqlite::Connection {
db::open_in_memory().expect("in-memory DB should open")
}
fn add(conn: &rusqlite::Connection, room: &str, content: &str, source: &str, chunk: usize) {
add_drawer(
conn,
"palace_rs",
room,
content,
None,
source,
chunk,
"test",
3.0,
)
.expect("drawer should be added");
}
#[test]
fn sanitized_prompt_dump_retrieves_the_actual_memory_question() {
let conn = test_db();
add(
&conn,
"decisions",
"We chose bundled SQLite because library consumers should not need system sqlite installed.",
"sessions/001.md",
0,
);
add(
&conn,
"frontend",
"The dashboard uses muted teal navigation and compact cards.",
"sessions/002.md",
0,
);
let noisy_query = r#"
You are a coding agent. Here is a huge unrelated prompt dump.
```json
{"tool":"shell","output":"dashboard dashboard dashboard frontend teal cards"}
```
Search query: why did we choose bundled sqlite for consumers?
"#;
let results = hybrid_search(&conn, noisy_query, None, &DrawerFilter::default(), 5)
.expect("search should work");
assert_eq!(results[0].drawer.source_file, "sessions/001.md");
}
#[test]
fn coding_agent_intents_boost_practical_memory_queries() {
let conn = test_db();
let cases = [
(
"decisions",
"We decided to keep the Palace facade small because downstream Rust callers need stable APIs.",
"why did we keep the Palace facade small?",
),
(
"problems",
"The migration test failed because created_at was missing; the fix was adding an idempotent column migration.",
"how did we fix the migration test failure last time?",
),
(
"commands",
"For full local verification, run cargo clippy --all-targets --all-features -- -D warnings.",
"what command should I run for clippy?",
),
(
"conventions",
"Project convention: always keep MCP search results source-grounded and never replace verbatim drawers with summaries.",
"what is the project convention for search results?",
),
(
"preferences",
"The user prefers focused implementation over extra CLI commands or broad feature sprawl.",
"what does the user prefer about feature scope?",
),
(
"current",
"Current direction changed from broad Palace parity to a narrow coding-agent memory engine.",
"what changed in the current direction?",
),
];
for (idx, (room, content, _)) in cases.iter().enumerate() {
add(&conn, room, content, &format!("sessions/{idx}.md"), 0);
}
for (room, _, query) in cases {
let results = hybrid_search(&conn, query, None, &DrawerFilter::default(), 5)
.expect("search should work");
assert_eq!(
results[0].drawer.room, room,
"query should retrieve the {room} memory first: {query}"
);
}
}
#[test]
fn mcp_search_returns_bounded_source_context_and_score_provenance() {
let conn = test_db();
add(
&conn,
"decisions",
"Earlier unrelated chunk.",
"sessions/source.md",
0,
);
add(
&conn,
"decisions",
"We chose source context expansion because agents need neighboring chunks to cite decisions correctly.",
"sessions/source.md",
1,
);
add(
&conn,
"decisions",
"Later implementation detail.",
"sessions/source.md",
2,
);
add(
&conn,
"decisions",
"Different source should not appear.",
"sessions/other.md",
1,
);
let value = search_memories(
&conn,
"why did we choose source context expansion?",
Some("palace_rs"),
None,
1,
);
let first = &value["results"][0];
assert_eq!(first["source_file"], "sessions/source.md");
assert!(first["combined"].is_number());
assert!(first["cosine"].is_number());
assert!(first["bm25"].is_number());
assert!(first["coding_boost"].is_number());
assert!(first["created_at"].is_string());
assert!(first["filed_at"].is_string());
let context = first["source_context"]
.as_array()
.expect("source_context should be an array");
let chunks: Vec<i64> = context
.iter()
.map(|value| value["chunk_index"].as_i64().expect("chunk index"))
.collect();
assert_eq!(chunks, vec![0, 2], "the hit itself is not repeated");
assert_eq!(context[0]["text"], "Earlier unrelated chunk.");
assert!(context[0]["id"].is_string());
assert!(
context[0].get("source_file").is_none(),
"neighbours share the hit's source, wing and room"
);
}
#[test]
fn mcp_search_context_skips_other_hits_and_can_be_turned_off() {
let conn = test_db();
add(
&conn,
"decisions",
"We chose source context expansion because agents cite decisions.",
"sessions/source.md",
0,
);
add(
&conn,
"decisions",
"Source context expansion returns neighbouring chunks of a decision.",
"sessions/source.md",
1,
);
add(
&conn,
"decisions",
"Later implementation detail.",
"sessions/source.md",
2,
);
let config = palace::config::PalaceConfig::new();
let search = |context: Option<bool>| {
let mut args = serde_json::json!({
"query": "source context expansion decisions",
"wing": "palace_rs",
"limit": 2
});
if let Some(context) = context {
args["context"] = context.into();
}
palace::mcp_server::dispatch_tool(&conn, &config, "palace_search", &args)
};
let with_context = search(None);
let hits = with_context["results"].as_array().expect("results");
assert_eq!(hits.len(), 2);
let neighbours: Vec<i64> = hits
.iter()
.flat_map(|hit| hit["source_context"].as_array().expect("context").clone())
.map(|value| value["chunk_index"].as_i64().expect("chunk index"))
.collect();
assert!(
neighbours.iter().all(|&chunk| chunk == 2),
"a chunk that is a hit is not a neighbour: {neighbours:?}"
);
assert!(!neighbours.is_empty());
let without = search(Some(false));
assert!(without["results"]
.as_array()
.expect("results")
.iter()
.all(|hit| hit.get("source_context").is_none()));
}
#[test]
fn extracted_memories_keep_source_provenance_when_stored() {
let memories = extract_memories(
"We decided to use SQLite because it keeps the coding-agent memory engine local.",
0.1,
);
assert!(
memories
.iter()
.any(|memory| memory.memory_type == "decision"
&& memory.content.contains("We decided to use SQLite")
&& memory.chunk_index == 0),
"decision memory should preserve original text and chunk index"
);
}
#[test]
fn palace_extracted_memories_point_back_to_voice_turn_source() {
let palace = Palace::open_in_memory().expect("palace should open");
palace
.ingest_turn(
"We decided to use SQLite because it is local instead of running Chroma.",
"I will keep that decision source-grounded.",
)
.expect("turn should ingest");
let source_file: String = palace
.conn()
.query_row(
"SELECT source_file FROM drawers WHERE room = 'decision' LIMIT 1",
[],
|row| row.get(0),
)
.expect("extracted decision should exist");
assert_eq!(source_file, "voice_turn");
}
#[test]
fn preference_recall_respects_search_filters() {
let conn = test_db();
let query_vec: Vec<f32> = (0..384).map(|i| (i as f32).sin()).collect();
let other_vec: Vec<f32> = (0..384).map(|i| (i as f32).cos()).collect();
add_drawer(
&conn,
"target_wing",
"preferences",
"I prefer small public APIs for embedded Rust callers.",
Some(&query_vec),
"target.md",
0,
"test",
3.0,
)
.expect("target preference should insert");
add_drawer(
&conn,
"other_wing",
"preferences",
"I prefer unrelated dashboard experiments.",
Some(&other_vec),
"other.md",
0,
"test",
3.0,
)
.expect("other preference should insert");
let filter = DrawerFilter {
wing: Some("target_wing".to_string()),
room: Some("preferences".to_string()),
};
let results =
preference_search_filtered(&conn, &query_vec, &filter, 10).expect("search should work");
assert_eq!(results.len(), 1);
assert_eq!(results[0].source_file, "target.md");
}
#[test]
fn palace_structured_search_exposes_score_provenance() {
let palace = Palace::open_in_memory().expect("palace should open");
add_drawer(
palace.conn(),
"palace_rs",
"decisions",
"We chose bundled SQLite because coding agents should not need Chroma.",
None,
"sqlite.md",
0,
"test",
3.0,
)
.expect("drawer should insert");
let results = palace
.search_with_provenance("why did we choose bundled sqlite?", None, None, 3)
.expect("structured search should work");
let first = results.first().expect("expected a search result");
assert_eq!(first.drawer.source_file, "sqlite.md");
assert!(first.combined > 0.0);
assert!(first.bm25 > 0.0);
assert_eq!(
first.drawer.similarity,
(first.combined * 1000.0).round() / 1000.0
);
}
#[test]
fn session_context_returns_recent_diary_metadata_and_compact_text() {
let conn = test_db();
let config = palace::config::PalaceConfig::new();
let write = dispatch_tool(
&conn,
&config,
"palace_diary_write",
&serde_json::json!({
"agent_name": "Codex",
"entry": "PROJ:palace_rs | Implemented preference recall reliability.",
"topic": "release",
"session_id": "session-123",
"project_path": "D:\\Dev\\Projects\\palace-rs",
"tags": ["0.1.9", "reliability"]
}),
);
assert_eq!(write["success"], true);
let context = dispatch_tool(
&conn,
&config,
"palace_session_context",
&serde_json::json!({
"agent_name": "Codex"
}),
);
assert_eq!(context["has_recent_session"], true);
assert_eq!(
context["last_active_project"],
"D:\\Dev\\Projects\\palace-rs"
);
let entries = context["recent_entries"]
.as_array()
.expect("recent_entries should be an array");
let first = entries.first().expect("expected recent diary entry");
assert_eq!(first["topic"], "release");
assert_eq!(first["session_id"], "session-123");
assert_eq!(first["project_path"], "D:\\Dev\\Projects\\palace-rs");
assert!(first["timestamp"].is_string());
assert!(first["text"].as_str().expect("compact text").len() <= 240);
}