#[cfg(all(test, feature = "mcp"))]
mod store_memory_importance {
use crate::config::Config;
use crate::mcp::store_wrapper::StoreWrapper;
use crate::mcp::tests::{seed_row, store_memory_args, store_memory_args_full, tool_result_id};
use crate::mcp::tools::ToolHandler;
use crate::memory::lifecycle::{MemoryImportance, MemoryStatus, MemoryType};
use crate::memory::{MemoryStore, NewMemory};
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn store_memory_writes_the_importance_the_agent_named() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let handler = ToolHandler::new(store, "imp-proj".to_string(), Config::default());
for (i, level) in ["critical", "high", "low", "medium"].iter().enumerate() {
let text = format!("importance probe {i}");
let result = handler
.call_tool_for_test(
"store_memory",
store_memory_args_full(&text, None, None, Some(level)),
)
.await
.expect("store_memory with a valid importance must succeed");
let id = tool_result_id(&result);
assert_eq!(
wrapper
.get(&id, "imp-proj")
.expect("get")
.expect("row stored")
.importance,
*level,
"the level named in the tool call must reach the column, not its DEFAULT"
);
}
}
#[tokio::test]
async fn store_memory_importance_defaults_to_medium_and_stores_lowercase() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let handler = ToolHandler::new(store, "imp-default-proj".to_string(), Config::default());
let omitted = handler
.call_tool_for_test(
"store_memory",
store_memory_args("a memory with no importance", None, None),
)
.await
.expect("omitting importance must succeed");
assert_eq!(
wrapper
.get(&tool_result_id(&omitted), "imp-default-proj")
.expect("get")
.expect("row stored")
.importance,
"medium",
"an omitted importance is the named default, not the column DEFAULT"
);
for (input, expected) in [("HIGH", "high"), ("Critical", "critical"), ("Low", "low")] {
let text = format!("uppercase probe for {input}");
let result = handler
.call_tool_for_test(
"store_memory",
store_memory_args_full(&text, None, None, Some(input)),
)
.await
.unwrap_or_else(|e| panic!("{input} must be accepted: {e:?}"));
assert_eq!(
wrapper
.get(&tool_result_id(&result), "imp-default-proj")
.expect("get")
.expect("row stored")
.importance,
expected,
"{input} must be stored as the canonical spelling"
);
}
}
#[tokio::test]
async fn store_memory_rejects_an_invalid_importance_without_writing() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let handler = ToolHandler::new(store, "imp-bad-proj".to_string(), Config::default());
let old = seed_row(&wrapper, "imp-bad-proj", "original fact");
for bad in ["urgent", "", "normal"] {
let text = format!("probe against {bad:?}");
for supersedes in [None, Some(old.id.as_str())] {
let err = handler
.call_tool_for_test(
"store_memory",
store_memory_args_full(&text, None, supersedes, Some(bad)),
)
.await
.expect_err("an invalid importance must be rejected");
assert!(
err.message.contains("Invalid importance"),
"the tool error must name the offending field ({bad:?}): {err:?}"
);
}
}
let listed: Vec<serde_json::Value> =
serde_json::from_value(wrapper.list("imp-bad-proj", 20, None, None).expect("list"))
.expect("list payload is an array");
assert_eq!(
listed.len(),
1,
"a rejected call must insert no row on either branch"
);
let original = wrapper
.get(&old.id, "imp-bad-proj")
.expect("get")
.expect("original still readable");
assert_eq!(original.status, "active");
assert_eq!(original.importance, "medium");
}
#[tokio::test]
async fn store_memory_supersedes_applies_the_importance_to_the_new_row() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let handler = ToolHandler::new(store, "imp-sup-proj".to_string(), Config::default());
let old = seed_row(&wrapper, "imp-sup-proj", "original fact");
let result = handler
.call_tool_for_test(
"store_memory",
store_memory_args_full("replacement fact", None, Some(&old.id), Some("critical")),
)
.await
.expect("store_memory with supersedes and importance must succeed");
let new_id = tool_result_id(&result);
let replacement = wrapper
.get(&new_id, "imp-sup-proj")
.expect("get")
.expect("replacement row stored");
assert_eq!(
replacement.importance, "critical",
"the superseding row must carry the level the agent asked for"
);
assert_eq!(replacement.content, "replacement fact");
let replaced = wrapper
.get(&old.id, "imp-sup-proj")
.expect("get")
.expect("superseded row still readable");
assert_eq!(replaced.status, "superseded");
assert_eq!(
replaced.importance, "medium",
"the new row's level must not leak into the row it replaced"
);
assert_eq!(replaced.superseded_by.as_deref(), Some(new_id.as_str()));
}
#[test]
fn store_memory_tool_schema_documents_the_importance_field() {
let tool = ToolHandler::test_tool_definition("store_memory").expect("store_memory tool");
let properties = tool
.input_schema
.get("properties")
.and_then(|v| v.as_object())
.expect("input_schema properties");
let importance = properties
.get("importance")
.expect("input_schema advertises an importance parameter");
let description = importance
.get("description")
.and_then(|v| v.as_str())
.expect("importance carries a description");
for level in ["low", "medium", "high", "critical"] {
assert!(
description.contains(level),
"the advertised description must name every level ({level}): {description}"
);
}
let levels = importance
.get("enum")
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("importance advertises a closed enum: {importance}"));
let advertised: Vec<&str> = levels
.iter()
.map(|v| v.as_str().expect("enum entry is a string"))
.collect();
assert_eq!(
advertised,
vec!["low", "medium", "high", "critical"],
"the enum must name exactly the four levels, in canonical spelling"
);
}
#[tokio::test]
async fn store_memory_rejects_supersedes_together_with_force() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let handler = ToolHandler::new(store, "sup-force-proj".to_string(), Config::default());
let seed = seed_row(&wrapper, "sup-force-proj", "original fact");
let mut args = store_memory_args_full(
"replacement fact",
None,
Some(seed.id.as_str()),
Some("critical"),
);
args["force"] = serde_json::json!(true);
let err = handler
.call_tool_for_test("store_memory", args)
.await
.expect_err("supersedes together with force must be refused");
assert!(
err.message.contains("--supersedes") && err.message.contains("--force"),
"the refusal must use the CLI's wording, got: {}",
err.message
);
let seed_after = wrapper
.get(&seed.id, "sup-force-proj")
.expect("get")
.expect("seed row still present");
assert_eq!(seed_after.status, "active");
assert_eq!(seed_after.importance, "medium");
assert_eq!(seed_after.superseded_by, None);
let listed: Vec<serde_json::Value> = serde_json::from_value(
wrapper
.list("sup-force-proj", 20, None, None)
.expect("list"),
)
.expect("list payload is an array");
assert_eq!(
listed.len(),
1,
"the refused call must not have stored a row: {listed:?}"
);
assert_eq!(listed[0]["content"], "original fact");
}
#[tokio::test]
async fn supersede_with_row_refuses_a_status_that_is_not_insertable() {
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let old_id = seed_row(&wrapper, "guard-proj", "original content").id;
for status in [MemoryStatus::Superseded, MemoryStatus::Deprecated] {
let err = wrapper
.supersede_with_row(
&NewMemory {
project_id: "guard-proj",
content: "replacement content",
metadata: None,
memory_type: MemoryType::Fact,
status,
},
&old_id,
MemoryImportance::Medium,
)
.expect_err("the widened wrapper must not reach the INSERT");
assert!(
err.message
.contains("is not valid for new memory insertion"),
"{status:?} reached the supersede INSERT: {err:?}"
);
}
let replaced_row = wrapper
.get(&old_id, "guard-proj")
.expect("get")
.expect("original row still readable");
assert_eq!(replaced_row.status, MemoryStatus::Active.as_str());
let listed: Vec<serde_json::Value> =
serde_json::from_value(wrapper.list("guard-proj", 20, None, None).expect("list"))
.expect("list payload is an array");
assert_eq!(
listed.len(),
1,
"a refused supersede must insert no replacement row"
);
}
}