use crate::config::Config;
use crate::mcp::params::*;
use crate::mcp::store_wrapper::StoreWrapper;
use crate::mcp::tools::ToolHandler;
use crate::memory::lifecycle::MemoryImportance;
use crate::memory::{MemoryStore, NewMemory};
use std::sync::{Arc, Mutex};
pub(super) fn seed_row_at(
wrapper: &StoreWrapper,
project: &str,
text: &str,
importance: MemoryImportance,
) -> crate::Memory {
use crate::memory::lifecycle::{MemoryStatus, MemoryType};
let row = NewMemory {
project_id: project,
content: text,
metadata: None,
memory_type: MemoryType::Fact,
status: MemoryStatus::Active,
};
let seeded = wrapper
.ingest_with_lifecycle(&row, true, importance)
.expect("seed add");
let id = seeded["id"].as_str().expect("seed id").to_string();
wrapper
.get(&id, project)
.expect("get")
.expect("seeded row readable")
}
pub(super) fn seed_row(wrapper: &StoreWrapper, project: &str, text: &str) -> crate::Memory {
seed_row_at(wrapper, project, text, MemoryImportance::Medium)
}
pub(super) fn tool_result_id(result: &rmcp::model::CallToolResult) -> String {
let text = result
.content
.iter()
.filter_map(|c| c.as_text().map(|t| t.text.as_str()))
.next()
.expect("tool result carries text content");
serde_json::from_str::<serde_json::Value>(text)
.expect("tool result text is JSON")
.get("id")
.and_then(|v| v.as_str())
.expect("tool result JSON carries an id")
.to_string()
}
pub(super) fn store_memory_args(
text: &str,
status: Option<&str>,
supersedes: Option<&str>,
) -> serde_json::Value {
store_memory_args_full(text, status, supersedes, None)
}
pub(super) fn store_memory_args_full(
text: &str,
status: Option<&str>,
supersedes: Option<&str>,
importance: Option<&str>,
) -> serde_json::Value {
serde_json::json!({
"text": text,
"status": status,
"supersedes": supersedes,
"importance": importance,
})
}
#[cfg(all(test, feature = "mcp"))]
mod tool_handler_tests {
use super::*;
#[test]
fn test_mcp_server_initialization() {
let (_dir, store) = MemoryStore::test_store();
let config = Config::default();
let _handler = ToolHandler::new(
Arc::new(Mutex::new(store)),
"test-project".to_string(),
config,
);
}
#[test]
fn test_tool_handler_retains_non_default_config() {
let (_dir, store) = MemoryStore::test_store();
let config = Config {
recency_weight: 0.91,
hybrid: true,
..Config::default()
};
let handler = ToolHandler::new(
Arc::new(Mutex::new(store)),
"test-project".to_string(),
config,
);
assert_eq!(handler.config().recency_weight, 0.91);
assert!(handler.config().hybrid);
}
#[test]
fn test_store_memory_params_serde() {
let params = StoreMemoryParams {
text: "test memory".to_string(),
metadata: Some(serde_json::json!({"topic": "test"})),
memory_type: None,
status: None,
importance: None,
supersedes: None,
force: None,
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: StoreMemoryParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.text, "test memory");
assert!(decoded.metadata.is_some());
assert!(decoded.memory_type.is_none());
assert!(decoded.status.is_none());
assert!(decoded.importance.is_none());
assert!(decoded.supersedes.is_none());
assert!(decoded.force.is_none());
}
#[test]
fn test_store_memory_params_with_all_fields() {
let params = StoreMemoryParams {
text: "test memory".to_string(),
metadata: Some(serde_json::json!({"topic": "test"})),
memory_type: Some("preference".to_string()),
status: Some("active".to_string()),
importance: Some("critical".to_string()),
supersedes: Some("old-memory-id".to_string()),
force: Some(true),
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: StoreMemoryParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.text, "test memory");
assert!(decoded.metadata.is_some());
assert_eq!(decoded.memory_type, Some("preference".to_string()));
assert_eq!(decoded.status, Some("active".to_string()));
assert_eq!(decoded.importance, Some("critical".to_string()));
assert_eq!(decoded.supersedes, Some("old-memory-id".to_string()));
assert_eq!(decoded.force, Some(true));
}
#[test]
fn test_supersede_memory_params_serde() {
let params = SupersedeMemoryParams {
new_text: "updated memory".to_string(),
old_memory_id: "old-memory-id".to_string(),
memory_type: Some("fact".to_string()),
metadata: Some(serde_json::json!({"updated": true})),
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: SupersedeMemoryParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.new_text, "updated memory");
assert_eq!(decoded.old_memory_id, "old-memory-id");
assert_eq!(decoded.memory_type, Some("fact".to_string()));
assert_eq!(decoded.metadata, Some(serde_json::json!({"updated": true})));
}
#[test]
fn test_supersede_memory_params_minimal() {
let params = SupersedeMemoryParams {
new_text: "updated memory".to_string(),
old_memory_id: "old-memory-id".to_string(),
memory_type: None,
metadata: None,
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: SupersedeMemoryParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.new_text, "updated memory");
assert_eq!(decoded.old_memory_id, "old-memory-id");
assert!(decoded.memory_type.is_none());
assert!(decoded.metadata.is_none());
}
#[test]
fn test_search_params_serde() {
let params = SearchMemoriesParams {
query: "test query".to_string(),
limit: Some(10),
memory_types: None,
statuses: None,
recency_weight: None,
hybrid: None,
no_touch: None,
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: SearchMemoriesParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.query, "test query");
assert_eq!(decoded.limit, Some(10));
assert!(decoded.memory_types.is_none());
assert!(decoded.statuses.is_none());
}
#[test]
fn test_list_params_serde() {
let params = ListMemoriesParams {
limit: Some(5),
memory_types: None,
statuses: None,
no_touch: None,
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: ListMemoriesParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.limit, Some(5));
assert!(decoded.memory_types.is_none());
assert!(decoded.statuses.is_none());
}
#[test]
fn test_list_params_default_limit() {
let params = ListMemoriesParams {
limit: None,
memory_types: None,
statuses: None,
no_touch: None,
};
let json = serde_json::to_string(¶ms).unwrap();
let decoded: ListMemoriesParams = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.limit, None);
}
#[test]
fn test_tool_router_exposes_all_expected_tools() {
let expected = [
"delete_memory",
"get_memory",
"list_memories",
"search_memories",
"store_memory",
"supersede_memory",
"update_memory",
];
let listed: Vec<String> = ToolHandler::test_tool_router()
.list_all()
.into_iter()
.map(|tool| tool.name.into_owned())
.collect();
assert_eq!(
listed,
expected.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
for name in expected {
assert!(
ToolHandler::test_tool_router().get(name).is_some(),
"tool '{name}' is missing from the MCP tool router"
);
}
}
#[test]
fn test_success_response_serde() {
let response = SuccessResponse {
id: "test-id".to_string(),
status: "added".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("test-id"));
assert!(json.contains("added"));
}
#[test]
fn test_conflict_memory_serde() {
let memory = ConflictMemory {
id: "test-id".to_string(),
content: "test content".to_string(),
similarity: 0.85,
};
let json = serde_json::to_string(&memory).unwrap();
assert!(json.contains("test-id"));
assert!(json.contains("test content"));
assert!(json.contains("0.85"));
}
#[tokio::test]
async fn test_store_and_search_integration() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let project_id = "test-project";
let result = wrapper.ingest(project_id, "rust is awesome", "null", false);
assert!(result.is_ok());
let value = result.unwrap();
let json_str = serde_json::to_string(&value).unwrap();
assert!(json_str.contains("added"));
let search_result = wrapper.search(
project_id,
"rust programming",
5,
0.0,
None::<Vec<&str>>,
None,
);
assert!(search_result.is_ok());
let search_value = search_result.unwrap();
let search_str = serde_json::to_string(&search_value).unwrap();
assert!(search_str.contains("rust is awesome"));
let list_result = wrapper.list(project_id, 10, None, None);
assert!(list_result.is_ok());
let list_value = list_result.unwrap();
let list_str = serde_json::to_string(&list_value).unwrap();
assert!(list_str.contains("rust is awesome"));
}
#[tokio::test]
async fn test_search_results_include_metadata_and_project_id() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let project_id = "test-project";
let metadata_json = serde_json::json!({"topic": "rust", "type": "fact"});
let metadata_str = serde_json::to_string(&metadata_json).unwrap();
let result = wrapper.ingest(project_id, "rust is awesome", &metadata_str, false);
assert!(result.is_ok());
let search_result = wrapper.search(project_id, "rust", 5, 0.0, None::<Vec<&str>>, None);
assert!(search_result.is_ok());
let search_value = search_result.unwrap();
let search_str = serde_json::to_string(&search_value).unwrap();
assert!(search_str.contains("\"id\""));
assert!(search_str.contains("\"content\""));
assert!(search_str.contains("\"similarity\""));
assert!(search_str.contains("\"created_at\""));
assert!(search_str.contains("\"updated_at\""));
assert!(search_str.contains("\"project_id\""));
assert!(search_str.contains("\"metadata\""));
assert!(search_str.contains("rust is awesome"));
assert!(search_str.contains("test-project"));
let parsed: serde_json::Value = serde_json::from_str(&search_str).unwrap();
assert!(parsed.is_array());
let results = parsed.as_array().unwrap();
assert!(!results.is_empty());
let first_result = &results[0];
assert!(first_result.get("id").is_some());
assert!(first_result.get("content").is_some());
assert!(first_result.get("similarity").is_some());
assert!(first_result.get("created_at").is_some());
assert!(first_result.get("updated_at").is_some());
assert!(first_result.get("project_id").is_some());
assert!(first_result.get("metadata").is_some());
assert_eq!(
first_result["project_id"],
serde_json::Value::String("test-project".to_string())
);
assert_eq!(first_result["metadata"], metadata_json);
}
#[tokio::test]
async fn test_delete_memory_integration() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let project_id = "test-project";
let result = wrapper.ingest(project_id, "memory to delete", "null", false);
assert!(result.is_ok());
let value = result.unwrap();
let json_str = serde_json::to_string(&value).unwrap();
let response: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let id = response["id"].as_str().unwrap();
let delete_result = wrapper.delete(id, project_id);
assert!(delete_result.is_ok());
assert!(delete_result.unwrap(), "delete should return true");
let get_result = wrapper.get(id, project_id);
assert!(get_result.is_ok());
assert!(
get_result.unwrap().is_none(),
"deleted memory should not be found"
);
let delete_again_result = wrapper.delete(id, project_id);
assert!(delete_again_result.is_ok());
assert!(
!delete_again_result.unwrap(),
"deleting non-existent memory should return false"
);
}
}