#![allow(clippy::too_many_lines)]
use crate::models::field_names;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::io::{self, BufRead, Read, Write};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use crate::config::{AppConfig, FeatureTier, ResolvedModels, TierConfig};
use crate::db;
use crate::embeddings::{Embed, Embedder};
use crate::hnsw::VectorIndex;
use crate::llm::OllamaClient;
use crate::reranker::{BatchedReranker, CrossEncoder};
const EFFECTIVE_TIER_AUTONOMOUS: &str = "autonomous";
pub(super) mod registry;
pub mod server_identity;
pub mod param_names;
pub mod jsonrpc;
#[cfg(test)]
pub(super) mod parity_test_helpers;
#[cfg(test)]
pub(super) static SHARED_PERMISSION_RULES_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub(crate) use registry::families_overview;
#[cfg(test)]
pub(crate) use registry::trim_optional_params;
pub use registry::{
handle_capabilities_family, tool_definitions, tool_definitions_for_profile,
tool_definitions_for_profile_verbose,
};
#[derive(Deserialize)]
struct RpcRequest {
jsonrpc: String,
id: Option<Value>,
method: String,
#[serde(default)]
params: Value,
}
#[derive(Debug, Serialize)]
struct RpcResponse {
jsonrpc: String,
id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<RpcError>,
}
#[derive(Debug, Serialize)]
struct RpcError {
code: i64,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
}
fn ok_response(id: Value, result: Value) -> RpcResponse {
RpcResponse {
jsonrpc: jsonrpc::VERSION.into(),
id,
result: Some(result),
error: None,
}
}
fn err_response(id: Value, code: i64, message: String) -> RpcResponse {
RpcResponse {
jsonrpc: jsonrpc::VERSION.into(),
id,
result: None,
error: Some(RpcError {
code,
message,
data: None,
}),
}
}
fn audit_emit_for_mcp_dispatch(
tool_name: &str,
arguments: &Value,
result: &Result<Value, String>,
mcp_client: Option<&str>,
) {
if !crate::audit::is_enabled() {
return;
}
use crate::mcp::registry::tool_names;
let action = match tool_name {
tool_names::MEMORY_STORE | tool_names::MEMORY_DELETE => return,
tool_names::MEMORY_RECALL
| tool_names::MEMORY_SEARCH
| tool_names::MEMORY_GET
| tool_names::MEMORY_LIST
| tool_names::MEMORY_SESSION_START => crate::audit::AuditAction::Recall,
tool_names::MEMORY_UPDATE => crate::audit::AuditAction::Update,
tool_names::MEMORY_PROMOTE => crate::audit::AuditAction::Promote,
tool_names::MEMORY_FORGET => crate::audit::AuditAction::Forget,
tool_names::MEMORY_LINK => crate::audit::AuditAction::Link,
tool_names::MEMORY_CONSOLIDATE => crate::audit::AuditAction::Consolidate,
tool_names::MEMORY_PENDING_APPROVE => crate::audit::AuditAction::Approve,
tool_names::MEMORY_PENDING_REJECT => crate::audit::AuditAction::Reject,
_ => return,
};
let agent_id = resolve_mcp_agent_id(arguments, mcp_client);
let namespace = arguments
.get("namespace")
.and_then(Value::as_str)
.unwrap_or(crate::DEFAULT_NAMESPACE)
.to_string();
let memory_id = arguments
.get("id")
.or_else(|| arguments.get("memory_id"))
.and_then(Value::as_str)
.unwrap_or("*")
.to_string();
let mut builder = crate::audit::EventBuilder::new(
action,
crate::audit::actor(
agent_id,
mcp_client.map_or(crate::audit::synthesis_sources::HOST_FALLBACK, |_| {
crate::audit::synthesis_sources::MCP_CLIENT_INFO
}),
None,
),
crate::audit::AuditTarget {
memory_id,
namespace,
title: None,
tier: None,
scope: None,
},
);
if let Err(e) = result {
builder = builder.error(e.clone());
}
crate::audit::emit(builder);
}
fn resolve_mcp_agent_id(arguments: &Value, mcp_client: Option<&str>) -> String {
arguments
.get("agent_id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| {
mcp_client
.map(|c| format!("ai:{c}"))
.unwrap_or_else(|| "anonymous".into())
})
}
fn observe_capture_nag(
nag_watcher: Option<&crate::recover::nag::CaptureNagWatcher>,
session_id: &str,
tool_name: &str,
arguments: &Value,
mcp_client: Option<&str>,
) -> crate::recover::nag::NagAction {
use crate::recover::nag::{NagAction, classify_tool};
let Some(watcher) = nag_watcher else {
return NagAction::None;
};
let agent_id = resolve_mcp_agent_id(arguments, mcp_client);
let action = watcher.observe_tool_call(&agent_id, session_id, classify_tool(tool_name));
match action {
NagAction::None => {}
NagAction::Warn => emit_capture_lag(
&agent_id,
session_id,
watcher.streak_for(&agent_id, session_id),
watcher.primary_threshold(),
false,
),
NagAction::WarnAndEscalate => emit_capture_lag(
&agent_id,
session_id,
watcher.streak_for(&agent_id, session_id),
watcher.escalation_threshold(),
true,
),
}
action
}
fn emit_capture_lag(
agent_id: &str,
session_id: &str,
streak: u32,
threshold: u32,
escalated: bool,
) {
let tier = if escalated { "escalation" } else { "warn" };
eprintln!(
"ai-memory capture_lag [{tier}]: agent={agent_id} session={session_id} — \
{streak} consecutive non-capture tool calls (threshold {threshold}); \
call memory_store or memory_capture_turn to record progress"
);
if !crate::audit::is_enabled() {
return;
}
let title = format!(
"capture_lag: {streak} non-capture tool calls (threshold {threshold}{})",
if escalated { ", escalation" } else { "" }
);
let mut builder = crate::audit::EventBuilder::new(
crate::audit::AuditAction::CaptureLag,
crate::audit::actor(
agent_id.to_string(),
crate::audit::synthesis_sources::MCP_CLIENT_INFO,
None,
),
crate::audit::AuditTarget {
memory_id: "*".to_string(),
namespace: crate::DEFAULT_NAMESPACE.to_string(),
title: Some(title),
tier: None,
scope: None,
},
);
builder.session_id = Some(session_id.to_string());
crate::audit::emit(builder);
}
pub fn prompt_definitions() -> Value {
json!({
"prompts": [
{
"name": "recall-first",
(field_names::DESCRIPTION): "System prompt for AI clients: proactive memory recall, TOON format, tier strategy.",
"arguments": [
{
"name": "namespace",
(field_names::DESCRIPTION): "Optional namespace to scope recall.",
"required": false
}
]
},
{
"name": "memory-workflow",
(field_names::DESCRIPTION): "Quick reference card for memory tool usage patterns."
}
]
})
}
fn prompt_content(name: &str, params: &Value) -> Result<Value, String> {
match name {
"recall-first" => {
let ns_hint = params
.get("arguments")
.and_then(|a| a.get("namespace"))
.and_then(|v| v.as_str())
.map(|ns| format!(" Scope recall to namespace \"{ns}\" when relevant."))
.unwrap_or_default();
Ok(json!({
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": format!(
"You have access to a persistent memory system (ai-memory). Follow these rules:\n\
1. RECALL FIRST: At conversation start, call memory_recall with the user's apparent topic. Before answering any question about prior work, recall first.\n\
2. STORE LEARNINGS: When the user corrects you or teaches something, call memory_store with tier:long, priority:9.\n\
3. TOON FORMAT: All recall/list/search responses default to TOON compact (79% smaller than JSON). Pass format:\"json\" only if you need structured parsing.\n\
4. TIERS: short=6h ephemeral, mid=7d working knowledge, long=permanent. Mid auto-promotes to long at 5 accesses.\n\
5. DEDUP: Storing with an existing title+namespace updates the existing memory, not a duplicate.\n\
6. NAMESPACES: Organize by project/topic. Always pass namespace when storing and recalling.\n\
7. CAPABILITIES: Call memory_capabilities once per session to discover available features (tier-dependent).\n\
8. TAGS: Use tags for cross-cutting concerns. memory_auto_tag can generate them if available.{ns_hint}")
}
}]
}))
}
"memory-workflow" => Ok(json!({
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": "\
STORE: memory_store(title, content, tier, namespace, tags, priority) — dedup by title+ns\n\
RECALL: memory_recall(context, namespace) → ranked results (TOON compact default)\n\
SEARCH: memory_search(query, namespace) → exact AND match (TOON compact default)\n\
LIST: memory_list(namespace, tier) → browse with filters (TOON compact default)\n\
GET: memory_get(id) → single memory with links\n\
PROMOTE: memory_promote(id) — mid→long, clears expiry\n\
CONSOLIDATE: memory_consolidate(ids, title) — merge N→1, LLM summary if available\n\
LINK: memory_link(source_id, target_id, relation) — related_to|supersedes|contradicts|derived_from|reflects_on\n\
TAG: memory_auto_tag(id) — LLM generates tags (smart+ tier)\n\
EXPAND: memory_expand_query(query) — LLM broadens search terms (smart+ tier)\n\
CONTRADICT: memory_detect_contradiction(id_a, id_b) — LLM checks conflict (smart+ tier)"
}
}]
})),
_ => Err(format!("unknown prompt: {name}")),
}
}
#[path = "tools/agent.rs"]
mod agent;
#[path = "tools/archive.rs"]
mod archive;
#[path = "tools/auto_tag.rs"]
mod auto_tag;
#[path = "tools/capabilities.rs"]
mod capabilities;
#[path = "tools/capture_turn.rs"]
mod capture_turn;
#[path = "tools/check_duplicate.rs"]
mod check_duplicate;
#[path = "tools/consolidate.rs"]
mod consolidate;
#[path = "tools/atomise.rs"]
mod atomise;
#[path = "tools/delete.rs"]
mod delete;
#[path = "tools/detect_contradiction.rs"]
mod detect_contradiction;
#[path = "tools/entity_get_by_alias.rs"]
mod entity_get_by_alias;
#[path = "tools/entity_register.rs"]
mod entity_register;
#[path = "tools/expand_query.rs"]
mod expand_query;
#[path = "tools/find_paths.rs"]
mod find_paths;
#[path = "tools/forget.rs"]
mod forget;
#[path = "tools/get.rs"]
mod get;
#[path = "tools/get_taxonomy.rs"]
mod get_taxonomy;
#[path = "tools/ingest_multistep.rs"]
mod ingest_multistep;
#[path = "tools/kg_invalidate.rs"]
mod kg_invalidate;
#[path = "tools/kg_query.rs"]
mod kg_query;
#[path = "tools/kg_timeline.rs"]
mod kg_timeline;
#[path = "tools/link.rs"]
mod link;
#[path = "tools/list.rs"]
mod list;
#[path = "tools/load_family.rs"]
mod load_family;
#[path = "tools/namespace.rs"]
mod namespace;
#[path = "tools/notify.rs"]
mod notify;
#[path = "tools/offload.rs"]
mod offload;
#[path = "tools/pending.rs"]
mod pending;
#[path = "tools/promote.rs"]
mod promote;
#[path = "tools/quota_status.rs"]
mod quota_status;
#[path = "tools/check_agent_action.rs"]
mod check_agent_action;
#[path = "tools/recall.rs"]
mod recall;
#[path = "tools/recall_observations.rs"]
mod recall_observations;
#[path = "tools/reflect.rs"]
mod reflect;
#[path = "tools/reflection_origin.rs"]
mod reflection_origin;
#[path = "tools/export_reflection.rs"]
mod export_reflection;
#[path = "tools/persona.rs"]
mod persona;
#[path = "tools/calibrate_confidence.rs"]
mod calibrate_confidence;
#[path = "tools/dependents_of_invalidated.rs"]
mod dependents_of_invalidated;
#[path = "tools/replay.rs"]
mod replay;
#[path = "tools/rule_list.rs"]
mod rule_list;
#[path = "tools/search.rs"]
mod search;
#[path = "tools/session_start.rs"]
mod session_start;
#[path = "tools/share.rs"]
pub mod share;
#[path = "tools/store/mod.rs"]
mod store;
#[path = "tools/subscribe.rs"]
mod subscribe;
#[path = "tools/update.rs"]
mod update;
#[path = "tools/verify.rs"]
mod verify;
#[path = "tools/skill_export.rs"]
mod skill_export;
#[path = "tools/skill_get.rs"]
mod skill_get;
#[path = "tools/skill_list.rs"]
mod skill_list;
#[path = "tools/skill_register.rs"]
mod skill_register;
#[path = "tools/skill_resource.rs"]
mod skill_resource;
#[path = "tools/skill_promote.rs"]
mod skill_promote;
#[path = "tools/skill_compositional_context.rs"]
mod skill_compositional_context;
#[cfg(test)]
#[path = "tools/d1_4_985_helpers.rs"]
pub(crate) mod d1_4_985_helpers;
pub use capabilities::{
CapabilitiesAccept, build_agent_permitted_families, build_capabilities_describe_to_user,
build_capabilities_summary, build_capabilities_tools, effective_tier_label,
format_rule_summary, handle_capabilities_with_conn, handle_capabilities_with_conn_v3,
overlay_tool_payloads,
};
pub use find_paths::handle_find_paths;
pub use check_duplicate::handle_check_duplicate;
pub use expand_query::handle_expand_query;
pub use kg_query::handle_kg_query;
pub use load_family::{handle_load_family, handle_smart_load};
pub(crate) use namespace::handle_namespace_clear_standard;
pub use namespace::{handle_namespace_get_standard, handle_namespace_set_standard};
pub use notify::{handle_inbox, handle_notify};
pub use pending::{handle_pending_approve, handle_pending_reject};
pub use capture_turn::{MemoryCaptureTurnRequest, handle_capture_turn};
pub(crate) use capture_turn::prepare_capture_turn;
pub use quota_status::handle_quota_status;
pub use check_agent_action::handle_check_agent_action;
pub use recall::handle_recall;
pub use recall::handle_recall_caller;
pub use recall::handle_recall_with_pre_recall_hook;
pub use recall::decorate_memory_many;
pub use recall_observations::handle_recall_observations;
#[cfg(feature = "sal")]
pub(crate) use recall_observations::{
DEFAULT_LIMIT as RECALL_OBS_DEFAULT_LIMIT, MAX_LIMIT as RECALL_OBS_MAX_LIMIT,
};
pub use replay::handle_replay;
pub use rule_list::handle_rule_list;
pub(crate) use session_start::handle_session_start;
pub use subscribe::handle_unsubscribe;
pub use entity_get_by_alias::handle_entity_get_by_alias;
pub use entity_register::handle_entity_register;
pub use ingest_multistep::{IngestMultistepHandler, handle_ingest_multistep};
pub use kg_invalidate::handle_kg_invalidate;
pub use kg_timeline::handle_kg_timeline;
pub use subscribe::{handle_list_subscriptions, handle_subscribe};
pub use verify::handle_verify;
pub use skill_compositional_context::handle_skill_compositional_context;
pub use skill_export::handle_skill_export;
pub use skill_get::handle_skill_get;
pub use skill_list::handle_skill_list;
pub use skill_promote::handle_skill_promote_from_reflection;
pub use skill_register::handle_skill_register;
pub use skill_resource::handle_skill_resource;
pub use calibrate_confidence::handle_calibrate_confidence;
pub use dependents_of_invalidated::handle_dependents_of_invalidated;
pub use export_reflection::handle_export_reflection;
pub use pending::handle_subscription_dlq_list;
pub use reflect::handle_reflect;
#[cfg(feature = "sal")]
pub(crate) use reflect::{map_reflect_error_to_wire_string, parse_reflect_input};
pub use reflection_origin::handle_reflection_origin;
pub use subscribe::handle_subscription_replay;
#[doc(hidden)]
pub fn handle_archive_purge_for_test(
conn: &rusqlite::Connection,
params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
archive::handle_archive_purge(conn, params)
}
#[doc(hidden)]
pub fn dispatch_handle_link_for_test(
conn: &rusqlite::Connection,
db_path: &std::path::Path,
params: &serde_json::Value,
active_keypair: Option<&crate::identity::keypair::AgentKeypair>,
) -> Result<serde_json::Value, String> {
link::handle_link(conn, db_path, params, active_keypair)
}
#[doc(hidden)]
pub fn dispatch_handle_dependents_for_test(
conn: &rusqlite::Connection,
params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
dependents_of_invalidated::handle_dependents_of_invalidated(conn, params)
}
#[must_use]
pub fn tools_check_agent_action_mutation_disabled_error() -> &'static str {
check_agent_action::MCP_MUTATION_DISABLED_ERROR
}
#[doc(hidden)]
pub mod schema_handler_parity_test_exports {
pub use super::capabilities::CapabilitiesRequest;
pub use super::link::LinkRequest;
pub use super::pending::PendingApproveRequest;
pub use super::store::StoreRequest;
pub use crate::models::recall_request::RecallRequest;
}
pub mod tools {
pub use super::atomise::{AtomiseToolHandler, handle_atomise};
pub use super::store::OnConflictMode;
pub mod capabilities {
pub use super::super::capabilities::tool_examples;
}
pub mod skill_register {
pub use super::super::skill_register::{SkillRegisterRequest, handle_skill_register};
}
pub use super::ingest_multistep::{IngestMultistepHandler, handle_ingest_multistep};
pub mod check_agent_action {
pub use super::super::check_agent_action::{
DEFAULT_AGENT_ID, build_action, handle_check_agent_action, run_check,
};
}
pub mod kg_invalidate {
pub use super::super::kg_invalidate::handle_kg_invalidate;
}
#[doc(hidden)]
pub fn handle_promote_for_tests(
conn: &rusqlite::Connection,
db_path: &std::path::Path,
params: &serde_json::Value,
mcp_client: Option<&str>,
) -> Result<serde_json::Value, String> {
super::promote::handle_promote(conn, db_path, params, mcp_client)
}
#[doc(hidden)]
#[allow(clippy::too_many_arguments)]
pub fn handle_store_for_tests(
conn: &rusqlite::Connection,
db_path: &std::path::Path,
params: &serde_json::Value,
embedder: Option<&dyn crate::embeddings::Embed>,
llm: Option<&crate::llm::OllamaClient>,
vector_index: Option<&crate::hnsw::VectorIndex>,
resolved_ttl: &crate::config::ResolvedTtl,
autonomous_hooks: bool,
mcp_client: Option<&str>,
federation_forward_url: Option<&str>,
) -> Result<serde_json::Value, String> {
super::store::handle_store(
conn,
db_path,
params,
embedder,
llm,
vector_index,
resolved_ttl,
autonomous_hooks,
mcp_client,
federation_forward_url,
None,
)
}
}
use agent::{handle_agent_list, handle_agent_register};
use archive::{
handle_archive_list, handle_archive_purge, handle_archive_restore, handle_archive_stats,
handle_gc,
};
use auto_tag::handle_auto_tag;
use consolidate::handle_consolidate;
use atomise::handle_atomise;
use delete::handle_delete;
use detect_contradiction::handle_detect_contradiction;
use forget::{handle_forget, handle_stats};
use get::handle_get;
use get_taxonomy::handle_get_taxonomy;
use link::{handle_get_links, handle_link};
use list::handle_list;
use pending::handle_pending_list;
use persona::{handle_persona, handle_persona_generate};
pub use persona::handle_persona_generate as persona_generate_call;
use promote::handle_promote;
use search::handle_search;
#[doc(hidden)]
pub fn skill_compositional_context_for_tests(
conn: &rusqlite::Connection,
params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
handle_skill_compositional_context(conn, params)
}
use store::handle_store;
use update::handle_update;
#[cfg(test)]
use agent::messages_namespace_for;
#[cfg(test)]
use namespace::{auto_register_path_hierarchy, extract_governance};
#[cfg(test)]
use replay::REPLAY_VERBOSE_THRESHOLD_BYTES;
fn build_namespace_chain(conn: &rusqlite::Connection, namespace: &str) -> Vec<String> {
db::build_namespace_chain(conn, namespace)
}
fn inject_namespace_standard(
conn: &rusqlite::Connection,
namespace: Option<&str>,
response: &mut Value,
) {
let mut standards: Vec<Value> = Vec::new();
let mut standard_ids: Vec<String> = Vec::new();
let add_standard = |std: Value, ids: &mut Vec<String>, stds: &mut Vec<Value>| {
let id = std["id"].as_str().unwrap_or_default().to_string();
if !ids.contains(&id) {
ids.push(id);
stds.push(std);
}
};
let chain = if let Some(ns) = namespace {
build_namespace_chain(conn, ns)
} else {
vec!["*".to_string()]
};
for link in chain {
if let Some(std) = lookup_namespace_standard(conn, &link) {
add_standard(std, &mut standard_ids, &mut standards);
}
}
if standards.is_empty() {
return;
}
if let Some(memories) = response["memories"].as_array_mut() {
memories.retain(|m| {
let mid = m["id"].as_str().unwrap_or_default();
!standard_ids.iter().any(|sid| sid == mid)
});
response["count"] = json!(memories.len());
}
if standards.len() == 1 {
response["standard"] = standards.into_iter().next().unwrap();
} else {
response["standards"] = json!(standards);
}
}
#[allow(clippy::too_many_arguments)]
fn lookup_namespace_standard(conn: &rusqlite::Connection, namespace: &str) -> Option<Value> {
let standard_id = db::get_namespace_standard(conn, namespace).ok()??;
let mem = db::get(conn, &standard_id).ok()??;
serde_json::to_value(&mem).ok()
}
pub(crate) struct ToolDispatchCtx<'a> {
pub conn: &'a rusqlite::Connection,
pub db_path: &'a Path,
pub arguments: &'a Value,
pub embedder: Option<&'a dyn Embed>,
pub llm: Option<&'a OllamaClient>,
pub reranker: Option<&'a BatchedReranker>,
pub tier_config: &'a TierConfig,
pub resolved_models: &'a ResolvedModels,
pub vector_index: Option<&'a VectorIndex>,
pub resolved_ttl: &'a crate::config::ResolvedTtl,
pub resolved_scoring: &'a crate::config::ResolvedScoring,
pub archive_on_gc: bool,
pub autonomous_hooks: bool,
pub mcp_client: Option<&'a str>,
pub profile: &'a crate::profile::Profile,
pub mcp_config: Option<&'a crate::config::McpConfig>,
pub active_keypair: Option<&'a crate::identity::keypair::AgentKeypair>,
pub harness: Option<&'a crate::harness::Harness>,
pub federation_forward_url: Option<&'a str>,
pub recall_scope: Option<&'a crate::config::RecallScope>,
pub atomise_handler: Option<&'a atomise::AtomiseToolHandler>,
pub ingest_multistep_handler: Option<&'a ingest_multistep::IngestMultistepHandler>,
}
pub(crate) type DispatchFn = fn(&ToolDispatchCtx<'_>) -> Result<Value, String>;
macro_rules! register_mcp_tool {
($name:expr, $f:path) => {
($name, $f as DispatchFn)
};
}
fn dispatch_memory_store(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_store(
ctx.conn,
ctx.db_path,
ctx.arguments,
ctx.embedder,
ctx.llm,
ctx.vector_index,
ctx.resolved_ttl,
ctx.autonomous_hooks,
ctx.mcp_client,
ctx.federation_forward_url,
ctx.active_keypair,
)
}
fn dispatch_memory_recall(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_recall_caller(
ctx.conn,
ctx.arguments,
ctx.embedder,
ctx.vector_index,
ctx.reranker,
ctx.archive_on_gc,
ctx.resolved_ttl,
ctx.resolved_scoring,
ctx.recall_scope,
caller.as_deref(),
)
}
fn dispatch_memory_recall_observations(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_recall_observations(ctx.conn, ctx.arguments)
}
fn dispatch_memory_search(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_search(ctx.conn, ctx.arguments, caller.as_deref())
}
fn dispatch_memory_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_list(ctx.conn, ctx.arguments, caller.as_deref())
}
fn dispatch_memory_load_family(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_load_family(ctx.conn, ctx.arguments, caller.as_deref())
}
fn dispatch_memory_smart_load(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_smart_load(ctx.conn, ctx.arguments, ctx.embedder, caller.as_deref())
}
fn dispatch_memory_get_taxonomy(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_get_taxonomy(ctx.conn, ctx.arguments)
}
fn dispatch_memory_check_duplicate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_check_duplicate(ctx.conn, ctx.arguments, ctx.embedder)
}
fn dispatch_memory_entity_register(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_entity_register(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_entity_get_by_alias(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_entity_get_by_alias(ctx.conn, ctx.arguments)
}
fn dispatch_memory_kg_timeline(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_kg_timeline(ctx.conn, ctx.arguments)
}
fn dispatch_memory_kg_invalidate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_kg_invalidate(ctx.conn, ctx.db_path, ctx.arguments)
}
fn dispatch_memory_kg_query(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_kg_query(ctx.conn, ctx.arguments)
}
fn dispatch_memory_find_paths(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_find_paths(ctx.conn, ctx.arguments)
}
fn dispatch_memory_delete(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_delete(
ctx.conn,
ctx.db_path,
ctx.arguments,
ctx.vector_index,
ctx.mcp_client,
)
}
fn dispatch_memory_promote(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_promote(ctx.conn, ctx.db_path, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_pending_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_pending_list(ctx.conn, ctx.arguments)
}
fn dispatch_memory_pending_approve(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_pending_approve(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_pending_reject(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_pending_reject(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_forget(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_forget(ctx.conn, ctx.arguments, ctx.archive_on_gc)
}
fn dispatch_memory_stats(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_stats(ctx.conn, ctx.db_path)
}
fn dispatch_memory_update(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_update(
ctx.conn,
ctx.arguments,
ctx.embedder,
ctx.vector_index,
ctx.mcp_client,
)
}
fn dispatch_memory_get(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_get(ctx.conn, ctx.arguments, caller.as_deref())
}
fn dispatch_memory_link(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_link(ctx.conn, ctx.db_path, ctx.arguments, ctx.active_keypair)
}
fn dispatch_memory_get_links(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_get_links(ctx.conn, ctx.arguments, caller.as_deref())
}
fn dispatch_memory_verify(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_verify(ctx.conn, ctx.arguments)
}
fn dispatch_memory_replay(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_replay(ctx.conn, ctx.arguments, ctx.mcp_client, caller.as_deref())
}
fn dispatch_memory_consolidate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_consolidate(
ctx.conn,
ctx.db_path,
ctx.arguments,
ctx.llm,
ctx.embedder,
ctx.vector_index,
ctx.mcp_client,
)
}
fn dispatch_memory_atomise(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_atomise(
ctx.conn,
ctx.arguments,
ctx.atomise_handler,
ctx.tier_config.tier,
ctx.mcp_client,
)
}
fn dispatch_memory_ingest_multistep(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_ingest_multistep(
ctx.arguments,
ctx.ingest_multistep_handler,
ctx.tier_config.tier,
)
}
fn dispatch_memory_reflect(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_reflect(
ctx.conn,
ctx.db_path,
ctx.arguments,
ctx.embedder,
ctx.vector_index,
ctx.mcp_client,
ctx.active_keypair,
)
}
fn dispatch_memory_capabilities(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let arguments = ctx.arguments;
if let Some(fam_name) = arguments.get("family").and_then(Value::as_str) {
let include_schema = arguments
.get("include_schema")
.and_then(Value::as_bool)
.unwrap_or(false);
let verbose = arguments
.get("verbose")
.and_then(Value::as_bool)
.unwrap_or(false);
let aid = arguments
.get("agent_id")
.and_then(Value::as_str)
.or(ctx.mcp_client);
return handle_capabilities_family(
fam_name,
include_schema,
verbose,
ctx.profile,
ctx.mcp_config,
aid,
Some(ctx.conn),
);
}
let accept = arguments
.get("accept")
.and_then(Value::as_str)
.map_or(CapabilitiesAccept::V3, CapabilitiesAccept::parse);
let top_verbose = arguments
.get("verbose")
.and_then(Value::as_bool)
.unwrap_or(false);
let top_include_schema = arguments
.get("include_schema")
.and_then(Value::as_bool)
.unwrap_or(false);
let v3_aid = arguments
.get("agent_id")
.and_then(Value::as_str)
.or(ctx.mcp_client);
let runtime_tier = effective_tier_label(
ctx.llm.is_some(),
ctx.embedder.is_some(),
ctx.reranker.is_some(),
);
let embedder_live = ctx.embedder.is_some_and(|e| !e.is_degraded());
let result = match accept {
CapabilitiesAccept::V3 => handle_capabilities_with_conn_v3(
ctx.tier_config,
ctx.resolved_models,
ctx.reranker,
embedder_live,
Some(ctx.conn),
ctx.profile,
ctx.mcp_config,
v3_aid,
ctx.harness,
),
_ => handle_capabilities_with_conn(
ctx.tier_config,
ctx.resolved_models,
ctx.reranker,
embedder_live,
Some(ctx.conn),
accept,
),
};
let profile = ctx.profile;
result.map(|mut value| {
if matches!(accept, CapabilitiesAccept::V2 | CapabilitiesAccept::V3) {
if let Some(obj) = value.as_object_mut() {
obj.insert("families".to_string(), families_overview(profile));
}
}
if matches!(accept, CapabilitiesAccept::V1)
&& let Some(obj) = value.as_object_mut()
&& !obj.contains_key(field_names::SCHEMA_VERSION)
{
obj.insert(
field_names::SCHEMA_VERSION.to_string(),
Value::String("1".to_string()),
);
}
if let Some(obj) = value.as_object_mut() {
obj.insert("tier".to_string(), Value::String(runtime_tier.to_string()));
}
if (top_include_schema || top_verbose)
&& matches!(accept, CapabilitiesAccept::V2 | CapabilitiesAccept::V3)
&& let Some(obj) = value.as_object_mut()
{
overlay_tool_payloads(obj, profile, top_include_schema, top_verbose);
}
value
})
}
fn dispatch_memory_expand_query(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_expand_query(ctx.llm, ctx.arguments)
}
fn dispatch_memory_auto_tag(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_auto_tag(ctx.conn, ctx.llm, ctx.arguments)
}
fn dispatch_memory_detect_contradiction(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_detect_contradiction(ctx.conn, ctx.llm, ctx.arguments)
}
fn dispatch_memory_archive_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_archive_list(ctx.conn, ctx.arguments)
}
fn dispatch_memory_archive_restore(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_archive_restore(ctx.conn, ctx.arguments)
}
fn dispatch_memory_archive_purge(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_archive_purge(ctx.conn, ctx.arguments)
}
fn dispatch_memory_archive_stats(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_archive_stats(ctx.conn)
}
fn dispatch_memory_gc(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_gc(ctx.conn, ctx.arguments, ctx.archive_on_gc)
}
fn dispatch_memory_session_start(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_session_start(ctx.conn, ctx.arguments, ctx.llm, caller.as_deref())
}
fn dispatch_memory_namespace_set_standard(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_namespace_set_standard(ctx.conn, ctx.arguments)
}
fn dispatch_memory_namespace_get_standard(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_namespace_get_standard(ctx.conn, ctx.arguments)
}
fn dispatch_memory_namespace_clear_standard(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_namespace_clear_standard(ctx.conn, ctx.arguments)
}
fn dispatch_memory_agent_register(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_agent_register(ctx.conn, ctx.arguments)
}
fn dispatch_memory_agent_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_agent_list(ctx.conn)
}
fn dispatch_memory_notify(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_notify(ctx.conn, ctx.arguments, ctx.resolved_ttl, ctx.mcp_client)
}
fn dispatch_memory_share(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
crate::mcp::share::handle_share(ctx.conn, ctx.arguments)
}
fn dispatch_memory_inbox(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let caller = crate::identity::resolve_read_visibility_caller();
handle_inbox(ctx.conn, ctx.arguments, ctx.mcp_client, caller.as_deref())
}
fn dispatch_memory_subscribe(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_subscribe(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_unsubscribe(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_unsubscribe(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_list_subscriptions(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_list_subscriptions(ctx.conn, ctx.mcp_client)
}
fn dispatch_memory_subscription_replay(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_subscription_replay(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_subscription_dlq_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_subscription_dlq_list(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_quota_status(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_quota_status(ctx.conn, ctx.arguments)
}
fn dispatch_memory_capture_turn(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_capture_turn(ctx.conn, ctx.arguments, ctx.mcp_client)
}
fn dispatch_memory_check_agent_action(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_check_agent_action(ctx.conn, ctx.arguments)
}
fn dispatch_memory_rule_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_rule_list(ctx.conn, ctx.arguments)
}
fn dispatch_memory_reflection_origin(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_reflection_origin(ctx.conn, ctx.arguments)
}
fn dispatch_memory_export_reflection(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_export_reflection(ctx.conn, ctx.arguments)
}
fn dispatch_memory_persona(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_persona(ctx.conn, ctx.arguments)
}
fn dispatch_memory_persona_generate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_persona_generate(
ctx.conn,
ctx.arguments,
ctx.llm.map(|c| c as &dyn crate::autonomy::AutonomyLlm),
ctx.tier_config.tier,
ctx.active_keypair,
)
}
fn dispatch_memory_calibrate_confidence(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_calibrate_confidence(ctx.conn, ctx.arguments)
}
fn dispatch_memory_dependents_of_invalidated(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_dependents_of_invalidated(ctx.conn, ctx.arguments)
}
fn dispatch_memory_skill_register(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_skill_register(ctx.conn, ctx.arguments, ctx.active_keypair)
}
fn dispatch_memory_skill_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_skill_list(ctx.conn, ctx.arguments)
}
fn dispatch_memory_skill_get(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_skill_get(ctx.conn, ctx.arguments)
}
fn dispatch_memory_skill_resource(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_skill_resource(ctx.conn, ctx.arguments)
}
fn dispatch_memory_skill_export(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_skill_export(ctx.conn, ctx.arguments, ctx.active_keypair)
}
fn dispatch_memory_skill_promote_from_reflection(
ctx: &ToolDispatchCtx<'_>,
) -> Result<Value, String> {
handle_skill_promote_from_reflection(ctx.conn, ctx.arguments, ctx.active_keypair)
}
fn dispatch_memory_skill_compositional_context(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
handle_skill_compositional_context(ctx.conn, ctx.arguments)
}
fn dispatch_memory_offload(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let explicit_agent_id = ctx
.arguments
.get("agent_id")
.and_then(Value::as_str)
.or_else(|| {
ctx.arguments
.get("metadata")
.and_then(|m| m.get("agent_id"))
.and_then(Value::as_str)
});
match crate::identity::resolve_agent_id(explicit_agent_id, ctx.mcp_client) {
Ok(agent_id) => offload::handle_offload(ctx.conn, ctx.arguments, &agent_id),
Err(e) => Err(e.to_string()),
}
}
fn dispatch_memory_deref(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
let explicit_agent_id = ctx
.arguments
.get("agent_id")
.and_then(Value::as_str)
.or_else(|| {
ctx.arguments
.get("metadata")
.and_then(|m| m.get("agent_id"))
.and_then(Value::as_str)
});
match crate::identity::resolve_agent_id(explicit_agent_id, ctx.mcp_client) {
Ok(agent_id) => offload::handle_deref(ctx.conn, ctx.arguments, &agent_id),
Err(e) => Err(e.to_string()),
}
}
pub(crate) static TOOL_DISPATCH_TABLE: &[(&str, DispatchFn)] = {
use crate::mcp::registry::tool_names;
&[
register_mcp_tool!(tool_names::MEMORY_STORE, dispatch_memory_store),
register_mcp_tool!(tool_names::MEMORY_RECALL, dispatch_memory_recall),
register_mcp_tool!(
tool_names::MEMORY_RECALL_OBSERVATIONS,
dispatch_memory_recall_observations
),
register_mcp_tool!(tool_names::MEMORY_SEARCH, dispatch_memory_search),
register_mcp_tool!(tool_names::MEMORY_LIST, dispatch_memory_list),
register_mcp_tool!(tool_names::MEMORY_LOAD_FAMILY, dispatch_memory_load_family),
register_mcp_tool!(tool_names::MEMORY_SMART_LOAD, dispatch_memory_smart_load),
register_mcp_tool!(
tool_names::MEMORY_GET_TAXONOMY,
dispatch_memory_get_taxonomy
),
register_mcp_tool!(
tool_names::MEMORY_CHECK_DUPLICATE,
dispatch_memory_check_duplicate
),
register_mcp_tool!(
tool_names::MEMORY_ENTITY_REGISTER,
dispatch_memory_entity_register
),
register_mcp_tool!(
tool_names::MEMORY_ENTITY_GET_BY_ALIAS,
dispatch_memory_entity_get_by_alias
),
register_mcp_tool!(tool_names::MEMORY_KG_TIMELINE, dispatch_memory_kg_timeline),
register_mcp_tool!(
tool_names::MEMORY_KG_INVALIDATE,
dispatch_memory_kg_invalidate
),
register_mcp_tool!(tool_names::MEMORY_KG_QUERY, dispatch_memory_kg_query),
register_mcp_tool!(tool_names::MEMORY_FIND_PATHS, dispatch_memory_find_paths),
register_mcp_tool!(tool_names::MEMORY_DELETE, dispatch_memory_delete),
register_mcp_tool!(tool_names::MEMORY_PROMOTE, dispatch_memory_promote),
register_mcp_tool!(
tool_names::MEMORY_PENDING_LIST,
dispatch_memory_pending_list
),
register_mcp_tool!(
tool_names::MEMORY_PENDING_APPROVE,
dispatch_memory_pending_approve
),
register_mcp_tool!(
tool_names::MEMORY_PENDING_REJECT,
dispatch_memory_pending_reject
),
register_mcp_tool!(tool_names::MEMORY_FORGET, dispatch_memory_forget),
register_mcp_tool!(tool_names::MEMORY_STATS, dispatch_memory_stats),
register_mcp_tool!(tool_names::MEMORY_UPDATE, dispatch_memory_update),
register_mcp_tool!(tool_names::MEMORY_GET, dispatch_memory_get),
register_mcp_tool!(tool_names::MEMORY_LINK, dispatch_memory_link),
register_mcp_tool!(tool_names::MEMORY_GET_LINKS, dispatch_memory_get_links),
register_mcp_tool!(tool_names::MEMORY_VERIFY, dispatch_memory_verify),
register_mcp_tool!(tool_names::MEMORY_REPLAY, dispatch_memory_replay),
register_mcp_tool!(tool_names::MEMORY_CONSOLIDATE, dispatch_memory_consolidate),
register_mcp_tool!(tool_names::MEMORY_ATOMISE, dispatch_memory_atomise),
register_mcp_tool!(
tool_names::MEMORY_INGEST_MULTISTEP,
dispatch_memory_ingest_multistep
),
register_mcp_tool!(tool_names::MEMORY_REFLECT, dispatch_memory_reflect),
register_mcp_tool!(
tool_names::MEMORY_CAPABILITIES,
dispatch_memory_capabilities
),
register_mcp_tool!(
tool_names::MEMORY_EXPAND_QUERY,
dispatch_memory_expand_query
),
register_mcp_tool!(tool_names::MEMORY_AUTO_TAG, dispatch_memory_auto_tag),
register_mcp_tool!(
tool_names::MEMORY_DETECT_CONTRADICTION,
dispatch_memory_detect_contradiction
),
register_mcp_tool!(
tool_names::MEMORY_ARCHIVE_LIST,
dispatch_memory_archive_list
),
register_mcp_tool!(
tool_names::MEMORY_ARCHIVE_RESTORE,
dispatch_memory_archive_restore
),
register_mcp_tool!(
tool_names::MEMORY_ARCHIVE_PURGE,
dispatch_memory_archive_purge
),
register_mcp_tool!(
tool_names::MEMORY_ARCHIVE_STATS,
dispatch_memory_archive_stats
),
register_mcp_tool!(tool_names::MEMORY_GC, dispatch_memory_gc),
register_mcp_tool!(
tool_names::MEMORY_SESSION_START,
dispatch_memory_session_start
),
register_mcp_tool!(
tool_names::MEMORY_NAMESPACE_SET_STANDARD,
dispatch_memory_namespace_set_standard
),
register_mcp_tool!(
tool_names::MEMORY_NAMESPACE_GET_STANDARD,
dispatch_memory_namespace_get_standard
),
register_mcp_tool!(
tool_names::MEMORY_NAMESPACE_CLEAR_STANDARD,
dispatch_memory_namespace_clear_standard
),
register_mcp_tool!(
tool_names::MEMORY_AGENT_REGISTER,
dispatch_memory_agent_register
),
register_mcp_tool!(tool_names::MEMORY_AGENT_LIST, dispatch_memory_agent_list),
register_mcp_tool!(tool_names::MEMORY_NOTIFY, dispatch_memory_notify),
register_mcp_tool!(tool_names::MEMORY_SHARE, dispatch_memory_share),
register_mcp_tool!(tool_names::MEMORY_INBOX, dispatch_memory_inbox),
register_mcp_tool!(tool_names::MEMORY_SUBSCRIBE, dispatch_memory_subscribe),
register_mcp_tool!(tool_names::MEMORY_UNSUBSCRIBE, dispatch_memory_unsubscribe),
register_mcp_tool!(
tool_names::MEMORY_LIST_SUBSCRIPTIONS,
dispatch_memory_list_subscriptions
),
register_mcp_tool!(
tool_names::MEMORY_SUBSCRIPTION_REPLAY,
dispatch_memory_subscription_replay
),
register_mcp_tool!(
tool_names::MEMORY_SUBSCRIPTION_DLQ_LIST,
dispatch_memory_subscription_dlq_list
),
register_mcp_tool!(
tool_names::MEMORY_QUOTA_STATUS,
dispatch_memory_quota_status
),
register_mcp_tool!(
tool_names::MEMORY_CAPTURE_TURN,
dispatch_memory_capture_turn
),
register_mcp_tool!(
tool_names::MEMORY_CHECK_AGENT_ACTION,
dispatch_memory_check_agent_action
),
register_mcp_tool!(tool_names::MEMORY_RULE_LIST, dispatch_memory_rule_list),
register_mcp_tool!(
tool_names::MEMORY_REFLECTION_ORIGIN,
dispatch_memory_reflection_origin
),
register_mcp_tool!(
tool_names::MEMORY_EXPORT_REFLECTION,
dispatch_memory_export_reflection
),
register_mcp_tool!(tool_names::MEMORY_PERSONA, dispatch_memory_persona),
register_mcp_tool!(
tool_names::MEMORY_PERSONA_GENERATE,
dispatch_memory_persona_generate
),
register_mcp_tool!(
tool_names::MEMORY_CALIBRATE_CONFIDENCE,
dispatch_memory_calibrate_confidence
),
register_mcp_tool!(
tool_names::MEMORY_DEPENDENTS_OF_INVALIDATED,
dispatch_memory_dependents_of_invalidated
),
register_mcp_tool!(
tool_names::MEMORY_SKILL_REGISTER,
dispatch_memory_skill_register
),
register_mcp_tool!(tool_names::MEMORY_SKILL_LIST, dispatch_memory_skill_list),
register_mcp_tool!(tool_names::MEMORY_SKILL_GET, dispatch_memory_skill_get),
register_mcp_tool!(
tool_names::MEMORY_SKILL_RESOURCE,
dispatch_memory_skill_resource
),
register_mcp_tool!(
tool_names::MEMORY_SKILL_EXPORT,
dispatch_memory_skill_export
),
register_mcp_tool!(
tool_names::MEMORY_SKILL_PROMOTE_FROM_REFLECTION,
dispatch_memory_skill_promote_from_reflection
),
register_mcp_tool!(
tool_names::MEMORY_SKILL_COMPOSITIONAL_CONTEXT,
dispatch_memory_skill_compositional_context
),
register_mcp_tool!(tool_names::MEMORY_OFFLOAD, dispatch_memory_offload),
register_mcp_tool!(tool_names::MEMORY_DEREF, dispatch_memory_deref),
]
};
pub(crate) fn lookup_dispatch(tool_name: &str) -> Option<DispatchFn> {
static MAP: std::sync::OnceLock<std::collections::HashMap<&'static str, DispatchFn>> =
std::sync::OnceLock::new();
let map = MAP.get_or_init(|| {
let mut m = std::collections::HashMap::with_capacity(TOOL_DISPATCH_TABLE.len());
for (name, f) in TOOL_DISPATCH_TABLE {
m.insert(*name, *f);
}
m
});
map.get(tool_name).copied()
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
fn handle_request(
conn: &rusqlite::Connection,
db_path: &Path,
req: &RpcRequest,
embedder: Option<&dyn Embed>,
llm: Option<&OllamaClient>,
reranker: Option<&BatchedReranker>,
tier_config: &TierConfig,
resolved_models: &ResolvedModels,
vector_index: Option<&VectorIndex>,
resolved_ttl: &crate::config::ResolvedTtl,
resolved_scoring: &crate::config::ResolvedScoring,
archive_on_gc: bool,
autonomous_hooks: bool,
mcp_client: Option<&str>,
profile: &crate::profile::Profile,
mcp_config: Option<&crate::config::McpConfig>,
active_keypair: Option<&crate::identity::keypair::AgentKeypair>,
harness: Option<&crate::harness::Harness>,
federation_forward_url: Option<&str>,
recall_scope: Option<&crate::config::RecallScope>,
atomise_handler: Option<&atomise::AtomiseToolHandler>,
ingest_multistep_handler: Option<&ingest_multistep::IngestMultistepHandler>,
nag_watcher: Option<&crate::recover::nag::CaptureNagWatcher>,
nag_session_id: &str,
) -> RpcResponse {
let id = req.id.clone().unwrap_or(Value::Null);
if req.jsonrpc != jsonrpc::VERSION {
return err_response(
id,
jsonrpc::INVALID_REQUEST,
format!(
"invalid JSON-RPC version (must be \"{}\")",
jsonrpc::VERSION
),
);
}
match req.method.as_str() {
jsonrpc::METHOD_INITIALIZE => {
let mut server_info = json!({
"name": "ai-memory",
"version": crate::PKG_VERSION,
});
let now_rfc3339 = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
if let Ok(Some(identity_block)) =
server_identity::build_signed_identity(active_keypair, &now_rfc3339)
&& let Some(obj) = server_info.as_object_mut()
{
obj.insert("ai_memory_identity".to_string(), identity_block);
}
ok_response(
id,
json!({
"protocolVersion": jsonrpc::PROTOCOL_REVISION,
(field_names::CAPABILITIES): { "tools": {}, "prompts": {} },
"serverInfo": server_info,
}),
)
}
jsonrpc::METHOD_NOTIFICATIONS_INITIALIZED | jsonrpc::METHOD_PING => {
ok_response(id, json!({}))
}
jsonrpc::METHOD_TOOLS_LIST => ok_response(id, tool_definitions_for_profile(profile)),
jsonrpc::METHOD_PROMPTS_LIST => ok_response(id, prompt_definitions()),
jsonrpc::METHOD_PROMPTS_GET => {
let prompt_name = match req.params["name"].as_str() {
Some(name) if !name.is_empty() => name,
_ => {
return err_response(
id,
jsonrpc::INVALID_PARAMS,
"missing or empty prompt name".into(),
);
}
};
match prompt_content(prompt_name, &req.params) {
Ok(val) => ok_response(id, val),
Err(e) => err_response(id, jsonrpc::INVALID_PARAMS, e),
}
}
jsonrpc::METHOD_TOOLS_CALL => {
let tool_name = match req.params["name"].as_str() {
Some(name) if !name.is_empty() => name,
_ => {
return err_response(
id,
jsonrpc::INVALID_PARAMS,
"missing or empty tool name".into(),
);
}
};
if !profile.loads(tool_name) {
let hint_enabled = mcp_config.is_some_and(|c| c.profile_hint_in_errors);
let hint = if hint_enabled {
let owning_family = crate::profile::Family::for_tool(tool_name);
match owning_family {
Some(f) => format!(
"tool '{tool_name}' is in family '{}' which is not loaded under \
the active profile. Restart with `--profile <name>` or \
`--profile core,{}` to load it, or call `memory_capabilities \
--include-schema family={}` to expand at runtime.",
f.name(),
f.name(),
f.name()
),
None => format!(
"tool '{tool_name}' is not registered in this build. Call \
`memory_capabilities` to see available tools."
),
}
} else {
tracing::debug!(
target: "ai_memory::mcp",
tool = tool_name,
owning_family = ?crate::profile::Family::for_tool(tool_name).map(crate::profile::Family::name),
"tools/call refused: tool not loaded under active profile (\
#1254 — set mcp.profile_hint_in_errors=true to surface family hint)",
);
format!("unknown tool: {tool_name}")
};
return err_response(id, jsonrpc::METHOD_NOT_FOUND, hint);
}
let span = tracing::info_span!(
"mcp_tool_call",
tool = tool_name,
rpc_id = ?id,
);
let _enter = span.enter();
let started = Instant::now();
let empty_obj = json!({});
let arguments = if req.params["arguments"].is_object() {
&req.params["arguments"]
} else {
&empty_obj
};
observe_capture_nag(
nag_watcher,
nag_session_id,
tool_name,
arguments,
mcp_client,
);
let ctx = ToolDispatchCtx {
conn,
db_path,
arguments,
embedder,
llm,
reranker,
tier_config,
resolved_models,
vector_index,
resolved_ttl,
resolved_scoring,
archive_on_gc,
autonomous_hooks,
mcp_client,
profile,
mcp_config,
active_keypair,
harness,
federation_forward_url,
recall_scope,
atomise_handler,
ingest_multistep_handler,
};
let Some(dispatch) = lookup_dispatch(tool_name) else {
return err_response(
id,
jsonrpc::METHOD_NOT_FOUND,
format!("unknown tool: {tool_name}"),
);
};
let result = dispatch(&ctx);
let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
match &result {
Ok(_) => tracing::info!(elapsed_ms, "ok"),
Err(err) => tracing::warn!(elapsed_ms, error = %err, "err"),
}
audit_emit_for_mcp_dispatch(tool_name, arguments, &result, mcp_client);
match result {
Ok(val) => {
let format_str = arguments
.get("format")
.and_then(|v| v.as_str())
.unwrap_or(crate::toon::FORMAT_TOON_COMPACT);
use crate::mcp::registry::tool_names as tn;
let text = match format_str {
"toon"
if matches!(
tool_name,
tn::MEMORY_RECALL | tn::MEMORY_LIST | tn::MEMORY_SESSION_START
) =>
{
crate::toon::memories_to_toon(&val, false)
}
crate::toon::FORMAT_TOON_COMPACT
if matches!(
tool_name,
tn::MEMORY_RECALL | tn::MEMORY_LIST | tn::MEMORY_SESSION_START
) =>
{
crate::toon::memories_to_toon(&val, true)
}
"toon" if tool_name == tn::MEMORY_SEARCH => {
crate::toon::search_to_toon(&val, false)
}
crate::toon::FORMAT_TOON_COMPACT if tool_name == tn::MEMORY_SEARCH => {
crate::toon::search_to_toon(&val, true)
}
_ => serde_json::to_string_pretty(&val).unwrap_or_default(),
};
ok_response(
id,
json!({
"content": [{
"type": "text",
"text": text
}]
}),
)
}
Err(e) => ok_response(
id,
json!({
"content": [{"type": "text", "text": e}],
"isError": true
}),
),
}
}
_ => err_response(
id,
jsonrpc::METHOD_NOT_FOUND,
format!("method not found: {}", req.method),
),
}
}
fn load_active_keypair_for_mcp() -> Option<crate::identity::keypair::AgentKeypair> {
let dir = crate::identity::keypair::default_key_dir().ok()?;
if !dir.exists() {
return None;
}
let agent_id = crate::identity::resolve_agent_id(None, None).ok();
load_active_keypair_for_mcp_in(&dir, agent_id.as_deref())
}
fn load_active_keypair_for_mcp_in(
dir: &std::path::Path,
agent_id: Option<&str>,
) -> Option<crate::identity::keypair::AgentKeypair> {
if let Some(agent_id) = agent_id {
match crate::identity::keypair::load(agent_id, dir) {
Ok(kp) if kp.can_sign() => return Some(kp),
Ok(_) => {}
Err(e) => {
let msg = format!("{e:#}");
if !(msg.contains("No such file") || msg.contains("not found")) {
eprintln!("ai-memory: keypair load failed for {agent_id}: {msg}");
}
}
}
}
match crate::identity::keypair::load(crate::identity::keypair::DAEMON_KEYPAIR_LABEL, dir) {
Ok(kp) if kp.can_sign() => Some(kp),
Ok(_) => None,
Err(e) => {
let msg = format!("{e:#}");
if !(msg.contains("No such file") || msg.contains("not found")) {
eprintln!("ai-memory: daemon keypair load failed: {msg}");
}
None
}
}
}
pub const DEFAULT_EMBED_BACKFILL_BATCH_SIZE: usize = 64;
pub fn run_embedding_backfill(
conn: &mut rusqlite::Connection,
emb: &dyn Embed,
) -> anyhow::Result<usize> {
let batch_size = std::env::var(crate::config::ENV_EMBED_BACKFILL_BATCH)
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or(DEFAULT_EMBED_BACKFILL_BATCH_SIZE);
run_embedding_backfill_with_batch_size(conn, emb, batch_size)
}
pub fn run_embedding_backfill_with_batch_size(
conn: &mut rusqlite::Connection,
emb: &dyn Embed,
batch_size: usize,
) -> anyhow::Result<usize> {
let batch_size = if batch_size == 0 {
DEFAULT_EMBED_BACKFILL_BATCH_SIZE
} else {
batch_size
};
let mut ok = 0usize;
let mut skipped = 0usize;
let mut scanned = 0usize;
let mut announced = false;
let mut cursor: Option<String> = None;
loop {
let chunk = db::get_unembedded_ids_batch_after(conn, cursor.as_deref(), batch_size)?;
if chunk.is_empty() {
break;
}
if !announced {
eprintln!("ai-memory: backfilling unembedded memories (batch_size={batch_size})...");
announced = true;
}
scanned += chunk.len();
cursor = chunk.last().map(|(id, _, _)| id.clone());
let embedded = embed_rows_with_fallback(emb, &chunk);
for (id, reason) in &embedded.skipped {
eprintln!("ai-memory: backfill skipped row {id}: {reason} (#1595)");
}
skipped += embedded.skipped.len();
if embedded.entries.is_empty() {
continue;
}
match db::set_embeddings_batch(conn, &embedded.entries) {
Ok(n) => ok += n,
Err(e) => {
eprintln!(
"ai-memory: set_embeddings_batch failed for chunk of {} rows: {e} \
— falling back to per-row writes (#1595)",
embedded.entries.len()
);
for (id, v) in &embedded.entries {
match db::set_embedding(conn, id, v) {
Ok(()) => ok += 1,
Err(e) => {
eprintln!("ai-memory: backfill skipped row {id}: {e} (#1595)");
skipped += 1;
}
}
}
}
}
}
if scanned > 0 {
eprintln!("ai-memory: backfilled {ok}/{scanned} (skipped {skipped})");
}
Ok(ok)
}
pub(crate) struct EmbeddedRows {
pub(crate) entries: Vec<(String, Vec<f32>)>,
pub(crate) skipped: Vec<(String, String)>,
}
pub(crate) fn embed_rows_with_fallback(
emb: &dyn Embed,
rows: &[(String, String, String)],
) -> EmbeddedRows {
let mut skipped: Vec<(String, String)> = Vec::new();
let mut candidates: Vec<(&str, String)> = Vec::with_capacity(rows.len());
for (id, title, content) in rows {
let text = crate::embeddings::embedding_document(title, content);
if let Some(reason) = crate::embeddings::oversize_embed_reason(text.len()) {
skipped.push((id.clone(), reason));
} else {
candidates.push((id.as_str(), text));
}
}
let text_refs: Vec<&str> = candidates.iter().map(|(_, t)| t.as_str()).collect();
let mut entries: Vec<(String, Vec<f32>)> = Vec::with_capacity(candidates.len());
let batch = if text_refs.is_empty() {
Ok(Vec::new())
} else {
emb.embed_batch(&text_refs)
};
match batch {
Ok(vectors) if vectors.len() == candidates.len() => {
entries.extend(
candidates
.iter()
.zip(vectors)
.map(|((id, _), v)| ((*id).to_string(), v)),
);
}
batch_fault => {
match &batch_fault {
Ok(vectors) => eprintln!(
"ai-memory: embed_batch returned {} vectors for {} inputs — \
falling back to per-row embeds for this chunk (#1595)",
vectors.len(),
candidates.len()
),
Err(e) => eprintln!(
"ai-memory: embed_batch failed for chunk of {} rows: {e} — \
falling back to per-row embeds (#1595)",
candidates.len()
),
}
for (id, text) in &candidates {
match emb.embed(text) {
Ok(v) => entries.push(((*id).to_string(), v)),
Err(e) => skipped.push(((*id).to_string(), format!("{e:#}"))),
}
}
}
}
EmbeddedRows { entries, skipped }
}
pub const MCP_MAX_LINE_BYTES: usize = 16 * 1024 * 1024;
pub const MCP_MAX_DRAIN_BYTES: usize = 64 * 1024 * 1024;
#[allow(clippy::too_many_lines)]
#[allow(deprecated)] pub fn run_mcp_server(
db_path: &Path,
tier: FeatureTier,
app_config: &AppConfig,
profile: &crate::profile::Profile,
) -> anyhow::Result<()> {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(crate::logging::DEFAULT_LOG_DIRECTIVE)
}),
)
.with_writer(std::io::stderr)
.try_init();
let mut conn = db::open(db_path)?;
let (mcp_governance_queue, _mcp_governance_supervisor) =
crate::governance::deferred_audit::install_deferred_audit_drainer(db_path);
let mcp_rule_cache = std::sync::Arc::new(crate::governance::rule_cache::RuleCache::new());
let mcp_hook_conn: Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>> =
match db::open(db_path) {
Ok(c) => Some(std::sync::Arc::new(std::sync::Mutex::new(c))),
Err(e) => {
eprintln!(
"ai-memory: #1583 — failed to open governance consultation connection \
({e}); the pre-write gate will fail CLOSED on every MCP write"
);
None
}
};
crate::daemon_runtime::install_governance_pre_write_hook(
db_path,
&mcp_governance_queue,
&mcp_rule_cache,
mcp_hook_conn.clone(),
);
crate::daemon_runtime::install_governance_pre_action_hook(
db_path,
&mcp_governance_queue,
&mcp_rule_cache,
mcp_hook_conn,
);
let stdin = io::stdin();
let mut stdout = io::stdout();
let mut tier_config = tier.config();
eprintln!("ai-memory: requested tier = {}", tier.as_str());
let family_names: Vec<&'static str> = profile.families().iter().map(|f| f.name()).collect();
eprintln!(
"ai-memory: profile = {} families ({}); expected tool count = {}",
profile.families().len(),
family_names.join(", "),
profile.expected_tool_count()
);
if tier_config.llm_model.is_some()
&& let Some(ref llm_override) = app_config.llm_model
{
let trimmed = llm_override.trim();
if trimmed.is_empty() {
eprintln!("ai-memory: empty llm_model override ignored, using tier default");
} else {
tier_config.llm_model = Some(trimmed.to_string());
eprintln!("ai-memory: llm_model override from config: {trimmed}");
}
}
if tier_config.embedding_model.is_some()
&& let Some(ref emb_override) = app_config.embedding_model
{
match emb_override.as_str() {
"mini_lm_l6_v2" => {
tier_config.embedding_model = Some(crate::config::EmbeddingModel::MiniLmL6V2);
eprintln!("ai-memory: embedding_model override from config: mini_lm_l6_v2 (local)");
}
"nomic_embed_v15" => {
tier_config.embedding_model = Some(crate::config::EmbeddingModel::NomicEmbedV15);
eprintln!(
"ai-memory: embedding_model override from config: nomic_embed_v15 (Ollama)"
);
}
other => {
eprintln!("ai-memory: unknown embedding_model '{other}', using tier default");
}
}
}
let resolved_llm = app_config.resolve_llm(None, None, None);
let llm: Option<Arc<OllamaClient>> = if tier_config.llm_model.is_none()
&& resolved_llm.source == crate::config::ConfigSource::CompiledDefault
{
None
} else {
match OllamaClient::build_from_resolved(&resolved_llm) {
Ok(Some(client)) => {
eprintln!(
"ai-memory: LLM ready (backend={}, model={}, source={}, key_source={})",
resolved_llm.backend,
resolved_llm.model,
resolved_llm.source.as_str(),
resolved_llm.api_key_source.as_str(),
);
if resolved_llm.backend == crate::llm::BACKEND_OLLAMA {
if let Err(e) = client.ensure_model() {
eprintln!("ai-memory: model pull failed: {e} (LLM features disabled)");
None
} else {
Some(Arc::new(client))
}
} else {
Some(Arc::new(client))
}
}
Ok(None) => {
eprintln!(
"ai-memory: LLM disabled — resolver returned no client \
(backend={}, source={})",
resolved_llm.backend,
resolved_llm.source.as_str(),
);
None
}
Err(e) => {
eprintln!(
"ai-memory: LLM init failed (backend={}, source={}): {e} \
(LLM features disabled)",
resolved_llm.backend,
resolved_llm.source.as_str(),
);
None
}
}
};
let resolved_embeddings = app_config.resolve_embeddings();
let embedder = match Embedder::from_resolved(&resolved_embeddings, tier_config.embedding_model)
{
Ok(Some(emb)) => {
eprintln!("ai-memory: embedder loaded ({})", emb.model_description());
let embed_batch_size = resolved_embeddings.backfill_batch as usize;
if let Err(e) =
run_embedding_backfill_with_batch_size(&mut conn, &emb, embed_batch_size)
{
eprintln!("ai-memory: backfill failed: {e}");
}
Some(emb)
}
Ok(None) => None,
Err(e) => {
eprintln!(
"ai-memory: embedder init failed (backend={}, model={}, url={}, \
source={}): {e:#} — semantic recall DEGRADED to keyword \
(#1143, #1593, #1598)",
resolved_embeddings.backend,
resolved_embeddings.model,
resolved_embeddings.url,
resolved_embeddings.source.as_str(),
);
None
}
};
let vector_index: Option<std::sync::Arc<VectorIndex>> = if embedder.is_some() {
let idx = std::sync::Arc::new(VectorIndex::empty());
eprintln!(
"ai-memory: HNSW index warming in background; semantic recall \
serves keyword/FTS results until the swap lands (#1579 B3)"
);
let warm_idx = std::sync::Arc::clone(&idx);
let warm_db_path = db_path.to_path_buf();
std::thread::spawn(move || {
let started = std::time::Instant::now();
let Some(entries) = crate::daemon_runtime::load_boot_index_entries(&warm_db_path)
else {
return;
};
if entries.is_empty() {
eprintln!("ai-memory: no embeddings for HNSW index, using linear scan");
return;
}
let total = entries.len();
warm_idx.warm_boot(entries);
eprintln!(
"ai-memory: HNSW index ready ({total} entries, warmed in {:.1}s)",
started.elapsed().as_secs_f64()
);
});
Some(idx)
} else {
None
};
let reranker = if tier_config.cross_encoder {
eprintln!("ai-memory: loading neural cross-encoder (ms-marco-MiniLM-L-6-v2)...");
let ce = CrossEncoder::new_neural();
if ce.is_neural() {
eprintln!("ai-memory: neural cross-encoder ready (batched)");
} else {
eprintln!("ai-memory: using lexical cross-encoder fallback");
}
Some(BatchedReranker::with_score_floor(
ce,
app_config.resolve_reranker_score_floor(),
))
} else {
None
};
let effective_tier = if llm.is_some() && embedder.is_some() && reranker.is_some() {
EFFECTIVE_TIER_AUTONOMOUS
} else if llm.is_some() && embedder.is_some() {
"smart"
} else if embedder.is_some() {
"semantic"
} else {
"keyword"
};
eprintln!("ai-memory MCP server started (stdio, tier={effective_tier})");
let active_keypair: Option<crate::identity::keypair::AgentKeypair> =
load_active_keypair_for_mcp();
if active_keypair.is_some() {
eprintln!("ai-memory: link signing enabled (Ed25519)");
}
let atomise_handler: Option<std::sync::Arc<atomise::AtomiseToolHandler>> =
if let Some(ref llm_client) = llm {
let curator: Box<dyn crate::atomisation::curator::Curator> = Box::new(
crate::atomisation::curator::LlmCurator::new(llm_client.clone()),
);
let keypair_arc = active_keypair
.as_ref()
.map(|kp| std::sync::Arc::new(kp.clone()));
let curator_model = llm_client.model_name().to_string();
let atomiser = std::sync::Arc::new(
crate::atomisation::Atomiser::new(
curator,
keypair_arc,
crate::atomisation::AtomiserConfig::default(),
tier_config.tier,
)
.with_curator_model(curator_model),
);
eprintln!("ai-memory: atomisation engine ready (curator=LlmCurator)");
Some(std::sync::Arc::new(atomise::AtomiseToolHandler::new(
atomiser,
tier_config.tier,
)))
} else {
None
};
let ingest_multistep_handler: Option<std::sync::Arc<ingest_multistep::IngestMultistepHandler>> =
if let Some(ref llm_client) = llm {
let dispatch: std::sync::Arc<dyn crate::multistep_ingest::LlmDispatch> =
std::sync::Arc::new(crate::multistep_ingest::executor::OllamaDispatch::new(
llm_client.clone(),
));
eprintln!("ai-memory: multi-step ingest orchestrator ready (Form 3)");
Some(std::sync::Arc::new(
ingest_multistep::IngestMultistepHandler::new(dispatch, tier_config.tier),
))
} else {
None
};
let resolved_models = app_config.resolve_models();
let mut mcp_client_name: Option<String> = None;
let mut detected_harness: Option<crate::harness::Harness> = None;
let nag_watcher = crate::recover::nag::CaptureNagWatcher::new_from_env();
let nag_session_id = format!("mcp-{}", uuid::Uuid::new_v4());
let mut stdin_locked = stdin.lock();
let mut line_buf: Vec<u8> = Vec::with_capacity(8192);
loop {
line_buf.clear();
let n = (&mut stdin_locked)
.take((MCP_MAX_LINE_BYTES as u64) + 1)
.read_until(b'\n', &mut line_buf)?;
if n == 0 {
break;
}
let overrun = line_buf.last() != Some(&b'\n') && n > MCP_MAX_LINE_BYTES;
if overrun {
let mut scratch = [0u8; 8192];
let mut drained: usize = 0;
loop {
if drained >= MCP_MAX_DRAIN_BYTES {
let resp = err_response(
Value::Null,
jsonrpc::PARSE_ERROR,
format!(
"parse error: line exceeded {MCP_MAX_LINE_BYTES} bytes \
and drain ceiling {MCP_MAX_DRAIN_BYTES} hit; closing stream"
),
);
let out = serde_json::to_string(&resp)?;
writeln!(stdout, "{out}")?;
stdout.flush()?;
let _ = db::checkpoint(&conn);
eprintln!("ai-memory MCP server stopped (drain ceiling exceeded)");
return Ok(());
}
let m = stdin_locked.read(&mut scratch)?;
if m == 0 {
break;
}
drained = drained.saturating_add(m);
if scratch[..m].contains(&b'\n') {
break;
}
}
let resp = err_response(
Value::Null,
jsonrpc::PARSE_ERROR,
format!("parse error: line exceeded {MCP_MAX_LINE_BYTES} bytes"),
);
let out = serde_json::to_string(&resp)?;
writeln!(stdout, "{out}")?;
stdout.flush()?;
continue;
}
if line_buf.last() == Some(&b'\n') {
line_buf.pop();
}
if line_buf.last() == Some(&b'\r') {
line_buf.pop();
}
let line = match std::str::from_utf8(&line_buf) {
Ok(s) => s,
Err(e) => {
let resp = err_response(
Value::Null,
jsonrpc::PARSE_ERROR,
format!("parse error: invalid UTF-8: {e}"),
);
let out = serde_json::to_string(&resp)?;
writeln!(stdout, "{out}")?;
stdout.flush()?;
continue;
}
};
if line.trim().is_empty() {
continue;
}
let req: RpcRequest = match serde_json::from_str(line) {
Ok(r) => r,
Err(e) => {
let resp = err_response(
Value::Null,
jsonrpc::PARSE_ERROR,
format!("parse error: {e}"),
);
let out = serde_json::to_string(&resp)?;
writeln!(stdout, "{out}")?;
stdout.flush()?;
continue;
}
};
if req.method == jsonrpc::METHOD_INITIALIZE
&& let Some(name) = req.params["clientInfo"]["name"].as_str()
&& !name.is_empty()
{
mcp_client_name = Some(name.to_string());
detected_harness = Some(crate::harness::Harness::detect(name));
}
if req.id.is_none() || req.id == Some(Value::Null) {
continue;
}
let resolved_ttl = app_config.effective_ttl();
let resolved_scoring = app_config.effective_scoring();
let archive_on_gc = app_config.effective_archive_on_gc();
let autonomous_hooks = app_config.effective_autonomous_hooks();
let resolved_recall_scope = app_config.effective_recall_scope();
let resp = handle_request(
&conn,
db_path,
&req,
embedder.as_ref().map(|e| e as &dyn Embed),
llm.as_deref(),
reranker.as_ref(),
&tier_config,
&resolved_models,
vector_index.as_deref(),
&resolved_ttl,
&resolved_scoring,
archive_on_gc,
autonomous_hooks,
mcp_client_name.as_deref(),
profile,
app_config.mcp.as_ref(),
active_keypair.as_ref(),
detected_harness.as_ref(),
app_config.mcp_federation_forward_url.as_deref(),
resolved_recall_scope,
atomise_handler.as_deref(),
ingest_multistep_handler.as_deref(),
Some(&nag_watcher),
&nag_session_id,
);
let out = serde_json::to_string(&resp)?;
writeln!(stdout, "{out}")?;
stdout.flush()?;
}
let _ = db::checkpoint(&conn);
eprintln!("ai-memory MCP server stopped");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Memory, Tier};
use serde_json::json;
#[test]
fn issue_965_audit_tool_dispatch_ctx_holds_plain_connection_ref() {
fn _type_check<'a>(ctx: &ToolDispatchCtx<'a>) -> &'a rusqlite::Connection {
ctx.conn
}
let _ = _type_check;
}
#[test]
fn issue_965_audit_handle_request_takes_plain_connection_ref() {
let _: fn(
&rusqlite::Connection,
&Path,
&RpcRequest,
Option<&dyn Embed>,
Option<&OllamaClient>,
Option<&BatchedReranker>,
&TierConfig,
&crate::config::ResolvedModels,
Option<&VectorIndex>,
&crate::config::ResolvedTtl,
&crate::config::ResolvedScoring,
bool,
bool,
Option<&str>,
&crate::profile::Profile,
Option<&crate::config::McpConfig>,
Option<&crate::identity::keypair::AgentKeypair>,
Option<&crate::harness::Harness>,
Option<&str>,
Option<&crate::config::RecallScope>,
Option<&atomise::AtomiseToolHandler>,
Option<&ingest_multistep::IngestMultistepHandler>,
Option<&crate::recover::nag::CaptureNagWatcher>,
&str,
) -> RpcResponse = handle_request;
}
#[test]
fn issue_965_audit_serial_dispatch_50_calls_through_single_connection() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
let conn = db::open(tmp.path()).expect("open db");
let tier_config = crate::config::FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
let profile = crate::profile::Profile::full();
for i in 0..50 {
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(i)),
method: "tools/call".into(),
params: json!({
"name": "memory_store",
"arguments": {
"title": format!("issue-965-stress-{i}"),
"content": format!("audit stress probe iteration {i}"),
"tier": Tier::Short.as_str(),
"namespace": "issue-965-audit",
},
}),
};
let resp = handle_request(
&conn,
tmp.path(),
&req,
None, None, None, &tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None, &resolved_ttl,
&resolved_scoring,
true, false, None, &profile,
None, None, None, None, None, None, None, None, "test-session", );
assert!(
resp.error.is_none(),
"iter {i} surfaced error: {:?}",
resp.error
);
}
let n: i64 = conn
.query_row(
"SELECT COUNT(*) FROM memories WHERE namespace = 'issue-965-audit'",
[],
|r| r.get(0),
)
.expect("count query");
assert_eq!(n, 50, "expected 50 audit-stress rows, found {n}");
}
#[test]
fn issue_811_load_active_keypair_for_mcp_picks_agent_specific_when_present() {
let dir = tempfile::TempDir::new().unwrap();
let kp = crate::identity::keypair::generate("ai:alice").unwrap();
crate::identity::keypair::save(&kp, dir.path()).unwrap();
let loaded = super::load_active_keypair_for_mcp_in(dir.path(), Some("ai:alice"))
.expect("agent-specific keypair should resolve when on disk");
assert!(
loaded.can_sign(),
"loaded agent-specific keypair must carry a private half"
);
assert_eq!(loaded.agent_id, "ai:alice");
}
#[test]
fn issue_811_load_active_keypair_for_mcp_falls_back_to_daemon_when_agent_specific_missing() {
let dir = tempfile::TempDir::new().unwrap();
let daemon_kp = crate::identity::keypair::generate("daemon").unwrap();
crate::identity::keypair::save(&daemon_kp, dir.path()).unwrap();
let loaded =
super::load_active_keypair_for_mcp_in(dir.path(), Some("host:host.local:pid-12345"))
.expect("daemon fallback must engage when agent-specific key absent");
assert!(
loaded.can_sign(),
"daemon fallback keypair must carry a private half"
);
assert_eq!(loaded.agent_id, "daemon");
}
#[test]
fn issue_811_load_active_keypair_for_mcp_returns_none_when_neither_present() {
let dir = tempfile::TempDir::new().unwrap();
let loaded = super::load_active_keypair_for_mcp_in(dir.path(), Some("ai:none"));
assert!(
loaded.is_none(),
"no key files → None (preserves v0.6.4 unsigned behaviour)"
);
}
#[test]
fn issue_811_load_active_keypair_for_mcp_falls_back_when_agent_id_unresolvable() {
let dir = tempfile::TempDir::new().unwrap();
let daemon_kp = crate::identity::keypair::generate("daemon").unwrap();
crate::identity::keypair::save(&daemon_kp, dir.path()).unwrap();
let loaded = super::load_active_keypair_for_mcp_in(dir.path(), None)
.expect("daemon fallback must engage when agent_id resolution fails");
assert_eq!(loaded.agent_id, "daemon");
}
#[test]
fn tool_definitions_returns_full_profile_count() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
assert_eq!(
tools.len(),
crate::profile::Profile::full().expected_tool_count()
);
}
#[test]
fn tool_definitions_for_profile_core_registers_core_family_plus_always_on() {
use crate::profile::{ALWAYS_ON_TOOLS, Family};
let defs = tool_definitions_for_profile(&crate::profile::Profile::core());
let tools = defs["tools"].as_array().unwrap();
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert_eq!(
tools.len(),
Family::Core.tool_names().len() + ALWAYS_ON_TOOLS.len(),
"core profile should register the Core family + always-on bootstrap; got {names:?}"
);
for required in [
"memory_store",
"memory_recall",
"memory_list",
"memory_get",
"memory_search",
"memory_load_family",
"memory_smart_load",
"memory_capabilities",
] {
assert!(
names.contains(&required),
"core profile missing {required}; got {names:?}"
);
}
for excluded in [
"memory_kg_query",
"memory_consolidate",
"memory_archive_list",
"memory_subscribe",
"memory_promote",
] {
assert!(
!names.contains(&excluded),
"core profile leaked {excluded}; got {names:?}"
);
}
}
#[test]
fn tool_definitions_for_profile_full_registers_expected_count() {
let defs = tool_definitions_for_profile(&crate::profile::Profile::full());
let tools = defs["tools"].as_array().unwrap();
assert_eq!(
tools.len(),
crate::profile::Profile::full().expected_tool_count(),
"full profile registration count must match \
`Profile::full().expected_tool_count()` — the SSOT derived \
from the per-Family `tool_names` slices; no literal is \
restated here so surface additions (e.g. #1389 L4 \
memory_capture_turn under Family::Lifecycle) flow through \
automatically"
);
}
#[test]
fn tool_definitions_for_profile_graph_registers_core_graph_plus_always_on() {
use crate::profile::{ALWAYS_ON_TOOLS, Family};
let defs = tool_definitions_for_profile(&crate::profile::Profile::graph());
let tools = defs["tools"].as_array().unwrap();
assert_eq!(
tools.len(),
Family::Core.tool_names().len()
+ Family::Graph.tool_names().len()
+ ALWAYS_ON_TOOLS.len(),
"graph profile = Core + Graph families + always-on bootstrap"
);
}
#[test]
fn tool_definitions_for_profile_custom_core_comma_graph_registers_union() {
use crate::profile::{ALWAYS_ON_TOOLS, Family};
let p = crate::profile::Profile::parse("core,graph").unwrap();
let defs = tool_definitions_for_profile(&p);
let tools = defs["tools"].as_array().unwrap();
assert_eq!(
tools.len(),
Family::Core.tool_names().len()
+ Family::Graph.tool_names().len()
+ ALWAYS_ON_TOOLS.len(),
"core,graph = Core + Graph families + always-on bootstrap"
);
}
#[test]
fn families_overview_lists_all_eight_with_correct_loaded_flags() {
let p = crate::profile::Profile::core();
let v = families_overview(&p);
let families = v["families"].as_array().unwrap();
assert_eq!(families.len(), 8, "all eight families must appear");
let core_row = families.iter().find(|r| r["name"] == "core").unwrap();
assert_eq!(core_row["loaded"], true);
assert_eq!(core_row["tool_count"], 7);
let graph_row = families.iter().find(|r| r["name"] == "graph").unwrap();
assert_eq!(graph_row["loaded"], false);
assert_eq!(graph_row["tool_count"], 11);
let always_on = v["always_on"].as_array().unwrap();
assert_eq!(always_on.len(), 1);
assert_eq!(always_on[0], "memory_capabilities");
}
#[test]
fn handle_capabilities_family_lists_tool_names() {
let p = crate::profile::Profile::core();
let v = handle_capabilities_family("graph", false, false, &p, None, None, None).unwrap();
assert_eq!(v["family"], "graph");
assert_eq!(v["loaded_under_active_profile"], false);
let tools = v["tools"].as_array().unwrap();
assert_eq!(tools.len(), 11);
assert!(tools.iter().any(|t| t == "memory_kg_query"));
assert!(tools.iter().any(|t| t == "memory_replay"));
assert!(tools.iter().any(|t| t == "memory_verify"));
assert!(tools.iter().any(|t| t == "memory_find_paths"));
}
#[test]
fn handle_capabilities_family_include_schema_returns_full_definitions() {
let p = crate::profile::Profile::core();
let v = handle_capabilities_family("graph", true, true, &p, None, None, None).unwrap();
assert_eq!(v["family"], "graph");
assert_eq!(v["verbose"], true);
let tools = v["tools"].as_array().unwrap();
assert_eq!(tools.len(), 11);
for tool in tools {
assert!(tool.get("name").is_some(), "missing name");
assert!(tool.get("description").is_some(), "missing description");
assert!(tool.get("inputSchema").is_some(), "missing inputSchema");
}
}
#[test]
fn handle_capabilities_family_verbose_preserves_docs_field() {
let p = crate::profile::Profile::core();
let v = handle_capabilities_family("core", true, true, &p, None, None, None).unwrap();
let tools = v["tools"].as_array().unwrap();
assert!(!tools.is_empty());
let with_docs = tools
.iter()
.filter(|t| t.get("docs").and_then(Value::as_str).is_some())
.count();
assert!(
with_docs >= 1,
"verbose=true must surface at least one docs string in family=core; got 0"
);
}
#[test]
fn handle_capabilities_family_unknown_returns_diagnostic_err() {
let p = crate::profile::Profile::core();
let err =
handle_capabilities_family("xyz", false, false, &p, None, None, None).unwrap_err();
assert!(err.contains("xyz"));
assert!(err.contains("Valid families"));
assert!(err.contains("core"));
assert!(err.contains("graph"));
}
#[test]
fn handle_capabilities_family_empty_name_errors() {
let p = crate::profile::Profile::core();
let err = handle_capabilities_family("", false, false, &p, None, None, None).unwrap_err();
assert!(err.contains("must not be empty"));
}
#[test]
fn tool_definitions_for_profile_preserves_optional_params_post_859() {
let p = crate::profile::Profile::full();
let defs = tool_definitions_for_profile(&p);
let store = defs["tools"]
.as_array()
.unwrap()
.iter()
.find(|t| t["name"] == "memory_store")
.expect("memory_store must be present in full profile");
let props = store["inputSchema"]["properties"].as_object().unwrap();
for kept in [
"title",
"content",
"namespace",
"confidence",
"priority",
"tier",
"metadata",
"agent_id",
"source",
"scope",
"tags",
"on_conflict",
"kind",
] {
assert!(
props.contains_key(kept),
"#859: wire schema must preserve property `{kept}` for client-side discovery"
);
}
let confidence = props
.get("confidence")
.and_then(serde_json::Value::as_object)
.expect("confidence property must be an object");
assert!(
!confidence.contains_key("description"),
"#859: per-property `description` prose must be stripped on the wire"
);
let confidence_type = confidence
.get("type")
.expect("confidence must declare a type discriminator");
let confidence_is_number = match confidence_type {
serde_json::Value::String(s) => s == "number",
serde_json::Value::Array(items) => items.iter().any(|v| v.as_str() == Some("number")),
_ => false,
};
assert!(
confidence_is_number,
"confidence.type must contain `number`; got {confidence_type:?}"
);
let _ = confidence; }
#[test]
fn tool_definitions_for_profile_verbose_keeps_every_optional() {
let p = crate::profile::Profile::full();
let defs = tool_definitions_for_profile_verbose(&p);
let store = defs["tools"]
.as_array()
.unwrap()
.iter()
.find(|t| t["name"] == "memory_store")
.expect("memory_store must be present");
let props = store["inputSchema"]["properties"].as_object().unwrap();
for kept in [
"title",
"content",
"namespace",
"confidence",
"priority",
"tier",
"metadata",
"agent_id",
"source",
"scope",
"tags",
"on_conflict",
] {
assert!(
props.contains_key(kept),
"verbose path should preserve `{kept}`"
);
}
}
#[test]
fn handle_capabilities_family_verbose_toggles_optional_params() {
let p = crate::profile::Profile::full();
let trimmed =
handle_capabilities_family("core", true, false, &p, None, None, None).unwrap();
assert_eq!(trimmed["verbose"], false);
let store_trimmed = trimmed["tools"]
.as_array()
.unwrap()
.iter()
.find(|t| t["name"] == "memory_store")
.expect("memory_store in core family");
let props_trimmed = store_trimmed["inputSchema"]["properties"]
.as_object()
.unwrap();
for kept in [
"title",
"content",
"namespace",
"confidence",
"priority",
"tier",
"metadata",
"agent_id",
"source",
"scope",
"tags",
"on_conflict",
"kind",
] {
assert!(
props_trimmed.contains_key(kept),
"wire schema (verbose=false) must preserve property `{kept}` (#859)"
);
}
let confidence_prop = props_trimmed
.get("confidence")
.and_then(serde_json::Value::as_object)
.expect("confidence property must be an object");
assert!(
!confidence_prop.contains_key("description"),
"wire schema must drop per-property `description` prose (#859)"
);
let confidence_type = confidence_prop
.get("type")
.expect("confidence must declare a type discriminator");
let confidence_is_number = match confidence_type {
serde_json::Value::String(s) => s == "number",
serde_json::Value::Array(items) => items.iter().any(|v| v.as_str() == Some("number")),
_ => false,
};
assert!(
confidence_is_number,
"confidence.type must contain `number`; got {confidence_type:?}"
);
let verbose = handle_capabilities_family("core", true, true, &p, None, None, None).unwrap();
assert_eq!(verbose["verbose"], true);
let store_verbose = verbose["tools"]
.as_array()
.unwrap()
.iter()
.find(|t| t["name"] == "memory_store")
.expect("memory_store in core family");
let props_verbose = store_verbose["inputSchema"]["properties"]
.as_object()
.unwrap();
assert!(props_verbose.contains_key("confidence"));
assert!(props_verbose.contains_key("priority"));
assert!(props_verbose.contains_key("metadata"));
assert!(props_verbose.contains_key("agent_id"));
}
#[test]
fn trim_optional_params_is_idempotent() {
let mut defs = tool_definitions();
let stripped_first = trim_optional_params(&mut defs);
assert!(
stripped_first > 0,
"first trim should strip a positive number of per-property descriptions"
);
let stripped_second = trim_optional_params(&mut defs);
assert_eq!(
stripped_second, 0,
"re-trim of an already-trimmed schema must be a no-op"
);
}
#[test]
fn c4_trim_shrinks_full_profile_payload_meaningfully() {
let p = crate::profile::Profile::full();
let trimmed = tool_definitions_for_profile(&p);
let verbose = tool_definitions_for_profile_verbose(&p);
let trimmed_bytes = serde_json::to_string(&trimmed).unwrap().len();
let verbose_bytes = serde_json::to_string(&verbose).unwrap().len();
assert!(
trimmed_bytes < verbose_bytes,
"trimmed ({trimmed_bytes}B) must be smaller than verbose ({verbose_bytes}B)"
);
let saved_pct = (verbose_bytes - trimmed_bytes) as f64 / verbose_bytes as f64 * 100.0;
assert!(
saved_pct >= 5.0,
"trim should save >=5% of full-profile bytes via top-level description \
compaction; got {saved_pct:.1}% (verbose={verbose_bytes}B, \
trimmed={trimmed_bytes}B) — `wire_compact_descriptions` may be broken"
);
}
#[test]
fn tool_definitions_include_check_duplicate() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(names.contains(&"memory_check_duplicate"));
}
#[test]
fn tool_definitions_include_entity_registry_tools() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(names.contains(&"memory_entity_register"));
assert!(names.contains(&"memory_entity_get_by_alias"));
}
#[test]
fn tool_definitions_include_kg_timeline() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(names.contains(&"memory_kg_timeline"));
}
#[test]
fn tool_definitions_include_kg_invalidate() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(names.contains(&"memory_kg_invalidate"));
}
#[test]
fn tool_definitions_include_kg_query() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(names.contains(&"memory_kg_query"));
}
#[test]
fn tool_definitions_include_agent_register_and_list() {
let defs = tool_definitions();
let names: Vec<&str> = defs["tools"]
.as_array()
.unwrap()
.iter()
.filter_map(|t| t["name"].as_str())
.collect();
assert!(names.contains(&"memory_agent_register"));
assert!(names.contains(&"memory_agent_list"));
}
#[test]
fn tool_definitions_include_notify_and_inbox() {
let defs = tool_definitions();
let names: Vec<&str> = defs["tools"]
.as_array()
.unwrap()
.iter()
.filter_map(|t| t["name"].as_str())
.collect();
assert!(names.contains(&"memory_notify"));
assert!(names.contains(&"memory_inbox"));
}
#[test]
fn messages_namespace_is_prefixed() {
assert_eq!(super::messages_namespace_for("alice"), "_messages/alice");
assert_eq!(
super::messages_namespace_for("ai:claude-opus-4.7"),
"_messages/ai:claude-opus-4.7"
);
}
#[test]
fn tool_definitions_all_have_names() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
for tool in tools {
assert!(tool["name"].as_str().unwrap().starts_with("memory_"));
}
}
#[test]
fn tool_definitions_recall_has_toon_default() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
let recall = tools.iter().find(|t| t["name"] == "memory_recall").unwrap();
let format_schema = &recall["inputSchema"]["properties"]["format"];
assert!(format_schema.is_object(), "format schema must be present");
let default = &format_schema["default"];
assert!(
default.is_null() || default.as_str() == Some("toon_compact"),
"format default must be null (post-D1.6 Option<T>) or \"toon_compact\" (legacy); \
got {default:?}"
);
}
#[test]
fn tool_definitions_recall_advertises_session_default_issue_518() {
let defs = tool_definitions();
let tools = defs["tools"].as_array().unwrap();
let recall = tools
.iter()
.find(|t| t["name"] == "memory_recall")
.expect("memory_recall tool must be defined");
let props = &recall["inputSchema"]["properties"];
let session_default = &props["session_default"];
let session_default_type = &session_default["type"];
let is_boolean = match session_default_type {
serde_json::Value::String(s) => s == "boolean",
serde_json::Value::Array(items) => items.iter().any(|v| v.as_str() == Some("boolean")),
_ => false,
};
assert!(
is_boolean,
"session_default.type must contain `boolean`; got {session_default_type:?}"
);
let default = &session_default["default"];
assert!(
default.is_null() || default.as_bool() == Some(false),
"session_default default must be null (post-D1.6) or false (legacy); got {default:?}"
);
assert!(
session_default["description"]
.as_str()
.is_some_and(|d| d.contains("agents.defaults.recall_scope")),
"session_default description must mention [agents.defaults.recall_scope] — got {session_default:?}"
);
}
#[test]
fn prompt_definitions_returns_2() {
let defs = prompt_definitions();
let prompts = defs["prompts"].as_array().unwrap();
assert_eq!(prompts.len(), 2);
assert_eq!(prompts[0]["name"], "recall-first");
assert_eq!(prompts[1]["name"], "memory-workflow");
}
#[test]
fn prompt_definitions_recall_first_has_arguments() {
let defs = prompt_definitions();
let prompts = defs["prompts"].as_array().unwrap();
let recall_first = &prompts[0];
let args = recall_first["arguments"].as_array().unwrap();
assert_eq!(args.len(), 1);
assert_eq!(args[0]["name"], "namespace");
}
#[test]
fn prompt_content_recall_first() {
let params = json!({});
let result = prompt_content("recall-first", ¶ms).unwrap();
let msgs = result["messages"].as_array().unwrap();
assert_eq!(msgs.len(), 1);
let text = msgs[0]["content"]["text"].as_str().unwrap();
assert!(text.contains("RECALL FIRST"));
assert!(text.contains("TOON"));
assert!(text.contains("memory_recall"));
assert!(text.contains("memory_store"));
assert!(text.contains("DEDUP"));
}
#[test]
fn prompt_content_recall_first_with_namespace() {
let params = json!({"arguments": {"namespace": "my-project"}});
let result = prompt_content("recall-first", ¶ms).unwrap();
let text = result["messages"][0]["content"]["text"].as_str().unwrap();
assert!(text.contains("my-project"));
}
#[test]
fn prompt_content_memory_workflow() {
let params = json!({});
let result = prompt_content("memory-workflow", ¶ms).unwrap();
let text = result["messages"][0]["content"]["text"].as_str().unwrap();
assert!(text.contains("STORE"));
assert!(text.contains("RECALL"));
assert!(text.contains("SEARCH"));
assert!(text.contains("CONSOLIDATE"));
assert!(text.contains("TOON"));
}
#[test]
fn prompt_content_unknown() {
let params = json!({});
let result = prompt_content("nonexistent", ¶ms);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unknown prompt"));
}
#[test]
fn prompt_content_role_is_user() {
let params = json!({});
let result = prompt_content("recall-first", ¶ms).unwrap();
assert_eq!(result["messages"][0]["role"], "user");
}
#[test]
fn ok_response_structure() {
let resp = ok_response(json!(1), json!({"test": true}));
assert_eq!(resp.jsonrpc, "2.0");
assert_eq!(resp.id, json!(1));
assert!(resp.result.is_some());
assert!(resp.error.is_none());
}
#[test]
fn err_response_structure() {
let resp = err_response(json!(1), -32600, "test error".to_string());
assert_eq!(resp.jsonrpc, "2.0");
assert!(resp.error.is_some());
let err = resp.error.unwrap();
assert_eq!(err.code, -32600);
assert_eq!(err.message, "test error");
}
#[derive(Clone)]
struct VecWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for VecWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for VecWriter {
type Writer = VecWriter;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
fn run_with_capture<F: FnOnce()>(f: F) -> String {
let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
let writer = VecWriter(buf.clone());
let subscriber = tracing_subscriber::fmt()
.with_writer(writer)
.with_max_level(tracing::Level::INFO)
.with_ansi(false)
.finish();
tracing::subscriber::with_default(subscriber, f);
String::from_utf8(buf.lock().unwrap().clone()).unwrap_or_default()
}
fn make_tools_call(tool: &str, args: Value) -> RpcRequest {
RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(1)),
method: "tools/call".into(),
params: json!({ "name": tool, "arguments": args }),
}
}
#[test]
fn tools_call_emits_span_with_tool_name_and_elapsed_ms() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
let req = make_tools_call("memory_list", json!({"limit": 1}));
let captured = run_with_capture(|| {
let resp = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&crate::profile::Profile::full(),
None,
None,
None,
None, None, None, None, None, "test-session", );
assert!(resp.error.is_none(), "expected ok rpc response");
});
assert!(
captured.contains("mcp_tool_call"),
"missing span name in: {captured}"
);
assert!(
captured.contains("memory_list"),
"missing tool field in: {captured}"
);
assert!(
captured.contains("elapsed_ms"),
"missing elapsed_ms field in: {captured}"
);
assert!(
captured.contains(" ok"),
"missing ok outcome event in: {captured}"
);
}
#[test]
fn tools_call_emits_warn_event_on_handler_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
let req = make_tools_call("memory_get", json!({"id": ""}));
let captured = run_with_capture(|| {
let resp = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&crate::profile::Profile::full(),
None,
None,
None,
None, None, None, None, None, "test-session", );
assert!(resp.error.is_none());
});
assert!(
captured.contains("mcp_tool_call"),
"missing span in err path: {captured}"
);
assert!(
captured.contains("memory_get"),
"missing tool field in err path: {captured}"
);
assert!(
captured.contains("WARN"),
"missing WARN level on err path: {captured}"
);
assert!(
captured.contains("err"),
"missing err outcome in: {captured}"
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn mcp_tools_smoke_matrix() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
struct ToolCase {
name: &'static str,
valid_args: Value,
required_arg: Option<&'static str>, }
let cases: &[ToolCase] = &[
ToolCase {
name: "memory_store",
valid_args: json!({"title": "test", "content": "test content"}),
required_arg: Some("title"),
},
ToolCase {
name: "memory_recall",
valid_args: json!({"context": "test"}),
required_arg: Some("context"),
},
ToolCase {
name: "memory_search",
valid_args: json!({"query": "test"}),
required_arg: Some("query"),
},
ToolCase {
name: "memory_list",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_load_family",
valid_args: json!({"family": "core"}),
required_arg: Some("family"),
},
ToolCase {
name: "memory_smart_load",
valid_args: json!({"intent": "load core memories"}),
required_arg: Some("intent"),
},
ToolCase {
name: "memory_get_taxonomy",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_check_duplicate",
valid_args: json!({"title": "test", "content": "test content"}),
required_arg: Some("title"),
},
ToolCase {
name: "memory_entity_register",
valid_args: json!({"canonical_name": "Entity", "namespace": "test"}),
required_arg: Some("canonical_name"),
},
ToolCase {
name: "memory_entity_get_by_alias",
valid_args: json!({"alias": "test"}),
required_arg: Some("alias"),
},
ToolCase {
name: "memory_kg_timeline",
valid_args: json!({"source_id": "fake-id-for-test"}),
required_arg: Some("source_id"),
},
ToolCase {
name: "memory_kg_invalidate",
valid_args: json!({"source_id": "s", "target_id": "t", "relation": "related_to"}),
required_arg: Some("source_id"),
},
ToolCase {
name: "memory_kg_query",
valid_args: json!({"source_id": "fake-id-for-test"}),
required_arg: Some("source_id"),
},
ToolCase {
name: "memory_find_paths",
valid_args: json!({
"source_id": "fake-src-for-test",
"target_id": "fake-dst-for-test",
}),
required_arg: Some("source_id"),
},
ToolCase {
name: "memory_delete",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_promote",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_forget",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_stats",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_update",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_get",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_link",
valid_args: json!({"source_id": "s", "target_id": "t"}),
required_arg: Some("source_id"),
},
ToolCase {
name: "memory_get_links",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_verify",
valid_args: json!({
"source_id": "fake-src-id",
"target_id": "fake-dst-id",
"relation": "related_to"
}),
required_arg: None,
},
ToolCase {
name: "memory_replay",
valid_args: json!({"memory_id": "fake-id-for-test"}),
required_arg: Some("memory_id"),
},
ToolCase {
name: "memory_consolidate",
valid_args: json!({"ids": ["id1", "id2"], "title": "consolidated"}),
required_arg: Some("ids"),
},
ToolCase {
name: "memory_capabilities",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_expand_query",
valid_args: json!({"query": "test"}),
required_arg: Some("query"),
},
ToolCase {
name: "memory_auto_tag",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_detect_contradiction",
valid_args: json!({"id_a": "a", "id_b": "b"}),
required_arg: Some("id_a"),
},
ToolCase {
name: "memory_archive_list",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_archive_restore",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_archive_purge",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_archive_stats",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_gc",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_session_start",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_namespace_set_standard",
valid_args: json!({"namespace": "test", "id": "fake-id-for-test"}),
required_arg: Some("namespace"),
},
ToolCase {
name: "memory_namespace_get_standard",
valid_args: json!({"namespace": "test"}),
required_arg: Some("namespace"),
},
ToolCase {
name: "memory_namespace_clear_standard",
valid_args: json!({"namespace": "test"}),
required_arg: Some("namespace"),
},
ToolCase {
name: "memory_pending_list",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_pending_approve",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_pending_reject",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_agent_register",
valid_args: json!({"agent_id": "test-agent", "agent_type": "human"}),
required_arg: Some("agent_id"),
},
ToolCase {
name: "memory_agent_list",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_notify",
valid_args: json!({"target_agent_id": "agent", "title": "msg", "payload": "body"}),
required_arg: Some("target_agent_id"),
},
ToolCase {
name: "memory_inbox",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_subscribe",
valid_args: json!({"url": "https://example.com/webhook", "secret": "tool-case-secret"}),
required_arg: Some("url"),
},
ToolCase {
name: "memory_unsubscribe",
valid_args: json!({"id": "fake-id-for-test"}),
required_arg: Some("id"),
},
ToolCase {
name: "memory_list_subscriptions",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_subscription_replay",
valid_args: json!({
"subscription_id": "smoke-id",
"since": "1970-01-01T00:00:00Z"
}),
required_arg: Some("subscription_id"),
},
ToolCase {
name: "memory_subscription_dlq_list",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_quota_status",
valid_args: json!({}),
required_arg: None,
},
ToolCase {
name: "memory_check_agent_action",
valid_args: json!({"kind": "bash", "command": "echo hello"}),
required_arg: Some("kind"),
},
ToolCase {
name: "memory_rule_list",
valid_args: json!({}),
required_arg: None,
},
];
for case in cases {
let req = make_tools_call(case.name, case.valid_args.clone());
let resp = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&crate::profile::Profile::full(),
None,
None,
None,
None, None, None, None, None, "test-session", );
assert!(
resp.error.is_none(),
"happy path failed for {}: {:?}",
case.name,
resp.error
);
assert!(
resp.result.is_some(),
"missing result for happy path {}: {:?}",
case.name,
resp
);
}
for case in cases {
if let Some(required_arg) = case.required_arg {
let mut bad_args = case.valid_args.clone();
bad_args.as_object_mut().unwrap().remove(required_arg);
let req = make_tools_call(case.name, bad_args);
let resp = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&crate::profile::Profile::full(),
None,
None,
None,
None, None, None, None, None, "test-session", );
assert!(
resp.error.is_none() || resp.result.is_some(),
"unexpected RPC-layer error for {} (missing {}) should be handler-level Err",
case.name,
required_arg
);
}
}
}
fn invoke_handle_request(conn: &rusqlite::Connection, req: &RpcRequest) -> RpcResponse {
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
handle_request(
conn,
std::path::Path::new(":memory:"),
req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&crate::profile::Profile::full(),
None,
None,
None,
None, None, None, None, None, "test-session", )
}
fn invoke_handle_request_with_nag(
conn: &rusqlite::Connection,
req: &RpcRequest,
nag_watcher: &crate::recover::nag::CaptureNagWatcher,
nag_session_id: &str,
mcp_client: Option<&str>,
) -> RpcResponse {
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
handle_request(
conn,
std::path::Path::new(":memory:"),
req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
mcp_client,
&crate::profile::Profile::full(),
None,
None,
None,
None, None, None, None, Some(nag_watcher),
nag_session_id,
)
}
fn count_capture_lag_lines(buf: &std::sync::Arc<std::sync::Mutex<Vec<u8>>>) -> usize {
let bytes = buf.lock().unwrap().clone();
String::from_utf8(bytes)
.unwrap()
.lines()
.filter(|l| l.contains("\"action\":\"capture_lag\""))
.count()
}
#[test]
fn observe_capture_nag_none_watcher_is_noop() {
use crate::recover::nag::NagAction;
let action = observe_capture_nag(None, "s", "memory_recall", &json!({}), Some("c"));
assert_eq!(action, NagAction::None);
}
#[test]
fn dispatch_loop_emits_capture_lag_past_threshold_and_resets_on_write() {
let _g = crate::audit::sink_test_lock();
let buf: std::sync::Arc<std::sync::Mutex<Vec<u8>>> =
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
crate::audit::init_for_test(buf.clone());
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let watcher = crate::recover::nag::CaptureNagWatcher::new(3, 0);
let session = "sess-1398";
let client = Some("testclient");
let cap = make_tools_call("memory_capabilities", json!({}));
for _ in 0..2 {
let resp = invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
assert!(resp.error.is_none());
}
assert_eq!(
count_capture_lag_lines(&buf),
0,
"no capture_lag before the threshold is crossed"
);
let resp = invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
assert!(resp.error.is_none());
assert_eq!(
count_capture_lag_lines(&buf),
1,
"primary threshold crossing emits exactly one capture_lag event"
);
invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
assert_eq!(
count_capture_lag_lines(&buf),
1,
"no duplicate capture_lag while still in the same warned tier"
);
let store = make_tools_call(
"memory_store",
json!({"title": "t", "content": "c", "tier": "short", "agent_id": "ai:testclient"}),
);
let store_resp = invoke_handle_request_with_nag(&conn, &store, &watcher, session, client);
assert!(
store_resp.error.is_none(),
"store should succeed: {store_resp:?}"
);
for _ in 0..3 {
invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
}
assert_eq!(
count_capture_lag_lines(&buf),
2,
"re-armed WARN after a write reset fires a second capture_lag"
);
crate::audit::shutdown_for_test();
}
#[test]
fn capture_lag_events_are_chained() {
let _g = crate::audit::sink_test_lock();
let buf: std::sync::Arc<std::sync::Mutex<Vec<u8>>> =
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
crate::audit::init_for_test(buf.clone());
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let watcher = crate::recover::nag::CaptureNagWatcher::new(1, 2);
let cap = make_tools_call("memory_capabilities", json!({}));
invoke_handle_request_with_nag(&conn, &cap, &watcher, "s", Some("c"));
invoke_handle_request_with_nag(&conn, &cap, &watcher, "s", Some("c"));
assert_eq!(
count_capture_lag_lines(&buf),
2,
"primary + escalation each emit a capture_lag event"
);
let bytes = buf.lock().unwrap().clone();
let report = crate::audit::verify_chain_from_reader(bytes.as_slice()).unwrap();
assert!(
report.first_failure.is_none(),
"capture_lag emissions must keep the hash chain intact: {:?}",
report.first_failure
);
crate::audit::shutdown_for_test();
}
fn dispatch_line(conn: &rusqlite::Connection, line: &str) -> Option<RpcResponse> {
if line.trim().is_empty() {
return None;
}
let req: RpcRequest = match serde_json::from_str(line) {
Ok(r) => r,
Err(e) => {
return Some(err_response(
Value::Null,
-32700,
format!("parse error: {e}"),
));
}
};
if req.id.is_none() || req.id == Some(Value::Null) {
return None;
}
Some(invoke_handle_request(conn, &req))
}
#[test]
fn handle_store_happy_returns_id_and_tier() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_store",
json!({"title": "t", "content": "c", "namespace": "m9-store", "tier": Tier::Short.as_str()}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["id"].is_string());
assert_eq!(val["tier"], Tier::Short.as_str());
}
#[test]
fn handle_store_error_missing_title() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_store", json!({"content": "c"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_recall_happy_returns_memories_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_recall",
json!({"context": "anything", "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["memories"].is_array());
assert!(val["count"].is_u64());
}
#[test]
fn handle_recall_budget_tokens_zero_returns_empty() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_recall",
json!({"context": "x", "budget_tokens": 0, "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "budget_tokens=0 must not error");
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["count"], 0, "budget_tokens=0 returns zero memories");
assert_eq!(val["budget_tokens"], 0);
assert_eq!(val["tokens_used"], 0);
assert_eq!(val["meta"]["budget_overflow"], false);
assert_eq!(val["meta"]["budget_tokens_used"], 0);
assert_eq!(val["meta"]["budget_tokens_remaining"], 0);
}
#[test]
fn handle_search_happy_returns_results_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_search",
json!({"query": "needle", "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["results"].is_array());
assert!(val["count"].is_u64());
}
#[test]
fn handle_search_error_missing_query() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_search", json!({}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_get_happy_returns_memory() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "m9-get".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_get", json!({"id": id}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["title"], "t");
assert_eq!(val["namespace"], "m9-get");
assert!(val["links"].is_array());
}
#[test]
fn handle_get_error_unknown_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_get",
json!({"id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
assert!(
result["content"][0]["text"]
.as_str()
.unwrap()
.contains("not found")
);
}
#[test]
fn handle_list_happy_returns_memories_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_list", json!({"format": "json"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["memories"].is_array());
}
#[test]
fn handle_list_error_invalid_agent_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_list", json!({"agent_id": "has space"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_delete_happy_removes_existing_memory() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "m9-del".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_delete", json!({"id": id}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["deleted"], true);
}
#[test]
fn handle_delete_error_empty_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_delete", json!({"id": ""}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_link_happy_returns_linked_true() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mut ids = Vec::new();
for tag in ["a", "b"] {
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "m9-link".into(),
title: tag.into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
ids.push(db::insert(&conn, &mem).unwrap());
}
let req = make_tools_call(
"memory_link",
json!({"source_id": ids[0], "target_id": ids[1], "relation": "related_to"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["linked"], true);
assert_eq!(val["attest_level"], "unsigned");
}
#[test]
fn handle_link_with_active_keypair_returns_self_signed() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mut ids = Vec::new();
for tag in ["a", "b"] {
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "h2-link".into(),
title: tag.into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
ids.push(db::insert(&conn, &mem).unwrap());
}
let req = make_tools_call(
"memory_link",
json!({"source_id": ids[0], "target_id": ids[1], "relation": "related_to"}),
);
let kp = crate::identity::keypair::generate("alice").unwrap();
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
let resp = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&crate::profile::Profile::full(),
None,
Some(&kp),
None,
None, None, None, None, None, "test-session", );
assert!(resp.error.is_none(), "MCP error: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["linked"], true);
assert_eq!(
val["attest_level"], "self_signed",
"active keypair path must surface self_signed"
);
let sig: Option<Vec<u8>> = conn
.query_row(
"SELECT signature FROM memory_links \
WHERE source_id = ?1 AND target_id = ?2",
rusqlite::params![&ids[0], &ids[1]],
|r| r.get(0),
)
.unwrap();
let sig_bytes = sig.expect("signed link must persist a signature blob");
assert_eq!(sig_bytes.len(), 64);
}
#[test]
fn handle_reflect_with_active_keypair_returns_signed_reflects_on_edges() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mut source_ids = Vec::new();
for tag in ["src-a", "src-b", "src-c"] {
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "issue-815-reflect".into(),
title: tag.into(),
content: format!("body for {tag}"),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
source_ids.push(db::insert(&conn, &mem).unwrap());
}
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": source_ids,
"title": "issue-815 reflect signing pin",
"content": "reflects_on edges must come back self_signed",
"namespace": "issue-815-reflect",
}),
);
let kp = crate::identity::keypair::generate("alice").unwrap();
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
let resp = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&crate::profile::Profile::full(),
None,
Some(&kp),
None,
None, None, None, None, None, "test-session", );
assert!(resp.error.is_none(), "MCP error: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
let reflection_id = val["id"]
.as_str()
.expect("reflect response must carry the new memory id")
.to_string();
let mut stmt = conn
.prepare(
"SELECT target_id, attest_level, signature \
FROM memory_links \
WHERE source_id = ?1 AND relation = 'reflects_on' \
ORDER BY created_at",
)
.unwrap();
let rows: Vec<(String, String, Option<Vec<u8>>)> = stmt
.query_map(rusqlite::params![&reflection_id], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, Option<Vec<u8>>>(2)?,
))
})
.unwrap()
.map(std::result::Result::unwrap)
.collect();
assert_eq!(
rows.len(),
source_ids.len(),
"expected one reflects_on edge per source; got {rows:?}"
);
for (target_id, attest_level, signature) in &rows {
assert_eq!(
attest_level, "self_signed",
"reflects_on edge to {target_id} must surface self_signed (got {attest_level})"
);
let sig_bytes = signature.as_ref().unwrap_or_else(|| {
panic!("reflects_on edge to {target_id} must persist a signature blob")
});
assert_eq!(
sig_bytes.len(),
64,
"reflects_on edge to {target_id} signature must be 64 bytes (got {})",
sig_bytes.len()
);
}
}
#[test]
fn issue_1315_memory_reflect_wire_layer_preserves_caller_metadata() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mut source_ids = Vec::new();
for tag in ["src-a", "src-b", "src-c"] {
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "issue-1315-wire".into(),
title: tag.into(),
content: format!("body for {tag}"),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "api".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
source_ids.push(db::insert(&conn, &mem).unwrap());
}
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": source_ids,
"title": "issue-1315 wire-layer metadata pin",
"content": "caller metadata.entity_id + probe must round-trip through tools/call",
"namespace": "issue-1315-wire",
"metadata": {
"entity_id": "entity-1315-wire",
"probe": "P2",
},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(
resp.error.is_none(),
"expected ok rpc envelope; got error: {:?}",
resp.error
);
let result = resp.result.expect("result envelope");
assert!(
result.get("isError").is_none_or(|v| v != true),
"tools/call must not surface isError=true; result: {result}"
);
let text = result["content"][0]["text"]
.as_str()
.expect("content[0].text on memory_reflect ok envelope");
let parsed: Value = serde_json::from_str(text).expect("reflect result text is JSON");
let reflection_id = parsed["id"]
.as_str()
.expect("reflect result carries the new memory id")
.to_string();
let (meta_str, mention): (String, Option<String>) = conn
.query_row(
"SELECT metadata, mentioned_entity_id FROM memories WHERE id = ?1",
rusqlite::params![&reflection_id],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.expect("select reflection row");
let meta: Value = serde_json::from_str(&meta_str).expect("metadata is JSON");
assert_eq!(
meta.get("entity_id").and_then(Value::as_str),
Some("entity-1315-wire"),
"wire path must preserve metadata.entity_id; full metadata = {meta}"
);
assert_eq!(
meta.get("probe").and_then(Value::as_str),
Some("P2"),
"wire path must preserve every caller-supplied metadata key; full metadata = {meta}"
);
assert_eq!(
mention.as_deref(),
Some("entity-1315-wire"),
"mentioned_entity_id column must be populated from the wire-supplied metadata.entity_id"
);
assert!(
meta.get("agent_id").is_some(),
"system-generated agent_id must still be present"
);
assert!(
meta.get("reflection_metadata").is_some(),
"system-generated reflection_metadata block must still be present"
);
}
#[test]
fn handle_link_error_missing_target_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_link", json!({"source_id": "x"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_promote_error_unknown_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_promote",
json!({"id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_consolidate_error_missing_summary_keyword_tier() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_consolidate",
json!({"ids": ["a", "b"], "title": "t"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("summary"));
}
#[test]
fn handle_capabilities_happy_returns_tier_struct() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_capabilities", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["tier"].is_string());
assert!(val["features"].is_object());
}
#[test]
fn mcp_capabilities_v2_schema_includes_all_blocks() {
let _gate = crate::config::lock_permissions_mode_for_test();
crate::config::clear_permissions_mode_override_for_test();
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_capabilities", json!({"accept": "v2"}));
let resp = invoke_handle_request(&conn, &req);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["schema_version"], "2", "schema_version bumped to 2");
assert!(val["permissions"].is_object(), "permissions block present");
assert_eq!(val["permissions"]["mode"], "advisory");
assert!(val["permissions"]["active_rules"].is_number());
assert!(
val["permissions"].get("rule_summary").is_none(),
"v2 drops rule_summary (no per-rule serializer)"
);
assert_eq!(val["permissions"]["inheritance"], "enforced");
assert!(val["hooks"].is_object(), "hooks block present");
assert!(val["hooks"]["registered_count"].is_number());
assert!(
val["hooks"].get("by_event").is_none(),
"v2 drops hooks.by_event (no event registry)"
);
assert!(val["compaction"].is_object(), "compaction block present");
assert_eq!(val["compaction"]["planned"], true);
assert_eq!(val["compaction"]["enabled"], false);
assert_eq!(val["compaction"]["version"], "v0.8+");
assert!(val["compaction"].get("interval_minutes").is_none());
assert!(val["compaction"].get("last_run_at").is_none());
assert!(val["compaction"].get("last_run_stats").is_none());
assert!(val["approval"].is_object(), "approval block present");
assert!(val["approval"]["pending_requests"].is_number());
assert!(
val["approval"].get("subscribers").is_none(),
"v2 drops approval.subscribers (no subscription API)"
);
assert!(
val["approval"].get("default_timeout_seconds").is_none(),
"v2 drops approval.default_timeout_seconds (no sweeper)"
);
assert!(val["transcripts"].is_object(), "transcripts block present");
assert_eq!(val["transcripts"]["planned"], false);
assert_eq!(val["transcripts"]["enabled"], false);
assert_eq!(val["transcripts"]["version"], env!("CARGO_PKG_VERSION"));
assert_eq!(val["features"]["memory_reflection"]["planned"], false);
assert_eq!(val["features"]["memory_reflection"]["enabled"], true);
assert_eq!(val["features"]["recall_mode_active"], "disabled");
assert_eq!(val["features"]["reranker_active"], "off");
}
#[test]
fn mcp_capabilities_v2_backwards_compatible() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_capabilities", json!({}));
let resp = invoke_handle_request(&conn, &req);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["tier"].is_string(), "v1: tier preserved");
assert!(val["version"].is_string(), "v1: version preserved");
assert!(val["features"].is_object(), "v1: features preserved");
assert!(val["models"].is_object(), "v1: models preserved");
assert!(val["features"]["keyword_search"].is_boolean());
assert!(val["features"]["semantic_search"].is_boolean());
assert!(val["features"]["embedder_loaded"].is_boolean());
assert!(val["models"]["embedding"].is_string());
assert!(val["models"]["llm"].is_string());
assert!(val["models"]["cross_encoder"].is_string());
}
#[test]
fn mcp_capabilities_accept_v1_returns_legacy_shape() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_capabilities", json!({"accept": "v1"}));
let resp = invoke_handle_request(&conn, &req);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(
val.get("schema_version").and_then(Value::as_str),
Some("1"),
"Round-2 F13 — v1 must carry schema_version on the wire"
);
assert!(val.get("permissions").is_none());
assert!(val.get("hooks").is_none());
assert!(val.get("compaction").is_none());
assert!(val.get("approval").is_none());
assert!(val.get("transcripts").is_none());
assert!(val["features"]["memory_reflection"].is_boolean());
assert!(val["features"].get("recall_mode_active").is_none());
assert!(val["features"].get("reranker_active").is_none());
}
#[test]
fn mcp_capabilities_pending_requests_reflects_db() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let payload = serde_json::json!({"foo": "bar"}).to_string();
conn.execute(
"INSERT INTO pending_actions (id, action_type, memory_id, namespace,
payload, requested_by, requested_at, status)
VALUES ('p-1', 'store', NULL, 'global', ?1, 'agent-1',
'2026-04-27T00:00:00Z', 'pending')",
rusqlite::params![payload],
)
.unwrap();
let req = make_tools_call("memory_capabilities", json!({}));
let resp = invoke_handle_request(&conn, &req);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(
val["approval"]["pending_requests"], 1,
"pending_actions(status=pending) count surfaces live"
);
}
#[test]
fn handle_subscribe_error_unregistered_agent() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_subscribe",
json!({"url": "https://example.com/hook", "secret": "mcp-sub-test-secret"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("not registered"));
}
#[test]
fn test_jsonrpc_handles_well_formed_request() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let line = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
let resp = dispatch_line(&conn, line).expect("expected response");
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert!(result["tools"].is_array());
}
#[test]
fn test_jsonrpc_handles_malformed_json() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let resp = dispatch_line(&conn, "this is not json at all").expect("expected response");
let err = resp.error.unwrap();
assert_eq!(err.code, -32700);
assert!(err.message.contains("parse error"));
assert_eq!(resp.id, Value::Null);
}
#[test]
fn test_jsonrpc_handles_truncated_request() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let resp = dispatch_line(&conn, r#"{"jsonrpc":"2.0","id":1,"method":"#)
.expect("expected response");
let err = resp.error.unwrap();
assert_eq!(err.code, -32700);
}
#[test]
fn test_jsonrpc_handles_two_requests_per_line() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let line = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"} {"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
let resp = dispatch_line(&conn, line).expect("expected response");
let err = resp.error.unwrap();
assert_eq!(err.code, -32700);
}
#[test]
fn test_jsonrpc_handles_blank_line() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
assert!(dispatch_line(&conn, "").is_none());
assert!(dispatch_line(&conn, " \t ").is_none());
}
#[test]
fn test_jsonrpc_handles_notification_no_response() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let line = r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
assert!(dispatch_line(&conn, line).is_none());
let line_null = r#"{"jsonrpc":"2.0","id":null,"method":"notifications/initialized"}"#;
assert!(dispatch_line(&conn, line_null).is_none());
}
#[test]
fn test_jsonrpc_handles_method_not_found() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(7)),
method: "no/such/method".into(),
params: json!({}),
};
let resp = invoke_handle_request(&conn, &req);
let err = resp.error.unwrap();
assert_eq!(err.code, -32601);
assert!(err.message.contains("method not found"));
}
#[test]
fn test_jsonrpc_handles_invalid_params() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(8)),
method: "tools/call".into(),
params: json!({"arguments": {}}),
};
let resp = invoke_handle_request(&conn, &req);
let err = resp.error.unwrap();
assert_eq!(err.code, -32602);
}
#[test]
fn test_jsonrpc_handles_unknown_tool_returns_minus_32601() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_does_not_exist", json!({}));
let resp = invoke_handle_request(&conn, &req);
let err = resp.error.unwrap();
assert_eq!(err.code, -32601);
assert!(err.message.contains("memory_does_not_exist"));
}
#[test]
fn issue_1254_tool_name_leak_gated_behind_profile_hint_in_errors() {
use crate::config::McpConfig;
let tier_config = FeatureTier::Keyword.config();
let resolved_ttl = crate::config::ResolvedTtl::default();
let resolved_scoring = crate::config::ResolvedScoring::default();
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let core_profile = crate::profile::Profile::core();
assert!(
!core_profile.loads("memory_atomise"),
"test sentinel: memory_atomise must not be loaded under --profile core; \
pick a different higher-profile tool if this changes"
);
let req = make_tools_call("memory_atomise", json!({}));
let resp_default = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&core_profile,
None,
None,
None,
None,
None,
None,
None,
None, "test-session", );
let err_default = resp_default
.error
.expect("tools/call against a non-loaded tool must error");
assert_eq!(
err_default.code, -32601,
"method-not-found code unchanged across the hint posture"
);
assert!(
err_default.message.starts_with("unknown tool: "),
"#1254: default posture must return a uniform 'unknown tool: <name>' \
error regardless of family membership; got: {}",
err_default.message
);
assert!(
err_default.message.contains("memory_atomise"),
"the refused tool name is fine — the leak was the FAMILY name, not the \
tool name (the client supplied that); got: {}",
err_default.message
);
assert!(
!err_default.message.contains("family"),
"#1254: default posture must NOT leak family membership; got: {}",
err_default.message
);
assert!(
!err_default.message.contains("--profile"),
"#1254: default posture must NOT advise which profile would load the tool; got: {}",
err_default.message
);
let cfg_with_hint = McpConfig {
profile_hint_in_errors: true,
..McpConfig::default()
};
let resp_hint = handle_request(
&conn,
std::path::Path::new(":memory:"),
&req,
None,
None,
None,
&tier_config,
&crate::config::ResolvedModels::from_tier_preset(&tier_config),
None,
&resolved_ttl,
&resolved_scoring,
true,
false,
None,
&core_profile,
Some(&cfg_with_hint),
None,
None,
None,
None,
None,
None,
None, "test-session", );
let err_hint = resp_hint
.error
.expect("hint-enabled tools/call must still error on non-loaded tool");
assert_eq!(err_hint.code, -32601);
assert!(
err_hint.message.contains("family"),
"#1254: profile_hint_in_errors=true must surface the family hint; \
got: {}",
err_hint.message
);
assert!(
err_hint.message.contains("--profile"),
"#1254: profile_hint_in_errors=true must advise the operator on \
how to load the tool; got: {}",
err_hint.message
);
}
#[test]
fn test_jsonrpc_rejects_wrong_version() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "1.0".into(),
id: Some(json!(1)),
method: "tools/list".into(),
params: json!({}),
};
let resp = invoke_handle_request(&conn, &req);
let err = resp.error.unwrap();
assert_eq!(err.code, -32600);
}
#[test]
fn test_jsonrpc_handles_initialize() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(1)),
method: "initialize".into(),
params: json!({"clientInfo": {"name": "test-client"}}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert_eq!(result["protocolVersion"], "2024-11-05");
assert_eq!(result["serverInfo"]["name"], "ai-memory");
}
#[test]
fn test_auto_register_creates_top_level_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
super::auto_register_path_hierarchy(&conn, "m9-top");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM namespace_meta", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_auto_register_creates_nested_hierarchy() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "m9-parent".into(),
title: "parent standard".into(),
content: "...".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let std_id = db::insert(&conn, &mem).unwrap();
db::set_namespace_standard(&conn, "m9-parent", &std_id, None).unwrap();
let child_mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "repo/team/sub".into(),
title: "child".into(),
content: "...".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let child_id = db::insert(&conn, &child_mem).unwrap();
db::set_namespace_standard(&conn, "repo/team/sub", &child_id, None).unwrap();
super::auto_register_path_hierarchy(&conn, "repo/team/sub");
let id = db::get_namespace_standard(&conn, "repo/team/sub")
.unwrap()
.unwrap();
assert_eq!(id, child_id);
}
#[test]
fn test_auto_register_idempotent() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
super::auto_register_path_hierarchy(&conn, "m9-idem");
super::auto_register_path_hierarchy(&conn, "m9-idem");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM namespace_meta", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_auto_register_handles_empty_string_or_root() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
super::auto_register_path_hierarchy(&conn, "");
super::auto_register_path_hierarchy(&conn, "/");
super::auto_register_path_hierarchy(&conn, "*");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM namespace_meta", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_auto_register_skips_when_explicit_parent_set() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let parent_mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "m9-explicit-parent".into(),
title: "p".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let parent_id = db::insert(&conn, &parent_mem).unwrap();
db::set_namespace_standard(&conn, "m9-explicit-parent", &parent_id, None).unwrap();
let child_mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "m9-explicit-child".into(),
title: "c".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let child_id = db::insert(&conn, &child_mem).unwrap();
db::set_namespace_standard(
&conn,
"m9-explicit-child",
&child_id,
Some("m9-explicit-parent"),
)
.unwrap();
assert_eq!(
db::get_namespace_parent(&conn, "m9-explicit-child"),
Some("m9-explicit-parent".to_string())
);
super::auto_register_path_hierarchy(&conn, "m9-explicit-child");
assert_eq!(
db::get_namespace_parent(&conn, "m9-explicit-child"),
Some("m9-explicit-parent".to_string())
);
}
fn make_recall_response(memories: Vec<Value>) -> Value {
let count = memories.len();
json!({
"memories": memories,
"count": count,
"mode": "keyword",
})
}
fn seed_namespace_standard(
conn: &rusqlite::Connection,
namespace: &str,
title: &str,
) -> String {
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: namespace.into(),
title: title.into(),
content: "policy text".into(),
tags: vec!["_standard".into()],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(conn, &mem).unwrap();
db::set_namespace_standard(conn, namespace, &id, None).unwrap();
id
}
#[test]
fn test_inject_namespace_standard_attaches_when_present() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let std_id = seed_namespace_standard(&conn, "m9-inject-attach", "S");
let mut resp = make_recall_response(vec![]);
super::inject_namespace_standard(&conn, Some("m9-inject-attach"), &mut resp);
assert!(resp["standard"].is_object(), "expected attached standard");
assert_eq!(resp["standard"]["id"].as_str().unwrap(), std_id);
}
#[test]
fn test_inject_namespace_standard_skips_when_absent() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mut resp = make_recall_response(vec![]);
let before = resp.clone();
super::inject_namespace_standard(&conn, Some("m9-inject-empty"), &mut resp);
assert_eq!(resp, before);
assert!(resp.get("standard").is_none());
assert!(resp.get("standards").is_none());
}
#[test]
fn test_inject_namespace_standard_top_of_recall_response() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let std_id = seed_namespace_standard(&conn, "m9-inject-dedup", "S");
let dup = json!({"id": std_id, "title": "S", "content": "policy text"});
let other = json!({"id": "other-id", "title": "noise", "content": "x"});
let mut resp = make_recall_response(vec![dup.clone(), other.clone()]);
super::inject_namespace_standard(&conn, Some("m9-inject-dedup"), &mut resp);
assert_eq!(resp["standard"]["id"].as_str().unwrap(), std_id);
let memories = resp["memories"].as_array().unwrap();
assert_eq!(memories.len(), 1);
assert_eq!(memories[0]["id"], "other-id");
assert_eq!(resp["count"], 1);
}
#[test]
fn test_inject_namespace_standard_preserves_other_response_fields() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
seed_namespace_standard(&conn, "m9-inject-preserve", "S");
let mut resp = json!({
"memories": [],
"count": 0,
"mode": "hybrid",
"diagnostics": {"latency_ms": 42},
});
super::inject_namespace_standard(&conn, Some("m9-inject-preserve"), &mut resp);
assert_eq!(resp["mode"], "hybrid");
assert_eq!(resp["diagnostics"]["latency_ms"], 42);
assert!(resp["standard"].is_object());
}
#[test]
fn test_inject_namespace_standard_no_namespace_uses_global() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
seed_namespace_standard(&conn, "*", "global standard");
let mut resp = make_recall_response(vec![]);
super::inject_namespace_standard(&conn, None, &mut resp);
assert_eq!(resp["standard"]["title"], "global standard");
}
#[test]
fn test_inject_namespace_standard_multiple_levels_emits_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
seed_namespace_standard(&conn, "*", "GLOBAL");
seed_namespace_standard(&conn, "m9-multi", "LOCAL");
let mut resp = make_recall_response(vec![]);
super::inject_namespace_standard(&conn, Some("m9-multi"), &mut resp);
assert!(resp["standards"].is_array());
let arr = resp["standards"].as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["title"], "GLOBAL");
assert_eq!(arr[1]["title"], "LOCAL");
assert!(resp.get("standard").is_none());
}
#[test]
fn handle_archive_list_returns_empty_when_no_archived() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_archive_list", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["count"], 0);
assert!(val["archived"].is_array());
}
#[test]
fn handle_archive_list_with_namespace_filter() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_archive_list",
json!({"namespace": "w12-archive", "limit": 5, "offset": 0}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_archive_restore_unknown_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_archive_restore",
json!({"id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("archive") || msg.contains("not found"));
}
#[test]
fn handle_archive_purge_with_older_than_zero() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_archive_purge", json!({"older_than_days": 0}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["purged"].is_u64() || val["purged"].is_i64());
}
#[test]
fn handle_archive_stats_returns_struct() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_archive_stats", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val.is_object() || val.is_number() || val.is_array());
}
#[test]
fn handle_kg_timeline_unknown_source_returns_empty_events() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_timeline",
json!({"source_id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["events"].is_array());
assert_eq!(val["count"], 0);
}
#[test]
fn handle_kg_timeline_with_since_until_filters() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_timeline",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"since": "2024-01-01T00:00:00Z",
"until": "2025-01-01T00:00:00Z",
"limit": 50,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_kg_timeline_invalid_since_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_timeline",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"since": "this-is-not-a-timestamp",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_kg_invalidate_no_match_returns_found_false() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"target_id": "11111111-1111-1111-1111-111111111111",
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["found"], false);
}
#[test]
fn handle_kg_invalidate_with_explicit_valid_until() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-kg".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
"valid_until": "2025-01-01T00:00:00Z",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["found"], true);
assert_eq!(val["valid_until"], "2025-01-01T00:00:00Z");
}
#[test]
fn handle_kg_invalidate_invalid_valid_until_format() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"target_id": "11111111-1111-1111-1111-111111111111",
"relation": "related_to",
"valid_until": "not-a-date",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_kg_query_with_max_depth_and_filters() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_query",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"max_depth": 2,
"valid_at": "2025-01-01T00:00:00Z",
"allowed_agents": ["agent-a", "agent-b"],
"limit": 10,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["max_depth"], 2);
assert!(val["memories"].is_array());
}
#[test]
fn handle_kg_query_invalid_valid_at() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_query",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"valid_at": "garbage",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_kg_query_rejects_invalid_agent_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_query",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"allowed_agents": ["bad agent with spaces!!"],
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_session_start_happy_returns_memories() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-session".into(),
title: "seed".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_session_start",
json!({"namespace": "w12-session", "limit": 5, "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["mode"], "session_start");
assert!(val["memories"].is_array());
}
#[test]
fn handle_session_start_empty_namespace_returns_zero() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_session_start",
json!({"namespace": "w12-empty-ns", "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["count"], 0);
}
#[test]
fn handle_session_start_rejects_invalid_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_session_start",
json!({"namespace": "foo bar", "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "must not surface as RPC error");
let result = resp.result.expect("ok_response present");
assert_eq!(
result.get("isError").and_then(|v| v.as_bool()),
Some(true),
"invalid namespace must return isError=true, got {result}"
);
let text = result["content"][0]["text"]
.as_str()
.unwrap_or_default()
.to_string();
assert!(
text.to_lowercase().contains("namespace"),
"error message should mention namespace, got: {text}"
);
}
#[test]
fn handle_inbox_returns_empty_for_unregistered_caller() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_inbox", json!({"agent_id": "test-bot"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["agent_id"], "test-bot");
assert!(val["namespace"].as_str().unwrap().starts_with("_messages/"));
assert_eq!(val["count"], 0);
}
#[test]
fn handle_inbox_with_unread_only_filter() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_inbox",
json!({"agent_id": "test-bot", "unread_only": true, "limit": 10}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["unread_only"], true);
}
#[test]
fn handle_notify_happy_returns_message_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_notify",
json!({
"target_agent_id": "alice",
"title": "hello",
"payload": "world",
"tier": Tier::Mid.as_str(),
"priority": 5,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["id"].is_string());
assert_eq!(val["to"], "alice");
assert_eq!(val["namespace"], "_messages/alice");
}
#[test]
fn handle_notify_invalid_tier_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_notify",
json!({
"target_agent_id": "bob",
"title": "hi",
"payload": "p",
"tier": "bogus-tier",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("invalid tier"));
}
#[test]
fn handle_agent_register_then_list() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_agent_register",
json!({
"agent_id": "w12-bot",
"agent_type": "ai:w12-bot",
"capabilities": ["read", "write"],
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["registered"], true);
let req2 = make_tools_call("memory_agent_list", json!({}));
let resp2 = invoke_handle_request(&conn, &req2);
assert!(resp2.error.is_none());
let text2 = resp2.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val2: Value = serde_json::from_str(&text2).unwrap();
assert!(val2["count"].as_u64().unwrap() >= 1);
}
#[test]
fn handle_agent_register_invalid_type_rejects() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_agent_register",
json!({"agent_id": "w12-bot2", "agent_type": " not-allowed-type with spaces "}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_namespace_set_get_clear_round_trip() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-ns".into(),
title: "policy".into(),
content: "be excellent".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let std_id = db::insert(&conn, &mem).unwrap();
let set_req = make_tools_call(
"memory_namespace_set_standard",
json!({"namespace": "w12-ns", "id": std_id.clone()}),
);
let set_resp = invoke_handle_request(&conn, &set_req);
assert!(set_resp.error.is_none());
let set_text = set_resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let set_val: Value = serde_json::from_str(&set_text).unwrap();
assert_eq!(set_val["set"], true);
let get_req = make_tools_call(
"memory_namespace_get_standard",
json!({"namespace": "w12-ns"}),
);
let get_resp = invoke_handle_request(&conn, &get_req);
assert!(get_resp.error.is_none());
let get_text = get_resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let get_val: Value = serde_json::from_str(&get_text).unwrap();
assert_eq!(get_val["standard_id"], std_id);
let clr_req = make_tools_call(
"memory_namespace_clear_standard",
json!({"namespace": "w12-ns"}),
);
let clr_resp = invoke_handle_request(&conn, &clr_req);
assert!(clr_resp.error.is_none());
let clr_text = clr_resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let clr_val: Value = serde_json::from_str(&clr_text).unwrap();
assert_eq!(clr_val["cleared"], true);
}
#[test]
fn handle_namespace_get_standard_missing_returns_null() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_namespace_get_standard",
json!({"namespace": "w12-no-standard-here"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["standard_id"].is_null());
}
#[test]
fn handle_namespace_get_standard_inherit_returns_chain() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
seed_namespace_standard(&conn, "*", "global rule");
seed_namespace_standard(&conn, "w12-inh", "specific rule");
let req = make_tools_call(
"memory_namespace_get_standard",
json!({"namespace": "w12-inh", "inherit": true}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["chain"].is_array());
assert!(val["standards"].is_array());
assert!(val["count"].as_u64().unwrap() >= 1);
}
#[test]
fn handle_namespace_set_standard_with_invalid_governance_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-gov".into(),
title: "p".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_namespace_set_standard",
json!({
"namespace": "w12-gov",
"id": id,
"governance": {"this": "is not a valid policy"},
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("invalid governance") || msg.contains("governance"));
}
#[test]
fn handle_namespace_set_standard_invalid_namespace_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_namespace_set_standard",
json!({"namespace": "bad ns with spaces!!", "id": "any"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_pending_list_happy_returns_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_pending_list",
json!({"status": "pending", "limit": 100}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["pending"].is_array());
assert!(val["count"].is_u64());
}
#[test]
fn handle_pending_approve_unknown_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_pending_approve",
json!({"id": "00000000-0000-0000-0000-000000000000", "agent_id": "human:approver"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert!(result.is_object());
}
#[test]
fn handle_pending_reject_unknown_id_returns_not_found() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_pending_reject",
json!({"id": "00000000-0000-0000-0000-000000000000", "agent_id": "human:rejector"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("not found") || msg.contains("already decided"));
}
#[test]
fn handle_gc_dry_run_returns_count_without_deleting() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_gc", json!({"dry_run": true}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["dry_run"], true);
assert!(val["collected"].is_u64() || val["collected"].is_i64());
}
#[test]
fn handle_gc_actual_run_returns_zero_on_empty_db() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_gc", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["dry_run"], false);
}
#[test]
fn handle_forget_dry_run_with_filters() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_forget",
json!({"namespace": "w12-forget", "tier": Tier::Short.as_str(), "dry_run": true}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["dry_run"], true);
}
#[test]
fn handle_forget_actual_with_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_forget",
json!({"namespace": "w12-forget-actual", "dry_run": false}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_unsubscribe_unknown_returns_false() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_unsubscribe",
json!({"id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(
val["removed"] == json!(false) || val["removed"] == json!(0),
"unexpected removed value: {:?}",
val["removed"]
);
}
#[test]
fn handle_list_subscriptions_returns_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_list_subscriptions", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_entity_register_happy() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_register",
json!({
"canonical_name": "Hugo Boss",
"namespace": "w12-people",
"aliases": ["HB", "Hugo"],
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["entity_id"].is_string());
assert_eq!(val["canonical_name"], "Hugo Boss");
}
#[test]
fn handle_entity_register_invalid_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_register",
json!({"canonical_name": "X", "namespace": "INVALID NS!"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_entity_get_by_alias_not_found_returns_null() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_get_by_alias",
json!({"alias": "no-such-alias", "namespace": "w12-people"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["found"], false);
}
#[test]
fn handle_get_taxonomy_with_prefix_and_depth() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_get_taxonomy",
json!({"namespace_prefix": "w12-tax", "depth": 4, "limit": 100}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["tree"].is_object() || val["tree"].is_array());
}
#[test]
fn handle_get_taxonomy_strips_trailing_slash() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_get_taxonomy",
json!({"namespace_prefix": "w12-tax/", "depth": 2}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_get_taxonomy_invalid_prefix_after_strip() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_get_taxonomy",
json!({"namespace_prefix": "BAD NS!"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_check_duplicate_no_embedder_errors() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_check_duplicate",
json!({"title": "T", "content": "C"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("embedder") || msg.contains("semantic"));
}
#[test]
fn handle_expand_query_no_llm_errors() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_expand_query", json!({"query": "test"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("smart") || msg.contains("LLM") || msg.contains("Ollama"));
}
#[test]
fn handle_auto_tag_no_llm_errors() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_auto_tag",
json!({"id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_detect_contradiction_no_llm_errors() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_detect_contradiction",
json!({"id_a": "00000000-0000-0000-0000-000000000000", "id_b": "11111111-1111-1111-1111-111111111111"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_update_unknown_id_returns_not_found() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_update",
json!({
"id": "00000000-0000-0000-0000-000000000000",
"title": "new title",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("not found"));
}
#[test]
fn handle_update_invalid_priority_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-update".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_update",
json!({"id": id, "priority": 99_i64}), );
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_update_with_metadata_object_accepted() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "w12-meta".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_update",
json!({
"id": id,
"metadata": {"custom": "field", "numbers": [1, 2, 3]},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_get_links_unknown_id_returns_empty() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_get_links",
json!({"id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["links"].is_array());
assert_eq!(val["count"], 0);
}
#[test]
fn handle_link_invalid_relation_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_link",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"target_id": "11111111-1111-1111-1111-111111111111",
"relation": "BADRELATIONNOTALLOWED",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_promote_to_namespace_with_explicit_target() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "w12-parent/w12-child".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_promote",
json!({"id": id, "to_namespace": "w12-parent"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["mode"], "vertical");
assert!(val["clone_id"].is_string());
}
#[test]
fn handle_promote_invalid_to_namespace_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "w12-pm".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_promote",
json!({"id": id, "to_namespace": "BAD NS WITH SPACES"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_consolidate_with_explicit_summary_no_llm() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem_a = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "w12-cons".into(),
title: "a".into(),
content: "alpha".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut mem_b = mem_a.clone();
mem_b.id = uuid::Uuid::new_v4().to_string();
mem_b.title = "b".into();
mem_b.content = "beta".into();
let id_a = db::insert(&conn, &mem_a).unwrap();
let id_b = db::insert(&conn, &mem_b).unwrap();
let req = make_tools_call(
"memory_consolidate",
json!({
"ids": [id_a, id_b],
"title": "merged",
"summary": "merged summary",
"namespace": "w12-cons",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["id"].is_string());
assert_eq!(val["consolidated"], 2);
}
#[test]
fn handle_consolidate_non_string_id_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_consolidate",
json!({"ids": [42, "valid-id"], "title": "t", "summary": "s"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("must be a string"));
}
#[test]
fn test_jsonrpc_handles_ping() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(1)),
method: "ping".into(),
params: json!({}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn test_jsonrpc_handles_notifications_initialized() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(2)),
method: "notifications/initialized".into(),
params: json!({}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn test_jsonrpc_prompts_list_returns_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(3)),
method: "prompts/list".into(),
params: json!({}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert!(result["prompts"].is_array());
}
#[test]
fn test_jsonrpc_prompts_get_known_name_returns_messages() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(4)),
method: "prompts/get".into(),
params: json!({"name": "recall-first"}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert!(result["messages"].is_array());
}
#[test]
fn test_jsonrpc_prompts_get_with_namespace_arg_includes_hint() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(5)),
method: "prompts/get".into(),
params: json!({"name": "recall-first", "arguments": {"namespace": "w12-test"}}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
let text = result["messages"][0]["content"]["text"].as_str().unwrap();
assert!(text.contains("w12-test"));
}
#[test]
fn test_jsonrpc_prompts_get_unknown_name_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(6)),
method: "prompts/get".into(),
params: json!({"name": "no-such-prompt"}),
};
let resp = invoke_handle_request(&conn, &req);
let err = resp.error.unwrap();
assert_eq!(err.code, -32602);
}
#[test]
fn test_jsonrpc_prompts_get_missing_name_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(7)),
method: "prompts/get".into(),
params: json!({}),
};
let resp = invoke_handle_request(&conn, &req);
let err = resp.error.unwrap();
assert_eq!(err.code, -32602);
}
#[test]
fn test_jsonrpc_prompts_get_memory_workflow() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(8)),
method: "prompts/get".into(),
params: json!({"name": "memory-workflow"}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert!(result["messages"].is_array());
}
#[test]
fn test_jsonrpc_tools_call_empty_tool_name_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(9)),
method: "tools/call".into(),
params: json!({"name": ""}),
};
let resp = invoke_handle_request(&conn, &req);
let err = resp.error.unwrap();
assert_eq!(err.code, -32602);
}
#[test]
fn test_jsonrpc_tools_call_arguments_not_object_uses_empty() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = RpcRequest {
jsonrpc: "2.0".into(),
id: Some(json!(10)),
method: "tools/call".into(),
params: json!({"name": "memory_capabilities", "arguments": null}),
};
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn test_jsonrpc_tools_call_unicode_in_args() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_store",
json!({"title": "тест", "content": "日本語 ✨", "namespace": "w12-unicode"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn test_jsonrpc_dispatch_line_with_id_zero_treated_as_request() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let line = r#"{"jsonrpc":"2.0","id":0,"method":"tools/list"}"#;
let resp = dispatch_line(&conn, line);
assert!(resp.is_some());
}
#[test]
fn test_jsonrpc_dispatch_line_string_id_passes_through() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let line = r#"{"jsonrpc":"2.0","id":"call-abc","method":"tools/list"}"#;
let resp = dispatch_line(&conn, line).expect("expected response");
assert_eq!(resp.id, json!("call-abc"));
}
#[test]
fn test_build_namespace_chain_global_only() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let chain = super::build_namespace_chain(&conn, "*");
assert_eq!(chain, vec!["*".to_string()]);
}
#[test]
fn test_build_namespace_chain_simple_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let chain = super::build_namespace_chain(&conn, "w12-flat");
assert!(chain.contains(&"*".to_string()));
assert!(chain.contains(&"w12-flat".to_string()));
}
#[test]
fn test_build_namespace_chain_nested_yields_ancestors() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let chain = super::build_namespace_chain(&conn, "a/b/c");
assert_eq!(chain.first().unwrap(), "*");
assert!(chain.contains(&"a/b/c".to_string()));
let pos_a = chain.iter().position(|s| s == "a").unwrap();
let pos_ab = chain.iter().position(|s| s == "a/b").unwrap();
let pos_abc = chain.iter().position(|s| s == "a/b/c").unwrap();
assert!(pos_a < pos_ab && pos_ab < pos_abc);
}
#[test]
fn test_build_namespace_chain_with_explicit_parent() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let parent_mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-explicit-grand".into(),
title: "g".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let pid = db::insert(&conn, &parent_mem).unwrap();
db::set_namespace_standard(&conn, "w12-explicit-grand", &pid, None).unwrap();
let mut child_mem = parent_mem.clone();
child_mem.id = uuid::Uuid::new_v4().to_string();
child_mem.namespace = "w12-explicit-leaf".into();
let cid = db::insert(&conn, &child_mem).unwrap();
db::set_namespace_standard(&conn, "w12-explicit-leaf", &cid, Some("w12-explicit-grand"))
.unwrap();
let chain = super::build_namespace_chain(&conn, "w12-explicit-leaf");
assert!(chain.contains(&"w12-explicit-grand".to_string()));
assert!(chain.contains(&"w12-explicit-leaf".to_string()));
}
#[test]
fn test_extract_governance_default_when_metadata_absent() {
let mem_val = json!({"id": "x"});
let gov = super::extract_governance(&mem_val);
assert!(gov.is_object() || gov.is_null());
}
#[test]
fn test_extract_governance_default_when_metadata_invalid() {
let mem_val = json!({"metadata": {"governance": {"unknown": "policy"}}});
let gov = super::extract_governance(&mem_val);
assert!(gov.is_object());
}
#[test]
fn test_messages_namespace_for_plain_id() {
assert_eq!(super::messages_namespace_for("alice"), "_messages/alice");
}
#[test]
fn test_messages_namespace_for_ai_prefixed_id() {
let ns = super::messages_namespace_for("ai:claude@host:pid-1");
assert!(ns.starts_with("_messages/"));
assert!(ns.contains("ai:"));
}
#[test]
fn test_inject_namespace_standard_no_namespace_no_global() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mut resp = make_recall_response(vec![]);
let before = resp.clone();
super::inject_namespace_standard(&conn, None, &mut resp);
assert_eq!(resp, before);
}
#[test]
fn handle_promote_default_tier_to_long() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "w12-tier-promote".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_promote", json!({"id": id}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["promoted"], true);
assert_eq!(val["mode"], "tier");
assert_eq!(val["tier"], Tier::Long.as_str());
}
#[test]
fn handle_store_dedup_updates_existing() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req1 = make_tools_call(
"memory_store",
json!({
"title": "dup-title",
"content": "first",
"namespace": "w12-dedup",
"tier": Tier::Mid.as_str(),
}),
);
let resp1 = invoke_handle_request(&conn, &req1);
assert!(resp1.error.is_none());
let text1 = resp1.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val1: Value = serde_json::from_str(&text1).unwrap();
let id1 = val1["id"].as_str().unwrap().to_string();
let req2 = make_tools_call(
"memory_store",
json!({
"title": "dup-title",
"content": "second-update",
"namespace": "w12-dedup",
"tier": Tier::Long.as_str(),
}),
);
let resp2 = invoke_handle_request(&conn, &req2);
assert!(resp2.error.is_none());
let text2 = resp2.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val2: Value = serde_json::from_str(&text2).unwrap();
assert_eq!(val2["id"], id1);
assert_eq!(val2["duplicate"], true);
assert_eq!(val2["action"], "updated existing memory");
}
#[test]
fn handle_subscribe_with_registered_agent_succeeds() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
db::register_agent(&conn, &resolved, "human", &[]).unwrap();
let req = make_tools_call(
"memory_subscribe",
json!({
"url": "https://example.com/hook",
"events": "memory_store,memory_delete",
"namespace_filter": "w12-sub",
"secret": "mcp-sub-test-secret",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["id"].is_string());
assert_eq!(val["url"], "https://example.com/hook");
}
#[test]
fn handle_subscribe_invalid_url_after_registered() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
db::register_agent(&conn, &resolved, "human", &[]).unwrap();
let req = make_tools_call(
"memory_subscribe",
json!({"url": "not-a-url-at-all", "secret": "mcp-sub-test-secret"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_namespace_set_standard_with_valid_governance() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-gov-ok".into(),
title: "p".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_namespace_set_standard",
json!({
"namespace": "w12-gov-ok",
"id": id,
"governance": {
"write": "any",
"promote": "any",
"delete": "owner",
"approver": "human",
},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["set"], true);
assert!(val["governance"].is_object());
}
#[test]
fn handle_namespace_set_standard_with_parent() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-parent-ns".into(),
title: "p".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_namespace_set_standard",
json!({
"namespace": "w12-parent-ns",
"id": id,
"parent": "w12-grand-ns",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["parent"], "w12-grand-ns");
}
#[test]
fn handle_get_resolves_by_prefix_and_includes_links() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-prefix".into(),
title: "T".into(),
content: "C".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_get", json!({"id": id}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["links"].is_array());
assert_eq!(val["id"], id);
}
#[test]
fn handle_link_creates_link_between_existing_memories() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-link".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
let req = make_tools_call(
"memory_link",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["linked"], true);
}
#[test]
fn handle_get_links_returns_outbound_and_inbound() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-getlinks".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
db::create_link(&conn, &src_id, &tgt_id, "supersedes").unwrap();
let req = make_tools_call("memory_get_links", json!({"id": src_id}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["count"].as_u64().unwrap() >= 1);
}
#[test]
fn handle_kg_timeline_with_seeded_link_returns_event() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-tl".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
let req = make_tools_call(
"memory_kg_timeline",
json!({"source_id": src_id, "limit": 10}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["count"], 1);
let events = val["events"].as_array().unwrap();
assert_eq!(events[0]["target_id"], tgt_id);
}
#[test]
fn handle_kg_query_by_source_uri_returns_roots() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mk = |ns: &str, t: &str, uri: Option<&str>| Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: ns.into(),
title: t.into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: uri.map(str::to_string),
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let uri = "doc:test-uplift/abc#section-1";
db::insert(&conn, &mk("kg-uplift", "a", Some(uri))).unwrap();
db::insert(&conn, &mk("kg-uplift", "b", Some(uri))).unwrap();
db::insert(&conn, &mk("kg-uplift", "c", None)).unwrap();
let req = make_tools_call(
"memory_kg_query",
json!({"by_source_uri": uri, "namespace": "kg-uplift"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "{resp:?}");
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["by_source_uri"], uri);
assert_eq!(val["count"], 2);
let mems = val["memories"].as_array().unwrap();
assert_eq!(mems.len(), 2);
assert!(mems.iter().all(|m| m["depth"].as_u64() == Some(0)));
}
#[test]
fn handle_kg_query_by_source_uri_rejects_invalid_uri() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_kg_query", json!({"by_source_uri": " "}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("source_id is required"));
}
#[test]
fn handle_kg_query_by_source_uri_validates_uri_shape() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_query",
json!({"by_source_uri": "bad\u{0007}uri"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_kg_query_with_seeded_link_returns_node() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-kgq".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
let req = make_tools_call(
"memory_kg_query",
json!({"source_id": src_id, "max_depth": 1, "limit": 10}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["count"].as_u64().unwrap() >= 1);
assert!(val["paths"].is_array());
}
#[test]
fn handle_archive_list_with_pagination() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_archive_list", json!({"limit": 100, "offset": 50}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_pending_list_with_status_filter() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
for status in &["pending", "approved", "rejected"] {
let req = make_tools_call(
"memory_pending_list",
json!({"status": status, "limit": 50}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "failed for status={status}");
}
}
#[test]
fn handle_pending_approve_with_seeded_pending_action() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let pending_id = db::queue_pending_action(
&conn,
crate::models::GovernedAction::Promote,
"w12-approve",
None,
"human:requestor",
&json!({"id": "00000000-0000-0000-0000-000000000000"}),
)
.unwrap();
let req = make_tools_call(
"memory_pending_approve",
json!({"id": pending_id, "agent_id": "human:approver"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert!(result.is_object());
}
#[test]
fn handle_pending_reject_with_seeded_pending_action() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let pending_id = db::queue_pending_action(
&conn,
crate::models::GovernedAction::Promote,
"w12-reject",
None,
"human:requestor",
&json!({"id": "00000000-0000-0000-0000-000000000000"}),
)
.unwrap();
let req = make_tools_call(
"memory_pending_reject",
json!({"id": pending_id, "agent_id": "human:rejector"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["rejected"], true);
}
#[test]
fn handle_session_start_toon_format_default() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_session_start", json!({"namespace": "w12-toon"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let result = resp.result.unwrap();
assert!(result["content"][0]["text"].is_string());
}
#[test]
fn handle_search_explicit_toon_format() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_search",
json!({"query": "anything", "format": "toon"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_recall_explicit_toon_format() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_recall", json!({"context": "ctx", "format": "toon"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_list_explicit_toon_compact_format() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_list",
json!({"namespace": "w12-toon-list", "format": "toon_compact"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_search_with_namespace_and_tier_filters() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_search",
json!({
"query": "test query",
"namespace": "w12-search",
"tier": Tier::Long.as_str(),
"limit": 10,
"agent_id": "ai:bot",
"format": "json",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_search_invalid_agent_id_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_search",
json!({"query": "x", "agent_id": "bad agent !!"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_search_invalid_as_agent_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_search",
json!({"query": "x", "as_agent": "BAD AS AGENT"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_recall_invalid_as_agent_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_recall",
json!({"context": "x", "as_agent": "INVALID NS"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_recall_with_context_tokens() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_recall",
json!({
"context": "main",
"context_tokens": ["recent", "tokens", "from", "convo"],
"format": "json",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_recall_with_budget_tokens_positive() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_recall",
json!({"context": "x", "budget_tokens": 1000, "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["tokens_used"].is_u64() || val["tokens_used"].is_i64());
assert_eq!(val["budget_tokens"], 1000);
}
#[test]
fn handle_recall_invalid_namespace_filter_passes_through() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_recall",
json!({
"context": "x",
"namespace": "w12-no-such-namespace",
"format": "json",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_list_with_tier_filter() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_list",
json!({
"namespace": "w12-list-tier",
"tier": Tier::Long.as_str(),
"agent_id": "ai:bot",
"limit": 25,
"format": "json",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_list_invalid_tier_treated_as_none() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_list",
json!({"namespace": "w12-list-bad-tier", "tier": "ULTRAMID", "format": "json"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_get_taxonomy_invalid_depth_clamps_to_max() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_get_taxonomy",
json!({"depth": 100_000_u64, "limit": 50_000_u64}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_archive_purge_no_filter_purges_all() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_archive_purge", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_check_duplicate_invalid_title_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_check_duplicate",
json!({"title": "", "content": "anything"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_check_duplicate_invalid_namespace_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_check_duplicate",
json!({"title": "T", "content": "C", "namespace": "BAD NS"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_entity_register_with_explicit_agent_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_register",
json!({
"canonical_name": "Org Alpha",
"namespace": "w12-orgs",
"aliases": ["alpha", "α"],
"agent_id": "ai:bot",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_entity_register_invalid_explicit_agent_id() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_register",
json!({
"canonical_name": "Org Beta",
"namespace": "w12-orgs",
"agent_id": "BAD AGENT !!",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_entity_get_by_alias_no_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_entity_get_by_alias", json!({"alias": "any-alias"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_inbox_with_message_seeded() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let notify = make_tools_call(
"memory_notify",
json!({
"target_agent_id": "alice-w12",
"title": "ping",
"payload": "are you there?",
"tier": Tier::Short.as_str(),
}),
);
let _ = invoke_handle_request(&conn, ¬ify);
let inbox = make_tools_call(
"memory_inbox",
json!({"agent_id": "alice-w12", "limit": 10}),
);
let resp = invoke_handle_request(&conn, &inbox);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["count"].as_u64().unwrap() >= 1);
assert_eq!(val["agent_id"], "alice-w12");
}
#[test]
fn handle_consolidate_succeeds_when_source_was_standard() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem_a = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-cons-warn".into(),
title: "a".into(),
content: "alpha".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut mem_b = mem_a.clone();
mem_b.id = uuid::Uuid::new_v4().to_string();
mem_b.title = "b".into();
mem_b.content = "beta".into();
let id_a = db::insert(&conn, &mem_a).unwrap();
let id_b = db::insert(&conn, &mem_b).unwrap();
db::set_namespace_standard(&conn, "w12-cons-warn", &id_a, None).unwrap();
let req = make_tools_call(
"memory_consolidate",
json!({
"ids": [id_a, id_b],
"title": "merged-warn",
"summary": "merged summary",
"namespace": "w12-cons-warn",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["id"].is_string());
assert_eq!(val["consolidated"], 2);
}
#[test]
fn handle_update_clears_expires_with_empty_string() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Short,
namespace: "w12-clear-exp".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: Some(chrono::Utc::now().to_rfc3339()),
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_update", json!({"id": id, "expires_at": ""}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert!(result.is_object());
}
#[test]
fn handle_update_change_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "w12-update-ns".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_update",
json!({
"id": id,
"namespace": "w12-update-ns-new",
"tags": ["a", "b"],
"title": "new-title",
"content": "new-content",
"tier": Tier::Long.as_str(),
"priority": 8_i64,
"confidence": 0.9_f64,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
}
#[test]
fn handle_delete_with_prefix_id_lookup() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: "w12-delete-prefix".into(),
title: "t".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_delete", json!({"id": id}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["deleted"], true);
}
#[test]
fn handle_unsubscribe_after_subscribe_removes_row() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
db::register_agent(&conn, &resolved, "human", &[]).unwrap();
let sub = make_tools_call(
"memory_subscribe",
json!({"url": "https://example.com/hook2", "secret": "mcp-sub-test-secret"}),
);
let sub_resp = invoke_handle_request(&conn, &sub);
let sub_text = sub_resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let sub_val: Value = serde_json::from_str(&sub_text).unwrap();
let id = sub_val["id"].as_str().unwrap().to_string();
let unsub = make_tools_call("memory_unsubscribe", json!({"id": id}));
let unsub_resp = invoke_handle_request(&conn, &unsub);
assert!(unsub_resp.error.is_none());
let unsub_text = unsub_resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let unsub_val: Value = serde_json::from_str(&unsub_text).unwrap();
assert!(
unsub_val["removed"] == json!(true) || unsub_val["removed"] == json!(1),
"unexpected removed value: {:?}",
unsub_val["removed"]
);
}
#[test]
fn handle_list_subscriptions_after_subscribe_returns_one() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
db::register_agent(&conn, &resolved, "human", &[]).unwrap();
let sub = make_tools_call(
"memory_subscribe",
json!({"url": "https://example.com/listed", "secret": "mcp-sub-test-secret"}),
);
let _ = invoke_handle_request(&conn, &sub);
let req = make_tools_call("memory_list_subscriptions", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val.get("subscriptions").is_some() || val.get("count").is_some() || val.is_array());
}
#[test]
fn test_inject_namespace_standard_dedup_keeps_originals_order() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let std_id = seed_namespace_standard(&conn, "w12-order", "S");
let mems = vec![
json!({"id": "first", "title": "f"}),
json!({"id": std_id, "title": "S"}),
json!({"id": "third", "title": "t"}),
];
let mut resp = make_recall_response(mems);
super::inject_namespace_standard(&conn, Some("w12-order"), &mut resp);
let memories = resp["memories"].as_array().unwrap();
assert_eq!(memories.len(), 2);
assert_eq!(memories[0]["id"], "first");
assert_eq!(memories[1]["id"], "third");
}
fn i4_insert_test_memory(conn: &rusqlite::Connection, id: &str) {
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO memories (
id, tier, namespace, title, content, created_at, updated_at, metadata
) VALUES (?1, 'short', 'team/eng', ?2, 'body', ?3, ?3, '{\"scope\":\"public\"}')",
rusqlite::params![id, format!("title-{id}"), now],
)
.unwrap();
}
fn i4_decode_response_payload(resp: &RpcResponse) -> Value {
let text = resp
.result
.as_ref()
.expect("expected ok response")
.get("content")
.and_then(|c| c.get(0))
.and_then(|c| c.get("text"))
.and_then(Value::as_str)
.expect("response wrapper must have content[0].text");
serde_json::from_str(text).expect("response payload must be JSON")
}
#[test]
fn i4_replay_no_links_returns_empty_array() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-empty");
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-empty"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["memory_id"], "mem-empty");
assert_eq!(payload["count"], 0);
let transcripts = payload["transcripts"].as_array().unwrap();
assert!(transcripts.is_empty());
}
#[test]
fn i4_replay_single_transcript_returns_content_and_metadata() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-single");
let body = "the canonical conversation that produced this memory";
let t = crate::transcripts::store(&conn, "team/eng", body, None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-single", &t.id, Some(2), Some(20)).unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-single"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["memory_id"], "mem-single");
assert_eq!(payload["count"], 1);
let transcripts = payload["transcripts"].as_array().unwrap();
assert_eq!(transcripts.len(), 1);
let entry = &transcripts[0];
assert_eq!(entry["id"], t.id);
assert_eq!(entry["content"], body);
assert_eq!(entry["span_start"], 2);
assert_eq!(entry["span_end"], 20);
assert_eq!(entry["original_size"].as_i64().unwrap(), body.len() as i64);
assert!(entry["compressed_size"].as_i64().unwrap() > 0);
assert!(entry["created_at"].is_string());
assert!(entry.get("truncated").is_none());
}
#[test]
fn i4_replay_multiple_transcripts_chronological_order() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-multi");
let older = crate::transcripts::store(&conn, "team/eng", "older body", None).unwrap();
let backdate =
(chrono::Utc::now() - chrono::Duration::seconds(crate::SECS_PER_HOUR)).to_rfc3339();
conn.execute(
"UPDATE memory_transcripts SET created_at = ?1 WHERE id = ?2",
rusqlite::params![backdate, older.id],
)
.unwrap();
let newer = crate::transcripts::store(&conn, "team/eng", "newer body", None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-multi", &newer.id, None, None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-multi", &older.id, None, None).unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-multi"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
let transcripts = payload["transcripts"].as_array().unwrap();
assert_eq!(transcripts.len(), 2);
assert_eq!(transcripts[0]["id"], older.id);
assert_eq!(transcripts[0]["content"], "older body");
assert_eq!(transcripts[1]["id"], newer.id);
assert_eq!(transcripts[1]["content"], "newer body");
}
#[test]
fn i4_replay_large_transcript_truncates_when_verbose_false() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-large");
let body: String = "abcdefghij".repeat(20_000); assert!(body.len() > REPLAY_VERBOSE_THRESHOLD_BYTES as usize);
let t = crate::transcripts::store(&conn, "team/eng", &body, None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-large", &t.id, None, None).unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-large"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
let transcripts = payload["transcripts"].as_array().unwrap();
assert_eq!(transcripts.len(), 1);
let entry = &transcripts[0];
assert_eq!(entry["truncated"], true);
assert!(
entry.get("content").is_none(),
"content must be OMITTED when truncated; got: {entry}"
);
assert_eq!(entry["original_size"].as_i64().unwrap(), body.len() as i64);
assert!(entry["compressed_size"].as_i64().unwrap() > 0);
}
#[test]
fn i4_replay_large_transcript_returns_content_when_verbose_true() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-large-verbose");
let body: String = "abcdefghij".repeat(20_000);
let t = crate::transcripts::store(&conn, "team/eng", &body, None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-large-verbose", &t.id, None, None).unwrap();
let req = make_tools_call(
"memory_replay",
json!({"memory_id": "mem-large-verbose", "verbose": true}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
let transcripts = payload["transcripts"].as_array().unwrap();
assert_eq!(transcripts.len(), 1);
let entry = &transcripts[0];
assert!(
entry.get("truncated").is_none(),
"verbose=true must NOT set truncated"
);
assert_eq!(entry["content"].as_str().unwrap(), body);
}
#[test]
fn i4_replay_missing_memory_id_yields_handler_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_replay", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(
resp.error.is_none(),
"expected handler-level error, not RPC error"
);
let result = resp.result.expect("must surface a result envelope");
assert_eq!(result["isError"], true);
}
#[test]
fn i4_replay_skips_dangling_transcript_link() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-dangling");
let live = crate::transcripts::store(&conn, "team/eng", "live body", None).unwrap();
let pruned = crate::transcripts::store(&conn, "team/eng", "pruned body", None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-dangling", &live.id, None, None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-dangling", &pruned.id, None, None).unwrap();
conn.execute("PRAGMA foreign_keys = OFF", []).unwrap();
conn.execute(
"DELETE FROM memory_transcripts WHERE id = ?1",
rusqlite::params![pruned.id],
)
.unwrap();
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-dangling"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
let transcripts = payload["transcripts"].as_array().unwrap();
assert_eq!(
transcripts.len(),
1,
"only the live transcript should appear; pruned id is silently dropped"
);
assert_eq!(transcripts[0]["id"], live.id);
}
fn reflect_test_seed_source(
conn: &rusqlite::Connection,
namespace: &str,
title: &str,
depth: i32,
) -> String {
let now = chrono::Utc::now().to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: namespace.to_string(),
title: title.to_string(),
content: format!("seed body for {title}"),
tags: vec!["reflect-test".to_string()],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: json!({"agent_id": "test-agent-reflect"}),
reflection_depth: depth,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(conn, &mem).unwrap()
}
fn reflect_test_seed_governance(
conn: &rusqlite::Connection,
namespace: &str,
governance: Value,
) {
let now = chrono::Utc::now().to_rfc3339();
let metadata = json!({
"agent_id": "test-agent-reflect",
"governance": governance,
});
let standard = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: format!("_standards-{namespace}"),
title: format!("standard for {namespace}"),
content: "reflect-test policy".to_string(),
tags: vec![],
priority: 9,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata,
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let std_id = db::insert(conn, &standard).unwrap();
db::set_namespace_standard(conn, namespace, &std_id, None).unwrap();
}
#[test]
fn handle_reflect_happy_path_single_source_returns_envelope() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/reflect-a", "src-1", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [src],
"title": "pattern: alpha",
"content": "synthesised reflection content",
"namespace": "team/reflect-a",
"tier": Tier::Mid.as_str(),
"priority": 7,
"confidence": 0.9,
"tags": ["reflection", "alpha"],
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert!(payload["id"].is_string());
assert_eq!(payload["reflection_depth"], 1);
assert_eq!(payload["namespace"], "team/reflect-a");
let reflects_on = payload["reflects_on"].as_array().unwrap();
assert_eq!(reflects_on.len(), 1);
}
#[test]
fn handle_reflect_happy_path_metadata_object_is_accepted() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/reflect-meta", "src-1", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [src],
"title": "with metadata",
"content": "body",
"metadata": {"custom_field": "abc"},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert!(payload["id"].is_string());
}
#[test]
fn handle_reflect_omitted_namespace_defaults_to_first_source_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/reflect-defns", "src-1", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [src],
"title": "defaulted namespace",
"content": "body",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["namespace"], "team/reflect-defns");
}
#[test]
fn handle_reflect_explicit_agent_id_is_honoured() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/reflect-aid", "src-1", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [src],
"title": "agent override",
"content": "body",
"namespace": "team/reflect-aid",
"agent_id": "ai:explicit-agent",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
let new_id = payload["id"].as_str().unwrap();
let stored = db::get(&conn, new_id).unwrap().unwrap();
assert_eq!(
stored.metadata["agent_id"].as_str(),
Some("ai:explicit-agent")
);
}
#[test]
fn handle_reflect_agent_id_from_metadata_blob_is_honoured() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/reflect-mid", "src-1", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [src],
"title": "agent from metadata",
"content": "body",
"namespace": "team/reflect-mid",
"metadata": {"agent_id": "ai:meta-agent"},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
let new_id = payload["id"].as_str().unwrap();
let stored = db::get(&conn, new_id).unwrap().unwrap();
assert_eq!(stored.metadata["agent_id"].as_str(), Some("ai:meta-agent"));
}
#[test]
fn handle_reflect_missing_source_ids_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_reflect", json!({"title": "t", "content": "c"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("source_ids"), "got {text}");
}
#[test]
fn handle_reflect_empty_source_ids_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_reflect",
json!({"source_ids": [], "title": "t", "content": "c"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("cannot be empty"), "got {text}");
}
#[test]
fn handle_reflect_non_string_source_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": ["valid-id", 42, "another"],
"title": "t",
"content": "c",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("source_ids[1]"), "got {text}");
}
#[test]
fn handle_reflect_missing_title_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/r-mt", "src", 0);
let req = make_tools_call(
"memory_reflect",
json!({"source_ids": [src], "content": "c"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("title"), "got {text}");
}
#[test]
fn handle_reflect_missing_content_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/r-mc", "src", 0);
let req = make_tools_call("memory_reflect", json!({"source_ids": [src], "title": "t"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("content"), "got {text}");
}
#[test]
fn handle_reflect_invalid_tier_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = reflect_test_seed_source(&conn, "team/r-tier", "src", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [src],
"title": "t",
"content": "c",
"tier": "ephemeral",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("invalid tier"), "got {text}");
}
#[test]
fn handle_reflect_source_not_found_returns_error_string() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": ["nonexistent-id"],
"title": "t",
"content": "c",
"namespace": "team/r-nf",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("source memory not found"), "got {text}",);
assert!(text.contains("nonexistent-id"), "got {text}");
}
#[test]
fn handle_reflect_depth_exceeded_returns_typed_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
reflect_test_seed_governance(
&conn,
"team/r-depth",
json!({
"write": "any",
"max_reflection_depth": 1,
}),
);
let s1 = reflect_test_seed_source(&conn, "team/r-depth", "src-1", 1);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [s1],
"title": "would be depth 2",
"content": "body",
"namespace": "team/r-depth",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("REFLECTION_DEPTH_EXCEEDED"),
"expected typed error prefix; got {text}",
);
assert!(text.contains("depth 2"), "got {text}");
assert!(text.contains("max_reflection_depth 1"), "got {text}",);
assert!(text.contains("namespace='team/r-depth'"), "got {text}",);
}
#[test]
fn handle_reflect_approval_gate_queues_pending_above_threshold() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
reflect_test_seed_governance(
&conn,
"team/r-approve",
json!({"require_approval_above_depth": 1}),
);
let s1 = reflect_test_seed_source(&conn, "team/r-approve", "src-1", 1);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [s1],
"title": "would need approval",
"content": "body",
"namespace": "team/r-approve",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["status"], "pending");
assert!(payload["pending_id"].is_string());
assert_eq!(payload["action"], "reflect");
assert_eq!(payload["namespace"], "team/r-approve");
assert_eq!(payload["proposed_depth"], 2);
assert_eq!(payload["require_approval_above_depth"], 1);
}
#[test]
fn handle_reflect_approval_gate_under_threshold_proceeds() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
reflect_test_seed_governance(
&conn,
"team/r-under",
json!({"require_approval_above_depth": 5}),
);
let s1 = reflect_test_seed_source(&conn, "team/r-under", "src-1", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [s1],
"title": "under threshold",
"content": "body",
"namespace": "team/r-under",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert!(payload["status"].as_str() != Some("pending"));
assert!(payload["id"].is_string());
assert_eq!(payload["reflection_depth"], 1);
}
#[test]
fn handle_check_duplicate_missing_title_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_check_duplicate", json!({"content": "c"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("title"), "got {text}");
}
#[test]
fn handle_check_duplicate_missing_content_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_check_duplicate", json!({"title": "t"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("content"), "got {text}");
}
#[test]
fn handle_check_duplicate_no_embedder_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_check_duplicate",
json!({
"title": "duplicate-check",
"content": "body",
"namespace": "team/dup",
"threshold": 0.85,
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("requires the embedder"),
"expected embedder-required error; got {text}",
);
}
#[test]
fn handle_check_duplicate_whitespace_only_namespace_is_filtered() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_check_duplicate",
json!({
"title": "t",
"content": "c",
"namespace": " ",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("requires the embedder"),
"expected fallthrough to embedder gate; got {text}",
);
}
#[test]
fn handle_check_duplicate_explicit_threshold_is_accepted() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_check_duplicate",
json!({
"title": "t",
"content": "c",
"threshold": 0.92,
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("requires the embedder"), "got {text}");
}
#[test]
fn handle_quota_status_with_agent_id_returns_single_envelope() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_quota_status", json!({"agent_id": "agent-status-a"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["agent_id"], "agent-status-a");
assert!(payload["quota"].is_object(), "expected quota object");
assert!(payload["count"].is_null());
assert!(payload["quotas"].is_null());
}
#[test]
fn handle_quota_status_without_agent_id_returns_list_envelope() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = invoke_handle_request(
&conn,
&make_tools_call("memory_quota_status", json!({"agent_id": "agent-a"})),
);
let _ = invoke_handle_request(
&conn,
&make_tools_call("memory_quota_status", json!({"agent_id": "agent-b"})),
);
let req = make_tools_call("memory_quota_status", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert!(payload["count"].is_number());
assert!(payload["quotas"].is_array());
assert!(payload["count"].as_u64().unwrap() >= 2);
assert!(payload["agent_id"].is_null());
assert!(payload["quota"].is_null());
}
#[test]
fn handle_entity_register_missing_namespace_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_entity_register", json!({"canonical_name": "Pluto"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("namespace"), "got {text}");
}
#[test]
fn handle_entity_register_metadata_object_is_accepted() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_register",
json!({
"canonical_name": "Charon",
"namespace": "team/dwarf",
"metadata": {"orbit": "outer"},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert!(payload["entity_id"].is_string());
assert_eq!(payload["canonical_name"], "Charon");
assert_eq!(payload["namespace"], "team/dwarf");
}
#[test]
fn handle_kg_invalidate_missing_target_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({"source_id": "abc", "relation": "related_to"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("target_id"), "got {text}");
}
#[test]
fn handle_kg_invalidate_missing_relation_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({"source_id": "abc", "target_id": "def"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("relation"), "got {text}");
}
#[test]
fn handle_reflect_approval_gate_uses_default_namespace() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
reflect_test_seed_governance(
&conn,
"team/r-defgate",
json!({"require_approval_above_depth": 0}),
);
let s1 = reflect_test_seed_source(&conn, "team/r-defgate", "src", 0);
let req = make_tools_call(
"memory_reflect",
json!({
"source_ids": [s1],
"title": "default-namespace gate",
"content": "body",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["status"], "pending");
assert_eq!(payload["namespace"], "team/r-defgate");
assert_eq!(payload["proposed_depth"], 1);
}
#[test]
fn handle_kg_invalidate_missing_source_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({"target_id": "11111111-1111-1111-1111-111111111111", "relation": "related_to"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("source_id"), "got {text}");
}
#[test]
fn handle_kg_invalidate_malformed_source_id_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({
"source_id": "abc\u{0000}def",
"target_id": "11111111-1111-1111-1111-111111111111",
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_kg_invalidate_orphan_link_uses_global_namespace_fallback() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-orphan".into(),
title: "orphan-src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "orphan-tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
conn.pragma_update(None, "foreign_keys", false).unwrap();
conn.execute(
"DELETE FROM memories WHERE id = ?1",
rusqlite::params![&src_id],
)
.unwrap();
conn.pragma_update(None, "foreign_keys", true).unwrap();
let req = make_tools_call(
"memory_kg_invalidate",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "unexpected err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["found"], true);
}
#[test]
fn handle_kg_timeline_invalid_until_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_kg_timeline",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"until": "not-a-timestamp",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_kg_timeline_missing_source_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_kg_timeline", json!({}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("source_id"), "got {text}");
}
#[test]
fn handle_find_paths_missing_source_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_find_paths",
json!({"target_id": "11111111-1111-1111-1111-111111111111"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("source_id"), "got {text}");
}
#[test]
fn handle_find_paths_missing_target_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_find_paths",
json!({"source_id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("target_id"), "got {text}");
}
#[test]
fn handle_find_paths_invalid_source_id_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_find_paths",
json!({
"source_id": "abc\u{0000}def",
"target_id": "11111111-1111-1111-1111-111111111111",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_find_paths_happy_path_with_explicit_depth_and_limit() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mk = |title: &str| Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-fp".into(),
title: title.into(),
content: "x".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let a = db::insert(&conn, &mk("a")).unwrap();
let b = db::insert(&conn, &mk("b")).unwrap();
let c = db::insert(&conn, &mk("c")).unwrap();
db::create_link(&conn, &a, &b, "related_to").unwrap();
db::create_link(&conn, &b, &c, "related_to").unwrap();
let req = make_tools_call(
"memory_find_paths",
json!({
"source_id": a,
"target_id": c,
"max_depth": 5_u64,
"max_results": 10_u64,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["count"].as_u64().unwrap() >= 1, "got {val}");
let paths = val["paths"].as_array().unwrap();
assert!(!paths.is_empty());
}
#[test]
fn handle_find_paths_zero_depth_surfaces_db_error_verbatim() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_find_paths",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"target_id": "11111111-1111-1111-1111-111111111111",
"max_depth": 0_u64,
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("max_depth"), "got {text}");
}
#[test]
fn handle_find_paths_excessive_depth_surfaces_max_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_find_paths",
json!({
"source_id": "00000000-0000-0000-0000-000000000000",
"target_id": "11111111-1111-1111-1111-111111111111",
"max_depth": 1_000_000_u64,
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("max_depth") || text.contains("FIND_PATHS_MAX_DEPTH"),
"got {text}"
);
}
#[test]
fn handle_find_paths_include_invalidated_true_round_trip() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-fp-inv".into(),
title: "solo".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let id = db::insert(&conn, &mem).unwrap();
let req = make_tools_call(
"memory_find_paths",
json!({
"source_id": id,
"target_id": id,
"include_invalidated": true,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert!(val["count"].as_u64().unwrap() >= 1);
}
#[test]
fn handle_entity_get_by_alias_missing_alias_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_entity_get_by_alias", json!({}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("alias"), "got {text}");
}
#[test]
fn handle_entity_get_by_alias_invalid_namespace_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_get_by_alias",
json!({"alias": "any", "namespace": "BAD NS"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_entity_get_by_alias_registered_alias_resolves() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let reg = make_tools_call(
"memory_entity_register",
json!({
"canonical_name": "Acme Inc",
"namespace": "w12-entities",
"aliases": ["Acme", "ACME"],
}),
);
let reg_resp = invoke_handle_request(&conn, ®);
assert!(
reg_resp.error.is_none(),
"entity_register err: {:?}",
reg_resp.error
);
let req = make_tools_call(
"memory_entity_get_by_alias",
json!({"alias": "Acme", "namespace": "w12-entities"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["found"], true, "got {val}");
assert_eq!(val["canonical_name"], "Acme Inc");
assert_eq!(val["namespace"], "w12-entities");
assert!(val["aliases"].is_array());
}
#[test]
fn handle_entity_get_by_alias_whitespace_only_namespace_treated_as_none() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_entity_get_by_alias",
json!({"alias": "x", "namespace": " "}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
}
#[test]
fn handle_verify_missing_required_args_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_verify", json!({}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("link_id") || text.contains("source_id"),
"got {text}"
);
}
#[test]
fn handle_verify_source_id_without_target_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_verify",
json!({"source_id": "00000000-0000-0000-0000-000000000000"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_verify_malformed_link_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_verify", json!({"link_id": "totally-bad-shape"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("link_id"), "got {text}");
}
#[test]
fn handle_verify_invalid_link_rejected_by_validator() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_verify",
json!({
"source_id": "not a uuid",
"target_id": "11111111-1111-1111-1111-111111111111",
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn handle_verify_missing_link_returns_not_found_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-vfn".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
let req = make_tools_call(
"memory_verify",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(text.contains("link not found"), "got {text}");
}
#[test]
fn handle_verify_unsigned_link_reports_unsigned_and_null_fields() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-vu".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
let attest = db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", None)
.expect("create_link_signed (unsigned)");
assert_eq!(attest, "unsigned");
let req = make_tools_call(
"memory_verify",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["signature_verified"], false);
assert_eq!(val["attest_level"], "unsigned");
assert!(val["signed_by"].is_null());
assert!(val["signed_at"].is_null());
}
#[test]
fn handle_verify_link_id_composite_form_resolves_same_row() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-vc".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", None)
.expect("create_link_signed");
let composite = format!("{src_id}--related_to-->{tgt_id}");
let req = make_tools_call("memory_verify", json!({"link_id": composite}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["signature_verified"], false);
assert_eq!(val["attest_level"], "unsigned");
}
fn verify_key_env_guard() -> &'static std::sync::Mutex<()> {
crate::identity::keypair::key_dir_env_lock()
}
#[test]
fn handle_verify_signed_link_without_local_pubkey_reports_stored_attest_and_unverified() {
let _g = verify_key_env_guard()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let prev = std::env::var("AI_MEMORY_KEY_DIR").ok();
let tmp = tempfile::TempDir::new().expect("tempdir");
unsafe {
std::env::set_var("AI_MEMORY_KEY_DIR", tmp.path());
}
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-vnk".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
let alice = crate::identity::keypair::generate("alice").unwrap();
let attest = db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", Some(&alice))
.expect("create_link_signed");
assert_eq!(attest, "self_signed");
let req = make_tools_call(
"memory_verify",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["signature_verified"], false);
assert_eq!(val["attest_level"], "self_signed");
unsafe {
match prev {
Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
}
}
}
#[test]
fn handle_verify_self_signed_link_verifies_and_populates_signed_fields() {
let _g = verify_key_env_guard()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let prev = std::env::var("AI_MEMORY_KEY_DIR").ok();
let key_tmp = tempfile::TempDir::new().expect("key tempdir");
unsafe {
std::env::set_var("AI_MEMORY_KEY_DIR", key_tmp.path());
}
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-vss".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
let alice = crate::identity::keypair::generate("alice").unwrap();
crate::identity::keypair::save(&alice, key_tmp.path()).unwrap();
let attest = db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", Some(&alice))
.expect("create_link_signed");
assert_eq!(attest, "self_signed");
let req = make_tools_call(
"memory_verify",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["signature_verified"], true, "got {val}");
assert_eq!(val["attest_level"], "self_signed");
assert_eq!(val["signed_by"], "alice");
assert!(
val["signed_at"].is_string(),
"signed_at must be RFC3339 string, got {:?}",
val["signed_at"]
);
unsafe {
match prev {
Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
}
}
}
#[test]
fn handle_verify_tampered_signature_returns_false_and_unsigned() {
let _g = verify_key_env_guard()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let prev = std::env::var("AI_MEMORY_KEY_DIR").ok();
let key_tmp = tempfile::TempDir::new().expect("key tempdir");
unsafe {
std::env::set_var("AI_MEMORY_KEY_DIR", key_tmp.path());
}
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let src = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "w12-vts".into(),
title: "src".into(),
content: "c".into(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let mut tgt = src.clone();
tgt.id = uuid::Uuid::new_v4().to_string();
tgt.title = "tgt".into();
let src_id = db::insert(&conn, &src).unwrap();
let tgt_id = db::insert(&conn, &tgt).unwrap();
let alice = crate::identity::keypair::generate("alice").unwrap();
crate::identity::keypair::save(&alice, key_tmp.path()).unwrap();
db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", Some(&alice))
.expect("create_link_signed");
let original_sig: Vec<u8> = conn
.query_row(
"SELECT signature FROM memory_links \
WHERE source_id = ?1 AND target_id = ?2",
rusqlite::params![&src_id, &tgt_id],
|row| row.get::<_, Vec<u8>>(0),
)
.expect("read signature");
assert_eq!(original_sig.len(), 64);
let mut tampered = original_sig.clone();
tampered[0] ^= 0xFF;
conn.execute(
"UPDATE memory_links SET signature = ?3 \
WHERE source_id = ?1 AND target_id = ?2",
rusqlite::params![&src_id, &tgt_id, &tampered],
)
.unwrap();
let req = make_tools_call(
"memory_verify",
json!({
"source_id": src_id,
"target_id": tgt_id,
"relation": "related_to",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "err: {:?}", resp.error);
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let val: Value = serde_json::from_str(&text).unwrap();
assert_eq!(val["signature_verified"], false, "got {val}");
assert_eq!(val["attest_level"], "unsigned");
assert!(val["signed_by"].is_null());
assert!(val["signed_at"].is_null());
unsafe {
match prev {
Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
}
}
}
fn chunkc_seed_memory(
conn: &rusqlite::Connection,
namespace: &str,
title: &str,
tier: Tier,
) -> String {
let now = chrono::Utc::now().to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier,
namespace: namespace.to_string(),
title: title.to_string(),
content: format!("body for {title}"),
tags: vec!["chunkc".to_string()],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: json!({"agent_id": "test-agent-chunkc", "scope": "public"}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(conn, &mem).unwrap()
}
fn seed_family_owned(
conn: &rusqlite::Connection,
namespace: &str,
family: &str,
owner: &str,
scope: &str,
) -> String {
let now = chrono::Utc::now().to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: namespace.to_string(),
title: format!("{family}-{owner}"),
content: format!("owned by {owner}"),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: json!({"family": family, "agent_id": owner, "scope": scope}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(conn, &mem).unwrap()
}
#[test]
fn load_family_filters_other_agents_private_1555() {
let (owner_a, owner_b) = ("alice", "bob");
let (ns, fam) = ("shared-fam-ns", "core");
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let a_id = seed_family_owned(&conn, ns, fam, owner_a, "private");
let b_id = seed_family_owned(&conn, ns, fam, owner_b, "private");
let q = json!({"family": fam, "namespace": ns, "k": 100});
let ids = |resp: &Value| -> Vec<String> {
resp["memories"]
.as_array()
.unwrap()
.iter()
.map(|m| m["id"].as_str().unwrap().to_string())
.collect()
};
let r_b = crate::mcp::handle_load_family(&conn, &q, Some(owner_b)).unwrap();
let b_ids = ids(&r_b);
assert!(b_ids.contains(&b_id), "{owner_b} sees own row");
assert!(
!b_ids.contains(&a_id),
"{owner_a}'s private row filtered for {owner_b}"
);
assert_eq!(
r_b["count"].as_u64(),
Some(1),
"count recomputed post-filter"
);
let r_a = crate::mcp::handle_load_family(&conn, &q, Some(owner_a)).unwrap();
assert!(ids(&r_a).contains(&a_id));
let r_all = crate::mcp::handle_load_family(&conn, &q, None).unwrap();
assert_eq!(
r_all["count"].as_u64(),
Some(2),
"None == trust-all (legacy)"
);
}
#[test]
fn smart_load_filters_other_agents_private_1555() {
let (owner_a, owner_b) = ("alice", "bob");
let (ns, fam) = ("shared-fam-ns2", "core");
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let a_id = seed_family_owned(&conn, ns, fam, owner_a, "private");
let b_id = seed_family_owned(&conn, ns, fam, owner_b, "collective");
let resp = crate::mcp::handle_smart_load(
&conn,
&json!({"intent": "", "namespace": ns, "k": 100}),
None,
Some(owner_b),
)
.unwrap();
let ids: Vec<String> = resp["memories"]
.as_array()
.unwrap()
.iter()
.map(|m| m["id"].as_str().unwrap().to_string())
.collect();
assert!(
!ids.contains(&a_id),
"{owner_a}'s private row filtered via smart_load for {owner_b}"
);
assert!(
ids.contains(&b_id),
"{owner_b}'s own collective row is visible"
);
}
fn chunkc_seed_family_memory(
conn: &rusqlite::Connection,
namespace: &str,
family: &str,
) -> String {
let now = chrono::Utc::now().to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: namespace.to_string(),
title: format!("{family}-mem"),
content: format!("seeded for {family}"),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: json!({"family": family, "agent_id": "test-agent-chunkc"}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(conn, &mem).unwrap()
}
fn chunkc_lock_perms() -> std::sync::MutexGuard<'static, ()> {
let g = crate::config::lock_permissions_mode_for_test();
crate::config::clear_permissions_mode_override_for_test();
crate::permissions::clear_active_permission_rules_for_test();
g
}
#[test]
fn chunkc_archive_list_then_restore_round_trip() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-archroot", "archived-mem", Tier::Mid);
db::forget(
&conn,
Some("chunkc-archroot"),
None,
None,
true, )
.unwrap();
let list_req = make_tools_call(
"memory_archive_list",
json!({"namespace": "chunkc-archroot", "limit": 10}),
);
let resp = invoke_handle_request(&conn, &list_req);
let payload = i4_decode_response_payload(&resp);
assert!(payload["count"].as_u64().unwrap() >= 1);
let restore_req = make_tools_call("memory_archive_restore", json!({"id": id}));
let resp = invoke_handle_request(&conn, &restore_req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["restored"], true);
assert_eq!(payload["id"], id);
}
#[test]
fn chunkc_archive_restore_invalid_id_returns_validation_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_archive_restore",
json!({"id": "bad id with spaces!"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn chunkc_archive_restore_missing_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_archive_restore", json!({}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("id"));
}
#[test]
fn chunkc_archive_purge_denied_by_permission_rule() {
let _gate = chunkc_lock_perms();
crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
namespace_pattern: crate::DEFAULT_NAMESPACE.to_string(),
op: "memory_archive".to_string(),
agent_pattern: "chunkc-archdeny-*".to_string(),
decision: crate::permissions::RuleDecision::Deny,
reason: Some("chunkc: archive denied".to_string()),
}]);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_archive_purge",
json!({
"older_than_days": 0,
"agent_id": "chunkc-archdeny-bot",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("archive denied") || msg.contains("denied"));
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_archive_purge_ask_returns_pending_payload() {
let _gate = chunkc_lock_perms();
crate::config::override_active_permissions_mode_for_test(
crate::config::PermissionsMode::Advisory,
);
crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
namespace_pattern: crate::DEFAULT_NAMESPACE.to_string(),
op: "memory_archive".to_string(),
agent_pattern: "chunkc-archask-*".to_string(),
decision: crate::permissions::RuleDecision::Ask,
reason: Some("chunkc: confirm purge".to_string()),
}]);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_archive_purge",
json!({
"older_than_days": 0,
"agent_id": "chunkc-archask-bot",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["status"], "ask");
assert_eq!(payload["action"], "archive");
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_stats_returns_struct() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_memory(&conn, "chunkc-stats", "title", Tier::Mid);
let req = make_tools_call("memory_stats", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert!(payload.is_object());
}
#[test]
fn chunkc_forget_pattern_filter_actual_run_deletes() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_memory(&conn, "chunkc-pat", "abc-xyz", Tier::Mid);
let _ = chunkc_seed_memory(&conn, "chunkc-pat", "def-xyz", Tier::Mid);
let _ = chunkc_seed_memory(&conn, "chunkc-pat", "qqq-only", Tier::Mid);
let req = make_tools_call(
"memory_forget",
json!({
"namespace": "chunkc-pat",
"pattern": "xyz",
"dry_run": false,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert!(payload["deleted"].as_u64().unwrap() >= 2);
}
#[test]
fn chunkc_forget_dry_run_pattern_with_tier_filter() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_memory(&conn, "chunkc-mix", "a-short", Tier::Short);
let _ = chunkc_seed_memory(&conn, "chunkc-mix", "a-long", Tier::Long);
let req = make_tools_call(
"memory_forget",
json!({
"namespace": "chunkc-mix",
"pattern": "a-",
"tier": Tier::Short.as_str(),
"dry_run": true,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["dry_run"], true);
assert_eq!(payload["would_delete"].as_u64().unwrap(), 1);
}
#[test]
fn chunkc_search_with_all_optional_filters() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_memory(&conn, "chunkc-search-ns", "needle target", Tier::Long);
let req = make_tools_call(
"memory_search",
json!({
"query": "needle",
"namespace": "chunkc-search-ns",
"tier": Tier::Long.as_str(),
"limit": 5,
"agent_id": "test-agent-chunkc",
"format": "json",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert!(payload["results"].is_array());
}
#[test]
fn chunkc_search_invalid_agent_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_search",
json!({"query": "x", "agent_id": "bad agent with spaces!"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn chunkc_search_invalid_as_agent_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_search",
json!({"query": "x", "as_agent": "bad agent with spaces!"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn chunkc_namespace_set_standard_invalid_parent_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-ns-bad-parent", "p", Tier::Long);
let req = make_tools_call(
"memory_namespace_set_standard",
json!({
"namespace": "chunkc-ns-bad-parent",
"id": id,
"parent": "bad parent with spaces!!",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn chunkc_namespace_set_standard_missing_memory_with_governance() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_namespace_set_standard",
json!({
"namespace": "chunkc-ns-missing",
"id": "00000000-0000-0000-0000-000000000000",
"governance": {
"write": "any",
"promote": "any",
"delete": "owner",
"approver": "human",
},
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("not found"));
}
#[test]
fn chunkc_namespace_get_standard_inherit_no_chain_returns_zero() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_namespace_get_standard",
json!({
"namespace": "chunkc-ns-empty/deep",
"inherit": true,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["count"], 0);
assert!(payload["chain"].is_array());
assert!(payload["standards"].is_array());
}
#[test]
fn chunkc_namespace_get_standard_dangling_returns_warning() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-ns-dangling", "std", Tier::Long);
db::set_namespace_standard(&conn, "chunkc-ns-dangling", &id, None).unwrap();
conn.execute("DELETE FROM memories WHERE id = ?1", rusqlite::params![id])
.unwrap();
let req = make_tools_call(
"memory_namespace_get_standard",
json!({"namespace": "chunkc-ns-dangling"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert!(
payload["warning"].as_str().is_some(),
"expected dangling warning; got: {payload}"
);
}
#[test]
fn chunkc_extract_governance_default_when_missing() {
let mem_val = json!({"id": "x", "metadata": {"agent_id": "a"}});
let gov = super::namespace::extract_governance(&mem_val);
assert!(gov.is_object());
}
#[test]
fn chunkc_extract_governance_default_when_no_metadata() {
let mem_val = json!({"id": "x"});
let gov = super::namespace::extract_governance(&mem_val);
assert!(gov.is_object());
}
#[test]
fn chunkc_extract_governance_default_when_governance_invalid() {
let mem_val = json!({"id": "x", "metadata": {"governance": "not-an-object"}});
let gov = super::namespace::extract_governance(&mem_val);
assert!(gov.is_object());
}
#[test]
fn chunkc_namespace_set_standard_non_object_metadata_becomes_object() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-ns-nullmeta", "p", Tier::Long);
conn.execute(
"UPDATE memories SET metadata = 'null' WHERE id = ?1",
rusqlite::params![id],
)
.unwrap();
let req = make_tools_call(
"memory_namespace_set_standard",
json!({
"namespace": "chunkc-ns-nullmeta",
"id": id,
"governance": {
"write": "any",
"promote": "any",
"delete": "owner",
"approver": "human",
},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["set"], true);
}
#[test]
fn chunkc_auto_register_path_hierarchy_finds_ancestor_parent() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let cwd = match std::env::current_dir() {
Ok(c) => c,
Err(_) => return,
};
let home = match dirs::home_dir() {
Some(h) => h,
None => return,
};
if !cwd.starts_with(&home) || cwd == home {
return;
}
let mut ancestor = cwd.parent();
let mut matched_dir: Option<String> = None;
while let Some(d) = ancestor {
if d == home || !d.starts_with(&home) {
break;
}
if let Some(name) = d.file_name().and_then(|n| n.to_str()) {
matched_dir = Some(name.to_string());
}
ancestor = d.parent();
}
let parent_dir_name = match matched_dir {
Some(n) if !n.is_empty() => n,
_ => return,
};
let parent_id = chunkc_seed_memory(&conn, &parent_dir_name, "ancestor-std", Tier::Long);
db::set_namespace_standard(&conn, &parent_dir_name, &parent_id, None).unwrap();
let child_id = chunkc_seed_memory(&conn, "chunkc-autoreg-leaf", "leaf", Tier::Long);
db::set_namespace_standard(&conn, "chunkc-autoreg-leaf", &child_id, None).unwrap();
super::auto_register_path_hierarchy(&conn, "chunkc-autoreg-leaf");
let parent = db::get_namespace_parent(&conn, "chunkc-autoreg-leaf");
assert!(
parent.is_some(),
"auto_register must have populated parent_namespace from a matching ancestor"
);
}
#[test]
fn chunkc_namespace_clear_standard_idempotent() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_namespace_clear_standard",
json!({"namespace": "chunkc-ns-noop"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["cleared"], false);
}
#[test]
fn chunkc_promote_missing_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_promote", json!({}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn chunkc_promote_resolves_by_prefix() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-pfx", "p", Tier::Mid);
let prefix = &id[..8];
let req = make_tools_call("memory_promote", json!({"id": prefix}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "got: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["promoted"], true);
assert_eq!(payload["mode"], "tier");
}
#[test]
fn chunkc_consolidate_missing_ids_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_consolidate", json!({"title": "t", "summary": "s"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("ids"));
}
#[test]
fn chunkc_consolidate_missing_title_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_consolidate", json!({"ids": ["a"], "summary": "s"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("title"));
}
#[test]
fn chunkc_consolidate_invalid_id_format_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_consolidate",
json!({
"ids": ["bad id with spaces!"],
"title": "t",
"summary": "s",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn chunkc_consolidate_denied_by_permission_rule() {
let _gate = chunkc_lock_perms();
crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
namespace_pattern: "chunkc-cons-deny/**".to_string(),
op: "memory_consolidate".to_string(),
agent_pattern: "*".to_string(),
decision: crate::permissions::RuleDecision::Deny,
reason: Some("chunkc: consolidate denied".to_string()),
}]);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id_a = chunkc_seed_memory(&conn, "chunkc-cons-deny/a", "a", Tier::Mid);
let id_b = chunkc_seed_memory(&conn, "chunkc-cons-deny/a", "b", Tier::Mid);
let req = make_tools_call(
"memory_consolidate",
json!({
"ids": [id_a, id_b],
"title": "merged",
"summary": "summary",
"namespace": "chunkc-cons-deny/a",
}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_consolidate_ask_returns_pending_payload() {
let _gate = chunkc_lock_perms();
crate::config::override_active_permissions_mode_for_test(
crate::config::PermissionsMode::Advisory,
);
crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
namespace_pattern: "chunkc-cons-ask/**".to_string(),
op: "memory_consolidate".to_string(),
agent_pattern: "*".to_string(),
decision: crate::permissions::RuleDecision::Ask,
reason: Some("chunkc: confirm consolidate".to_string()),
}]);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id_a = chunkc_seed_memory(&conn, "chunkc-cons-ask/a", "a", Tier::Mid);
let id_b = chunkc_seed_memory(&conn, "chunkc-cons-ask/a", "b", Tier::Mid);
let req = make_tools_call(
"memory_consolidate",
json!({
"ids": [id_a, id_b],
"title": "merged",
"summary": "summary",
"namespace": "chunkc-cons-ask/a",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["status"], "ask");
assert_eq!(payload["action"], "consolidate");
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_consolidate_handler_embedder_branch_writes_embedding() {
use crate::embeddings::Embed;
use crate::embeddings::test_support::MockEmbedder;
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id_a = chunkc_seed_memory(&conn, "chunkc-cons-emb", "a", Tier::Mid);
let id_b = chunkc_seed_memory(&conn, "chunkc-cons-emb", "b", Tier::Mid);
let embedder = MockEmbedder::new_local().unwrap();
let res = super::consolidate::handle_consolidate(
&conn,
std::path::Path::new(":memory:"),
&json!({
"ids": [id_a, id_b],
"title": "merged-embed",
"summary": "merged summary text",
"namespace": "chunkc-cons-emb",
}),
None, Some(&embedder as &dyn Embed), None, Some("test-mcp-client"), )
.expect("consolidate handler must succeed");
let new_id = res["id"].as_str().unwrap();
let emb = db::get_embedding(&conn, new_id).unwrap();
assert!(emb.is_some(), "embedder branch must store embedding");
}
#[tokio::test(flavor = "multi_thread")]
async fn chunkc_consolidate_llm_path_missing_source_id() {
use wiremock::matchers::{method, path as wpath};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(wpath("/api/tags"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"models": [{"name": "test-model"}]
})))
.mount(&server)
.await;
let uri = server.uri();
let _outcome: () = tokio::task::spawn_blocking(move || {
let llm = crate::llm::OllamaClient::new_with_url(&uri, "test-model").unwrap();
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let res = super::consolidate::handle_consolidate(
&conn,
std::path::Path::new(":memory:"),
&json!({
"ids": ["00000000-0000-0000-0000-000000000000"],
"title": "t",
"namespace": "chunkc-cons-miss",
}),
Some(&llm),
None,
None,
None,
);
let err = res.unwrap_err();
assert!(err.contains("memory not found"), "got: {err}");
})
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn chunkc_consolidate_llm_path_synthesises_summary() {
use wiremock::matchers::{method, path as wpath};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(wpath("/api/tags"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"models": [{"name": "test-model"}]
})))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(wpath("/api/chat"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"message": {"role": "assistant", "content": "synthesised consolidated summary"}
})))
.mount(&server)
.await;
let uri = server.uri();
let _outcome: () = tokio::task::spawn_blocking(move || {
let llm = crate::llm::OllamaClient::new_with_url(&uri, "test-model").unwrap();
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id_a = chunkc_seed_memory(&conn, "chunkc-cons-llm", "a", Tier::Mid);
let id_b = chunkc_seed_memory(&conn, "chunkc-cons-llm", "b", Tier::Mid);
let res = super::consolidate::handle_consolidate(
&conn,
std::path::Path::new(":memory:"),
&json!({
"ids": [id_a, id_b],
"title": "merged-llm",
"namespace": "chunkc-cons-llm",
}),
Some(&llm),
None,
None,
None,
)
.expect("LLM consolidate must succeed");
assert!(res["auto_summary"] == json!(true));
assert!(
res["summary_preview"]
.as_str()
.unwrap()
.contains("synthesised")
);
})
.await
.unwrap();
}
#[test]
fn chunkc_replay_invalid_memory_id_returns_validation_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": " "}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
}
#[test]
fn chunkc_replay_missing_memory_id_returns_error() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_replay", json!({}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("memory_id"));
}
#[test]
fn chunkc_replay_dangling_transcript_link_silently_dropped() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-dangle");
let t = crate::transcripts::store(&conn, "team/eng", "dangling body", None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-dangle", &t.id, None, None).unwrap();
conn.execute(
"DELETE FROM memory_transcripts WHERE id = ?1",
rusqlite::params![t.id],
)
.unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-dangle"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["count"], 0);
}
#[test]
fn chunkc_replay_denied_by_permission_rule() {
let _gate = chunkc_lock_perms();
crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
namespace_pattern: "team/eng-denyrule".to_string(),
op: "memory_replay".to_string(),
agent_pattern: "*".to_string(),
decision: crate::permissions::RuleDecision::Deny,
reason: Some("chunkc: replay denied".to_string()),
}]);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO memories (id, tier, namespace, title, content, created_at, updated_at, metadata)
VALUES (?1, 'short', 'team/eng-denyrule', ?2, 'body', ?3, ?3, '{\"scope\":\"public\"}')",
rusqlite::params!["mem-deny-uniq", "title-mem-deny-uniq", now],
)
.unwrap();
let t = crate::transcripts::store(&conn, "team/eng-denyrule", "denied body", None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-deny-uniq", &t.id, None, None).unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-deny-uniq"}));
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert_eq!(result["isError"], true);
let msg = result["content"][0]["text"].as_str().unwrap();
assert!(msg.contains("replay denied") || msg.contains("denied"));
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_replay_ask_returns_pending_payload() {
let _gate = chunkc_lock_perms();
crate::config::override_active_permissions_mode_for_test(
crate::config::PermissionsMode::Advisory,
);
crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
namespace_pattern: "team/eng-askrule".to_string(),
op: "memory_replay".to_string(),
agent_pattern: "*".to_string(),
decision: crate::permissions::RuleDecision::Ask,
reason: Some("chunkc: confirm replay".to_string()),
}]);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO memories (id, tier, namespace, title, content, created_at, updated_at, metadata)
VALUES (?1, 'short', 'team/eng-askrule', ?2, 'body', ?3, ?3, '{\"scope\":\"public\"}')",
rusqlite::params!["mem-ask-uniq", "title-mem-ask-uniq", now],
)
.unwrap();
let t = crate::transcripts::store(&conn, "team/eng-askrule", "ask body", None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-ask-uniq", &t.id, None, None).unwrap();
let req = make_tools_call("memory_replay", json!({"memory_id": "mem-ask-uniq"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["status"], "ask");
assert_eq!(payload["action"], "replay");
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_capabilities_v3_dispatch_returns_summary_block() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call("memory_capabilities", json!({"accept": "v3"}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert!(payload["summary"].as_str().is_some());
assert!(payload["to_describe_to_user"].as_str().is_some());
assert!(payload["tools"].is_array());
}
#[test]
fn chunkc_capabilities_v3_with_verbose_and_schema_overlays_tools() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let req = make_tools_call(
"memory_capabilities",
json!({
"accept": "v3",
"verbose": true,
"include_schema": true,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
let tools = payload["tools"].as_array().unwrap();
assert!(
tools.iter().any(|t| t.get("inputSchema").is_some()),
"verbose+include_schema must overlay inputSchema"
);
assert!(
tools.iter().any(|t| t.get("docstring").is_some()),
"verbose must overlay docstring"
);
}
#[test]
fn chunkc_overlay_tool_payloads_noop_when_both_flags_false() {
let mut obj = serde_json::Map::new();
obj.insert("tools".to_string(), json!([]));
let before = obj.clone();
crate::mcp::overlay_tool_payloads(&mut obj, &crate::profile::Profile::core(), false, false);
assert_eq!(obj, before, "no-op when neither flag set");
}
#[test]
fn chunkc_overlay_tool_payloads_synthesises_v2_tool_payloads() {
let mut obj = serde_json::Map::new();
obj.insert("schema_version".to_string(), json!("2"));
crate::mcp::overlay_tool_payloads(
&mut obj,
&crate::profile::Profile::core(),
true, true, );
let payloads = obj.get("tool_payloads").and_then(Value::as_array).unwrap();
assert!(
!payloads.is_empty(),
"tool_payloads must be synthesised for v2-shape"
);
}
#[test]
fn chunkc_effective_tier_label_all_four_arms() {
use crate::mcp::effective_tier_label;
assert_eq!(effective_tier_label(true, true, true), "autonomous");
assert_eq!(effective_tier_label(true, true, false), "smart");
assert_eq!(effective_tier_label(false, true, false), "semantic");
assert_eq!(effective_tier_label(false, false, false), "keyword");
}
#[test]
fn chunkc_format_rule_summary_renders_each_approver_variant() {
use crate::mcp::format_rule_summary;
use crate::models::{ApproverType, GovernanceLevel, GovernancePolicy};
let mut p = GovernancePolicy::default();
p.core.write = GovernanceLevel::Any;
p.core.promote = GovernanceLevel::Any;
p.core.delete = GovernanceLevel::Owner;
p.core.approver = ApproverType::Human;
p.core.inherit = true;
let s = format_rule_summary("alpha/eng", &p);
assert!(s.contains("alpha/eng"));
assert!(s.contains("approver=human"));
assert!(s.contains("inherit=true"));
p.core.approver = ApproverType::Agent("ops-bot".to_string());
let s = format_rule_summary("alpha/eng", &p);
assert!(s.contains("approver=agent:ops-bot"));
p.core.approver = ApproverType::Consensus(3);
let s = format_rule_summary("alpha/eng", &p);
assert!(s.contains("approver=consensus:3"));
}
#[test]
fn chunkc_capabilities_accept_parse_all_variants() {
use crate::mcp::CapabilitiesAccept;
assert_eq!(CapabilitiesAccept::parse("v1"), CapabilitiesAccept::V1);
assert_eq!(CapabilitiesAccept::parse("1"), CapabilitiesAccept::V1);
assert_eq!(CapabilitiesAccept::parse("v2"), CapabilitiesAccept::V2);
assert_eq!(CapabilitiesAccept::parse("2"), CapabilitiesAccept::V2);
assert_eq!(CapabilitiesAccept::parse("v3"), CapabilitiesAccept::V3);
assert_eq!(CapabilitiesAccept::parse("3"), CapabilitiesAccept::V3);
assert_eq!(CapabilitiesAccept::parse(""), CapabilitiesAccept::V3);
assert_eq!(CapabilitiesAccept::parse("garbage"), CapabilitiesAccept::V3);
assert_eq!(CapabilitiesAccept::parse(" V2 "), CapabilitiesAccept::V2);
}
#[test]
fn chunkc_handle_capabilities_with_conn_rejects_v3() {
let tier = crate::config::FeatureTier::Keyword.config();
let res = crate::mcp::handle_capabilities_with_conn(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
None,
false,
None,
crate::mcp::CapabilitiesAccept::V3,
);
let err = res.unwrap_err();
assert!(err.contains("handle_capabilities_with_conn_v3"));
}
#[test]
fn chunkc_handle_capabilities_with_conn_v1_returns_legacy_shape() {
let tier = crate::config::FeatureTier::Keyword.config();
let v = crate::mcp::handle_capabilities_with_conn(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
None,
false,
None,
crate::mcp::CapabilitiesAccept::V1,
)
.unwrap();
assert!(v.is_object());
}
#[test]
fn chunkc_smart_load_f14_control_intents_13_of_13() {
use crate::mcp::handle_smart_load;
let cases: &[(&str, &str)] = &[
("recall and search for stored memories", "core"),
(
"delete and forget the stale memories then promote the survivors",
"lifecycle",
),
("I'm about to debug a flaky test", "graph"),
("query the knowledge graph for entity timeline", "graph"),
("approve the pending governance review", "governance"),
(
"consolidate duplicate memories that contradict each other",
"power",
),
("restore an archived backup of old memories", "archive"),
("register a new agent and start a session", "meta"),
("send a notification to another agent", "other"),
("expand a query and find related memories", "power"),
("call memory_notify on the other agent", "other"),
("audit the namespace permission policy rules", "governance"),
("migrate and rotate the stale records", "lifecycle"),
];
for (intent, expected) in cases {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
for fam in [
"core",
"lifecycle",
"graph",
"governance",
"power",
"meta",
"archive",
"other",
] {
let _ = chunkc_seed_family_memory(&conn, "ns", fam);
}
let resp = handle_smart_load(&conn, &json!({"intent": intent}), None, None)
.expect("smart_load must succeed");
assert_eq!(
resp["chosen_family"], *expected,
"F14 control intent {intent:?} expected {expected}; got: {resp}"
);
assert_eq!(resp["chosen_family_source"], "keyword");
}
}
#[test]
fn chunkc_load_family_k_zero_clamps_to_one() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_family_memory(&conn, "ns", "core");
let _ = chunkc_seed_family_memory(&conn, "ns", "core");
let resp = crate::mcp::handle_load_family(
&conn,
&json!({"family": "core", "namespace": "ns", "k": 0}),
None,
)
.expect("must succeed");
assert_eq!(resp["k"], 1);
assert_eq!(resp["count"], 1);
}
#[test]
fn chunkc_load_family_invalid_namespace_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let err = crate::mcp::handle_load_family(
&conn,
&json!({"family": "core", "namespace": "bad ns with spaces!"}),
None,
)
.unwrap_err();
assert!(err.contains("namespace") || err.contains("invalid"));
}
#[test]
fn chunkc_load_family_expired_rows_are_filtered_out() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_family_memory(&conn, "ns-exp", "core");
let now = chrono::Utc::now().to_rfc3339();
let past = (chrono::Utc::now() - chrono::Duration::hours(2)).to_rfc3339();
let stale = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Short,
namespace: "ns-exp".to_string(),
title: "stale".to_string(),
content: "stale".to_string(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: Some(past),
metadata: json!({"family": "core"}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(&conn, &stale).unwrap();
let resp = crate::mcp::handle_load_family(
&conn,
&json!({"family": "core", "namespace": "ns-exp"}),
None,
)
.expect("must succeed");
assert_eq!(resp["count"], 1, "expired row must be filtered");
}
#[test]
fn chunkc_smart_load_embedder_path_reports_embedder_source() {
use crate::embeddings::Embed;
use crate::embeddings::test_support::MockEmbedder;
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
for fam in [
"core",
"lifecycle",
"graph",
"governance",
"power",
"meta",
"archive",
"other",
] {
let _ = chunkc_seed_family_memory(&conn, "ns", fam);
}
let embedder = MockEmbedder::new_local().unwrap();
let resp = crate::mcp::handle_smart_load(
&conn,
&json!({"intent": "blortzfribblequx zarflargle"}),
Some(&embedder as &dyn Embed),
None,
)
.expect("smart_load must succeed");
assert!(resp["chosen_family"].is_string());
assert!(resp["score"].is_number());
}
#[test]
fn chunkc_build_capabilities_summary_each_named_profile() {
use crate::mcp::build_capabilities_summary;
use crate::profile::Profile;
for p in [
Profile::core(),
Profile::graph(),
Profile::admin(),
Profile::power(),
Profile::full(),
] {
let s = build_capabilities_summary(&p);
assert!(s.contains("memory tools"));
assert!(s.contains("memory_load_family"));
assert!(s.contains("memory_smart_load"));
}
let custom = Profile::parse("core,archive").unwrap();
let s = build_capabilities_summary(&custom);
assert!(s.contains("memory tools"));
assert!(s.contains("core") && s.contains("archive"));
}
#[test]
fn chunkc_build_capabilities_describe_to_user_both_branches() {
use crate::mcp::build_capabilities_describe_to_user;
use crate::profile::Profile;
let s_full = build_capabilities_describe_to_user(&Profile::full());
assert!(s_full.contains("all"));
let s_core = build_capabilities_describe_to_user(&Profile::core());
assert!(s_core.contains("memory tool"));
assert!(s_core.contains("tools") || s_core.contains("tool"));
}
#[test]
fn chunkc_build_capabilities_tools_with_allowlist_denying_agent() {
use crate::config::McpConfig;
use crate::mcp::build_capabilities_tools;
use crate::profile::Profile;
use std::collections::HashMap;
let mut allowlist = HashMap::new();
allowlist.insert("alice".to_string(), vec!["core".to_string()]);
let cfg = McpConfig {
allowlist: Some(allowlist),
..McpConfig::default()
};
let tools = build_capabilities_tools(&Profile::full(), Some(&cfg), Some("alice"));
let core_entry = tools.iter().find(|t| t.family == "core").unwrap();
assert!(core_entry.callable_now);
let non_core = tools.iter().find(|t| t.family != "core").unwrap();
assert!(!non_core.callable_now);
}
#[test]
fn issue_1673_n13_unknown_caller_does_not_falsely_deny_callable_now() {
use crate::config::McpConfig;
use crate::mcp::build_capabilities_tools;
use crate::profile::Profile;
use std::collections::HashMap;
let mut allowlist = HashMap::new();
allowlist.insert("alice".to_string(), vec!["core".to_string()]);
let cfg = McpConfig {
allowlist: Some(allowlist),
..McpConfig::default()
};
let tools = build_capabilities_tools(&Profile::full(), Some(&cfg), None);
for t in tools.iter().filter(|t| t.loaded) {
assert!(
t.callable_now,
"loaded tool {} must be callable_now for an unknown caller",
t.name
);
}
}
#[test]
fn chunkc_build_agent_permitted_families_empty_allowlist_returns_none() {
use crate::config::McpConfig;
use crate::mcp::build_agent_permitted_families;
use std::collections::HashMap;
let cfg = McpConfig {
allowlist: Some(HashMap::new()),
..McpConfig::default()
};
assert_eq!(
build_agent_permitted_families(Some(&cfg), Some("alice")),
None
);
}
#[test]
fn chunkc_build_agent_permitted_families_populated_allowlist() {
use crate::config::McpConfig;
use crate::mcp::build_agent_permitted_families;
use std::collections::HashMap;
let mut allowlist = HashMap::new();
allowlist.insert(
"alice".to_string(),
vec!["core".to_string(), "graph".to_string()],
);
let cfg = McpConfig {
allowlist: Some(allowlist),
..McpConfig::default()
};
let perm = build_agent_permitted_families(Some(&cfg), Some("alice")).unwrap();
assert!(perm.contains(&"core".to_string()));
assert!(perm.contains(&"graph".to_string()));
}
#[test]
fn chunkc_build_capabilities_summary_drives_all_label_arms() {
use crate::mcp::build_capabilities_summary;
use crate::profile::Profile;
let labels = [
Profile::full(),
Profile::core(),
Profile::graph(),
Profile::admin(),
Profile::power(),
];
for p in labels {
let s = build_capabilities_summary(&p);
assert!(s.contains("memory tools"));
}
let custom = Profile::parse("core,graph,archive").unwrap();
let s = build_capabilities_summary(&custom);
assert!(s.contains("core,graph,archive") || s.contains("core") && s.contains("graph"));
}
#[test]
fn chunkc_handle_capabilities_with_conn_v3_full_overlay() {
use crate::config::{FeatureTier, McpConfig};
use crate::harness::Harness;
use crate::mcp::handle_capabilities_with_conn_v3;
use crate::profile::Profile;
use std::collections::HashMap;
let tier = FeatureTier::Keyword.config();
let mut allowlist = HashMap::new();
allowlist.insert("alice".to_string(), vec!["core".to_string()]);
let cfg = McpConfig {
allowlist: Some(allowlist),
..McpConfig::default()
};
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let harness = Harness::detect("claude-code");
let v = handle_capabilities_with_conn_v3(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
None,
false,
Some(&conn),
&Profile::core(),
Some(&cfg),
Some("alice"),
Some(&harness),
)
.unwrap();
assert_eq!(v["schema_version"], "3");
assert!(v["summary"].as_str().is_some());
assert!(v["to_describe_to_user"].as_str().is_some());
assert!(v["tools"].is_array());
assert!(v["agent_permitted_families"].is_array());
}
#[test]
fn chunkc_handle_capabilities_with_conn_v2_db_count_overlay() {
use crate::config::FeatureTier;
use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
let tier = FeatureTier::Keyword.config();
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let v = handle_capabilities_with_conn(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
None,
false,
Some(&conn),
CapabilitiesAccept::V2,
)
.unwrap();
assert_eq!(v["schema_version"], "2");
assert!(v["permissions"]["active_rules"].as_u64().is_some());
assert!(v["hooks"]["registered_count"].as_u64().is_some());
assert!(v["approval"]["pending_requests"].as_u64().is_some());
}
#[test]
fn chunkc_promote_governance_pending() {
use crate::models::{ApproverType, CorePolicy, GovernanceLevel, GovernancePolicy};
let _gate = chunkc_lock_perms();
crate::config::override_active_permissions_mode_for_test(
crate::config::PermissionsMode::Enforce,
);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-prom-pend", "p", Tier::Mid);
let std_id = {
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "chunkc-prom-pend".into(),
title: "std".into(),
content: "policy".into(),
tags: vec![],
priority: 9,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({
"governance": GovernancePolicy {
core: CorePolicy {
write: GovernanceLevel::Any,
promote: GovernanceLevel::Approve,
delete: GovernanceLevel::Any,
approver: ApproverType::Human,
inherit: false,
max_reflection_depth: None,
},
..Default::default()
}
}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(&conn, &mem).unwrap()
};
db::set_namespace_standard(&conn, "chunkc-prom-pend", &std_id, None).unwrap();
let req = make_tools_call("memory_promote", json!({"id": id}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["status"], "pending");
assert_eq!(payload["action"], "promote");
assert_eq!(payload["memory_id"], id);
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_promote_governance_denied() {
use crate::models::{ApproverType, CorePolicy, GovernanceLevel, GovernancePolicy};
let _gate = chunkc_lock_perms();
crate::config::override_active_permissions_mode_for_test(
crate::config::PermissionsMode::Enforce,
);
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-prom-deny", "p", Tier::Mid);
let std_id = {
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "chunkc-prom-deny".into(),
title: "std".into(),
content: "policy".into(),
tags: vec![],
priority: 9,
confidence: 1.0,
source: "test".into(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: None,
metadata: json!({
"governance": GovernancePolicy {
core: CorePolicy {
write: GovernanceLevel::Any,
promote: GovernanceLevel::Owner,
delete: GovernanceLevel::Any,
approver: ApproverType::Agent("not-me".to_string()),
inherit: false,
max_reflection_depth: None,
},
..Default::default()
}
}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(&conn, &mem).unwrap()
};
db::set_namespace_standard(&conn, "chunkc-prom-deny", &std_id, None).unwrap();
let req = make_tools_call(
"memory_promote",
json!({"id": id, "agent_id": "calling-agent"}),
);
let resp = invoke_handle_request(&conn, &req);
let result = resp.result.unwrap();
assert!(result.is_object());
crate::permissions::clear_active_permission_rules_for_test();
}
#[test]
fn chunkc_consolidate_vector_index_branch_inserts_new_id() {
use crate::embeddings::Embed;
use crate::embeddings::test_support::MockEmbedder;
use crate::hnsw::VectorIndex;
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id_a = chunkc_seed_memory(&conn, "chunkc-cons-vidx", "a", Tier::Mid);
let id_b = chunkc_seed_memory(&conn, "chunkc-cons-vidx", "b", Tier::Mid);
let embedder = MockEmbedder::new_local().unwrap();
let index = VectorIndex::empty();
index.insert(id_a.clone(), embedder.embed("a").unwrap());
index.insert(id_b.clone(), embedder.embed("b").unwrap());
let res = super::consolidate::handle_consolidate(
&conn,
std::path::Path::new(":memory:"),
&json!({
"ids": [id_a, id_b],
"title": "merged-vidx",
"summary": "vidx summary",
"namespace": "chunkc-cons-vidx",
}),
None,
Some(&embedder as &dyn Embed),
Some(&index),
None,
)
.expect("must succeed");
let new_id = res["id"].as_str().unwrap();
let emb = db::get_embedding(&conn, new_id).unwrap();
assert!(emb.is_some());
}
#[test]
fn chunkc_consolidate_iterates_namespace_standard_check() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id_a = chunkc_seed_memory(&conn, "chunkc-cons-warn", "a", Tier::Long);
let id_b = chunkc_seed_memory(&conn, "chunkc-cons-warn", "b", Tier::Mid);
db::set_namespace_standard(&conn, "chunkc-cons-warn", &id_a, None).unwrap();
let req = make_tools_call(
"memory_consolidate",
json!({
"ids": [id_a, id_b],
"title": "merged-warn",
"summary": "warn summary",
"namespace": "chunkc-cons-warn",
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["consolidated"], 2);
}
#[test]
fn chunkc_archive_list_returns_inserted_rows() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_memory(&conn, "chunkc-archlist", "row-one", Tier::Mid);
let _ = chunkc_seed_memory(&conn, "chunkc-archlist", "row-two", Tier::Mid);
db::forget(&conn, Some("chunkc-archlist"), None, None, true).unwrap();
let req = make_tools_call(
"memory_archive_list",
json!({"namespace": "chunkc-archlist", "limit": 50}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert!(payload["count"].as_u64().unwrap() >= 2);
let archived = payload["archived"].as_array().unwrap();
assert!(!archived.is_empty());
}
#[test]
fn chunkc_replay_verbose_true_small_transcript_inlines_content() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-vsmall");
let body = "tiny body that fits well below the threshold";
let t = crate::transcripts::store(&conn, "team/eng", body, None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-vsmall", &t.id, Some(0), Some(10)).unwrap();
let req = make_tools_call(
"memory_replay",
json!({"memory_id": "mem-vsmall", "verbose": true}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
let transcripts = payload["transcripts"].as_array().unwrap();
assert_eq!(transcripts[0]["content"].as_str().unwrap(), body);
}
#[test]
fn chunkc_replay_with_explicit_agent_id_resolves() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
i4_insert_test_memory(&conn, "mem-explicit-agent");
let t = crate::transcripts::store(&conn, "team/eng", "body", None).unwrap();
crate::transcripts::link_transcript(&conn, "mem-explicit-agent", &t.id, None, None)
.unwrap();
let req = make_tools_call(
"memory_replay",
json!({"memory_id": "mem-explicit-agent", "agent_id": "agent-explicit"}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["count"], 1);
}
#[test]
fn chunkc_namespace_inherit_chain_surfaces_governance() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let parent_id = chunkc_seed_memory(&conn, "chunkc-inh-parent", "p", Tier::Long);
db::set_namespace_standard(&conn, "chunkc-inh-parent", &parent_id, None).unwrap();
let leaf_id = chunkc_seed_memory(&conn, "chunkc-inh-parent/leaf", "l", Tier::Long);
db::set_namespace_standard(
&conn,
"chunkc-inh-parent/leaf",
&leaf_id,
Some("chunkc-inh-parent"),
)
.unwrap();
let req = make_tools_call(
"memory_namespace_get_standard",
json!({
"namespace": "chunkc-inh-parent/leaf",
"inherit": true,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert!(payload["count"].as_u64().unwrap() >= 1);
let standards = payload["standards"].as_array().unwrap();
for entry in standards {
assert!(entry["governance"].is_object());
}
}
#[test]
fn chunkc_handle_capabilities_with_conn_v2_reranker_none() {
use crate::config::FeatureTier;
use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
let tier = FeatureTier::Keyword.config();
let v = handle_capabilities_with_conn(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
None, false,
None,
CapabilitiesAccept::V2,
)
.unwrap();
assert_eq!(v["features"]["reranker_active"], "off");
}
#[test]
fn chunkc_handle_capabilities_with_conn_reranker_lexical_fallback() {
use crate::config::FeatureTier;
use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
use crate::reranker::{BatchedReranker, CrossEncoder};
let tier = FeatureTier::Keyword.config();
let lexical = BatchedReranker::new(CrossEncoder::new());
let v = handle_capabilities_with_conn(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
Some(&lexical),
false,
None,
CapabilitiesAccept::V2,
)
.unwrap();
assert_eq!(v["features"]["reranker_active"], "lexical_fallback");
assert_eq!(v["features"]["cross_encoder_reranking"], false);
}
#[test]
fn chunkc_compute_recall_mode_hybrid_when_embedder_loaded() {
use crate::config::FeatureTier;
use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
let tier = FeatureTier::Semantic.config();
let v = handle_capabilities_with_conn(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
None,
true, None,
CapabilitiesAccept::V2,
)
.unwrap();
assert_eq!(v["features"]["recall_mode_active"], "hybrid");
}
#[test]
fn chunkc_compute_recall_mode_degraded_when_embedder_not_loaded() {
use crate::config::FeatureTier;
use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
let tier = FeatureTier::Semantic.config();
let v = handle_capabilities_with_conn(
&tier,
&crate::config::ResolvedModels::from_tier_preset(&tier),
None,
false, None,
CapabilitiesAccept::V2,
)
.unwrap();
assert_eq!(v["features"]["recall_mode_active"], "degraded");
}
#[test]
fn chunkc_overlay_tool_payloads_handles_malformed_tool_entries() {
let mut obj = serde_json::Map::new();
obj.insert(
"tools".to_string(),
json!([
"not-an-object", {"family": "x"}, {"name": "no_such_tool"}, {"name": "memory_capabilities"}, ]),
);
crate::mcp::overlay_tool_payloads(&mut obj, &crate::profile::Profile::core(), true, true);
let tools = obj.get("tools").and_then(Value::as_array).unwrap();
let real = tools
.iter()
.find(|t| t.get("name").and_then(Value::as_str) == Some("memory_capabilities"))
.unwrap();
assert!(real.get("inputSchema").is_some());
assert!(real.get("docstring").is_some());
}
#[test]
fn chunkc_smart_load_keyword_veto_overrides_embedder() {
use crate::embeddings::Embed;
use crate::embeddings::test_support::MockEmbedder;
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
for fam in [
"core",
"lifecycle",
"graph",
"governance",
"power",
"meta",
"archive",
"other",
] {
let _ = chunkc_seed_family_memory(&conn, "ns", fam);
}
let embedder = MockEmbedder::new_local().unwrap();
let resp = crate::mcp::handle_smart_load(
&conn,
&json!({"intent": "call memory_notify on the other agent"}),
Some(&embedder as &dyn Embed),
None,
)
.expect("must succeed");
assert_eq!(resp["chosen_family"], "other");
}
#[test]
fn chunkc_smart_load_empty_intent_routes_to_core_fallback() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
for fam in ["core", "lifecycle"] {
let _ = chunkc_seed_family_memory(&conn, "ns-empty", fam);
}
let resp = crate::mcp::handle_smart_load(
&conn,
&json!({"intent": " ", "namespace": "ns-empty", "k": 5}),
None,
None,
)
.expect("smart_load must succeed on whitespace intent");
assert_eq!(resp["chosen_family"], "core");
assert_eq!(resp["chosen_family_source"], "fallback");
assert_eq!(resp["intent"], "");
assert_eq!(resp["namespace"], "ns-empty");
assert_eq!(resp["k"], 5);
}
#[test]
fn chunkc_smart_load_punctuation_only_intent_keyword_fallback() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_family_memory(&conn, "ns-punct", "core");
let resp =
crate::mcp::handle_smart_load(&conn, &json!({"intent": "!!!---???"}), None, None)
.expect("smart_load must succeed on punctuation-only intent");
assert_eq!(resp["chosen_family"], "core");
assert_eq!(resp["chosen_family_source"], "fallback");
}
#[test]
fn chunkc_smart_load_failing_embedder_falls_back_to_keyword() {
use crate::embeddings::Embed;
struct FailingEmbedder;
impl Embed for FailingEmbedder {
fn embed(&self, _: &str) -> anyhow::Result<Vec<f32>> {
Err(anyhow::anyhow!("simulated embedder failure"))
}
fn embed_batch(&self, _: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
Err(anyhow::anyhow!("simulated embedder batch failure"))
}
}
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
for fam in [
"core",
"lifecycle",
"graph",
"governance",
"power",
"meta",
"archive",
"other",
] {
let _ = chunkc_seed_family_memory(&conn, "ns-fail-emb", fam);
}
let embedder = FailingEmbedder;
let resp = crate::mcp::handle_smart_load(
&conn,
&json!({"intent": "delete and forget stale memories"}),
Some(&embedder as &dyn Embed),
None,
)
.expect("smart_load must succeed even when embedder fails");
assert_eq!(resp["chosen_family"], "lifecycle");
assert_eq!(resp["chosen_family_source"], "keyword");
}
#[test]
fn chunkc_load_family_k_above_cap_clamps_to_100() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_family_memory(&conn, "ns-cap", "core");
let resp = crate::mcp::handle_load_family(
&conn,
&json!({"family": "core", "namespace": "ns-cap", "k": 5_000}),
None,
)
.expect("must succeed");
assert_eq!(resp["k"], 100);
}
#[test]
fn chunkc_load_family_unknown_family_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let err = crate::mcp::handle_load_family(&conn, &json!({"family": "not-a-family"}), None)
.unwrap_err();
assert!(
err.to_lowercase().contains("family") || err.to_lowercase().contains("unknown"),
"expected an UnknownFamily diagnostic, got: {err}"
);
}
#[test]
fn chunkc_load_family_missing_family_rejected() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let err = crate::mcp::handle_load_family(&conn, &json!({"k": 5}), None).unwrap_err();
assert!(err.contains("family"));
}
#[test]
fn chunkc_namespace_set_standard_with_governance_merges_metadata() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-gov-set", "p", Tier::Long);
let req = make_tools_call(
"memory_namespace_set_standard",
json!({
"namespace": "chunkc-gov-set",
"id": id,
"governance": {
"policy": "auto",
},
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(
resp.error.is_none(),
"happy-path governance merge failed: {:?}",
resp.error
);
}
#[test]
fn chunkc_extract_governance_returns_parsed_policy_when_valid() {
let policy = crate::models::GovernancePolicy::default();
let policy_val = serde_json::to_value(&policy).unwrap();
let mem_val = json!({
"metadata": {
"governance": policy_val,
}
});
let gov = super::namespace::extract_governance(&mem_val);
assert!(
gov.is_object(),
"expected parsed governance object, got {gov}"
);
}
#[test]
fn chunkc_replay_truncates_large_transcript_when_not_verbose() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let memory_id = chunkc_seed_memory(&conn, "chunkc-replay-big", "m", Tier::Long);
let big_content = "x".repeat(150 * 1024);
let transcript = crate::transcripts::store(&conn, "chunkc-replay-big", &big_content, None)
.expect("store transcript");
crate::transcripts::link_transcript(&conn, &memory_id, &transcript.id, None, None)
.expect("link transcript");
let req = make_tools_call(
"memory_replay",
json!({"memory_id": memory_id, "verbose": false}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(
resp.error.is_none(),
"replay returned error: {:?}",
resp.error
);
let payload = i4_decode_response_payload(&resp);
let transcripts = payload["transcripts"]
.as_array()
.expect("transcripts array");
assert_eq!(transcripts.len(), 1);
assert_eq!(transcripts[0]["truncated"], true);
assert!(transcripts[0].get("content").is_none());
}
#[test]
fn chunkc_forget_invalid_tier_string_silently_dropped() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_memory(&conn, "chunkc-forget-tier", "v1", Tier::Mid);
let req = make_tools_call(
"memory_forget",
json!({
"namespace": "chunkc-forget-tier",
"tier": "not-a-tier",
"dry_run": true,
}),
);
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none());
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["dry_run"], true);
assert!(payload["would_delete"].as_u64().unwrap() >= 1);
}
#[test]
fn chunkc_archive_restore_success_returns_restored_true() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let id = chunkc_seed_memory(&conn, "chunkc-restore-ok", "rmem", Tier::Mid);
db::forget(&conn, Some("chunkc-restore-ok"), None, None, true).unwrap();
let req = make_tools_call("memory_archive_restore", json!({"id": id}));
let resp = invoke_handle_request(&conn, &req);
assert!(
resp.error.is_none(),
"restore should succeed: {:?}",
resp.error
);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["restored"], true);
}
#[test]
fn chunkc_archive_purge_allowed_returns_purged_count() {
let _gate = chunkc_lock_perms();
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let _ = chunkc_seed_memory(&conn, "chunkc-purge-ok", "victim", Tier::Mid);
db::forget(&conn, Some("chunkc-purge-ok"), None, None, true).unwrap();
crate::permissions::clear_active_permission_rules_for_test();
let req = make_tools_call("memory_archive_purge", json!({}));
let resp = invoke_handle_request(&conn, &req);
assert!(
resp.error.is_none(),
"purge happy path failed: {:?}",
resp.error
);
let payload = i4_decode_response_payload(&resp);
assert!(payload["purged"].as_u64().is_some());
}
#[test]
fn chunkc_archive_gc_real_run_invokes_db_gc() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let past = (chrono::Utc::now() - chrono::Duration::hours(2)).to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Short,
namespace: "chunkc-gc-real".to_string(),
title: "stale".to_string(),
content: "stale".to_string(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: Some(past),
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_gc", json!({"dry_run": false}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "gc real run failed: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["dry_run"], false);
}
#[test]
fn chunkc_archive_gc_dry_run_returns_count() {
let conn = db::open(std::path::Path::new(":memory:")).unwrap();
let past = (chrono::Utc::now() - chrono::Duration::hours(2)).to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Short,
namespace: "chunkc-gc-dry".to_string(),
title: "stale".to_string(),
content: "stale".to_string(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: chrono::Utc::now().to_rfc3339(),
updated_at: chrono::Utc::now().to_rfc3339(),
last_accessed_at: None,
expires_at: Some(past),
metadata: json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(&conn, &mem).unwrap();
let req = make_tools_call("memory_gc", json!({"dry_run": true}));
let resp = invoke_handle_request(&conn, &req);
assert!(resp.error.is_none(), "gc dry-run failed: {:?}", resp.error);
let payload = i4_decode_response_payload(&resp);
assert_eq!(payload["dry_run"], true);
assert!(payload["collected"].as_u64().unwrap() >= 1);
}
#[test]
fn every_registered_tool_has_dispatch_arm_1050() {
for tool in crate::mcp::registry::registered_tools() {
let name = tool.name;
assert!(
super::lookup_dispatch(name).is_some(),
"tool '{name}' is registered in registered_tools() but has no \
dispatch arm in TOOL_DISPATCH_TABLE — clients calling \
tools/call '{name}' will get -32601 unknown tool (#1050)"
);
}
}
#[test]
fn every_dispatch_arm_has_registered_tool_1050() {
let registered: std::collections::HashSet<&str> = crate::mcp::registry::registered_tools()
.iter()
.map(|t| t.name)
.collect();
for (name, _f) in super::TOOL_DISPATCH_TABLE {
assert!(
registered.contains(*name),
"dispatch arm '{name}' exists in TOOL_DISPATCH_TABLE but is not \
registered in registered_tools() — orphan dispatch wrapper (#1050)"
);
}
}
#[test]
fn mcp_line_length_cap_1249_const_invariants() {
assert!(
super::MCP_MAX_LINE_BYTES > 1_000_000,
"16 MiB cap must comfortably exceed largest realistic MCP request"
);
assert!(
super::MCP_MAX_DRAIN_BYTES > super::MCP_MAX_LINE_BYTES,
"drain ceiling must exceed line cap so overrun handling can drain to next \\n"
);
assert!(
super::MCP_MAX_LINE_BYTES <= 64 * 1024 * 1024,
"16 MiB upper bound — bigger caps make OOM-vector peers viable again"
);
}
#[test]
fn mcp_line_length_cap_1249_read_until_take_overrun() {
use std::io::{BufRead, Read};
let cap: usize = 1024 * 1024;
let payload = vec![b'A'; cap + 8192]; let mut src = std::io::Cursor::new(payload);
let mut buf: Vec<u8> = Vec::new();
let n = (&mut src)
.take((cap as u64) + 1)
.read_until(b'\n', &mut buf)
.expect("read_until succeeds against in-memory cursor");
assert_eq!(n, cap + 1, "should stop at the take() cap");
assert_ne!(
buf.last(),
Some(&b'\n'),
"buf must NOT end in \\n when the cap was hit — that's the overrun signal"
);
let mut scratch = [0u8; 64];
let m = src.read(&mut scratch).expect("further reads succeed");
assert!(
m > 0,
"underlying stream still has bytes for the drain path"
);
}
#[test]
fn mcp_line_length_cap_1249_clean_newline_termination() {
use std::io::BufRead;
let mut payload: Vec<u8> = vec![b'X'; 4096];
payload.push(b'\n');
payload.extend_from_slice(b"second line\n");
let mut src = std::io::Cursor::new(payload);
let mut buf: Vec<u8> = Vec::new();
let n = (&mut src)
.take((super::MCP_MAX_LINE_BYTES as u64) + 1)
.read_until(b'\n', &mut buf)
.expect("read_until OK");
assert_eq!(n, 4097);
assert_eq!(buf.last(), Some(&b'\n'));
buf.clear();
let m = (&mut src)
.take((super::MCP_MAX_LINE_BYTES as u64) + 1)
.read_until(b'\n', &mut buf)
.expect("read_until OK");
assert_eq!(m, "second line\n".len());
assert_eq!(buf.last(), Some(&b'\n'));
}
}
#[cfg(test)]
mod backfill_resilience_1595_tests {
use super::*;
use crate::models::{Memory, Tier};
const POISON_MARKER: &str = "poison-row-marker-1595";
fn seed(conn: &rusqlite::Connection, title: &str, content: &str) -> String {
let now = chrono::Utc::now().to_rfc3339();
let mem = Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Long,
namespace: "bf-1595".to_string(),
title: title.to_string(),
content: content.to_string(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: serde_json::json!({}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(conn, &mem).unwrap()
}
struct PoisonEmbedder;
impl Embed for PoisonEmbedder {
fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>> {
if text.contains(POISON_MARKER) {
anyhow::bail!("test: the input length exceeds the context length");
}
Ok(vec![0.5_f32; 4])
}
}
struct RecordingEmbedder {
seen_lens: std::sync::Mutex<Vec<usize>>,
}
impl Embed for RecordingEmbedder {
fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>> {
self.seen_lens.lock().unwrap().push(text.len());
Ok(vec![0.5_f32; 4])
}
}
#[test]
fn backfill_skips_poison_row_and_continues_1595() {
let mut conn = db::open(std::path::Path::new(":memory:")).unwrap();
for i in 0..2 {
seed(&conn, &format!("ok-head-{i}"), "plain healthy content");
}
seed(&conn, "poison", POISON_MARKER);
for i in 0..2 {
seed(&conn, &format!("ok-tail-{i}"), "plain healthy content");
}
let ok = run_embedding_backfill_with_batch_size(&mut conn, &PoisonEmbedder, 2)
.expect("sweep must not error");
assert_eq!(ok, 4, "all healthy rows backfilled");
let remaining = db::get_unembedded_ids(&conn).unwrap();
assert_eq!(remaining.len(), 1, "exactly skipped=1 (the poison row)");
assert!(
remaining[0].2.contains(POISON_MARKER),
"the surviving unembedded row is the poison row"
);
}
#[test]
fn backfill_oversize_row_skipped_client_side_1595() {
let mut conn = db::open(std::path::Path::new(":memory:")).unwrap();
seed(&conn, "small-a", "fits fine");
seed(
&conn,
"huge",
&"a".repeat(crate::embeddings::EMBED_MAX_BYTES + 1),
);
seed(&conn, "small-b", "also fits");
let emb = RecordingEmbedder {
seen_lens: std::sync::Mutex::new(Vec::new()),
};
let ok = run_embedding_backfill_with_batch_size(&mut conn, &emb, 10)
.expect("sweep must not error");
assert_eq!(ok, 2, "both small rows backfilled");
let remaining = db::get_unembedded_ids(&conn).unwrap();
assert_eq!(remaining.len(), 1, "oversize row skipped, not embedded");
assert_eq!(remaining[0].1, "huge");
let lens = emb.seen_lens.lock().unwrap();
assert!(
lens.iter()
.all(|&l| l <= crate::embeddings::EMBED_MAX_BYTES),
"oversize text must never be sent to the embedder, seen lens: {lens:?}"
);
}
#[test]
fn embed_rows_with_fallback_batch_fault_recovers_per_row_1595() {
struct BatchFailsRowsWork;
impl Embed for BatchFailsRowsWork {
fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
Ok(vec![0.25_f32; 3])
}
fn embed_batch(&self, _texts: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
anyhow::bail!("test: synthetic chunk-level failure")
}
}
let rows: Vec<(String, String, String)> = (0..3)
.map(|i| (format!("id-{i}"), format!("t-{i}"), format!("c-{i}")))
.collect();
let out = embed_rows_with_fallback(&BatchFailsRowsWork, &rows);
assert_eq!(out.entries.len(), 3);
assert!(out.skipped.is_empty());
assert_eq!(out.entries[0].0, "id-0");
}
#[test]
fn embed_rows_with_fallback_misaligned_batch_recovers_per_row_1595() {
struct MisalignedEmbedder;
impl Embed for MisalignedEmbedder {
fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
Ok(vec![0.75_f32; 3])
}
fn embed_batch(&self, _texts: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
Ok(vec![vec![0.1_f32; 3]])
}
}
let rows: Vec<(String, String, String)> = (0..2)
.map(|i| (format!("id-{i}"), format!("t-{i}"), format!("c-{i}")))
.collect();
let out = embed_rows_with_fallback(&MisalignedEmbedder, &rows);
assert_eq!(out.entries.len(), 2, "per-row fallback recovers both");
assert!(out.skipped.is_empty());
assert!(
out.entries.iter().all(|(_, v)| v.len() == 3),
"vectors come from the per-row path, not the misaligned batch"
);
}
#[test]
fn embed_rows_with_fallback_reports_per_row_skips_1595() {
let rows = vec![
("id-ok".to_string(), "t".to_string(), "fine".to_string()),
(
"id-bad".to_string(),
"t".to_string(),
POISON_MARKER.to_string(),
),
];
let out = embed_rows_with_fallback(&PoisonEmbedder, &rows);
assert_eq!(out.entries.len(), 1);
assert_eq!(out.entries[0].0, "id-ok");
assert_eq!(out.skipped.len(), 1);
assert_eq!(out.skipped[0].0, "id-bad");
assert!(
out.skipped[0].1.contains("context length"),
"skip reason carries the embedder error: {}",
out.skipped[0].1
);
}
#[test]
fn embed_rows_with_fallback_empty_rows_is_noop_1595() {
struct PanickingEmbedder;
impl Embed for PanickingEmbedder {
fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
unreachable!("must not be called for an empty chunk")
}
}
let out = embed_rows_with_fallback(&PanickingEmbedder, &[]);
assert!(out.entries.is_empty());
assert!(out.skipped.is_empty());
}
#[test]
fn backfill_write_fault_falls_back_per_row_1595() {
struct EightDimEmbedder;
impl Embed for EightDimEmbedder {
fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
Ok(vec![0.5_f32; 8])
}
}
let mut conn = db::open(std::path::Path::new(":memory:")).unwrap();
let est = seed(&conn, "established", "already embedded");
db::set_embedding(&conn, &est, &[0.1, 0.2, 0.3, 0.4]).unwrap();
seed(&conn, "new-a", "needs embedding");
seed(&conn, "new-b", "needs embedding");
let ok = run_embedding_backfill_with_batch_size(&mut conn, &EightDimEmbedder, 10)
.expect("write faults must not propagate");
assert_eq!(ok, 0, "dim-mismatched rows cannot land");
assert_eq!(
db::get_unembedded_ids(&conn).unwrap().len(),
2,
"both rows skipped (left for the next sweep), sweep terminated"
);
}
}