#[cfg(all(test, feature = "mcp"))]
mod mcp_supersede {
use crate::config::Config;
use crate::mcp::store_wrapper::StoreWrapper;
use crate::mcp::tests::{seed_row, store_memory_args, tool_result_id};
use crate::mcp::tools::ToolHandler;
use crate::memory::lifecycle::MemoryImportance;
use crate::memory::{MemoryStore, NewMemory};
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn store_memory_supersedes_honours_the_requested_status() {
use crate::memory::lifecycle::{MemoryStatus, MemoryType};
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let handler = ToolHandler::new(store, "status-proj".to_string(), Config::default());
let seeded = seed_row(&wrapper, "status-proj", "original content");
let old_id = seeded.id.clone();
let result = handler
.call_tool_for_test(
"store_memory",
store_memory_args("replacement content", Some("candidate"), Some(&old_id)),
)
.await
.expect("store_memory with supersedes must succeed");
let new_id = tool_result_id(&result);
let replacement = wrapper
.get(&new_id, "status-proj")
.expect("get")
.expect("replacement row stored");
assert_eq!(
replacement.status,
MemoryStatus::Candidate.as_str(),
"the tool handler must hand its validated status to the store, not the wrapper's default"
);
assert_eq!(replacement.content, "replacement content");
let replaced_row = wrapper
.get(&old_id, "status-proj")
.expect("get")
.expect("superseded row still readable");
assert_eq!(replaced_row.status, "superseded");
assert_eq!(replaced_row.superseded_by.as_deref(), Some(new_id.as_str()));
let control = wrapper
.ingest_with_lifecycle(
&NewMemory {
project_id: "status-proj",
content: "unrelated active control",
metadata: None,
memory_type: MemoryType::Fact,
status: MemoryStatus::Active,
},
true,
MemoryImportance::Medium,
)
.expect("seed control");
let control_id = control["id"].as_str().expect("control id").to_string();
let listed: Vec<serde_json::Value> =
serde_json::from_value(wrapper.list("status-proj", 20, None, None).expect("list"))
.expect("list payload is an array");
let listed_ids: Vec<&str> = listed
.iter()
.map(|m| m["id"].as_str().expect("listed id"))
.collect();
assert!(
listed_ids.contains(&control_id.as_str()),
"the default list itself works, so its silence about the candidate is meaningful: {listed_ids:?}"
);
assert!(
!listed_ids.contains(&new_id.as_str()),
"a candidate replacement must stay out of the default list: {listed_ids:?}"
);
let plain = wrapper
.supersede(
"status-proj",
"plain replacement",
"null",
MemoryType::Fact,
&new_id,
)
.expect("level-less supersede");
let plain_id = plain["id"].as_str().expect("plain id").to_string();
assert_eq!(
wrapper
.get(&plain_id, "status-proj")
.expect("get")
.expect("plain row stored")
.status,
MemoryStatus::Active.as_str(),
"the status-less wrapper keeps writing active explicitly"
);
}
#[tokio::test]
async fn store_memory_supersedes_honours_an_explicit_active_status() {
use crate::memory::lifecycle::{MemoryStatus, MemoryType};
let (_dir, store) = MemoryStore::test_store();
let store = Arc::new(Mutex::new(store));
let wrapper = StoreWrapper::new(store.clone());
let handler = ToolHandler::new(store, "active-proj".to_string(), Config::default());
let old = seed_row(&wrapper, "active-proj", "original content");
let result = handler
.call_tool_for_test(
"store_memory",
store_memory_args("replacement content", Some("active"), Some(&old.id)),
)
.await
.expect("store_memory with supersedes must succeed");
let new_id = tool_result_id(&result);
let replacement = wrapper
.get(&new_id, "active-proj")
.expect("get")
.expect("replacement row stored");
assert_eq!(replacement.status, MemoryStatus::Active.as_str());
assert_eq!(replacement.content, "replacement content");
assert_eq!(replacement.memory_type, MemoryType::Fact.as_str());
let replaced_row = wrapper
.get(&old.id, "active-proj")
.expect("get")
.expect("superseded row still readable");
assert_eq!(replaced_row.status, "superseded");
assert_eq!(replaced_row.superseded_by.as_deref(), Some(new_id.as_str()));
}
#[tokio::test]
async fn store_memory_refuses_to_supersede_a_superseded_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, "chain-proj".to_string(), Config::default());
let old = seed_row(&wrapper, "chain-proj", "original content");
let first = handler
.call_tool_for_test(
"store_memory",
store_memory_args("first replacement", None, Some(&old.id)),
)
.await
.expect("first supersede must succeed");
let first_id = tool_result_id(&first);
let err = handler
.call_tool_for_test(
"store_memory",
store_memory_args("second replacement", None, Some(&old.id)),
)
.await
.expect_err("re-superseding a superseded row must be refused");
assert!(
err.message.contains("superseded") && err.message.contains(&first_id),
"the refusal must name the current status and its superseded_by: {err:?}"
);
let replaced = wrapper
.get(&old.id, "chain-proj")
.expect("get")
.expect("superseded row still readable");
assert_eq!(replaced.superseded_by.as_deref(), Some(first_id.as_str()));
let listed: Vec<serde_json::Value> =
serde_json::from_value(wrapper.list("chain-proj", 20, None, None).expect("list"))
.expect("list payload is an array");
assert_eq!(
listed.len(),
1,
"only the live replacement is listed: {listed:?}"
);
let all: Vec<serde_json::Value> = serde_json::from_value(
wrapper
.list(
"chain-proj",
20,
None,
Some(["active", "candidate", "superseded", "deprecated"].as_slice()),
)
.expect("list"),
)
.expect("list payload is an array");
assert_eq!(all.len(), 2, "the refused call inserted nothing: {all:?}");
}
}