1#![allow(clippy::too_many_lines)]
14
15use crate::models::field_names;
16use serde::{Deserialize, Serialize};
17use serde_json::{Value, json};
18use std::io::{self, BufRead, Read, Write};
19use std::path::Path;
20use std::sync::Arc;
21use std::time::Instant;
22
23use crate::config::{AppConfig, FeatureTier, ResolvedModels, TierConfig};
24use crate::db;
25use crate::embeddings::{Embed, Embedder};
26use crate::hnsw::VectorIndex;
27use crate::llm::OllamaClient;
28use crate::reranker::{BatchedReranker, CrossEncoder};
29
30const EFFECTIVE_TIER_AUTONOMOUS: &str = "autonomous";
34
35pub(super) mod registry;
36
37pub mod server_identity;
42
43pub mod param_names;
49
50pub mod jsonrpc;
53
54#[cfg(test)]
60pub(super) mod parity_test_helpers;
61
62#[cfg(test)]
71pub(super) static SHARED_PERMISSION_RULES_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
72
73pub(crate) use registry::families_overview;
78#[cfg(test)]
84pub(crate) use registry::trim_optional_params;
85pub use registry::{
86 handle_capabilities_family, tool_definitions, tool_definitions_for_profile,
87 tool_definitions_for_profile_verbose,
88};
89
90#[derive(Deserialize)]
93struct RpcRequest {
94 jsonrpc: String,
95 id: Option<Value>,
96 method: String,
97 #[serde(default)]
98 params: Value,
99}
100
101#[derive(Debug, Serialize)]
102struct RpcResponse {
103 jsonrpc: String,
104 id: Value,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 result: Option<Value>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 error: Option<RpcError>,
109}
110
111#[derive(Debug, Serialize)]
112struct RpcError {
113 code: i64,
114 message: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 data: Option<Value>,
117}
118
119fn ok_response(id: Value, result: Value) -> RpcResponse {
120 RpcResponse {
121 jsonrpc: jsonrpc::VERSION.into(),
122 id,
123 result: Some(result),
124 error: None,
125 }
126}
127
128fn err_response(id: Value, code: i64, message: String) -> RpcResponse {
129 RpcResponse {
130 jsonrpc: jsonrpc::VERSION.into(),
131 id,
132 result: None,
133 error: Some(RpcError {
134 code,
135 message,
136 data: None,
137 }),
138 }
139}
140
141fn audit_emit_for_mcp_dispatch(
147 tool_name: &str,
148 arguments: &Value,
149 result: &Result<Value, String>,
150 mcp_client: Option<&str>,
151) {
152 if !crate::audit::is_enabled() {
153 return;
154 }
155 use crate::mcp::registry::tool_names;
156 let action = match tool_name {
157 tool_names::MEMORY_STORE | tool_names::MEMORY_DELETE => return,
159 tool_names::MEMORY_RECALL
160 | tool_names::MEMORY_SEARCH
161 | tool_names::MEMORY_GET
162 | tool_names::MEMORY_LIST
163 | tool_names::MEMORY_SESSION_START => crate::audit::AuditAction::Recall,
164 tool_names::MEMORY_UPDATE => crate::audit::AuditAction::Update,
165 tool_names::MEMORY_PROMOTE => crate::audit::AuditAction::Promote,
166 tool_names::MEMORY_FORGET => crate::audit::AuditAction::Forget,
167 tool_names::MEMORY_LINK => crate::audit::AuditAction::Link,
168 tool_names::MEMORY_CONSOLIDATE => crate::audit::AuditAction::Consolidate,
169 tool_names::MEMORY_PENDING_APPROVE => crate::audit::AuditAction::Approve,
170 tool_names::MEMORY_PENDING_REJECT => crate::audit::AuditAction::Reject,
171 _ => return,
173 };
174 let agent_id = resolve_mcp_agent_id(arguments, mcp_client);
175 let namespace = arguments
176 .get("namespace")
177 .and_then(Value::as_str)
178 .unwrap_or(crate::DEFAULT_NAMESPACE)
179 .to_string();
180 let memory_id = arguments
181 .get("id")
182 .or_else(|| arguments.get("memory_id"))
183 .and_then(Value::as_str)
184 .unwrap_or("*")
185 .to_string();
186 let mut builder = crate::audit::EventBuilder::new(
187 action,
188 crate::audit::actor(
189 agent_id,
190 mcp_client.map_or(crate::audit::synthesis_sources::HOST_FALLBACK, |_| {
191 crate::audit::synthesis_sources::MCP_CLIENT_INFO
192 }),
193 None,
194 ),
195 crate::audit::AuditTarget {
196 memory_id,
197 namespace,
198 title: None,
199 tier: None,
200 scope: None,
201 },
202 );
203 if let Err(e) = result {
204 builder = builder.error(e.clone());
205 }
206 crate::audit::emit(builder);
207}
208
209fn resolve_mcp_agent_id(arguments: &Value, mcp_client: Option<&str>) -> String {
215 arguments
216 .get("agent_id")
217 .and_then(Value::as_str)
218 .map(str::to_string)
219 .unwrap_or_else(|| {
220 mcp_client
221 .map(|c| format!("ai:{c}"))
222 .unwrap_or_else(|| "anonymous".into())
223 })
224}
225
226fn observe_capture_nag(
234 nag_watcher: Option<&crate::recover::nag::CaptureNagWatcher>,
235 session_id: &str,
236 tool_name: &str,
237 arguments: &Value,
238 mcp_client: Option<&str>,
239) -> crate::recover::nag::NagAction {
240 use crate::recover::nag::{NagAction, classify_tool};
241 let Some(watcher) = nag_watcher else {
242 return NagAction::None;
243 };
244 let agent_id = resolve_mcp_agent_id(arguments, mcp_client);
245 let action = watcher.observe_tool_call(&agent_id, session_id, classify_tool(tool_name));
246 match action {
247 NagAction::None => {}
248 NagAction::Warn => emit_capture_lag(
249 &agent_id,
250 session_id,
251 watcher.streak_for(&agent_id, session_id),
252 watcher.primary_threshold(),
253 false,
254 ),
255 NagAction::WarnAndEscalate => emit_capture_lag(
256 &agent_id,
257 session_id,
258 watcher.streak_for(&agent_id, session_id),
259 watcher.escalation_threshold(),
260 true,
261 ),
262 }
263 action
264}
265
266fn emit_capture_lag(
272 agent_id: &str,
273 session_id: &str,
274 streak: u32,
275 threshold: u32,
276 escalated: bool,
277) {
278 let tier = if escalated { "escalation" } else { "warn" };
279 eprintln!(
280 "ai-memory capture_lag [{tier}]: agent={agent_id} session={session_id} — \
281 {streak} consecutive non-capture tool calls (threshold {threshold}); \
282 call memory_store or memory_capture_turn to record progress"
283 );
284 if !crate::audit::is_enabled() {
285 return;
286 }
287 let title = format!(
288 "capture_lag: {streak} non-capture tool calls (threshold {threshold}{})",
289 if escalated { ", escalation" } else { "" }
290 );
291 let mut builder = crate::audit::EventBuilder::new(
292 crate::audit::AuditAction::CaptureLag,
293 crate::audit::actor(
294 agent_id.to_string(),
295 crate::audit::synthesis_sources::MCP_CLIENT_INFO,
296 None,
297 ),
298 crate::audit::AuditTarget {
299 memory_id: "*".to_string(),
300 namespace: crate::DEFAULT_NAMESPACE.to_string(),
301 title: Some(title),
302 tier: None,
303 scope: None,
304 },
305 );
306 builder.session_id = Some(session_id.to_string());
307 crate::audit::emit(builder);
308}
309
310pub fn prompt_definitions() -> Value {
314 json!({
315 "prompts": [
316 {
317 "name": "recall-first",
318 (field_names::DESCRIPTION): "System prompt for AI clients: proactive memory recall, TOON format, tier strategy.",
319 "arguments": [
320 {
321 "name": "namespace",
322 (field_names::DESCRIPTION): "Optional namespace to scope recall.",
323 "required": false
324 }
325 ]
326 },
327 {
328 "name": "memory-workflow",
329 (field_names::DESCRIPTION): "Quick reference card for memory tool usage patterns."
330 }
331 ]
332 })
333}
334
335fn prompt_content(name: &str, params: &Value) -> Result<Value, String> {
337 match name {
338 "recall-first" => {
339 let ns_hint = params
340 .get("arguments")
341 .and_then(|a| a.get("namespace"))
342 .and_then(|v| v.as_str())
343 .map(|ns| format!(" Scope recall to namespace \"{ns}\" when relevant."))
344 .unwrap_or_default();
345
346 Ok(json!({
347 "messages": [{
348 "role": "user",
349 "content": {
350 "type": "text",
351 "text": format!(
352 "You have access to a persistent memory system (ai-memory). Follow these rules:\n\
353 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\
354 2. STORE LEARNINGS: When the user corrects you or teaches something, call memory_store with tier:long, priority:9.\n\
355 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\
356 4. TIERS: short=6h ephemeral, mid=7d working knowledge, long=permanent. Mid auto-promotes to long at 5 accesses.\n\
357 5. DEDUP: Storing with an existing title+namespace updates the existing memory, not a duplicate.\n\
358 6. NAMESPACES: Organize by project/topic. Always pass namespace when storing and recalling.\n\
359 7. CAPABILITIES: Call memory_capabilities once per session to discover available features (tier-dependent).\n\
360 8. TAGS: Use tags for cross-cutting concerns. memory_auto_tag can generate them if available.{ns_hint}")
361 }
362 }]
363 }))
364 }
365 "memory-workflow" => Ok(json!({
366 "messages": [{
367 "role": "user",
368 "content": {
369 "type": "text",
370 "text": "\
371 STORE: memory_store(title, content, tier, namespace, tags, priority) — dedup by title+ns\n\
372 RECALL: memory_recall(context, namespace) → ranked results (TOON compact default)\n\
373 SEARCH: memory_search(query, namespace) → exact AND match (TOON compact default)\n\
374 LIST: memory_list(namespace, tier) → browse with filters (TOON compact default)\n\
375 GET: memory_get(id) → single memory with links\n\
376 PROMOTE: memory_promote(id) — mid→long, clears expiry\n\
377 CONSOLIDATE: memory_consolidate(ids, title) — merge N→1, LLM summary if available\n\
378 LINK: memory_link(source_id, target_id, relation) — related_to|supersedes|contradicts|derived_from|reflects_on\n\
379 TAG: memory_auto_tag(id) — LLM generates tags (smart+ tier)\n\
380 EXPAND: memory_expand_query(query) — LLM broadens search terms (smart+ tier)\n\
381 CONTRADICT: memory_detect_contradiction(id_a, id_b) — LLM checks conflict (smart+ tier)"
382 }
383 }]
384 })),
385 _ => Err(format!("unknown prompt: {name}")),
386 }
387}
388
389#[path = "tools/agent.rs"]
405mod agent;
406#[path = "tools/archive.rs"]
407mod archive;
408#[path = "tools/auto_tag.rs"]
409mod auto_tag;
410#[path = "tools/capabilities.rs"]
411mod capabilities;
412#[path = "tools/capture_turn.rs"]
417mod capture_turn;
418#[path = "tools/check_duplicate.rs"]
419mod check_duplicate;
420#[path = "tools/consolidate.rs"]
421mod consolidate;
422#[path = "tools/atomise.rs"]
424mod atomise;
425#[path = "tools/delete.rs"]
428mod delete;
429#[path = "tools/detect_contradiction.rs"]
430mod detect_contradiction;
431#[path = "tools/entity_get_by_alias.rs"]
432mod entity_get_by_alias;
433#[path = "tools/entity_register.rs"]
434mod entity_register;
435#[path = "tools/expand_query.rs"]
436mod expand_query;
437#[path = "tools/find_paths.rs"]
438mod find_paths;
439#[path = "tools/forget.rs"]
440mod forget;
441#[path = "tools/get.rs"]
442mod get;
443#[path = "tools/get_taxonomy.rs"]
444mod get_taxonomy;
445#[path = "tools/ingest_multistep.rs"]
446mod ingest_multistep;
447#[path = "tools/kg_invalidate.rs"]
448mod kg_invalidate;
449#[path = "tools/kg_query.rs"]
450mod kg_query;
451#[path = "tools/kg_timeline.rs"]
452mod kg_timeline;
453#[path = "tools/link.rs"]
454mod link;
455#[path = "tools/list.rs"]
456mod list;
457#[path = "tools/load_family.rs"]
458mod load_family;
459#[path = "tools/namespace.rs"]
460mod namespace;
461#[path = "tools/notify.rs"]
462mod notify;
463#[path = "tools/offload.rs"]
467mod offload;
468#[path = "tools/pending.rs"]
469mod pending;
470#[path = "tools/promote.rs"]
471mod promote;
472#[path = "tools/quota_status.rs"]
473mod quota_status;
474#[path = "tools/check_agent_action.rs"]
476mod check_agent_action;
477#[path = "tools/recall.rs"]
478mod recall;
479#[path = "tools/recall_observations.rs"]
481mod recall_observations;
482#[path = "tools/reflect.rs"]
483mod reflect;
484#[path = "tools/reflection_origin.rs"]
485mod reflection_origin;
486#[path = "tools/export_reflection.rs"]
488mod export_reflection;
489#[path = "tools/persona.rs"]
491mod persona;
492#[path = "tools/calibrate_confidence.rs"]
495mod calibrate_confidence;
496#[path = "tools/dependents_of_invalidated.rs"]
498mod dependents_of_invalidated;
499#[path = "tools/replay.rs"]
500mod replay;
501#[path = "tools/rule_list.rs"]
502mod rule_list;
503#[path = "tools/search.rs"]
504mod search;
505#[path = "tools/session_start.rs"]
506mod session_start;
507#[path = "tools/share.rs"]
516pub mod share;
517#[path = "tools/store/mod.rs"]
518mod store;
519#[path = "tools/subscribe.rs"]
520mod subscribe;
521#[path = "tools/update.rs"]
522mod update;
523#[path = "tools/verify.rs"]
524mod verify;
525#[path = "tools/skill_export.rs"]
527mod skill_export;
528#[path = "tools/skill_get.rs"]
529mod skill_get;
530#[path = "tools/skill_list.rs"]
531mod skill_list;
532#[path = "tools/skill_register.rs"]
533mod skill_register;
534#[path = "tools/skill_resource.rs"]
535mod skill_resource;
536#[path = "tools/skill_promote.rs"]
539mod skill_promote;
540#[path = "tools/skill_compositional_context.rs"]
542mod skill_compositional_context;
543#[cfg(test)]
547#[path = "tools/d1_4_985_helpers.rs"]
548pub(crate) mod d1_4_985_helpers;
549
550pub use capabilities::{
556 CapabilitiesAccept, build_agent_permitted_families, build_capabilities_describe_to_user,
557 build_capabilities_summary, build_capabilities_tools, effective_tier_label,
558 format_rule_summary, handle_capabilities_with_conn, handle_capabilities_with_conn_v3,
559 overlay_tool_payloads,
560};
561pub use find_paths::handle_find_paths;
562pub use check_duplicate::handle_check_duplicate;
568pub use expand_query::handle_expand_query;
574pub use kg_query::handle_kg_query;
575pub use load_family::{handle_load_family, handle_smart_load};
576pub(crate) use namespace::handle_namespace_clear_standard;
577pub use namespace::{handle_namespace_get_standard, handle_namespace_set_standard};
589pub use notify::{handle_inbox, handle_notify};
590pub use pending::{handle_pending_approve, handle_pending_reject};
591pub use capture_turn::{MemoryCaptureTurnRequest, handle_capture_turn};
596pub(crate) use capture_turn::prepare_capture_turn;
600pub use quota_status::handle_quota_status;
601pub use check_agent_action::handle_check_agent_action;
603pub use recall::handle_recall;
604pub use recall::handle_recall_caller;
605pub use recall::handle_recall_with_pre_recall_hook;
606pub use recall::decorate_memory_many;
622pub use recall_observations::handle_recall_observations;
627#[cfg(feature = "sal")]
629pub(crate) use recall_observations::{
630 DEFAULT_LIMIT as RECALL_OBS_DEFAULT_LIMIT, MAX_LIMIT as RECALL_OBS_MAX_LIMIT,
631};
632pub use replay::handle_replay;
633pub use rule_list::handle_rule_list;
634pub(crate) use session_start::handle_session_start;
635pub use subscribe::handle_unsubscribe;
636pub use entity_get_by_alias::handle_entity_get_by_alias;
643pub use entity_register::handle_entity_register;
644pub use ingest_multistep::{IngestMultistepHandler, handle_ingest_multistep};
645pub use kg_invalidate::handle_kg_invalidate;
646pub use kg_timeline::handle_kg_timeline;
647pub use subscribe::{handle_list_subscriptions, handle_subscribe};
648pub use verify::handle_verify;
649pub use skill_compositional_context::handle_skill_compositional_context;
660pub use skill_export::handle_skill_export;
661pub use skill_get::handle_skill_get;
662pub use skill_list::handle_skill_list;
663pub use skill_promote::handle_skill_promote_from_reflection;
664pub use skill_register::handle_skill_register;
665pub use skill_resource::handle_skill_resource;
666
667pub use calibrate_confidence::handle_calibrate_confidence;
674pub use dependents_of_invalidated::handle_dependents_of_invalidated;
675pub use export_reflection::handle_export_reflection;
676pub use pending::handle_subscription_dlq_list;
677pub use reflect::handle_reflect;
678#[cfg(feature = "sal")]
682pub(crate) use reflect::{map_reflect_error_to_wire_string, parse_reflect_input};
683pub use reflection_origin::handle_reflection_origin;
684pub use subscribe::handle_subscription_replay;
685
686#[doc(hidden)]
691pub fn handle_archive_purge_for_test(
692 conn: &rusqlite::Connection,
693 params: &serde_json::Value,
694) -> Result<serde_json::Value, String> {
695 archive::handle_archive_purge(conn, params)
696}
697
698#[doc(hidden)]
706pub fn dispatch_handle_link_for_test(
707 conn: &rusqlite::Connection,
708 db_path: &std::path::Path,
709 params: &serde_json::Value,
710 active_keypair: Option<&crate::identity::keypair::AgentKeypair>,
711) -> Result<serde_json::Value, String> {
712 link::handle_link(conn, db_path, params, active_keypair)
713}
714
715#[doc(hidden)]
719pub fn dispatch_handle_dependents_for_test(
720 conn: &rusqlite::Connection,
721 params: &serde_json::Value,
722) -> Result<serde_json::Value, String> {
723 dependents_of_invalidated::handle_dependents_of_invalidated(conn, params)
724}
725
726#[must_use]
733pub fn tools_check_agent_action_mutation_disabled_error() -> &'static str {
734 check_agent_action::MCP_MUTATION_DISABLED_ERROR
735}
736
737#[doc(hidden)]
747pub mod schema_handler_parity_test_exports {
748 pub use super::capabilities::CapabilitiesRequest;
749 pub use super::link::LinkRequest;
750 pub use super::pending::PendingApproveRequest;
751 pub use super::store::StoreRequest;
752 pub use crate::models::recall_request::RecallRequest;
753}
754
755pub mod tools {
764 pub use super::atomise::{AtomiseToolHandler, handle_atomise};
765
766 pub use super::store::OnConflictMode;
773
774 pub mod capabilities {
781 pub use super::super::capabilities::tool_examples;
782 }
783
784 pub mod skill_register {
789 pub use super::super::skill_register::{SkillRegisterRequest, handle_skill_register};
790 }
791
792 pub use super::ingest_multistep::{IngestMultistepHandler, handle_ingest_multistep};
797
798 pub mod check_agent_action {
806 pub use super::super::check_agent_action::{
807 DEFAULT_AGENT_ID, build_action, handle_check_agent_action, run_check,
808 };
809 }
810
811 pub mod kg_invalidate {
818 pub use super::super::kg_invalidate::handle_kg_invalidate;
819 }
820
821 #[doc(hidden)]
827 pub fn handle_promote_for_tests(
828 conn: &rusqlite::Connection,
829 db_path: &std::path::Path,
830 params: &serde_json::Value,
831 mcp_client: Option<&str>,
832 ) -> Result<serde_json::Value, String> {
833 super::promote::handle_promote(conn, db_path, params, mcp_client)
834 }
835
836 #[doc(hidden)]
841 #[allow(clippy::too_many_arguments)]
842 pub fn handle_store_for_tests(
843 conn: &rusqlite::Connection,
844 db_path: &std::path::Path,
845 params: &serde_json::Value,
846 embedder: Option<&dyn crate::embeddings::Embed>,
847 llm: Option<&crate::llm::OllamaClient>,
848 vector_index: Option<&crate::hnsw::VectorIndex>,
849 resolved_ttl: &crate::config::ResolvedTtl,
850 autonomous_hooks: bool,
851 mcp_client: Option<&str>,
852 federation_forward_url: Option<&str>,
853 ) -> Result<serde_json::Value, String> {
854 super::store::handle_store(
855 conn,
856 db_path,
857 params,
858 embedder,
859 llm,
860 vector_index,
861 resolved_ttl,
862 autonomous_hooks,
863 mcp_client,
864 federation_forward_url,
865 None,
871 )
872 }
873}
874
875use agent::{handle_agent_list, handle_agent_register};
880use archive::{
881 handle_archive_list, handle_archive_purge, handle_archive_restore, handle_archive_stats,
882 handle_gc,
883};
884use auto_tag::handle_auto_tag;
885use consolidate::handle_consolidate;
888use atomise::handle_atomise;
890use delete::handle_delete;
896use detect_contradiction::handle_detect_contradiction;
899use forget::{handle_forget, handle_stats};
904use get::handle_get;
905use get_taxonomy::handle_get_taxonomy;
906use link::{handle_get_links, handle_link};
912use list::handle_list;
913use pending::handle_pending_list;
914use persona::{handle_persona, handle_persona_generate};
916pub use persona::handle_persona_generate as persona_generate_call;
924use promote::handle_promote;
925use search::handle_search;
928#[doc(hidden)]
953pub fn skill_compositional_context_for_tests(
954 conn: &rusqlite::Connection,
955 params: &serde_json::Value,
956) -> Result<serde_json::Value, String> {
957 handle_skill_compositional_context(conn, params)
958}
959use store::handle_store;
966use update::handle_update;
971
972#[cfg(test)]
979use agent::messages_namespace_for;
980#[cfg(test)]
981use namespace::{auto_register_path_hierarchy, extract_governance};
982#[cfg(test)]
983use replay::REPLAY_VERBOSE_THRESHOLD_BYTES;
984
985fn build_namespace_chain(conn: &rusqlite::Connection, namespace: &str) -> Vec<String> {
990 db::build_namespace_chain(conn, namespace)
991}
992
993fn inject_namespace_standard(
997 conn: &rusqlite::Connection,
998 namespace: Option<&str>,
999 response: &mut Value,
1000) {
1001 let mut standards: Vec<Value> = Vec::new();
1002 let mut standard_ids: Vec<String> = Vec::new();
1003
1004 let add_standard = |std: Value, ids: &mut Vec<String>, stds: &mut Vec<Value>| {
1006 let id = std["id"].as_str().unwrap_or_default().to_string();
1007 if !ids.contains(&id) {
1008 ids.push(id);
1009 stds.push(std);
1010 }
1011 };
1012
1013 let chain = if let Some(ns) = namespace {
1014 build_namespace_chain(conn, ns)
1015 } else {
1016 vec!["*".to_string()]
1018 };
1019
1020 for link in chain {
1021 if let Some(std) = lookup_namespace_standard(conn, &link) {
1022 add_standard(std, &mut standard_ids, &mut standards);
1023 }
1024 }
1025
1026 if standards.is_empty() {
1027 return;
1028 }
1029
1030 if let Some(memories) = response["memories"].as_array_mut() {
1032 memories.retain(|m| {
1033 let mid = m["id"].as_str().unwrap_or_default();
1034 !standard_ids.iter().any(|sid| sid == mid)
1035 });
1036 response["count"] = json!(memories.len());
1037 }
1038
1039 if standards.len() == 1 {
1041 response["standard"] = standards.into_iter().next().unwrap();
1042 } else {
1043 response["standards"] = json!(standards);
1044 }
1045}
1046
1047#[allow(clippy::too_many_arguments)]
1064
1065fn lookup_namespace_standard(conn: &rusqlite::Connection, namespace: &str) -> Option<Value> {
1068 let standard_id = db::get_namespace_standard(conn, namespace).ok()??;
1069 let mem = db::get(conn, &standard_id).ok()??;
1070 serde_json::to_value(&mem).ok()
1071}
1072
1073pub(crate) struct ToolDispatchCtx<'a> {
1114 pub conn: &'a rusqlite::Connection,
1115 pub db_path: &'a Path,
1116 pub arguments: &'a Value,
1117 pub embedder: Option<&'a dyn Embed>,
1118 pub llm: Option<&'a OllamaClient>,
1119 pub reranker: Option<&'a BatchedReranker>,
1120 pub tier_config: &'a TierConfig,
1121 pub resolved_models: &'a ResolvedModels,
1129 pub vector_index: Option<&'a VectorIndex>,
1130 pub resolved_ttl: &'a crate::config::ResolvedTtl,
1131 pub resolved_scoring: &'a crate::config::ResolvedScoring,
1132 pub archive_on_gc: bool,
1133 pub autonomous_hooks: bool,
1134 pub mcp_client: Option<&'a str>,
1135 pub profile: &'a crate::profile::Profile,
1136 pub mcp_config: Option<&'a crate::config::McpConfig>,
1137 pub active_keypair: Option<&'a crate::identity::keypair::AgentKeypair>,
1138 pub harness: Option<&'a crate::harness::Harness>,
1139 pub federation_forward_url: Option<&'a str>,
1140 pub recall_scope: Option<&'a crate::config::RecallScope>,
1141 pub atomise_handler: Option<&'a atomise::AtomiseToolHandler>,
1142 pub ingest_multistep_handler: Option<&'a ingest_multistep::IngestMultistepHandler>,
1143}
1144
1145pub(crate) type DispatchFn = fn(&ToolDispatchCtx<'_>) -> Result<Value, String>;
1149
1150macro_rules! register_mcp_tool {
1167 ($name:expr, $f:path) => {
1168 ($name, $f as DispatchFn)
1169 };
1170}
1171
1172fn dispatch_memory_store(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1183 handle_store(
1184 ctx.conn,
1185 ctx.db_path,
1186 ctx.arguments,
1187 ctx.embedder,
1188 ctx.llm,
1189 ctx.vector_index,
1190 ctx.resolved_ttl,
1191 ctx.autonomous_hooks,
1192 ctx.mcp_client,
1193 ctx.federation_forward_url,
1194 ctx.active_keypair,
1199 )
1200}
1201
1202fn dispatch_memory_recall(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1203 let caller = crate::identity::resolve_read_visibility_caller();
1207 handle_recall_caller(
1208 ctx.conn,
1209 ctx.arguments,
1210 ctx.embedder,
1211 ctx.vector_index,
1212 ctx.reranker,
1213 ctx.archive_on_gc,
1214 ctx.resolved_ttl,
1215 ctx.resolved_scoring,
1216 ctx.recall_scope,
1217 caller.as_deref(),
1218 )
1219}
1220
1221fn dispatch_memory_recall_observations(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1224 handle_recall_observations(ctx.conn, ctx.arguments)
1225}
1226
1227fn dispatch_memory_search(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1228 let caller = crate::identity::resolve_read_visibility_caller();
1230 handle_search(ctx.conn, ctx.arguments, caller.as_deref())
1231}
1232
1233fn dispatch_memory_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1234 let caller = crate::identity::resolve_read_visibility_caller();
1236 handle_list(ctx.conn, ctx.arguments, caller.as_deref())
1237}
1238
1239fn dispatch_memory_load_family(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1240 let caller = crate::identity::resolve_read_visibility_caller();
1242 handle_load_family(ctx.conn, ctx.arguments, caller.as_deref())
1243}
1244
1245fn dispatch_memory_smart_load(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1246 let caller = crate::identity::resolve_read_visibility_caller();
1248 handle_smart_load(ctx.conn, ctx.arguments, ctx.embedder, caller.as_deref())
1249}
1250
1251fn dispatch_memory_get_taxonomy(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1252 handle_get_taxonomy(ctx.conn, ctx.arguments)
1253}
1254
1255fn dispatch_memory_check_duplicate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1256 handle_check_duplicate(ctx.conn, ctx.arguments, ctx.embedder)
1257}
1258
1259fn dispatch_memory_entity_register(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1260 handle_entity_register(ctx.conn, ctx.arguments, ctx.mcp_client)
1261}
1262
1263fn dispatch_memory_entity_get_by_alias(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1264 handle_entity_get_by_alias(ctx.conn, ctx.arguments)
1265}
1266
1267fn dispatch_memory_kg_timeline(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1268 handle_kg_timeline(ctx.conn, ctx.arguments)
1269}
1270
1271fn dispatch_memory_kg_invalidate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1272 handle_kg_invalidate(ctx.conn, ctx.db_path, ctx.arguments)
1273}
1274
1275fn dispatch_memory_kg_query(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1276 handle_kg_query(ctx.conn, ctx.arguments)
1277}
1278
1279fn dispatch_memory_find_paths(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1280 handle_find_paths(ctx.conn, ctx.arguments)
1281}
1282
1283fn dispatch_memory_delete(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1284 handle_delete(
1285 ctx.conn,
1286 ctx.db_path,
1287 ctx.arguments,
1288 ctx.vector_index,
1289 ctx.mcp_client,
1290 )
1291}
1292
1293fn dispatch_memory_promote(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1294 handle_promote(ctx.conn, ctx.db_path, ctx.arguments, ctx.mcp_client)
1295}
1296
1297fn dispatch_memory_pending_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1298 handle_pending_list(ctx.conn, ctx.arguments)
1299}
1300
1301fn dispatch_memory_pending_approve(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1302 handle_pending_approve(ctx.conn, ctx.arguments, ctx.mcp_client)
1303}
1304
1305fn dispatch_memory_pending_reject(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1306 handle_pending_reject(ctx.conn, ctx.arguments, ctx.mcp_client)
1307}
1308
1309fn dispatch_memory_forget(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1310 handle_forget(ctx.conn, ctx.arguments, ctx.archive_on_gc)
1311}
1312
1313fn dispatch_memory_stats(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1314 handle_stats(ctx.conn, ctx.db_path)
1315}
1316
1317fn dispatch_memory_update(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1318 handle_update(
1319 ctx.conn,
1320 ctx.arguments,
1321 ctx.embedder,
1322 ctx.vector_index,
1323 ctx.mcp_client,
1324 )
1325}
1326
1327fn dispatch_memory_get(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1328 let caller = crate::identity::resolve_read_visibility_caller();
1330 handle_get(ctx.conn, ctx.arguments, caller.as_deref())
1331}
1332
1333fn dispatch_memory_link(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1334 handle_link(ctx.conn, ctx.db_path, ctx.arguments, ctx.active_keypair)
1335}
1336
1337fn dispatch_memory_get_links(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1338 let caller = crate::identity::resolve_read_visibility_caller();
1341 handle_get_links(ctx.conn, ctx.arguments, caller.as_deref())
1342}
1343
1344fn dispatch_memory_verify(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1345 handle_verify(ctx.conn, ctx.arguments)
1346}
1347
1348fn dispatch_memory_replay(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1349 let caller = crate::identity::resolve_read_visibility_caller();
1353 handle_replay(ctx.conn, ctx.arguments, ctx.mcp_client, caller.as_deref())
1354}
1355
1356fn dispatch_memory_consolidate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1357 handle_consolidate(
1358 ctx.conn,
1359 ctx.db_path,
1360 ctx.arguments,
1361 ctx.llm,
1362 ctx.embedder,
1363 ctx.vector_index,
1364 ctx.mcp_client,
1365 )
1366}
1367
1368fn dispatch_memory_atomise(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1369 handle_atomise(
1370 ctx.conn,
1371 ctx.arguments,
1372 ctx.atomise_handler,
1373 ctx.tier_config.tier,
1374 ctx.mcp_client,
1375 )
1376}
1377
1378fn dispatch_memory_ingest_multistep(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1379 handle_ingest_multistep(
1380 ctx.arguments,
1381 ctx.ingest_multistep_handler,
1382 ctx.tier_config.tier,
1383 )
1384}
1385
1386fn dispatch_memory_reflect(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1387 handle_reflect(
1388 ctx.conn,
1389 ctx.db_path,
1390 ctx.arguments,
1391 ctx.embedder,
1392 ctx.vector_index,
1393 ctx.mcp_client,
1394 ctx.active_keypair,
1395 )
1396}
1397
1398fn dispatch_memory_capabilities(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1403 let arguments = ctx.arguments;
1404 if let Some(fam_name) = arguments.get("family").and_then(Value::as_str) {
1405 let include_schema = arguments
1406 .get("include_schema")
1407 .and_then(Value::as_bool)
1408 .unwrap_or(false);
1409 let verbose = arguments
1410 .get("verbose")
1411 .and_then(Value::as_bool)
1412 .unwrap_or(false);
1413 let aid = arguments
1414 .get("agent_id")
1415 .and_then(Value::as_str)
1416 .or(ctx.mcp_client);
1417 return handle_capabilities_family(
1418 fam_name,
1419 include_schema,
1420 verbose,
1421 ctx.profile,
1422 ctx.mcp_config,
1423 aid,
1424 Some(ctx.conn),
1425 );
1426 }
1427
1428 let accept = arguments
1429 .get("accept")
1430 .and_then(Value::as_str)
1431 .map_or(CapabilitiesAccept::V3, CapabilitiesAccept::parse);
1432 let top_verbose = arguments
1433 .get("verbose")
1434 .and_then(Value::as_bool)
1435 .unwrap_or(false);
1436 let top_include_schema = arguments
1437 .get("include_schema")
1438 .and_then(Value::as_bool)
1439 .unwrap_or(false);
1440 let v3_aid = arguments
1441 .get("agent_id")
1442 .and_then(Value::as_str)
1443 .or(ctx.mcp_client);
1444 let runtime_tier = effective_tier_label(
1445 ctx.llm.is_some(),
1446 ctx.embedder.is_some(),
1447 ctx.reranker.is_some(),
1448 );
1449 let embedder_live = ctx.embedder.is_some_and(|e| !e.is_degraded());
1454 let result = match accept {
1455 CapabilitiesAccept::V3 => handle_capabilities_with_conn_v3(
1456 ctx.tier_config,
1457 ctx.resolved_models,
1458 ctx.reranker,
1459 embedder_live,
1460 Some(ctx.conn),
1461 ctx.profile,
1462 ctx.mcp_config,
1463 v3_aid,
1464 ctx.harness,
1465 ),
1466 _ => handle_capabilities_with_conn(
1467 ctx.tier_config,
1468 ctx.resolved_models,
1469 ctx.reranker,
1470 embedder_live,
1471 Some(ctx.conn),
1472 accept,
1473 ),
1474 };
1475 let profile = ctx.profile;
1476 result.map(|mut value| {
1477 if matches!(accept, CapabilitiesAccept::V2 | CapabilitiesAccept::V3) {
1478 if let Some(obj) = value.as_object_mut() {
1479 obj.insert("families".to_string(), families_overview(profile));
1480 }
1481 }
1482 if matches!(accept, CapabilitiesAccept::V1)
1483 && let Some(obj) = value.as_object_mut()
1484 && !obj.contains_key(field_names::SCHEMA_VERSION)
1485 {
1486 obj.insert(
1487 field_names::SCHEMA_VERSION.to_string(),
1488 Value::String("1".to_string()),
1489 );
1490 }
1491 if let Some(obj) = value.as_object_mut() {
1492 obj.insert("tier".to_string(), Value::String(runtime_tier.to_string()));
1493 }
1494 if (top_include_schema || top_verbose)
1495 && matches!(accept, CapabilitiesAccept::V2 | CapabilitiesAccept::V3)
1496 && let Some(obj) = value.as_object_mut()
1497 {
1498 overlay_tool_payloads(obj, profile, top_include_schema, top_verbose);
1499 }
1500 value
1501 })
1502}
1503
1504fn dispatch_memory_expand_query(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1505 handle_expand_query(ctx.llm, ctx.arguments)
1506}
1507
1508fn dispatch_memory_auto_tag(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1509 handle_auto_tag(ctx.conn, ctx.llm, ctx.arguments)
1510}
1511
1512fn dispatch_memory_detect_contradiction(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1513 handle_detect_contradiction(ctx.conn, ctx.llm, ctx.arguments)
1514}
1515
1516fn dispatch_memory_archive_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1517 handle_archive_list(ctx.conn, ctx.arguments)
1518}
1519
1520fn dispatch_memory_archive_restore(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1521 handle_archive_restore(ctx.conn, ctx.arguments)
1522}
1523
1524fn dispatch_memory_archive_purge(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1525 handle_archive_purge(ctx.conn, ctx.arguments)
1526}
1527
1528fn dispatch_memory_archive_stats(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1529 handle_archive_stats(ctx.conn)
1530}
1531
1532fn dispatch_memory_gc(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1533 handle_gc(ctx.conn, ctx.arguments, ctx.archive_on_gc)
1534}
1535
1536fn dispatch_memory_session_start(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1537 let caller = crate::identity::resolve_read_visibility_caller();
1550 handle_session_start(ctx.conn, ctx.arguments, ctx.llm, caller.as_deref())
1551}
1552
1553fn dispatch_memory_namespace_set_standard(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1554 handle_namespace_set_standard(ctx.conn, ctx.arguments)
1555}
1556
1557fn dispatch_memory_namespace_get_standard(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1558 handle_namespace_get_standard(ctx.conn, ctx.arguments)
1559}
1560
1561fn dispatch_memory_namespace_clear_standard(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1562 handle_namespace_clear_standard(ctx.conn, ctx.arguments)
1563}
1564
1565fn dispatch_memory_agent_register(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1566 handle_agent_register(ctx.conn, ctx.arguments)
1567}
1568
1569fn dispatch_memory_agent_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1570 handle_agent_list(ctx.conn)
1571}
1572
1573fn dispatch_memory_notify(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1574 handle_notify(ctx.conn, ctx.arguments, ctx.resolved_ttl, ctx.mcp_client)
1575}
1576
1577fn dispatch_memory_share(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1588 crate::mcp::share::handle_share(ctx.conn, ctx.arguments)
1589}
1590
1591fn dispatch_memory_inbox(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1592 let caller = crate::identity::resolve_read_visibility_caller();
1595 handle_inbox(ctx.conn, ctx.arguments, ctx.mcp_client, caller.as_deref())
1596}
1597
1598fn dispatch_memory_subscribe(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1599 handle_subscribe(ctx.conn, ctx.arguments, ctx.mcp_client)
1600}
1601
1602fn dispatch_memory_unsubscribe(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1603 handle_unsubscribe(ctx.conn, ctx.arguments, ctx.mcp_client)
1604}
1605
1606fn dispatch_memory_list_subscriptions(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1607 handle_list_subscriptions(ctx.conn, ctx.mcp_client)
1608}
1609
1610fn dispatch_memory_subscription_replay(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1611 handle_subscription_replay(ctx.conn, ctx.arguments, ctx.mcp_client)
1612}
1613
1614fn dispatch_memory_subscription_dlq_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1615 handle_subscription_dlq_list(ctx.conn, ctx.arguments, ctx.mcp_client)
1616}
1617
1618fn dispatch_memory_quota_status(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1619 handle_quota_status(ctx.conn, ctx.arguments)
1620}
1621
1622fn dispatch_memory_capture_turn(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1630 handle_capture_turn(ctx.conn, ctx.arguments, ctx.mcp_client)
1631}
1632
1633fn dispatch_memory_check_agent_action(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1634 handle_check_agent_action(ctx.conn, ctx.arguments)
1635}
1636
1637fn dispatch_memory_rule_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1638 handle_rule_list(ctx.conn, ctx.arguments)
1639}
1640
1641fn dispatch_memory_reflection_origin(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1642 handle_reflection_origin(ctx.conn, ctx.arguments)
1643}
1644
1645fn dispatch_memory_export_reflection(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1646 handle_export_reflection(ctx.conn, ctx.arguments)
1647}
1648
1649fn dispatch_memory_persona(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1650 handle_persona(ctx.conn, ctx.arguments)
1651}
1652
1653fn dispatch_memory_persona_generate(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1654 handle_persona_generate(
1655 ctx.conn,
1656 ctx.arguments,
1657 ctx.llm.map(|c| c as &dyn crate::autonomy::AutonomyLlm),
1658 ctx.tier_config.tier,
1659 ctx.active_keypair,
1660 )
1661}
1662
1663fn dispatch_memory_calibrate_confidence(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1664 handle_calibrate_confidence(ctx.conn, ctx.arguments)
1665}
1666
1667fn dispatch_memory_dependents_of_invalidated(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1668 handle_dependents_of_invalidated(ctx.conn, ctx.arguments)
1669}
1670
1671fn dispatch_memory_skill_register(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1672 handle_skill_register(ctx.conn, ctx.arguments, ctx.active_keypair)
1673}
1674
1675fn dispatch_memory_skill_list(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1676 handle_skill_list(ctx.conn, ctx.arguments)
1677}
1678
1679fn dispatch_memory_skill_get(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1680 handle_skill_get(ctx.conn, ctx.arguments)
1681}
1682
1683fn dispatch_memory_skill_resource(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1684 handle_skill_resource(ctx.conn, ctx.arguments)
1685}
1686
1687fn dispatch_memory_skill_export(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1688 handle_skill_export(ctx.conn, ctx.arguments, ctx.active_keypair)
1689}
1690
1691fn dispatch_memory_skill_promote_from_reflection(
1692 ctx: &ToolDispatchCtx<'_>,
1693) -> Result<Value, String> {
1694 handle_skill_promote_from_reflection(ctx.conn, ctx.arguments, ctx.active_keypair)
1695}
1696
1697fn dispatch_memory_skill_compositional_context(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1698 handle_skill_compositional_context(ctx.conn, ctx.arguments)
1699}
1700
1701fn dispatch_memory_offload(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1706 let explicit_agent_id = ctx
1707 .arguments
1708 .get("agent_id")
1709 .and_then(Value::as_str)
1710 .or_else(|| {
1711 ctx.arguments
1712 .get("metadata")
1713 .and_then(|m| m.get("agent_id"))
1714 .and_then(Value::as_str)
1715 });
1716 match crate::identity::resolve_agent_id(explicit_agent_id, ctx.mcp_client) {
1717 Ok(agent_id) => offload::handle_offload(ctx.conn, ctx.arguments, &agent_id),
1718 Err(e) => Err(e.to_string()),
1719 }
1720}
1721
1722fn dispatch_memory_deref(ctx: &ToolDispatchCtx<'_>) -> Result<Value, String> {
1727 let explicit_agent_id = ctx
1728 .arguments
1729 .get("agent_id")
1730 .and_then(Value::as_str)
1731 .or_else(|| {
1732 ctx.arguments
1733 .get("metadata")
1734 .and_then(|m| m.get("agent_id"))
1735 .and_then(Value::as_str)
1736 });
1737 match crate::identity::resolve_agent_id(explicit_agent_id, ctx.mcp_client) {
1738 Ok(agent_id) => offload::handle_deref(ctx.conn, ctx.arguments, &agent_id),
1739 Err(e) => Err(e.to_string()),
1740 }
1741}
1742
1743pub(crate) static TOOL_DISPATCH_TABLE: &[(&str, DispatchFn)] = {
1755 use crate::mcp::registry::tool_names;
1756 &[
1757 register_mcp_tool!(tool_names::MEMORY_STORE, dispatch_memory_store),
1758 register_mcp_tool!(tool_names::MEMORY_RECALL, dispatch_memory_recall),
1759 register_mcp_tool!(
1760 tool_names::MEMORY_RECALL_OBSERVATIONS,
1761 dispatch_memory_recall_observations
1762 ),
1763 register_mcp_tool!(tool_names::MEMORY_SEARCH, dispatch_memory_search),
1764 register_mcp_tool!(tool_names::MEMORY_LIST, dispatch_memory_list),
1765 register_mcp_tool!(tool_names::MEMORY_LOAD_FAMILY, dispatch_memory_load_family),
1766 register_mcp_tool!(tool_names::MEMORY_SMART_LOAD, dispatch_memory_smart_load),
1767 register_mcp_tool!(
1768 tool_names::MEMORY_GET_TAXONOMY,
1769 dispatch_memory_get_taxonomy
1770 ),
1771 register_mcp_tool!(
1772 tool_names::MEMORY_CHECK_DUPLICATE,
1773 dispatch_memory_check_duplicate
1774 ),
1775 register_mcp_tool!(
1776 tool_names::MEMORY_ENTITY_REGISTER,
1777 dispatch_memory_entity_register
1778 ),
1779 register_mcp_tool!(
1780 tool_names::MEMORY_ENTITY_GET_BY_ALIAS,
1781 dispatch_memory_entity_get_by_alias
1782 ),
1783 register_mcp_tool!(tool_names::MEMORY_KG_TIMELINE, dispatch_memory_kg_timeline),
1784 register_mcp_tool!(
1785 tool_names::MEMORY_KG_INVALIDATE,
1786 dispatch_memory_kg_invalidate
1787 ),
1788 register_mcp_tool!(tool_names::MEMORY_KG_QUERY, dispatch_memory_kg_query),
1789 register_mcp_tool!(tool_names::MEMORY_FIND_PATHS, dispatch_memory_find_paths),
1790 register_mcp_tool!(tool_names::MEMORY_DELETE, dispatch_memory_delete),
1791 register_mcp_tool!(tool_names::MEMORY_PROMOTE, dispatch_memory_promote),
1792 register_mcp_tool!(
1793 tool_names::MEMORY_PENDING_LIST,
1794 dispatch_memory_pending_list
1795 ),
1796 register_mcp_tool!(
1797 tool_names::MEMORY_PENDING_APPROVE,
1798 dispatch_memory_pending_approve
1799 ),
1800 register_mcp_tool!(
1801 tool_names::MEMORY_PENDING_REJECT,
1802 dispatch_memory_pending_reject
1803 ),
1804 register_mcp_tool!(tool_names::MEMORY_FORGET, dispatch_memory_forget),
1805 register_mcp_tool!(tool_names::MEMORY_STATS, dispatch_memory_stats),
1806 register_mcp_tool!(tool_names::MEMORY_UPDATE, dispatch_memory_update),
1807 register_mcp_tool!(tool_names::MEMORY_GET, dispatch_memory_get),
1808 register_mcp_tool!(tool_names::MEMORY_LINK, dispatch_memory_link),
1809 register_mcp_tool!(tool_names::MEMORY_GET_LINKS, dispatch_memory_get_links),
1810 register_mcp_tool!(tool_names::MEMORY_VERIFY, dispatch_memory_verify),
1811 register_mcp_tool!(tool_names::MEMORY_REPLAY, dispatch_memory_replay),
1812 register_mcp_tool!(tool_names::MEMORY_CONSOLIDATE, dispatch_memory_consolidate),
1813 register_mcp_tool!(tool_names::MEMORY_ATOMISE, dispatch_memory_atomise),
1814 register_mcp_tool!(
1815 tool_names::MEMORY_INGEST_MULTISTEP,
1816 dispatch_memory_ingest_multistep
1817 ),
1818 register_mcp_tool!(tool_names::MEMORY_REFLECT, dispatch_memory_reflect),
1819 register_mcp_tool!(
1820 tool_names::MEMORY_CAPABILITIES,
1821 dispatch_memory_capabilities
1822 ),
1823 register_mcp_tool!(
1824 tool_names::MEMORY_EXPAND_QUERY,
1825 dispatch_memory_expand_query
1826 ),
1827 register_mcp_tool!(tool_names::MEMORY_AUTO_TAG, dispatch_memory_auto_tag),
1828 register_mcp_tool!(
1829 tool_names::MEMORY_DETECT_CONTRADICTION,
1830 dispatch_memory_detect_contradiction
1831 ),
1832 register_mcp_tool!(
1833 tool_names::MEMORY_ARCHIVE_LIST,
1834 dispatch_memory_archive_list
1835 ),
1836 register_mcp_tool!(
1837 tool_names::MEMORY_ARCHIVE_RESTORE,
1838 dispatch_memory_archive_restore
1839 ),
1840 register_mcp_tool!(
1841 tool_names::MEMORY_ARCHIVE_PURGE,
1842 dispatch_memory_archive_purge
1843 ),
1844 register_mcp_tool!(
1845 tool_names::MEMORY_ARCHIVE_STATS,
1846 dispatch_memory_archive_stats
1847 ),
1848 register_mcp_tool!(tool_names::MEMORY_GC, dispatch_memory_gc),
1849 register_mcp_tool!(
1850 tool_names::MEMORY_SESSION_START,
1851 dispatch_memory_session_start
1852 ),
1853 register_mcp_tool!(
1854 tool_names::MEMORY_NAMESPACE_SET_STANDARD,
1855 dispatch_memory_namespace_set_standard
1856 ),
1857 register_mcp_tool!(
1858 tool_names::MEMORY_NAMESPACE_GET_STANDARD,
1859 dispatch_memory_namespace_get_standard
1860 ),
1861 register_mcp_tool!(
1862 tool_names::MEMORY_NAMESPACE_CLEAR_STANDARD,
1863 dispatch_memory_namespace_clear_standard
1864 ),
1865 register_mcp_tool!(
1866 tool_names::MEMORY_AGENT_REGISTER,
1867 dispatch_memory_agent_register
1868 ),
1869 register_mcp_tool!(tool_names::MEMORY_AGENT_LIST, dispatch_memory_agent_list),
1870 register_mcp_tool!(tool_names::MEMORY_NOTIFY, dispatch_memory_notify),
1871 register_mcp_tool!(tool_names::MEMORY_SHARE, dispatch_memory_share),
1872 register_mcp_tool!(tool_names::MEMORY_INBOX, dispatch_memory_inbox),
1873 register_mcp_tool!(tool_names::MEMORY_SUBSCRIBE, dispatch_memory_subscribe),
1874 register_mcp_tool!(tool_names::MEMORY_UNSUBSCRIBE, dispatch_memory_unsubscribe),
1875 register_mcp_tool!(
1876 tool_names::MEMORY_LIST_SUBSCRIPTIONS,
1877 dispatch_memory_list_subscriptions
1878 ),
1879 register_mcp_tool!(
1880 tool_names::MEMORY_SUBSCRIPTION_REPLAY,
1881 dispatch_memory_subscription_replay
1882 ),
1883 register_mcp_tool!(
1884 tool_names::MEMORY_SUBSCRIPTION_DLQ_LIST,
1885 dispatch_memory_subscription_dlq_list
1886 ),
1887 register_mcp_tool!(
1888 tool_names::MEMORY_QUOTA_STATUS,
1889 dispatch_memory_quota_status
1890 ),
1891 register_mcp_tool!(
1893 tool_names::MEMORY_CAPTURE_TURN,
1894 dispatch_memory_capture_turn
1895 ),
1896 register_mcp_tool!(
1897 tool_names::MEMORY_CHECK_AGENT_ACTION,
1898 dispatch_memory_check_agent_action
1899 ),
1900 register_mcp_tool!(tool_names::MEMORY_RULE_LIST, dispatch_memory_rule_list),
1901 register_mcp_tool!(
1902 tool_names::MEMORY_REFLECTION_ORIGIN,
1903 dispatch_memory_reflection_origin
1904 ),
1905 register_mcp_tool!(
1906 tool_names::MEMORY_EXPORT_REFLECTION,
1907 dispatch_memory_export_reflection
1908 ),
1909 register_mcp_tool!(tool_names::MEMORY_PERSONA, dispatch_memory_persona),
1910 register_mcp_tool!(
1911 tool_names::MEMORY_PERSONA_GENERATE,
1912 dispatch_memory_persona_generate
1913 ),
1914 register_mcp_tool!(
1915 tool_names::MEMORY_CALIBRATE_CONFIDENCE,
1916 dispatch_memory_calibrate_confidence
1917 ),
1918 register_mcp_tool!(
1919 tool_names::MEMORY_DEPENDENTS_OF_INVALIDATED,
1920 dispatch_memory_dependents_of_invalidated
1921 ),
1922 register_mcp_tool!(
1923 tool_names::MEMORY_SKILL_REGISTER,
1924 dispatch_memory_skill_register
1925 ),
1926 register_mcp_tool!(tool_names::MEMORY_SKILL_LIST, dispatch_memory_skill_list),
1927 register_mcp_tool!(tool_names::MEMORY_SKILL_GET, dispatch_memory_skill_get),
1928 register_mcp_tool!(
1929 tool_names::MEMORY_SKILL_RESOURCE,
1930 dispatch_memory_skill_resource
1931 ),
1932 register_mcp_tool!(
1933 tool_names::MEMORY_SKILL_EXPORT,
1934 dispatch_memory_skill_export
1935 ),
1936 register_mcp_tool!(
1937 tool_names::MEMORY_SKILL_PROMOTE_FROM_REFLECTION,
1938 dispatch_memory_skill_promote_from_reflection
1939 ),
1940 register_mcp_tool!(
1941 tool_names::MEMORY_SKILL_COMPOSITIONAL_CONTEXT,
1942 dispatch_memory_skill_compositional_context
1943 ),
1944 register_mcp_tool!(tool_names::MEMORY_OFFLOAD, dispatch_memory_offload),
1945 register_mcp_tool!(tool_names::MEMORY_DEREF, dispatch_memory_deref),
1946 ]
1947};
1948
1949pub(crate) fn lookup_dispatch(tool_name: &str) -> Option<DispatchFn> {
1960 static MAP: std::sync::OnceLock<std::collections::HashMap<&'static str, DispatchFn>> =
1961 std::sync::OnceLock::new();
1962 let map = MAP.get_or_init(|| {
1963 let mut m = std::collections::HashMap::with_capacity(TOOL_DISPATCH_TABLE.len());
1964 for (name, f) in TOOL_DISPATCH_TABLE {
1965 m.insert(*name, *f);
1966 }
1967 m
1968 });
1969 map.get(tool_name).copied()
1970}
1971
1972#[allow(clippy::too_many_arguments)]
1973#[allow(clippy::too_many_lines)]
1974fn handle_request(
1975 conn: &rusqlite::Connection,
1976 db_path: &Path,
1977 req: &RpcRequest,
1978 embedder: Option<&dyn Embed>,
1979 llm: Option<&OllamaClient>,
1980 reranker: Option<&BatchedReranker>,
1981 tier_config: &TierConfig,
1982 resolved_models: &ResolvedModels,
1987 vector_index: Option<&VectorIndex>,
1988 resolved_ttl: &crate::config::ResolvedTtl,
1989 resolved_scoring: &crate::config::ResolvedScoring,
1990 archive_on_gc: bool,
1991 autonomous_hooks: bool,
1992 mcp_client: Option<&str>,
1993 profile: &crate::profile::Profile,
1994 mcp_config: Option<&crate::config::McpConfig>,
1995 active_keypair: Option<&crate::identity::keypair::AgentKeypair>,
2000 harness: Option<&crate::harness::Harness>,
2007 federation_forward_url: Option<&str>,
2012 recall_scope: Option<&crate::config::RecallScope>,
2018 atomise_handler: Option<&atomise::AtomiseToolHandler>,
2022 ingest_multistep_handler: Option<&ingest_multistep::IngestMultistepHandler>,
2026 nag_watcher: Option<&crate::recover::nag::CaptureNagWatcher>,
2033 nag_session_id: &str,
2037) -> RpcResponse {
2038 let id = req.id.clone().unwrap_or(Value::Null);
2039
2040 if req.jsonrpc != jsonrpc::VERSION {
2042 return err_response(
2043 id,
2044 jsonrpc::INVALID_REQUEST,
2045 format!(
2046 "invalid JSON-RPC version (must be \"{}\")",
2047 jsonrpc::VERSION
2048 ),
2049 );
2050 }
2051
2052 match req.method.as_str() {
2053 jsonrpc::METHOD_INITIALIZE => {
2054 let mut server_info = json!({
2068 "name": "ai-memory",
2069 "version": crate::PKG_VERSION,
2070 });
2071 let now_rfc3339 = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
2072 if let Ok(Some(identity_block)) =
2073 server_identity::build_signed_identity(active_keypair, &now_rfc3339)
2074 && let Some(obj) = server_info.as_object_mut()
2075 {
2076 obj.insert("ai_memory_identity".to_string(), identity_block);
2077 }
2078 ok_response(
2079 id,
2080 json!({
2081 "protocolVersion": jsonrpc::PROTOCOL_REVISION,
2082 (field_names::CAPABILITIES): { "tools": {}, "prompts": {} },
2083 "serverInfo": server_info,
2084 }),
2085 )
2086 }
2087 jsonrpc::METHOD_NOTIFICATIONS_INITIALIZED | jsonrpc::METHOD_PING => {
2088 ok_response(id, json!({}))
2089 }
2090 jsonrpc::METHOD_TOOLS_LIST => ok_response(id, tool_definitions_for_profile(profile)),
2091 jsonrpc::METHOD_PROMPTS_LIST => ok_response(id, prompt_definitions()),
2092 jsonrpc::METHOD_PROMPTS_GET => {
2093 let prompt_name = match req.params["name"].as_str() {
2094 Some(name) if !name.is_empty() => name,
2095 _ => {
2096 return err_response(
2097 id,
2098 jsonrpc::INVALID_PARAMS,
2099 "missing or empty prompt name".into(),
2100 );
2101 }
2102 };
2103 match prompt_content(prompt_name, &req.params) {
2104 Ok(val) => ok_response(id, val),
2105 Err(e) => err_response(id, jsonrpc::INVALID_PARAMS, e),
2106 }
2107 }
2108 jsonrpc::METHOD_TOOLS_CALL => {
2109 let tool_name = match req.params["name"].as_str() {
2110 Some(name) if !name.is_empty() => name,
2111 _ => {
2112 return err_response(
2113 id,
2114 jsonrpc::INVALID_PARAMS,
2115 "missing or empty tool name".into(),
2116 );
2117 }
2118 };
2119
2120 if !profile.loads(tool_name) {
2138 let hint_enabled = mcp_config.is_some_and(|c| c.profile_hint_in_errors);
2139 let hint = if hint_enabled {
2140 let owning_family = crate::profile::Family::for_tool(tool_name);
2141 match owning_family {
2142 Some(f) => format!(
2143 "tool '{tool_name}' is in family '{}' which is not loaded under \
2144 the active profile. Restart with `--profile <name>` or \
2145 `--profile core,{}` to load it, or call `memory_capabilities \
2146 --include-schema family={}` to expand at runtime.",
2147 f.name(),
2148 f.name(),
2149 f.name()
2150 ),
2151 None => format!(
2152 "tool '{tool_name}' is not registered in this build. Call \
2153 `memory_capabilities` to see available tools."
2154 ),
2155 }
2156 } else {
2157 tracing::debug!(
2163 target: "ai_memory::mcp",
2164 tool = tool_name,
2165 owning_family = ?crate::profile::Family::for_tool(tool_name).map(crate::profile::Family::name),
2166 "tools/call refused: tool not loaded under active profile (\
2167 #1254 — set mcp.profile_hint_in_errors=true to surface family hint)",
2168 );
2169 format!("unknown tool: {tool_name}")
2170 };
2171 return err_response(id, jsonrpc::METHOD_NOT_FOUND, hint);
2172 }
2173
2174 let span = tracing::info_span!(
2180 "mcp_tool_call",
2181 tool = tool_name,
2182 rpc_id = ?id,
2183 );
2184 let _enter = span.enter();
2185 let started = Instant::now();
2186
2187 let empty_obj = json!({});
2188 let arguments = if req.params["arguments"].is_object() {
2189 &req.params["arguments"]
2190 } else {
2191 &empty_obj
2192 };
2193
2194 observe_capture_nag(
2201 nag_watcher,
2202 nag_session_id,
2203 tool_name,
2204 arguments,
2205 mcp_client,
2206 );
2207
2208 let ctx = ToolDispatchCtx {
2217 conn,
2218 db_path,
2219 arguments,
2220 embedder,
2221 llm,
2222 reranker,
2223 tier_config,
2224 resolved_models,
2225 vector_index,
2226 resolved_ttl,
2227 resolved_scoring,
2228 archive_on_gc,
2229 autonomous_hooks,
2230 mcp_client,
2231 profile,
2232 mcp_config,
2233 active_keypair,
2234 harness,
2235 federation_forward_url,
2236 recall_scope,
2237 atomise_handler,
2238 ingest_multistep_handler,
2239 };
2240 let Some(dispatch) = lookup_dispatch(tool_name) else {
2241 return err_response(
2248 id,
2249 jsonrpc::METHOD_NOT_FOUND,
2250 format!("unknown tool: {tool_name}"),
2251 );
2252 };
2253 let result = dispatch(&ctx);
2254
2255 let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
2259 match &result {
2260 Ok(_) => tracing::info!(elapsed_ms, "ok"),
2261 Err(err) => tracing::warn!(elapsed_ms, error = %err, "err"),
2262 }
2263
2264 audit_emit_for_mcp_dispatch(tool_name, arguments, &result, mcp_client);
2270
2271 match result {
2272 Ok(val) => {
2273 let format_str = arguments
2275 .get("format")
2276 .and_then(|v| v.as_str())
2277 .unwrap_or(crate::toon::FORMAT_TOON_COMPACT);
2278 use crate::mcp::registry::tool_names as tn;
2279 let text = match format_str {
2280 "toon"
2281 if matches!(
2282 tool_name,
2283 tn::MEMORY_RECALL | tn::MEMORY_LIST | tn::MEMORY_SESSION_START
2284 ) =>
2285 {
2286 crate::toon::memories_to_toon(&val, false)
2287 }
2288 crate::toon::FORMAT_TOON_COMPACT
2289 if matches!(
2290 tool_name,
2291 tn::MEMORY_RECALL | tn::MEMORY_LIST | tn::MEMORY_SESSION_START
2292 ) =>
2293 {
2294 crate::toon::memories_to_toon(&val, true)
2295 }
2296 "toon" if tool_name == tn::MEMORY_SEARCH => {
2297 crate::toon::search_to_toon(&val, false)
2298 }
2299 crate::toon::FORMAT_TOON_COMPACT if tool_name == tn::MEMORY_SEARCH => {
2300 crate::toon::search_to_toon(&val, true)
2301 }
2302 _ => serde_json::to_string_pretty(&val).unwrap_or_default(),
2303 };
2304 ok_response(
2305 id,
2306 json!({
2307 "content": [{
2308 "type": "text",
2309 "text": text
2310 }]
2311 }),
2312 )
2313 }
2314 Err(e) => ok_response(
2340 id,
2341 json!({
2342 "content": [{"type": "text", "text": e}],
2343 "isError": true
2344 }),
2345 ),
2346 }
2347 }
2348 _ => err_response(
2349 id,
2350 jsonrpc::METHOD_NOT_FOUND,
2351 format!("method not found: {}", req.method),
2352 ),
2353 }
2354}
2355
2356fn load_active_keypair_for_mcp() -> Option<crate::identity::keypair::AgentKeypair> {
2377 let dir = crate::identity::keypair::default_key_dir().ok()?;
2378 if !dir.exists() {
2379 return None;
2380 }
2381 let agent_id = crate::identity::resolve_agent_id(None, None).ok();
2382 load_active_keypair_for_mcp_in(&dir, agent_id.as_deref())
2383}
2384
2385fn load_active_keypair_for_mcp_in(
2388 dir: &std::path::Path,
2389 agent_id: Option<&str>,
2390) -> Option<crate::identity::keypair::AgentKeypair> {
2391 if let Some(agent_id) = agent_id {
2392 match crate::identity::keypair::load(agent_id, dir) {
2393 Ok(kp) if kp.can_sign() => return Some(kp),
2394 Ok(_) => {}
2395 Err(e) => {
2396 let msg = format!("{e:#}");
2397 if !(msg.contains("No such file") || msg.contains("not found")) {
2398 eprintln!("ai-memory: keypair load failed for {agent_id}: {msg}");
2399 }
2400 }
2401 }
2402 }
2403 match crate::identity::keypair::load(crate::identity::keypair::DAEMON_KEYPAIR_LABEL, dir) {
2406 Ok(kp) if kp.can_sign() => Some(kp),
2407 Ok(_) => None,
2408 Err(e) => {
2409 let msg = format!("{e:#}");
2410 if !(msg.contains("No such file") || msg.contains("not found")) {
2411 eprintln!("ai-memory: daemon keypair load failed: {msg}");
2412 }
2413 None
2414 }
2415 }
2416}
2417
2418pub const DEFAULT_EMBED_BACKFILL_BATCH_SIZE: usize = 64;
2433
2434pub fn run_embedding_backfill(
2487 conn: &mut rusqlite::Connection,
2488 emb: &dyn Embed,
2489) -> anyhow::Result<usize> {
2490 let batch_size = std::env::var(crate::config::ENV_EMBED_BACKFILL_BATCH)
2500 .ok()
2501 .and_then(|s| s.parse::<usize>().ok())
2502 .filter(|&n| n > 0)
2503 .unwrap_or(DEFAULT_EMBED_BACKFILL_BATCH_SIZE);
2504 run_embedding_backfill_with_batch_size(conn, emb, batch_size)
2505}
2506
2507pub fn run_embedding_backfill_with_batch_size(
2523 conn: &mut rusqlite::Connection,
2524 emb: &dyn Embed,
2525 batch_size: usize,
2526) -> anyhow::Result<usize> {
2527 let batch_size = if batch_size == 0 {
2533 DEFAULT_EMBED_BACKFILL_BATCH_SIZE
2534 } else {
2535 batch_size
2536 };
2537
2538 let mut ok = 0usize;
2548 let mut skipped = 0usize;
2549 let mut scanned = 0usize;
2550 let mut announced = false;
2551 let mut cursor: Option<String> = None;
2552 loop {
2553 let chunk = db::get_unembedded_ids_batch_after(conn, cursor.as_deref(), batch_size)?;
2554 if chunk.is_empty() {
2555 break;
2559 }
2560 if !announced {
2561 eprintln!("ai-memory: backfilling unembedded memories (batch_size={batch_size})...");
2562 announced = true;
2563 }
2564 scanned += chunk.len();
2565 cursor = chunk.last().map(|(id, _, _)| id.clone());
2566
2567 let embedded = embed_rows_with_fallback(emb, &chunk);
2568 for (id, reason) in &embedded.skipped {
2569 eprintln!("ai-memory: backfill skipped row {id}: {reason} (#1595)");
2570 }
2571 skipped += embedded.skipped.len();
2572 if embedded.entries.is_empty() {
2573 continue;
2574 }
2575
2576 match db::set_embeddings_batch(conn, &embedded.entries) {
2577 Ok(n) => ok += n,
2578 Err(e) => {
2579 eprintln!(
2584 "ai-memory: set_embeddings_batch failed for chunk of {} rows: {e} \
2585 — falling back to per-row writes (#1595)",
2586 embedded.entries.len()
2587 );
2588 for (id, v) in &embedded.entries {
2589 match db::set_embedding(conn, id, v) {
2590 Ok(()) => ok += 1,
2591 Err(e) => {
2592 eprintln!("ai-memory: backfill skipped row {id}: {e} (#1595)");
2593 skipped += 1;
2594 }
2595 }
2596 }
2597 }
2598 }
2599 }
2600
2601 if scanned > 0 {
2602 eprintln!("ai-memory: backfilled {ok}/{scanned} (skipped {skipped})");
2603 }
2604 Ok(ok)
2605}
2606
2607pub(crate) struct EmbeddedRows {
2611 pub(crate) entries: Vec<(String, Vec<f32>)>,
2613 pub(crate) skipped: Vec<(String, String)>,
2616}
2617
2618pub(crate) fn embed_rows_with_fallback(
2636 emb: &dyn Embed,
2637 rows: &[(String, String, String)],
2638) -> EmbeddedRows {
2639 let mut skipped: Vec<(String, String)> = Vec::new();
2640 let mut candidates: Vec<(&str, String)> = Vec::with_capacity(rows.len());
2642 for (id, title, content) in rows {
2643 let text = crate::embeddings::embedding_document(title, content);
2644 if let Some(reason) = crate::embeddings::oversize_embed_reason(text.len()) {
2645 skipped.push((id.clone(), reason));
2646 } else {
2647 candidates.push((id.as_str(), text));
2648 }
2649 }
2650
2651 let text_refs: Vec<&str> = candidates.iter().map(|(_, t)| t.as_str()).collect();
2652 let mut entries: Vec<(String, Vec<f32>)> = Vec::with_capacity(candidates.len());
2653 let batch = if text_refs.is_empty() {
2654 Ok(Vec::new())
2655 } else {
2656 emb.embed_batch(&text_refs)
2657 };
2658 match batch {
2659 Ok(vectors) if vectors.len() == candidates.len() => {
2663 entries.extend(
2664 candidates
2665 .iter()
2666 .zip(vectors)
2667 .map(|((id, _), v)| ((*id).to_string(), v)),
2668 );
2669 }
2670 batch_fault => {
2671 match &batch_fault {
2672 Ok(vectors) => eprintln!(
2673 "ai-memory: embed_batch returned {} vectors for {} inputs — \
2674 falling back to per-row embeds for this chunk (#1595)",
2675 vectors.len(),
2676 candidates.len()
2677 ),
2678 Err(e) => eprintln!(
2679 "ai-memory: embed_batch failed for chunk of {} rows: {e} — \
2680 falling back to per-row embeds (#1595)",
2681 candidates.len()
2682 ),
2683 }
2684 for (id, text) in &candidates {
2685 match emb.embed(text) {
2686 Ok(v) => entries.push(((*id).to_string(), v)),
2687 Err(e) => skipped.push(((*id).to_string(), format!("{e:#}"))),
2688 }
2689 }
2690 }
2691 }
2692
2693 EmbeddedRows { entries, skipped }
2694}
2695
2696pub const MCP_MAX_LINE_BYTES: usize = 16 * 1024 * 1024;
2714
2715pub const MCP_MAX_DRAIN_BYTES: usize = 64 * 1024 * 1024;
2721
2722#[allow(clippy::too_many_lines)]
2723#[allow(deprecated)] pub fn run_mcp_server(
2725 db_path: &Path,
2726 tier: FeatureTier,
2727 app_config: &AppConfig,
2728 profile: &crate::profile::Profile,
2729) -> anyhow::Result<()> {
2730 let _ = tracing_subscriber::fmt()
2737 .with_env_filter(
2738 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
2739 tracing_subscriber::EnvFilter::new(crate::logging::DEFAULT_LOG_DIRECTIVE)
2740 }),
2741 )
2742 .with_writer(std::io::stderr)
2743 .try_init();
2744
2745 let mut conn = db::open(db_path)?;
2746
2747 let (mcp_governance_queue, _mcp_governance_supervisor) =
2762 crate::governance::deferred_audit::install_deferred_audit_drainer(db_path);
2763 let mcp_rule_cache = std::sync::Arc::new(crate::governance::rule_cache::RuleCache::new());
2764 let mcp_hook_conn: Option<std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>> =
2765 match db::open(db_path) {
2766 Ok(c) => Some(std::sync::Arc::new(std::sync::Mutex::new(c))),
2767 Err(e) => {
2768 eprintln!(
2769 "ai-memory: #1583 — failed to open governance consultation connection \
2770 ({e}); the pre-write gate will fail CLOSED on every MCP write"
2771 );
2772 None
2773 }
2774 };
2775 crate::daemon_runtime::install_governance_pre_write_hook(
2776 db_path,
2777 &mcp_governance_queue,
2778 &mcp_rule_cache,
2779 mcp_hook_conn.clone(),
2780 );
2781 crate::daemon_runtime::install_governance_pre_action_hook(
2787 db_path,
2788 &mcp_governance_queue,
2789 &mcp_rule_cache,
2790 mcp_hook_conn,
2791 );
2792
2793 let stdin = io::stdin();
2794 let mut stdout = io::stdout();
2795
2796 let mut tier_config = tier.config();
2797 eprintln!("ai-memory: requested tier = {}", tier.as_str());
2798 let family_names: Vec<&'static str> = profile.families().iter().map(|f| f.name()).collect();
2802 eprintln!(
2803 "ai-memory: profile = {} families ({}); expected tool count = {}",
2804 profile.families().len(),
2805 family_names.join(", "),
2806 profile.expected_tool_count()
2807 );
2808
2809 if tier_config.llm_model.is_some()
2816 && let Some(ref llm_override) = app_config.llm_model
2817 {
2818 let trimmed = llm_override.trim();
2819 if trimmed.is_empty() {
2820 eprintln!("ai-memory: empty llm_model override ignored, using tier default");
2821 } else {
2822 tier_config.llm_model = Some(trimmed.to_string());
2823 eprintln!("ai-memory: llm_model override from config: {trimmed}");
2824 }
2825 }
2826
2827 if tier_config.embedding_model.is_some()
2829 && let Some(ref emb_override) = app_config.embedding_model
2830 {
2831 match emb_override.as_str() {
2832 "mini_lm_l6_v2" => {
2833 tier_config.embedding_model = Some(crate::config::EmbeddingModel::MiniLmL6V2);
2834 eprintln!("ai-memory: embedding_model override from config: mini_lm_l6_v2 (local)");
2835 }
2836 "nomic_embed_v15" => {
2837 tier_config.embedding_model = Some(crate::config::EmbeddingModel::NomicEmbedV15);
2838 eprintln!(
2839 "ai-memory: embedding_model override from config: nomic_embed_v15 (Ollama)"
2840 );
2841 }
2842 other => {
2843 eprintln!("ai-memory: unknown embedding_model '{other}', using tier default");
2844 }
2845 }
2846 }
2847
2848 let resolved_llm = app_config.resolve_llm(None, None, None);
2867 let llm: Option<Arc<OllamaClient>> = if tier_config.llm_model.is_none()
2868 && resolved_llm.source == crate::config::ConfigSource::CompiledDefault
2869 {
2870 None
2872 } else {
2873 match OllamaClient::build_from_resolved(&resolved_llm) {
2874 Ok(Some(client)) => {
2875 eprintln!(
2876 "ai-memory: LLM ready (backend={}, model={}, source={}, key_source={})",
2877 resolved_llm.backend,
2878 resolved_llm.model,
2879 resolved_llm.source.as_str(),
2880 resolved_llm.api_key_source.as_str(),
2881 );
2882 if resolved_llm.backend == crate::llm::BACKEND_OLLAMA {
2888 if let Err(e) = client.ensure_model() {
2889 eprintln!("ai-memory: model pull failed: {e} (LLM features disabled)");
2890 None
2891 } else {
2892 Some(Arc::new(client))
2893 }
2894 } else {
2895 Some(Arc::new(client))
2896 }
2897 }
2898 Ok(None) => {
2899 eprintln!(
2900 "ai-memory: LLM disabled — resolver returned no client \
2901 (backend={}, source={})",
2902 resolved_llm.backend,
2903 resolved_llm.source.as_str(),
2904 );
2905 None
2906 }
2907 Err(e) => {
2908 eprintln!(
2909 "ai-memory: LLM init failed (backend={}, source={}): {e} \
2910 (LLM features disabled)",
2911 resolved_llm.backend,
2912 resolved_llm.source.as_str(),
2913 );
2914 None
2915 }
2916 }
2917 };
2918
2919 let resolved_embeddings = app_config.resolve_embeddings();
2936 let embedder = match Embedder::from_resolved(&resolved_embeddings, tier_config.embedding_model)
2937 {
2938 Ok(Some(emb)) => {
2939 eprintln!("ai-memory: embedder loaded ({})", emb.model_description());
2940 let embed_batch_size = resolved_embeddings.backfill_batch as usize;
2954 if let Err(e) =
2955 run_embedding_backfill_with_batch_size(&mut conn, &emb, embed_batch_size)
2956 {
2957 eprintln!("ai-memory: backfill failed: {e}");
2958 }
2959 Some(emb)
2960 }
2961 Ok(None) => None,
2962 Err(e) => {
2963 eprintln!(
2964 "ai-memory: embedder init failed (backend={}, model={}, url={}, \
2965 source={}): {e:#} — semantic recall DEGRADED to keyword \
2966 (#1143, #1593, #1598)",
2967 resolved_embeddings.backend,
2968 resolved_embeddings.model,
2969 resolved_embeddings.url,
2970 resolved_embeddings.source.as_str(),
2971 );
2972 None
2973 }
2974 };
2975
2976 let vector_index: Option<std::sync::Arc<VectorIndex>> = if embedder.is_some() {
2992 let idx = std::sync::Arc::new(VectorIndex::empty());
2993 eprintln!(
2994 "ai-memory: HNSW index warming in background; semantic recall \
2995 serves keyword/FTS results until the swap lands (#1579 B3)"
2996 );
2997 let warm_idx = std::sync::Arc::clone(&idx);
2998 let warm_db_path = db_path.to_path_buf();
2999 std::thread::spawn(move || {
3000 let started = std::time::Instant::now();
3001 let Some(entries) = crate::daemon_runtime::load_boot_index_entries(&warm_db_path)
3002 else {
3003 return;
3004 };
3005 if entries.is_empty() {
3006 eprintln!("ai-memory: no embeddings for HNSW index, using linear scan");
3007 return;
3008 }
3009 let total = entries.len();
3010 warm_idx.warm_boot(entries);
3011 eprintln!(
3012 "ai-memory: HNSW index ready ({total} entries, warmed in {:.1}s)",
3013 started.elapsed().as_secs_f64()
3014 );
3015 });
3016 Some(idx)
3017 } else {
3018 None
3019 };
3020
3021 let reranker = if tier_config.cross_encoder {
3028 eprintln!("ai-memory: loading neural cross-encoder (ms-marco-MiniLM-L-6-v2)...");
3029 let ce = CrossEncoder::new_neural();
3030 if ce.is_neural() {
3031 eprintln!("ai-memory: neural cross-encoder ready (batched)");
3032 } else {
3033 eprintln!("ai-memory: using lexical cross-encoder fallback");
3034 }
3035 Some(BatchedReranker::with_score_floor(
3040 ce,
3041 app_config.resolve_reranker_score_floor(),
3042 ))
3043 } else {
3044 None
3045 };
3046
3047 let effective_tier = if llm.is_some() && embedder.is_some() && reranker.is_some() {
3049 EFFECTIVE_TIER_AUTONOMOUS
3050 } else if llm.is_some() && embedder.is_some() {
3051 "smart"
3052 } else if embedder.is_some() {
3053 "semantic"
3054 } else {
3055 "keyword"
3056 };
3057 eprintln!("ai-memory MCP server started (stdio, tier={effective_tier})");
3058
3059 let active_keypair: Option<crate::identity::keypair::AgentKeypair> =
3064 load_active_keypair_for_mcp();
3065 if active_keypair.is_some() {
3066 eprintln!("ai-memory: link signing enabled (Ed25519)");
3067 }
3068
3069 let atomise_handler: Option<std::sync::Arc<atomise::AtomiseToolHandler>> =
3075 if let Some(ref llm_client) = llm {
3076 let curator: Box<dyn crate::atomisation::curator::Curator> = Box::new(
3077 crate::atomisation::curator::LlmCurator::new(llm_client.clone()),
3078 );
3079 let keypair_arc = active_keypair
3080 .as_ref()
3081 .map(|kp| std::sync::Arc::new(kp.clone()));
3082 let curator_model = llm_client.model_name().to_string();
3088 let atomiser = std::sync::Arc::new(
3089 crate::atomisation::Atomiser::new(
3090 curator,
3091 keypair_arc,
3092 crate::atomisation::AtomiserConfig::default(),
3093 tier_config.tier,
3094 )
3095 .with_curator_model(curator_model),
3096 );
3097 eprintln!("ai-memory: atomisation engine ready (curator=LlmCurator)");
3098 Some(std::sync::Arc::new(atomise::AtomiseToolHandler::new(
3099 atomiser,
3100 tier_config.tier,
3101 )))
3102 } else {
3103 None
3104 };
3105
3106 let ingest_multistep_handler: Option<std::sync::Arc<ingest_multistep::IngestMultistepHandler>> =
3112 if let Some(ref llm_client) = llm {
3113 let dispatch: std::sync::Arc<dyn crate::multistep_ingest::LlmDispatch> =
3114 std::sync::Arc::new(crate::multistep_ingest::executor::OllamaDispatch::new(
3115 llm_client.clone(),
3116 ));
3117 eprintln!("ai-memory: multi-step ingest orchestrator ready (Form 3)");
3118 Some(std::sync::Arc::new(
3119 ingest_multistep::IngestMultistepHandler::new(dispatch, tier_config.tier),
3120 ))
3121 } else {
3122 None
3123 };
3124
3125 let resolved_models = app_config.resolve_models();
3134
3135 let mut mcp_client_name: Option<String> = None;
3139
3140 let mut detected_harness: Option<crate::harness::Harness> = None;
3146
3147 let nag_watcher = crate::recover::nag::CaptureNagWatcher::new_from_env();
3157 let nag_session_id = format!("mcp-{}", uuid::Uuid::new_v4());
3158
3159 let mut stdin_locked = stdin.lock();
3169 let mut line_buf: Vec<u8> = Vec::with_capacity(8192);
3170 loop {
3171 line_buf.clear();
3172 let n = (&mut stdin_locked)
3178 .take((MCP_MAX_LINE_BYTES as u64) + 1)
3179 .read_until(b'\n', &mut line_buf)?;
3180 if n == 0 {
3181 break;
3183 }
3184 let overrun = line_buf.last() != Some(&b'\n') && n > MCP_MAX_LINE_BYTES;
3185 if overrun {
3186 let mut scratch = [0u8; 8192];
3191 let mut drained: usize = 0;
3192 loop {
3193 if drained >= MCP_MAX_DRAIN_BYTES {
3194 let resp = err_response(
3197 Value::Null,
3198 jsonrpc::PARSE_ERROR,
3199 format!(
3200 "parse error: line exceeded {MCP_MAX_LINE_BYTES} bytes \
3201 and drain ceiling {MCP_MAX_DRAIN_BYTES} hit; closing stream"
3202 ),
3203 );
3204 let out = serde_json::to_string(&resp)?;
3205 writeln!(stdout, "{out}")?;
3206 stdout.flush()?;
3207 let _ = db::checkpoint(&conn);
3208 eprintln!("ai-memory MCP server stopped (drain ceiling exceeded)");
3209 return Ok(());
3210 }
3211 let m = stdin_locked.read(&mut scratch)?;
3212 if m == 0 {
3213 break;
3214 }
3215 drained = drained.saturating_add(m);
3216 if scratch[..m].contains(&b'\n') {
3217 break;
3218 }
3219 }
3220 let resp = err_response(
3221 Value::Null,
3222 jsonrpc::PARSE_ERROR,
3223 format!("parse error: line exceeded {MCP_MAX_LINE_BYTES} bytes"),
3224 );
3225 let out = serde_json::to_string(&resp)?;
3226 writeln!(stdout, "{out}")?;
3227 stdout.flush()?;
3228 continue;
3229 }
3230 if line_buf.last() == Some(&b'\n') {
3232 line_buf.pop();
3233 }
3234 if line_buf.last() == Some(&b'\r') {
3235 line_buf.pop();
3236 }
3237 let line = match std::str::from_utf8(&line_buf) {
3238 Ok(s) => s,
3239 Err(e) => {
3240 let resp = err_response(
3241 Value::Null,
3242 jsonrpc::PARSE_ERROR,
3243 format!("parse error: invalid UTF-8: {e}"),
3244 );
3245 let out = serde_json::to_string(&resp)?;
3246 writeln!(stdout, "{out}")?;
3247 stdout.flush()?;
3248 continue;
3249 }
3250 };
3251 if line.trim().is_empty() {
3252 continue;
3253 }
3254
3255 let req: RpcRequest = match serde_json::from_str(line) {
3256 Ok(r) => r,
3257 Err(e) => {
3258 let resp = err_response(
3259 Value::Null,
3260 jsonrpc::PARSE_ERROR,
3261 format!("parse error: {e}"),
3262 );
3263 let out = serde_json::to_string(&resp)?;
3264 writeln!(stdout, "{out}")?;
3265 stdout.flush()?;
3266 continue;
3267 }
3268 };
3269
3270 if req.method == jsonrpc::METHOD_INITIALIZE
3272 && let Some(name) = req.params["clientInfo"]["name"].as_str()
3273 && !name.is_empty()
3274 {
3275 mcp_client_name = Some(name.to_string());
3276 detected_harness = Some(crate::harness::Harness::detect(name));
3280 }
3281
3282 if req.id.is_none() || req.id == Some(Value::Null) {
3284 continue;
3285 }
3286
3287 let resolved_ttl = app_config.effective_ttl();
3288 let resolved_scoring = app_config.effective_scoring();
3289 let archive_on_gc = app_config.effective_archive_on_gc();
3290 let autonomous_hooks = app_config.effective_autonomous_hooks();
3291 let resolved_recall_scope = app_config.effective_recall_scope();
3292 let resp = handle_request(
3293 &conn,
3294 db_path,
3295 &req,
3296 embedder.as_ref().map(|e| e as &dyn Embed),
3297 llm.as_deref(),
3298 reranker.as_ref(),
3299 &tier_config,
3300 &resolved_models,
3301 vector_index.as_deref(),
3302 &resolved_ttl,
3303 &resolved_scoring,
3304 archive_on_gc,
3305 autonomous_hooks,
3306 mcp_client_name.as_deref(),
3307 profile,
3308 app_config.mcp.as_ref(),
3309 active_keypair.as_ref(),
3310 detected_harness.as_ref(),
3311 app_config.mcp_federation_forward_url.as_deref(),
3312 resolved_recall_scope,
3313 atomise_handler.as_deref(),
3314 ingest_multistep_handler.as_deref(),
3315 Some(&nag_watcher),
3316 &nag_session_id,
3317 );
3318 let out = serde_json::to_string(&resp)?;
3319 writeln!(stdout, "{out}")?;
3320 stdout.flush()?;
3321 }
3322
3323 let _ = db::checkpoint(&conn);
3324 eprintln!("ai-memory MCP server stopped");
3325 Ok(())
3326}
3327
3328#[cfg(test)]
3329mod tests {
3330 use super::*;
3331 use crate::models::{Memory, Tier};
3332 use serde_json::json;
3333
3334 #[test]
3371 fn issue_965_audit_tool_dispatch_ctx_holds_plain_connection_ref() {
3372 fn _type_check<'a>(ctx: &ToolDispatchCtx<'a>) -> &'a rusqlite::Connection {
3376 ctx.conn
3377 }
3378 let _ = _type_check;
3381 }
3382
3383 #[test]
3388 fn issue_965_audit_handle_request_takes_plain_connection_ref() {
3389 let _: fn(
3393 &rusqlite::Connection,
3394 &Path,
3395 &RpcRequest,
3396 Option<&dyn Embed>,
3397 Option<&OllamaClient>,
3398 Option<&BatchedReranker>,
3399 &TierConfig,
3400 &crate::config::ResolvedModels,
3401 Option<&VectorIndex>,
3402 &crate::config::ResolvedTtl,
3403 &crate::config::ResolvedScoring,
3404 bool,
3405 bool,
3406 Option<&str>,
3407 &crate::profile::Profile,
3408 Option<&crate::config::McpConfig>,
3409 Option<&crate::identity::keypair::AgentKeypair>,
3410 Option<&crate::harness::Harness>,
3411 Option<&str>,
3412 Option<&crate::config::RecallScope>,
3413 Option<&atomise::AtomiseToolHandler>,
3414 Option<&ingest_multistep::IngestMultistepHandler>,
3415 Option<&crate::recover::nag::CaptureNagWatcher>,
3416 &str,
3417 ) -> RpcResponse = handle_request;
3418 }
3419
3420 #[test]
3430 fn issue_965_audit_serial_dispatch_50_calls_through_single_connection() {
3431 let tmp = tempfile::NamedTempFile::new().expect("tempfile");
3432 let conn = db::open(tmp.path()).expect("open db");
3433 let tier_config = crate::config::FeatureTier::Keyword.config();
3434 let resolved_ttl = crate::config::ResolvedTtl::default();
3435 let resolved_scoring = crate::config::ResolvedScoring::default();
3436 let profile = crate::profile::Profile::full();
3437
3438 for i in 0..50 {
3439 let req = RpcRequest {
3440 jsonrpc: "2.0".into(),
3441 id: Some(json!(i)),
3442 method: "tools/call".into(),
3443 params: json!({
3444 "name": "memory_store",
3445 "arguments": {
3446 "title": format!("issue-965-stress-{i}"),
3447 "content": format!("audit stress probe iteration {i}"),
3448 "tier": Tier::Short.as_str(),
3449 "namespace": "issue-965-audit",
3450 },
3451 }),
3452 };
3453 let resp = handle_request(
3454 &conn,
3455 tmp.path(),
3456 &req,
3457 None, None, None, &tier_config,
3461 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
3462 None, &resolved_ttl,
3464 &resolved_scoring,
3465 true, false, None, &profile,
3469 None, None, None, None, None, None, None, None, "test-session", );
3479 assert!(
3480 resp.error.is_none(),
3481 "iter {i} surfaced error: {:?}",
3482 resp.error
3483 );
3484 }
3485
3486 let n: i64 = conn
3490 .query_row(
3491 "SELECT COUNT(*) FROM memories WHERE namespace = 'issue-965-audit'",
3492 [],
3493 |r| r.get(0),
3494 )
3495 .expect("count query");
3496 assert_eq!(n, 50, "expected 50 audit-stress rows, found {n}");
3497 }
3498
3499 #[test]
3502 fn issue_811_load_active_keypair_for_mcp_picks_agent_specific_when_present() {
3503 let dir = tempfile::TempDir::new().unwrap();
3504 let kp = crate::identity::keypair::generate("ai:alice").unwrap();
3505 crate::identity::keypair::save(&kp, dir.path()).unwrap();
3506 let loaded = super::load_active_keypair_for_mcp_in(dir.path(), Some("ai:alice"))
3507 .expect("agent-specific keypair should resolve when on disk");
3508 assert!(
3509 loaded.can_sign(),
3510 "loaded agent-specific keypair must carry a private half"
3511 );
3512 assert_eq!(loaded.agent_id, "ai:alice");
3513 }
3514
3515 #[test]
3516 fn issue_811_load_active_keypair_for_mcp_falls_back_to_daemon_when_agent_specific_missing() {
3517 let dir = tempfile::TempDir::new().unwrap();
3523 let daemon_kp = crate::identity::keypair::generate("daemon").unwrap();
3524 crate::identity::keypair::save(&daemon_kp, dir.path()).unwrap();
3525 let loaded =
3526 super::load_active_keypair_for_mcp_in(dir.path(), Some("host:host.local:pid-12345"))
3527 .expect("daemon fallback must engage when agent-specific key absent");
3528 assert!(
3529 loaded.can_sign(),
3530 "daemon fallback keypair must carry a private half"
3531 );
3532 assert_eq!(loaded.agent_id, "daemon");
3533 }
3534
3535 #[test]
3536 fn issue_811_load_active_keypair_for_mcp_returns_none_when_neither_present() {
3537 let dir = tempfile::TempDir::new().unwrap();
3538 let loaded = super::load_active_keypair_for_mcp_in(dir.path(), Some("ai:none"));
3539 assert!(
3540 loaded.is_none(),
3541 "no key files → None (preserves v0.6.4 unsigned behaviour)"
3542 );
3543 }
3544
3545 #[test]
3546 fn issue_811_load_active_keypair_for_mcp_falls_back_when_agent_id_unresolvable() {
3547 let dir = tempfile::TempDir::new().unwrap();
3550 let daemon_kp = crate::identity::keypair::generate("daemon").unwrap();
3551 crate::identity::keypair::save(&daemon_kp, dir.path()).unwrap();
3552 let loaded = super::load_active_keypair_for_mcp_in(dir.path(), None)
3553 .expect("daemon fallback must engage when agent_id resolution fails");
3554 assert_eq!(loaded.agent_id, "daemon");
3555 }
3556
3557 #[test]
3558 fn tool_definitions_returns_full_profile_count() {
3559 let defs = tool_definitions();
3565 let tools = defs["tools"].as_array().unwrap();
3566 assert_eq!(
3567 tools.len(),
3568 crate::profile::Profile::full().expected_tool_count()
3569 );
3570 }
3571
3572 #[test]
3578 fn tool_definitions_for_profile_core_registers_core_family_plus_always_on() {
3579 use crate::profile::{ALWAYS_ON_TOOLS, Family};
3580 let defs = tool_definitions_for_profile(&crate::profile::Profile::core());
3581 let tools = defs["tools"].as_array().unwrap();
3582 let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
3583 assert_eq!(
3586 tools.len(),
3587 Family::Core.tool_names().len() + ALWAYS_ON_TOOLS.len(),
3588 "core profile should register the Core family + always-on bootstrap; got {names:?}"
3589 );
3590 for required in [
3591 "memory_store",
3592 "memory_recall",
3593 "memory_list",
3594 "memory_get",
3595 "memory_search",
3596 "memory_load_family",
3597 "memory_smart_load",
3598 "memory_capabilities",
3599 ] {
3600 assert!(
3601 names.contains(&required),
3602 "core profile missing {required}; got {names:?}"
3603 );
3604 }
3605 for excluded in [
3607 "memory_kg_query",
3608 "memory_consolidate",
3609 "memory_archive_list",
3610 "memory_subscribe",
3611 "memory_promote",
3612 ] {
3613 assert!(
3614 !names.contains(&excluded),
3615 "core profile leaked {excluded}; got {names:?}"
3616 );
3617 }
3618 }
3619
3620 #[test]
3621 fn tool_definitions_for_profile_full_registers_expected_count() {
3622 let defs = tool_definitions_for_profile(&crate::profile::Profile::full());
3623 let tools = defs["tools"].as_array().unwrap();
3624 assert_eq!(
3625 tools.len(),
3626 crate::profile::Profile::full().expected_tool_count(),
3627 "full profile registration count must match \
3628 `Profile::full().expected_tool_count()` — the SSOT derived \
3629 from the per-Family `tool_names` slices; no literal is \
3630 restated here so surface additions (e.g. #1389 L4 \
3631 memory_capture_turn under Family::Lifecycle) flow through \
3632 automatically"
3633 );
3634 }
3635
3636 #[test]
3637 fn tool_definitions_for_profile_graph_registers_core_graph_plus_always_on() {
3638 use crate::profile::{ALWAYS_ON_TOOLS, Family};
3639 let defs = tool_definitions_for_profile(&crate::profile::Profile::graph());
3640 let tools = defs["tools"].as_array().unwrap();
3641 assert_eq!(
3644 tools.len(),
3645 Family::Core.tool_names().len()
3646 + Family::Graph.tool_names().len()
3647 + ALWAYS_ON_TOOLS.len(),
3648 "graph profile = Core + Graph families + always-on bootstrap"
3649 );
3650 }
3651
3652 #[test]
3654 fn tool_definitions_for_profile_custom_core_comma_graph_registers_union() {
3655 use crate::profile::{ALWAYS_ON_TOOLS, Family};
3656 let p = crate::profile::Profile::parse("core,graph").unwrap();
3657 let defs = tool_definitions_for_profile(&p);
3658 let tools = defs["tools"].as_array().unwrap();
3659 assert_eq!(
3660 tools.len(),
3661 Family::Core.tool_names().len()
3662 + Family::Graph.tool_names().len()
3663 + ALWAYS_ON_TOOLS.len(),
3664 "core,graph = Core + Graph families + always-on bootstrap"
3665 );
3666 }
3667
3668 #[test]
3671 fn families_overview_lists_all_eight_with_correct_loaded_flags() {
3672 let p = crate::profile::Profile::core();
3673 let v = families_overview(&p);
3674 let families = v["families"].as_array().unwrap();
3675 assert_eq!(families.len(), 8, "all eight families must appear");
3676
3677 let core_row = families.iter().find(|r| r["name"] == "core").unwrap();
3678 assert_eq!(core_row["loaded"], true);
3679 assert_eq!(core_row["tool_count"], 7);
3682 let graph_row = families.iter().find(|r| r["name"] == "graph").unwrap();
3683 assert_eq!(graph_row["loaded"], false);
3684 assert_eq!(graph_row["tool_count"], 11);
3687
3688 let always_on = v["always_on"].as_array().unwrap();
3689 assert_eq!(always_on.len(), 1);
3690 assert_eq!(always_on[0], "memory_capabilities");
3691 }
3692
3693 #[test]
3694 fn handle_capabilities_family_lists_tool_names() {
3695 let p = crate::profile::Profile::core();
3696 let v = handle_capabilities_family("graph", false, false, &p, None, None, None).unwrap();
3697 assert_eq!(v["family"], "graph");
3698 assert_eq!(v["loaded_under_active_profile"], false);
3699 let tools = v["tools"].as_array().unwrap();
3700 assert_eq!(tools.len(), 11);
3703 assert!(tools.iter().any(|t| t == "memory_kg_query"));
3705 assert!(tools.iter().any(|t| t == "memory_replay"));
3706 assert!(tools.iter().any(|t| t == "memory_verify"));
3707 assert!(tools.iter().any(|t| t == "memory_find_paths"));
3708 }
3709
3710 #[test]
3711 fn handle_capabilities_family_include_schema_returns_full_definitions() {
3712 let p = crate::profile::Profile::core();
3713 let v = handle_capabilities_family("graph", true, true, &p, None, None, None).unwrap();
3719 assert_eq!(v["family"], "graph");
3720 assert_eq!(v["verbose"], true);
3721 let tools = v["tools"].as_array().unwrap();
3722 assert_eq!(tools.len(), 11);
3725 for tool in tools {
3727 assert!(tool.get("name").is_some(), "missing name");
3728 assert!(tool.get("description").is_some(), "missing description");
3729 assert!(tool.get("inputSchema").is_some(), "missing inputSchema");
3730 }
3731 }
3732
3733 #[test]
3734 fn handle_capabilities_family_verbose_preserves_docs_field() {
3735 let p = crate::profile::Profile::core();
3738 let v = handle_capabilities_family("core", true, true, &p, None, None, None).unwrap();
3739 let tools = v["tools"].as_array().unwrap();
3740 assert!(!tools.is_empty());
3741 let with_docs = tools
3742 .iter()
3743 .filter(|t| t.get("docs").and_then(Value::as_str).is_some())
3744 .count();
3745 assert!(
3746 with_docs >= 1,
3747 "verbose=true must surface at least one docs string in family=core; got 0"
3748 );
3749 }
3750
3751 #[test]
3752 fn handle_capabilities_family_unknown_returns_diagnostic_err() {
3753 let p = crate::profile::Profile::core();
3754 let err =
3755 handle_capabilities_family("xyz", false, false, &p, None, None, None).unwrap_err();
3756 assert!(err.contains("xyz"));
3757 assert!(err.contains("Valid families"));
3758 assert!(err.contains("core"));
3759 assert!(err.contains("graph"));
3760 }
3761
3762 #[test]
3763 fn handle_capabilities_family_empty_name_errors() {
3764 let p = crate::profile::Profile::core();
3765 let err = handle_capabilities_family("", false, false, &p, None, None, None).unwrap_err();
3766 assert!(err.contains("must not be empty"));
3767 }
3768
3769 #[test]
3785 fn tool_definitions_for_profile_preserves_optional_params_post_859() {
3786 let p = crate::profile::Profile::full();
3787 let defs = tool_definitions_for_profile(&p);
3788 let store = defs["tools"]
3789 .as_array()
3790 .unwrap()
3791 .iter()
3792 .find(|t| t["name"] == "memory_store")
3793 .expect("memory_store must be present in full profile");
3794 let props = store["inputSchema"]["properties"].as_object().unwrap();
3795 for kept in [
3797 "title",
3798 "content",
3799 "namespace",
3800 "confidence",
3801 "priority",
3802 "tier",
3803 "metadata",
3804 "agent_id",
3805 "source",
3806 "scope",
3807 "tags",
3808 "on_conflict",
3809 "kind",
3810 ] {
3811 assert!(
3812 props.contains_key(kept),
3813 "#859: wire schema must preserve property `{kept}` for client-side discovery"
3814 );
3815 }
3816 let confidence = props
3818 .get("confidence")
3819 .and_then(serde_json::Value::as_object)
3820 .expect("confidence property must be an object");
3821 assert!(
3822 !confidence.contains_key("description"),
3823 "#859: per-property `description` prose must be stripped on the wire"
3824 );
3825 let confidence_type = confidence
3832 .get("type")
3833 .expect("confidence must declare a type discriminator");
3834 let confidence_is_number = match confidence_type {
3835 serde_json::Value::String(s) => s == "number",
3836 serde_json::Value::Array(items) => items.iter().any(|v| v.as_str() == Some("number")),
3837 _ => false,
3838 };
3839 assert!(
3840 confidence_is_number,
3841 "confidence.type must contain `number`; got {confidence_type:?}"
3842 );
3843 let _ = confidence; }
3859
3860 #[test]
3864 fn tool_definitions_for_profile_verbose_keeps_every_optional() {
3865 let p = crate::profile::Profile::full();
3866 let defs = tool_definitions_for_profile_verbose(&p);
3867 let store = defs["tools"]
3868 .as_array()
3869 .unwrap()
3870 .iter()
3871 .find(|t| t["name"] == "memory_store")
3872 .expect("memory_store must be present");
3873 let props = store["inputSchema"]["properties"].as_object().unwrap();
3874 for kept in [
3875 "title",
3876 "content",
3877 "namespace",
3878 "confidence",
3879 "priority",
3880 "tier",
3881 "metadata",
3882 "agent_id",
3883 "source",
3884 "scope",
3885 "tags",
3886 "on_conflict",
3887 ] {
3888 assert!(
3889 props.contains_key(kept),
3890 "verbose path should preserve `{kept}`"
3891 );
3892 }
3893 }
3894
3895 #[test]
3904 fn handle_capabilities_family_verbose_toggles_optional_params() {
3905 let p = crate::profile::Profile::full();
3906 let trimmed =
3909 handle_capabilities_family("core", true, false, &p, None, None, None).unwrap();
3910 assert_eq!(trimmed["verbose"], false);
3911 let store_trimmed = trimmed["tools"]
3912 .as_array()
3913 .unwrap()
3914 .iter()
3915 .find(|t| t["name"] == "memory_store")
3916 .expect("memory_store in core family");
3917 let props_trimmed = store_trimmed["inputSchema"]["properties"]
3918 .as_object()
3919 .unwrap();
3920 for kept in [
3923 "title",
3924 "content",
3925 "namespace",
3926 "confidence",
3927 "priority",
3928 "tier",
3929 "metadata",
3930 "agent_id",
3931 "source",
3932 "scope",
3933 "tags",
3934 "on_conflict",
3935 "kind",
3936 ] {
3937 assert!(
3938 props_trimmed.contains_key(kept),
3939 "wire schema (verbose=false) must preserve property `{kept}` (#859)"
3940 );
3941 }
3942 let confidence_prop = props_trimmed
3944 .get("confidence")
3945 .and_then(serde_json::Value::as_object)
3946 .expect("confidence property must be an object");
3947 assert!(
3948 !confidence_prop.contains_key("description"),
3949 "wire schema must drop per-property `description` prose (#859)"
3950 );
3951 let confidence_type = confidence_prop
3958 .get("type")
3959 .expect("confidence must declare a type discriminator");
3960 let confidence_is_number = match confidence_type {
3961 serde_json::Value::String(s) => s == "number",
3962 serde_json::Value::Array(items) => items.iter().any(|v| v.as_str() == Some("number")),
3963 _ => false,
3964 };
3965 assert!(
3966 confidence_is_number,
3967 "confidence.type must contain `number`; got {confidence_type:?}"
3968 );
3969
3970 let verbose = handle_capabilities_family("core", true, true, &p, None, None, None).unwrap();
3972 assert_eq!(verbose["verbose"], true);
3973 let store_verbose = verbose["tools"]
3974 .as_array()
3975 .unwrap()
3976 .iter()
3977 .find(|t| t["name"] == "memory_store")
3978 .expect("memory_store in core family");
3979 let props_verbose = store_verbose["inputSchema"]["properties"]
3980 .as_object()
3981 .unwrap();
3982 assert!(props_verbose.contains_key("confidence"));
3983 assert!(props_verbose.contains_key("priority"));
3984 assert!(props_verbose.contains_key("metadata"));
3985 assert!(props_verbose.contains_key("agent_id"));
3986 }
3987
3988 #[test]
3993 fn trim_optional_params_is_idempotent() {
3994 let mut defs = tool_definitions();
3995 let stripped_first = trim_optional_params(&mut defs);
3996 assert!(
3997 stripped_first > 0,
3998 "first trim should strip a positive number of per-property descriptions"
3999 );
4000 let stripped_second = trim_optional_params(&mut defs);
4001 assert_eq!(
4002 stripped_second, 0,
4003 "re-trim of an already-trimmed schema must be a no-op"
4004 );
4005 }
4006
4007 #[test]
4015 fn c4_trim_shrinks_full_profile_payload_meaningfully() {
4016 let p = crate::profile::Profile::full();
4017 let trimmed = tool_definitions_for_profile(&p);
4018 let verbose = tool_definitions_for_profile_verbose(&p);
4019 let trimmed_bytes = serde_json::to_string(&trimmed).unwrap().len();
4020 let verbose_bytes = serde_json::to_string(&verbose).unwrap().len();
4021 assert!(
4022 trimmed_bytes < verbose_bytes,
4023 "trimmed ({trimmed_bytes}B) must be smaller than verbose ({verbose_bytes}B)"
4024 );
4025 let saved_pct = (verbose_bytes - trimmed_bytes) as f64 / verbose_bytes as f64 * 100.0;
4026 assert!(
4037 saved_pct >= 5.0,
4038 "trim should save >=5% of full-profile bytes via top-level description \
4039 compaction; got {saved_pct:.1}% (verbose={verbose_bytes}B, \
4040 trimmed={trimmed_bytes}B) — `wire_compact_descriptions` may be broken"
4041 );
4042 }
4043
4044 #[test]
4045 fn tool_definitions_include_check_duplicate() {
4046 let defs = tool_definitions();
4047 let tools = defs["tools"].as_array().unwrap();
4048 let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
4049 assert!(names.contains(&"memory_check_duplicate"));
4050 }
4051
4052 #[test]
4053 fn tool_definitions_include_entity_registry_tools() {
4054 let defs = tool_definitions();
4055 let tools = defs["tools"].as_array().unwrap();
4056 let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
4057 assert!(names.contains(&"memory_entity_register"));
4058 assert!(names.contains(&"memory_entity_get_by_alias"));
4059 }
4060
4061 #[test]
4062 fn tool_definitions_include_kg_timeline() {
4063 let defs = tool_definitions();
4064 let tools = defs["tools"].as_array().unwrap();
4065 let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
4066 assert!(names.contains(&"memory_kg_timeline"));
4067 }
4068
4069 #[test]
4070 fn tool_definitions_include_kg_invalidate() {
4071 let defs = tool_definitions();
4072 let tools = defs["tools"].as_array().unwrap();
4073 let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
4074 assert!(names.contains(&"memory_kg_invalidate"));
4075 }
4076
4077 #[test]
4078 fn tool_definitions_include_kg_query() {
4079 let defs = tool_definitions();
4080 let tools = defs["tools"].as_array().unwrap();
4081 let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
4082 assert!(names.contains(&"memory_kg_query"));
4083 }
4084
4085 #[test]
4086 fn tool_definitions_include_agent_register_and_list() {
4087 let defs = tool_definitions();
4088 let names: Vec<&str> = defs["tools"]
4089 .as_array()
4090 .unwrap()
4091 .iter()
4092 .filter_map(|t| t["name"].as_str())
4093 .collect();
4094 assert!(names.contains(&"memory_agent_register"));
4095 assert!(names.contains(&"memory_agent_list"));
4096 }
4097
4098 #[test]
4099 fn tool_definitions_include_notify_and_inbox() {
4100 let defs = tool_definitions();
4102 let names: Vec<&str> = defs["tools"]
4103 .as_array()
4104 .unwrap()
4105 .iter()
4106 .filter_map(|t| t["name"].as_str())
4107 .collect();
4108 assert!(names.contains(&"memory_notify"));
4109 assert!(names.contains(&"memory_inbox"));
4110 }
4111
4112 #[test]
4113 fn messages_namespace_is_prefixed() {
4114 assert_eq!(super::messages_namespace_for("alice"), "_messages/alice");
4115 assert_eq!(
4116 super::messages_namespace_for("ai:claude-opus-4.7"),
4117 "_messages/ai:claude-opus-4.7"
4118 );
4119 }
4120
4121 #[test]
4122 fn tool_definitions_all_have_names() {
4123 let defs = tool_definitions();
4124 let tools = defs["tools"].as_array().unwrap();
4125 for tool in tools {
4126 assert!(tool["name"].as_str().unwrap().starts_with("memory_"));
4127 }
4128 }
4129
4130 #[test]
4131 fn tool_definitions_recall_has_toon_default() {
4132 let defs = tool_definitions();
4140 let tools = defs["tools"].as_array().unwrap();
4141 let recall = tools.iter().find(|t| t["name"] == "memory_recall").unwrap();
4142 let format_schema = &recall["inputSchema"]["properties"]["format"];
4143 assert!(format_schema.is_object(), "format schema must be present");
4144 let default = &format_schema["default"];
4147 assert!(
4148 default.is_null() || default.as_str() == Some("toon_compact"),
4149 "format default must be null (post-D1.6 Option<T>) or \"toon_compact\" (legacy); \
4150 got {default:?}"
4151 );
4152 }
4153
4154 #[test]
4163 fn tool_definitions_recall_advertises_session_default_issue_518() {
4164 let defs = tool_definitions();
4165 let tools = defs["tools"].as_array().unwrap();
4166 let recall = tools
4167 .iter()
4168 .find(|t| t["name"] == "memory_recall")
4169 .expect("memory_recall tool must be defined");
4170 let props = &recall["inputSchema"]["properties"];
4171 let session_default = &props["session_default"];
4172 let session_default_type = &session_default["type"];
4176 let is_boolean = match session_default_type {
4177 serde_json::Value::String(s) => s == "boolean",
4178 serde_json::Value::Array(items) => items.iter().any(|v| v.as_str() == Some("boolean")),
4179 _ => false,
4180 };
4181 assert!(
4182 is_boolean,
4183 "session_default.type must contain `boolean`; got {session_default_type:?}"
4184 );
4185 let default = &session_default["default"];
4187 assert!(
4188 default.is_null() || default.as_bool() == Some(false),
4189 "session_default default must be null (post-D1.6) or false (legacy); got {default:?}"
4190 );
4191 assert!(
4192 session_default["description"]
4193 .as_str()
4194 .is_some_and(|d| d.contains("agents.defaults.recall_scope")),
4195 "session_default description must mention [agents.defaults.recall_scope] — got {session_default:?}"
4196 );
4197 }
4198
4199 #[test]
4200 fn prompt_definitions_returns_2() {
4201 let defs = prompt_definitions();
4202 let prompts = defs["prompts"].as_array().unwrap();
4203 assert_eq!(prompts.len(), 2);
4204 assert_eq!(prompts[0]["name"], "recall-first");
4205 assert_eq!(prompts[1]["name"], "memory-workflow");
4206 }
4207
4208 #[test]
4209 fn prompt_definitions_recall_first_has_arguments() {
4210 let defs = prompt_definitions();
4211 let prompts = defs["prompts"].as_array().unwrap();
4212 let recall_first = &prompts[0];
4213 let args = recall_first["arguments"].as_array().unwrap();
4214 assert_eq!(args.len(), 1);
4215 assert_eq!(args[0]["name"], "namespace");
4216 }
4217
4218 #[test]
4219 fn prompt_content_recall_first() {
4220 let params = json!({});
4221 let result = prompt_content("recall-first", ¶ms).unwrap();
4222 let msgs = result["messages"].as_array().unwrap();
4223 assert_eq!(msgs.len(), 1);
4224 let text = msgs[0]["content"]["text"].as_str().unwrap();
4225 assert!(text.contains("RECALL FIRST"));
4226 assert!(text.contains("TOON"));
4227 assert!(text.contains("memory_recall"));
4228 assert!(text.contains("memory_store"));
4229 assert!(text.contains("DEDUP"));
4230 }
4231
4232 #[test]
4233 fn prompt_content_recall_first_with_namespace() {
4234 let params = json!({"arguments": {"namespace": "my-project"}});
4235 let result = prompt_content("recall-first", ¶ms).unwrap();
4236 let text = result["messages"][0]["content"]["text"].as_str().unwrap();
4237 assert!(text.contains("my-project"));
4238 }
4239
4240 #[test]
4241 fn prompt_content_memory_workflow() {
4242 let params = json!({});
4243 let result = prompt_content("memory-workflow", ¶ms).unwrap();
4244 let text = result["messages"][0]["content"]["text"].as_str().unwrap();
4245 assert!(text.contains("STORE"));
4246 assert!(text.contains("RECALL"));
4247 assert!(text.contains("SEARCH"));
4248 assert!(text.contains("CONSOLIDATE"));
4249 assert!(text.contains("TOON"));
4250 }
4251
4252 #[test]
4253 fn prompt_content_unknown() {
4254 let params = json!({});
4255 let result = prompt_content("nonexistent", ¶ms);
4256 assert!(result.is_err());
4257 assert!(result.unwrap_err().contains("unknown prompt"));
4258 }
4259
4260 #[test]
4261 fn prompt_content_role_is_user() {
4262 let params = json!({});
4263 let result = prompt_content("recall-first", ¶ms).unwrap();
4264 assert_eq!(result["messages"][0]["role"], "user");
4265 }
4266
4267 #[test]
4268 fn ok_response_structure() {
4269 let resp = ok_response(json!(1), json!({"test": true}));
4270 assert_eq!(resp.jsonrpc, "2.0");
4271 assert_eq!(resp.id, json!(1));
4272 assert!(resp.result.is_some());
4273 assert!(resp.error.is_none());
4274 }
4275
4276 #[test]
4277 fn err_response_structure() {
4278 let resp = err_response(json!(1), -32600, "test error".to_string());
4279 assert_eq!(resp.jsonrpc, "2.0");
4280 assert!(resp.error.is_some());
4281 let err = resp.error.unwrap();
4282 assert_eq!(err.code, -32600);
4283 assert_eq!(err.message, "test error");
4284 }
4285
4286 #[derive(Clone)]
4290 struct VecWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
4291
4292 impl std::io::Write for VecWriter {
4293 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
4294 self.0.lock().unwrap().extend_from_slice(buf);
4295 Ok(buf.len())
4296 }
4297 fn flush(&mut self) -> std::io::Result<()> {
4298 Ok(())
4299 }
4300 }
4301
4302 impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for VecWriter {
4303 type Writer = VecWriter;
4304 fn make_writer(&'a self) -> Self::Writer {
4305 self.clone()
4306 }
4307 }
4308
4309 fn run_with_capture<F: FnOnce()>(f: F) -> String {
4310 let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
4311 let writer = VecWriter(buf.clone());
4312 let subscriber = tracing_subscriber::fmt()
4313 .with_writer(writer)
4314 .with_max_level(tracing::Level::INFO)
4315 .with_ansi(false)
4316 .finish();
4317 tracing::subscriber::with_default(subscriber, f);
4318 String::from_utf8(buf.lock().unwrap().clone()).unwrap_or_default()
4319 }
4320
4321 fn make_tools_call(tool: &str, args: Value) -> RpcRequest {
4322 RpcRequest {
4323 jsonrpc: "2.0".into(),
4324 id: Some(json!(1)),
4325 method: "tools/call".into(),
4326 params: json!({ "name": tool, "arguments": args }),
4327 }
4328 }
4329
4330 #[test]
4335 fn tools_call_emits_span_with_tool_name_and_elapsed_ms() {
4336 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
4337 let tier_config = FeatureTier::Keyword.config();
4338 let resolved_ttl = crate::config::ResolvedTtl::default();
4339 let resolved_scoring = crate::config::ResolvedScoring::default();
4340 let req = make_tools_call("memory_list", json!({"limit": 1}));
4341
4342 let captured = run_with_capture(|| {
4343 let resp = handle_request(
4344 &conn,
4345 std::path::Path::new(":memory:"),
4346 &req,
4347 None,
4348 None,
4349 None,
4350 &tier_config,
4351 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
4352 None,
4353 &resolved_ttl,
4354 &resolved_scoring,
4355 true,
4356 false,
4357 None,
4358 &crate::profile::Profile::full(),
4359 None,
4360 None,
4361 None,
4362 None, None, None, None, None, "test-session", );
4369 assert!(resp.error.is_none(), "expected ok rpc response");
4370 });
4371
4372 assert!(
4373 captured.contains("mcp_tool_call"),
4374 "missing span name in: {captured}"
4375 );
4376 assert!(
4377 captured.contains("memory_list"),
4378 "missing tool field in: {captured}"
4379 );
4380 assert!(
4381 captured.contains("elapsed_ms"),
4382 "missing elapsed_ms field in: {captured}"
4383 );
4384 assert!(
4385 captured.contains(" ok"),
4386 "missing ok outcome event in: {captured}"
4387 );
4388 }
4389
4390 #[test]
4394 fn tools_call_emits_warn_event_on_handler_error() {
4395 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
4396 let tier_config = FeatureTier::Keyword.config();
4397 let resolved_ttl = crate::config::ResolvedTtl::default();
4398 let resolved_scoring = crate::config::ResolvedScoring::default();
4399 let req = make_tools_call("memory_get", json!({"id": ""}));
4402
4403 let captured = run_with_capture(|| {
4404 let resp = handle_request(
4405 &conn,
4406 std::path::Path::new(":memory:"),
4407 &req,
4408 None,
4409 None,
4410 None,
4411 &tier_config,
4412 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
4413 None,
4414 &resolved_ttl,
4415 &resolved_scoring,
4416 true,
4417 false,
4418 None,
4419 &crate::profile::Profile::full(),
4420 None,
4421 None,
4422 None,
4423 None, None, None, None, None, "test-session", );
4430 assert!(resp.error.is_none());
4434 });
4435
4436 assert!(
4437 captured.contains("mcp_tool_call"),
4438 "missing span in err path: {captured}"
4439 );
4440 assert!(
4441 captured.contains("memory_get"),
4442 "missing tool field in err path: {captured}"
4443 );
4444 assert!(
4445 captured.contains("WARN"),
4446 "missing WARN level on err path: {captured}"
4447 );
4448 assert!(
4449 captured.contains("err"),
4450 "missing err outcome in: {captured}"
4451 );
4452 }
4453 #[test]
4458 #[allow(clippy::too_many_lines)]
4459 fn mcp_tools_smoke_matrix() {
4460 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
4461 let tier_config = FeatureTier::Keyword.config();
4462 let resolved_ttl = crate::config::ResolvedTtl::default();
4463 let resolved_scoring = crate::config::ResolvedScoring::default();
4464
4465 struct ToolCase {
4466 name: &'static str,
4467 valid_args: Value,
4468 required_arg: Option<&'static str>, }
4470
4471 let cases: &[ToolCase] = &[
4472 ToolCase {
4473 name: "memory_store",
4474 valid_args: json!({"title": "test", "content": "test content"}),
4475 required_arg: Some("title"),
4476 },
4477 ToolCase {
4478 name: "memory_recall",
4479 valid_args: json!({"context": "test"}),
4480 required_arg: Some("context"),
4481 },
4482 ToolCase {
4483 name: "memory_search",
4484 valid_args: json!({"query": "test"}),
4485 required_arg: Some("query"),
4486 },
4487 ToolCase {
4488 name: "memory_list",
4489 valid_args: json!({}),
4490 required_arg: None,
4491 },
4492 ToolCase {
4493 name: "memory_load_family",
4494 valid_args: json!({"family": "core"}),
4495 required_arg: Some("family"),
4496 },
4497 ToolCase {
4502 name: "memory_smart_load",
4503 valid_args: json!({"intent": "load core memories"}),
4504 required_arg: Some("intent"),
4505 },
4506 ToolCase {
4507 name: "memory_get_taxonomy",
4508 valid_args: json!({}),
4509 required_arg: None,
4510 },
4511 ToolCase {
4512 name: "memory_check_duplicate",
4513 valid_args: json!({"title": "test", "content": "test content"}),
4514 required_arg: Some("title"),
4515 },
4516 ToolCase {
4517 name: "memory_entity_register",
4518 valid_args: json!({"canonical_name": "Entity", "namespace": "test"}),
4519 required_arg: Some("canonical_name"),
4520 },
4521 ToolCase {
4522 name: "memory_entity_get_by_alias",
4523 valid_args: json!({"alias": "test"}),
4524 required_arg: Some("alias"),
4525 },
4526 ToolCase {
4527 name: "memory_kg_timeline",
4528 valid_args: json!({"source_id": "fake-id-for-test"}),
4529 required_arg: Some("source_id"),
4530 },
4531 ToolCase {
4532 name: "memory_kg_invalidate",
4533 valid_args: json!({"source_id": "s", "target_id": "t", "relation": "related_to"}),
4534 required_arg: Some("source_id"),
4535 },
4536 ToolCase {
4537 name: "memory_kg_query",
4538 valid_args: json!({"source_id": "fake-id-for-test"}),
4539 required_arg: Some("source_id"),
4540 },
4541 ToolCase {
4545 name: "memory_find_paths",
4546 valid_args: json!({
4547 "source_id": "fake-src-for-test",
4548 "target_id": "fake-dst-for-test",
4549 }),
4550 required_arg: Some("source_id"),
4551 },
4552 ToolCase {
4553 name: "memory_delete",
4554 valid_args: json!({"id": "fake-id-for-test"}),
4555 required_arg: Some("id"),
4556 },
4557 ToolCase {
4558 name: "memory_promote",
4559 valid_args: json!({"id": "fake-id-for-test"}),
4560 required_arg: Some("id"),
4561 },
4562 ToolCase {
4563 name: "memory_forget",
4564 valid_args: json!({}),
4565 required_arg: None,
4566 },
4567 ToolCase {
4568 name: "memory_stats",
4569 valid_args: json!({}),
4570 required_arg: None,
4571 },
4572 ToolCase {
4573 name: "memory_update",
4574 valid_args: json!({"id": "fake-id-for-test"}),
4575 required_arg: Some("id"),
4576 },
4577 ToolCase {
4578 name: "memory_get",
4579 valid_args: json!({"id": "fake-id-for-test"}),
4580 required_arg: Some("id"),
4581 },
4582 ToolCase {
4583 name: "memory_link",
4584 valid_args: json!({"source_id": "s", "target_id": "t"}),
4585 required_arg: Some("source_id"),
4586 },
4587 ToolCase {
4588 name: "memory_get_links",
4589 valid_args: json!({"id": "fake-id-for-test"}),
4590 required_arg: Some("id"),
4591 },
4592 ToolCase {
4593 name: "memory_verify",
4594 valid_args: json!({
4600 "source_id": "fake-src-id",
4601 "target_id": "fake-dst-id",
4602 "relation": "related_to"
4603 }),
4604 required_arg: None,
4608 },
4609 ToolCase {
4614 name: "memory_replay",
4615 valid_args: json!({"memory_id": "fake-id-for-test"}),
4616 required_arg: Some("memory_id"),
4617 },
4618 ToolCase {
4619 name: "memory_consolidate",
4620 valid_args: json!({"ids": ["id1", "id2"], "title": "consolidated"}),
4621 required_arg: Some("ids"),
4622 },
4623 ToolCase {
4624 name: "memory_capabilities",
4625 valid_args: json!({}),
4626 required_arg: None,
4627 },
4628 ToolCase {
4629 name: "memory_expand_query",
4630 valid_args: json!({"query": "test"}),
4631 required_arg: Some("query"),
4632 },
4633 ToolCase {
4634 name: "memory_auto_tag",
4635 valid_args: json!({"id": "fake-id-for-test"}),
4636 required_arg: Some("id"),
4637 },
4638 ToolCase {
4639 name: "memory_detect_contradiction",
4640 valid_args: json!({"id_a": "a", "id_b": "b"}),
4641 required_arg: Some("id_a"),
4642 },
4643 ToolCase {
4644 name: "memory_archive_list",
4645 valid_args: json!({}),
4646 required_arg: None,
4647 },
4648 ToolCase {
4649 name: "memory_archive_restore",
4650 valid_args: json!({"id": "fake-id-for-test"}),
4651 required_arg: Some("id"),
4652 },
4653 ToolCase {
4654 name: "memory_archive_purge",
4655 valid_args: json!({}),
4656 required_arg: None,
4657 },
4658 ToolCase {
4659 name: "memory_archive_stats",
4660 valid_args: json!({}),
4661 required_arg: None,
4662 },
4663 ToolCase {
4664 name: "memory_gc",
4665 valid_args: json!({}),
4666 required_arg: None,
4667 },
4668 ToolCase {
4669 name: "memory_session_start",
4670 valid_args: json!({}),
4671 required_arg: None,
4672 },
4673 ToolCase {
4674 name: "memory_namespace_set_standard",
4675 valid_args: json!({"namespace": "test", "id": "fake-id-for-test"}),
4676 required_arg: Some("namespace"),
4677 },
4678 ToolCase {
4679 name: "memory_namespace_get_standard",
4680 valid_args: json!({"namespace": "test"}),
4681 required_arg: Some("namespace"),
4682 },
4683 ToolCase {
4684 name: "memory_namespace_clear_standard",
4685 valid_args: json!({"namespace": "test"}),
4686 required_arg: Some("namespace"),
4687 },
4688 ToolCase {
4689 name: "memory_pending_list",
4690 valid_args: json!({}),
4691 required_arg: None,
4692 },
4693 ToolCase {
4694 name: "memory_pending_approve",
4695 valid_args: json!({"id": "fake-id-for-test"}),
4696 required_arg: Some("id"),
4697 },
4698 ToolCase {
4699 name: "memory_pending_reject",
4700 valid_args: json!({"id": "fake-id-for-test"}),
4701 required_arg: Some("id"),
4702 },
4703 ToolCase {
4704 name: "memory_agent_register",
4705 valid_args: json!({"agent_id": "test-agent", "agent_type": "human"}),
4706 required_arg: Some("agent_id"),
4707 },
4708 ToolCase {
4709 name: "memory_agent_list",
4710 valid_args: json!({}),
4711 required_arg: None,
4712 },
4713 ToolCase {
4714 name: "memory_notify",
4715 valid_args: json!({"target_agent_id": "agent", "title": "msg", "payload": "body"}),
4716 required_arg: Some("target_agent_id"),
4717 },
4718 ToolCase {
4719 name: "memory_inbox",
4720 valid_args: json!({}),
4721 required_arg: None,
4722 },
4723 ToolCase {
4724 name: "memory_subscribe",
4727 valid_args: json!({"url": "https://example.com/webhook", "secret": "tool-case-secret"}),
4728 required_arg: Some("url"),
4729 },
4730 ToolCase {
4731 name: "memory_unsubscribe",
4732 valid_args: json!({"id": "fake-id-for-test"}),
4733 required_arg: Some("id"),
4734 },
4735 ToolCase {
4736 name: "memory_list_subscriptions",
4737 valid_args: json!({}),
4738 required_arg: None,
4739 },
4740 ToolCase {
4742 name: "memory_subscription_replay",
4743 valid_args: json!({
4744 "subscription_id": "smoke-id",
4745 "since": "1970-01-01T00:00:00Z"
4746 }),
4747 required_arg: Some("subscription_id"),
4748 },
4749 ToolCase {
4750 name: "memory_subscription_dlq_list",
4751 valid_args: json!({}),
4752 required_arg: None,
4753 },
4754 ToolCase {
4757 name: "memory_quota_status",
4758 valid_args: json!({}),
4759 required_arg: None,
4760 },
4761 ToolCase {
4765 name: "memory_check_agent_action",
4766 valid_args: json!({"kind": "bash", "command": "echo hello"}),
4767 required_arg: Some("kind"),
4768 },
4769 ToolCase {
4772 name: "memory_rule_list",
4773 valid_args: json!({}),
4774 required_arg: None,
4775 },
4776 ];
4777
4778 for case in cases {
4780 let req = make_tools_call(case.name, case.valid_args.clone());
4781 let resp = handle_request(
4782 &conn,
4783 std::path::Path::new(":memory:"),
4784 &req,
4785 None,
4786 None,
4787 None,
4788 &tier_config,
4789 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
4790 None,
4791 &resolved_ttl,
4792 &resolved_scoring,
4793 true,
4794 false,
4795 None,
4796 &crate::profile::Profile::full(),
4797 None,
4798 None,
4799 None,
4800 None, None, None, None, None, "test-session", );
4807 assert!(
4808 resp.error.is_none(),
4809 "happy path failed for {}: {:?}",
4810 case.name,
4811 resp.error
4812 );
4813 assert!(
4814 resp.result.is_some(),
4815 "missing result for happy path {}: {:?}",
4816 case.name,
4817 resp
4818 );
4819 }
4820
4821 for case in cases {
4823 if let Some(required_arg) = case.required_arg {
4824 let mut bad_args = case.valid_args.clone();
4825 bad_args.as_object_mut().unwrap().remove(required_arg);
4826
4827 let req = make_tools_call(case.name, bad_args);
4828 let resp = handle_request(
4829 &conn,
4830 std::path::Path::new(":memory:"),
4831 &req,
4832 None,
4833 None,
4834 None,
4835 &tier_config,
4836 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
4837 None,
4838 &resolved_ttl,
4839 &resolved_scoring,
4840 true,
4841 false,
4842 None,
4843 &crate::profile::Profile::full(),
4844 None,
4845 None,
4846 None,
4847 None, None, None, None, None, "test-session", );
4854
4855 assert!(
4858 resp.error.is_none() || resp.result.is_some(),
4859 "unexpected RPC-layer error for {} (missing {}) should be handler-level Err",
4860 case.name,
4861 required_arg
4862 );
4863 }
4864 }
4865 }
4866
4867 fn invoke_handle_request(conn: &rusqlite::Connection, req: &RpcRequest) -> RpcResponse {
4889 let tier_config = FeatureTier::Keyword.config();
4890 let resolved_ttl = crate::config::ResolvedTtl::default();
4891 let resolved_scoring = crate::config::ResolvedScoring::default();
4892 handle_request(
4893 conn,
4894 std::path::Path::new(":memory:"),
4895 req,
4896 None,
4897 None,
4898 None,
4899 &tier_config,
4900 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
4901 None,
4902 &resolved_ttl,
4903 &resolved_scoring,
4904 true,
4905 false,
4906 None,
4907 &crate::profile::Profile::full(),
4908 None,
4909 None,
4910 None,
4911 None, None, None, None, None, "test-session", )
4918 }
4919
4920 fn invoke_handle_request_with_nag(
4924 conn: &rusqlite::Connection,
4925 req: &RpcRequest,
4926 nag_watcher: &crate::recover::nag::CaptureNagWatcher,
4927 nag_session_id: &str,
4928 mcp_client: Option<&str>,
4929 ) -> RpcResponse {
4930 let tier_config = FeatureTier::Keyword.config();
4931 let resolved_ttl = crate::config::ResolvedTtl::default();
4932 let resolved_scoring = crate::config::ResolvedScoring::default();
4933 handle_request(
4934 conn,
4935 std::path::Path::new(":memory:"),
4936 req,
4937 None,
4938 None,
4939 None,
4940 &tier_config,
4941 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
4942 None,
4943 &resolved_ttl,
4944 &resolved_scoring,
4945 true,
4946 false,
4947 mcp_client,
4948 &crate::profile::Profile::full(),
4949 None,
4950 None,
4951 None,
4952 None, None, None, None, Some(nag_watcher),
4957 nag_session_id,
4958 )
4959 }
4960
4961 fn count_capture_lag_lines(buf: &std::sync::Arc<std::sync::Mutex<Vec<u8>>>) -> usize {
4965 let bytes = buf.lock().unwrap().clone();
4966 String::from_utf8(bytes)
4967 .unwrap()
4968 .lines()
4969 .filter(|l| l.contains("\"action\":\"capture_lag\""))
4970 .count()
4971 }
4972
4973 #[test]
4979 fn observe_capture_nag_none_watcher_is_noop() {
4980 use crate::recover::nag::NagAction;
4981 let action = observe_capture_nag(None, "s", "memory_recall", &json!({}), Some("c"));
4982 assert_eq!(action, NagAction::None);
4983 }
4984
4985 #[test]
4991 fn dispatch_loop_emits_capture_lag_past_threshold_and_resets_on_write() {
4992 let _g = crate::audit::sink_test_lock();
4993 let buf: std::sync::Arc<std::sync::Mutex<Vec<u8>>> =
4994 std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
4995 crate::audit::init_for_test(buf.clone());
4996
4997 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
4998 let watcher = crate::recover::nag::CaptureNagWatcher::new(3, 0);
5002 let session = "sess-1398";
5003 let client = Some("testclient");
5004
5005 let cap = make_tools_call("memory_capabilities", json!({}));
5006 for _ in 0..2 {
5008 let resp = invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
5009 assert!(resp.error.is_none());
5010 }
5011 assert_eq!(
5012 count_capture_lag_lines(&buf),
5013 0,
5014 "no capture_lag before the threshold is crossed"
5015 );
5016
5017 let resp = invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
5019 assert!(resp.error.is_none());
5020 assert_eq!(
5021 count_capture_lag_lines(&buf),
5022 1,
5023 "primary threshold crossing emits exactly one capture_lag event"
5024 );
5025
5026 invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
5028 assert_eq!(
5029 count_capture_lag_lines(&buf),
5030 1,
5031 "no duplicate capture_lag while still in the same warned tier"
5032 );
5033
5034 let store = make_tools_call(
5037 "memory_store",
5038 json!({"title": "t", "content": "c", "tier": "short", "agent_id": "ai:testclient"}),
5039 );
5040 let store_resp = invoke_handle_request_with_nag(&conn, &store, &watcher, session, client);
5041 assert!(
5042 store_resp.error.is_none(),
5043 "store should succeed: {store_resp:?}"
5044 );
5045 for _ in 0..3 {
5046 invoke_handle_request_with_nag(&conn, &cap, &watcher, session, client);
5047 }
5048 assert_eq!(
5049 count_capture_lag_lines(&buf),
5050 2,
5051 "re-armed WARN after a write reset fires a second capture_lag"
5052 );
5053
5054 crate::audit::shutdown_for_test();
5055 }
5056
5057 #[test]
5060 fn capture_lag_events_are_chained() {
5061 let _g = crate::audit::sink_test_lock();
5062 let buf: std::sync::Arc<std::sync::Mutex<Vec<u8>>> =
5063 std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
5064 crate::audit::init_for_test(buf.clone());
5065
5066 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5067 let watcher = crate::recover::nag::CaptureNagWatcher::new(1, 2);
5068 let cap = make_tools_call("memory_capabilities", json!({}));
5069 invoke_handle_request_with_nag(&conn, &cap, &watcher, "s", Some("c"));
5071 invoke_handle_request_with_nag(&conn, &cap, &watcher, "s", Some("c"));
5072 assert_eq!(
5073 count_capture_lag_lines(&buf),
5074 2,
5075 "primary + escalation each emit a capture_lag event"
5076 );
5077
5078 let bytes = buf.lock().unwrap().clone();
5079 let report = crate::audit::verify_chain_from_reader(bytes.as_slice()).unwrap();
5080 assert!(
5081 report.first_failure.is_none(),
5082 "capture_lag emissions must keep the hash chain intact: {:?}",
5083 report.first_failure
5084 );
5085
5086 crate::audit::shutdown_for_test();
5087 }
5088
5089 fn dispatch_line(conn: &rusqlite::Connection, line: &str) -> Option<RpcResponse> {
5100 if line.trim().is_empty() {
5101 return None;
5102 }
5103 let req: RpcRequest = match serde_json::from_str(line) {
5104 Ok(r) => r,
5105 Err(e) => {
5106 return Some(err_response(
5107 Value::Null,
5108 -32700,
5109 format!("parse error: {e}"),
5110 ));
5111 }
5112 };
5113 if req.id.is_none() || req.id == Some(Value::Null) {
5114 return None;
5115 }
5116 Some(invoke_handle_request(conn, &req))
5117 }
5118
5119 #[test]
5127 fn handle_store_happy_returns_id_and_tier() {
5128 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5129 let req = make_tools_call(
5130 "memory_store",
5131 json!({"title": "t", "content": "c", "namespace": "m9-store", "tier": Tier::Short.as_str()}),
5132 );
5133 let resp = invoke_handle_request(&conn, &req);
5134 assert!(resp.error.is_none());
5135 let text = resp.result.unwrap()["content"][0]["text"]
5136 .as_str()
5137 .unwrap()
5138 .to_string();
5139 let val: Value = serde_json::from_str(&text).unwrap();
5140 assert!(val["id"].is_string());
5141 assert_eq!(val["tier"], Tier::Short.as_str());
5142 }
5143
5144 #[test]
5145 fn handle_store_error_missing_title() {
5146 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5147 let req = make_tools_call("memory_store", json!({"content": "c"}));
5148 let resp = invoke_handle_request(&conn, &req);
5149 let result = resp.result.unwrap();
5151 assert_eq!(result["isError"], true);
5152 }
5153
5154 #[test]
5155 fn handle_recall_happy_returns_memories_array() {
5156 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5157 let req = make_tools_call(
5158 "memory_recall",
5159 json!({"context": "anything", "format": "json"}),
5160 );
5161 let resp = invoke_handle_request(&conn, &req);
5162 assert!(resp.error.is_none());
5163 let text = resp.result.unwrap()["content"][0]["text"]
5164 .as_str()
5165 .unwrap()
5166 .to_string();
5167 let val: Value = serde_json::from_str(&text).unwrap();
5168 assert!(val["memories"].is_array());
5169 assert!(val["count"].is_u64());
5170 }
5171
5172 #[test]
5173 fn handle_recall_budget_tokens_zero_returns_empty() {
5174 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5180 let req = make_tools_call(
5181 "memory_recall",
5182 json!({"context": "x", "budget_tokens": 0, "format": "json"}),
5183 );
5184 let resp = invoke_handle_request(&conn, &req);
5185 assert!(resp.error.is_none(), "budget_tokens=0 must not error");
5186 let text = resp.result.unwrap()["content"][0]["text"]
5187 .as_str()
5188 .unwrap()
5189 .to_string();
5190 let val: Value = serde_json::from_str(&text).unwrap();
5191 assert_eq!(val["count"], 0, "budget_tokens=0 returns zero memories");
5192 assert_eq!(val["budget_tokens"], 0);
5193 assert_eq!(val["tokens_used"], 0);
5194 assert_eq!(val["meta"]["budget_overflow"], false);
5195 assert_eq!(val["meta"]["budget_tokens_used"], 0);
5196 assert_eq!(val["meta"]["budget_tokens_remaining"], 0);
5197 }
5198
5199 #[test]
5200 fn handle_search_happy_returns_results_array() {
5201 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5202 let req = make_tools_call(
5203 "memory_search",
5204 json!({"query": "needle", "format": "json"}),
5205 );
5206 let resp = invoke_handle_request(&conn, &req);
5207 assert!(resp.error.is_none());
5208 let text = resp.result.unwrap()["content"][0]["text"]
5209 .as_str()
5210 .unwrap()
5211 .to_string();
5212 let val: Value = serde_json::from_str(&text).unwrap();
5213 assert!(val["results"].is_array());
5214 assert!(val["count"].is_u64());
5215 }
5216
5217 #[test]
5218 fn handle_search_error_missing_query() {
5219 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5220 let req = make_tools_call("memory_search", json!({}));
5221 let resp = invoke_handle_request(&conn, &req);
5222 let result = resp.result.unwrap();
5223 assert_eq!(result["isError"], true);
5224 }
5225
5226 #[test]
5227 fn handle_get_happy_returns_memory() {
5228 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5229 let mem = Memory {
5231 id: uuid::Uuid::new_v4().to_string(),
5232 tier: Tier::Mid,
5233 namespace: "m9-get".into(),
5234 title: "t".into(),
5235 content: "c".into(),
5236 tags: vec![],
5237 priority: 5,
5238 confidence: 1.0,
5239 source: "test".into(),
5240 access_count: 0,
5241 created_at: chrono::Utc::now().to_rfc3339(),
5242 updated_at: chrono::Utc::now().to_rfc3339(),
5243 last_accessed_at: None,
5244 expires_at: None,
5245 metadata: json!({}),
5246 reflection_depth: 0,
5247 memory_kind: crate::models::MemoryKind::Observation,
5248 entity_id: None,
5249 persona_version: None,
5250 citations: Vec::new(),
5251 source_uri: None,
5252 source_span: None,
5253 confidence_source: crate::models::ConfidenceSource::CallerProvided,
5254 confidence_signals: None,
5255 confidence_decayed_at: None,
5256 version: 1,
5257 };
5258 let id = db::insert(&conn, &mem).unwrap();
5259 let req = make_tools_call("memory_get", json!({"id": id}));
5260 let resp = invoke_handle_request(&conn, &req);
5261 assert!(resp.error.is_none());
5262 let text = resp.result.unwrap()["content"][0]["text"]
5263 .as_str()
5264 .unwrap()
5265 .to_string();
5266 let val: Value = serde_json::from_str(&text).unwrap();
5267 assert_eq!(val["title"], "t");
5268 assert_eq!(val["namespace"], "m9-get");
5269 assert!(val["links"].is_array());
5270 }
5271
5272 #[test]
5273 fn handle_get_error_unknown_id() {
5274 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5275 let req = make_tools_call(
5276 "memory_get",
5277 json!({"id": "00000000-0000-0000-0000-000000000000"}),
5278 );
5279 let resp = invoke_handle_request(&conn, &req);
5280 let result = resp.result.unwrap();
5281 assert_eq!(result["isError"], true);
5282 assert!(
5283 result["content"][0]["text"]
5284 .as_str()
5285 .unwrap()
5286 .contains("not found")
5287 );
5288 }
5289
5290 #[test]
5291 fn handle_list_happy_returns_memories_array() {
5292 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5293 let req = make_tools_call("memory_list", json!({"format": "json"}));
5294 let resp = invoke_handle_request(&conn, &req);
5295 assert!(resp.error.is_none());
5296 let text = resp.result.unwrap()["content"][0]["text"]
5297 .as_str()
5298 .unwrap()
5299 .to_string();
5300 let val: Value = serde_json::from_str(&text).unwrap();
5301 assert!(val["memories"].is_array());
5302 }
5303
5304 #[test]
5305 fn handle_list_error_invalid_agent_id() {
5306 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5308 let req = make_tools_call("memory_list", json!({"agent_id": "has space"}));
5309 let resp = invoke_handle_request(&conn, &req);
5310 let result = resp.result.unwrap();
5311 assert_eq!(result["isError"], true);
5312 }
5313
5314 #[test]
5315 fn handle_delete_happy_removes_existing_memory() {
5316 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5317 let mem = Memory {
5318 id: uuid::Uuid::new_v4().to_string(),
5319 tier: Tier::Mid,
5320 namespace: "m9-del".into(),
5321 title: "t".into(),
5322 content: "c".into(),
5323 tags: vec![],
5324 priority: 5,
5325 confidence: 1.0,
5326 source: "test".into(),
5327 access_count: 0,
5328 created_at: chrono::Utc::now().to_rfc3339(),
5329 updated_at: chrono::Utc::now().to_rfc3339(),
5330 last_accessed_at: None,
5331 expires_at: None,
5332 metadata: json!({}),
5333 reflection_depth: 0,
5334 memory_kind: crate::models::MemoryKind::Observation,
5335 entity_id: None,
5336 persona_version: None,
5337 citations: Vec::new(),
5338 source_uri: None,
5339 source_span: None,
5340 confidence_source: crate::models::ConfidenceSource::CallerProvided,
5341 confidence_signals: None,
5342 confidence_decayed_at: None,
5343 version: 1,
5344 };
5345 let id = db::insert(&conn, &mem).unwrap();
5346 let req = make_tools_call("memory_delete", json!({"id": id}));
5347 let resp = invoke_handle_request(&conn, &req);
5348 assert!(resp.error.is_none());
5349 let text = resp.result.unwrap()["content"][0]["text"]
5350 .as_str()
5351 .unwrap()
5352 .to_string();
5353 let val: Value = serde_json::from_str(&text).unwrap();
5354 assert_eq!(val["deleted"], true);
5355 }
5356
5357 #[test]
5358 fn handle_delete_error_empty_id() {
5359 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5360 let req = make_tools_call("memory_delete", json!({"id": ""}));
5361 let resp = invoke_handle_request(&conn, &req);
5362 let result = resp.result.unwrap();
5363 assert_eq!(result["isError"], true);
5364 }
5365
5366 #[test]
5367 fn handle_link_happy_returns_linked_true() {
5368 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5369 let mut ids = Vec::new();
5370 for tag in ["a", "b"] {
5371 let mem = Memory {
5372 id: uuid::Uuid::new_v4().to_string(),
5373 tier: Tier::Mid,
5374 namespace: "m9-link".into(),
5375 title: tag.into(),
5376 content: "c".into(),
5377 tags: vec![],
5378 priority: 5,
5379 confidence: 1.0,
5380 source: "test".into(),
5381 access_count: 0,
5382 created_at: chrono::Utc::now().to_rfc3339(),
5383 updated_at: chrono::Utc::now().to_rfc3339(),
5384 last_accessed_at: None,
5385 expires_at: None,
5386 metadata: json!({}),
5387 reflection_depth: 0,
5388 memory_kind: crate::models::MemoryKind::Observation,
5389 entity_id: None,
5390 persona_version: None,
5391 citations: Vec::new(),
5392 source_uri: None,
5393 source_span: None,
5394 confidence_source: crate::models::ConfidenceSource::CallerProvided,
5395 confidence_signals: None,
5396 confidence_decayed_at: None,
5397 version: 1,
5398 };
5399 ids.push(db::insert(&conn, &mem).unwrap());
5400 }
5401 let req = make_tools_call(
5402 "memory_link",
5403 json!({"source_id": ids[0], "target_id": ids[1], "relation": "related_to"}),
5404 );
5405 let resp = invoke_handle_request(&conn, &req);
5406 assert!(resp.error.is_none());
5407 let text = resp.result.unwrap()["content"][0]["text"]
5408 .as_str()
5409 .unwrap()
5410 .to_string();
5411 let val: Value = serde_json::from_str(&text).unwrap();
5412 assert_eq!(val["linked"], true);
5413 assert_eq!(val["attest_level"], "unsigned");
5417 }
5418
5419 #[test]
5424 fn handle_link_with_active_keypair_returns_self_signed() {
5425 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5426 let mut ids = Vec::new();
5427 for tag in ["a", "b"] {
5428 let mem = Memory {
5429 id: uuid::Uuid::new_v4().to_string(),
5430 tier: Tier::Mid,
5431 namespace: "h2-link".into(),
5432 title: tag.into(),
5433 content: "c".into(),
5434 tags: vec![],
5435 priority: 5,
5436 confidence: 1.0,
5437 source: "test".into(),
5438 access_count: 0,
5439 created_at: chrono::Utc::now().to_rfc3339(),
5440 updated_at: chrono::Utc::now().to_rfc3339(),
5441 last_accessed_at: None,
5442 expires_at: None,
5443 metadata: json!({}),
5444 reflection_depth: 0,
5445 memory_kind: crate::models::MemoryKind::Observation,
5446 entity_id: None,
5447 persona_version: None,
5448 citations: Vec::new(),
5449 source_uri: None,
5450 source_span: None,
5451 confidence_source: crate::models::ConfidenceSource::CallerProvided,
5452 confidence_signals: None,
5453 confidence_decayed_at: None,
5454 version: 1,
5455 };
5456 ids.push(db::insert(&conn, &mem).unwrap());
5457 }
5458 let req = make_tools_call(
5459 "memory_link",
5460 json!({"source_id": ids[0], "target_id": ids[1], "relation": "related_to"}),
5461 );
5462
5463 let kp = crate::identity::keypair::generate("alice").unwrap();
5465 let tier_config = FeatureTier::Keyword.config();
5466 let resolved_ttl = crate::config::ResolvedTtl::default();
5467 let resolved_scoring = crate::config::ResolvedScoring::default();
5468 let resp = handle_request(
5469 &conn,
5470 std::path::Path::new(":memory:"),
5471 &req,
5472 None,
5473 None,
5474 None,
5475 &tier_config,
5476 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
5477 None,
5478 &resolved_ttl,
5479 &resolved_scoring,
5480 true,
5481 false,
5482 None,
5483 &crate::profile::Profile::full(),
5484 None,
5485 Some(&kp),
5486 None,
5487 None, None, None, None, None, "test-session", );
5494 assert!(resp.error.is_none(), "MCP error: {:?}", resp.error);
5495 let text = resp.result.unwrap()["content"][0]["text"]
5496 .as_str()
5497 .unwrap()
5498 .to_string();
5499 let val: Value = serde_json::from_str(&text).unwrap();
5500 assert_eq!(val["linked"], true);
5501 assert_eq!(
5502 val["attest_level"], "self_signed",
5503 "active keypair path must surface self_signed"
5504 );
5505
5506 let sig: Option<Vec<u8>> = conn
5508 .query_row(
5509 "SELECT signature FROM memory_links \
5510 WHERE source_id = ?1 AND target_id = ?2",
5511 rusqlite::params![&ids[0], &ids[1]],
5512 |r| r.get(0),
5513 )
5514 .unwrap();
5515 let sig_bytes = sig.expect("signed link must persist a signature blob");
5516 assert_eq!(sig_bytes.len(), 64);
5517 }
5518
5519 #[test]
5535 fn handle_reflect_with_active_keypair_returns_signed_reflects_on_edges() {
5536 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5537 let mut source_ids = Vec::new();
5541 for tag in ["src-a", "src-b", "src-c"] {
5542 let mem = Memory {
5543 id: uuid::Uuid::new_v4().to_string(),
5544 tier: Tier::Mid,
5545 namespace: "issue-815-reflect".into(),
5546 title: tag.into(),
5547 content: format!("body for {tag}"),
5548 tags: vec![],
5549 priority: 5,
5550 confidence: 1.0,
5551 source: "test".into(),
5552 access_count: 0,
5553 created_at: chrono::Utc::now().to_rfc3339(),
5554 updated_at: chrono::Utc::now().to_rfc3339(),
5555 last_accessed_at: None,
5556 expires_at: None,
5557 metadata: json!({}),
5558 reflection_depth: 0,
5559 memory_kind: crate::models::MemoryKind::Observation,
5560 entity_id: None,
5561 persona_version: None,
5562 citations: Vec::new(),
5563 source_uri: None,
5564 source_span: None,
5565 confidence_source: crate::models::ConfidenceSource::CallerProvided,
5566 confidence_signals: None,
5567 confidence_decayed_at: None,
5568 version: 1,
5569 };
5570 source_ids.push(db::insert(&conn, &mem).unwrap());
5571 }
5572 let req = make_tools_call(
5573 "memory_reflect",
5574 json!({
5575 "source_ids": source_ids,
5576 "title": "issue-815 reflect signing pin",
5577 "content": "reflects_on edges must come back self_signed",
5578 "namespace": "issue-815-reflect",
5579 }),
5580 );
5581
5582 let kp = crate::identity::keypair::generate("alice").unwrap();
5583 let tier_config = FeatureTier::Keyword.config();
5584 let resolved_ttl = crate::config::ResolvedTtl::default();
5585 let resolved_scoring = crate::config::ResolvedScoring::default();
5586 let resp = handle_request(
5587 &conn,
5588 std::path::Path::new(":memory:"),
5589 &req,
5590 None,
5591 None,
5592 None,
5593 &tier_config,
5594 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
5595 None,
5596 &resolved_ttl,
5597 &resolved_scoring,
5598 true,
5599 false,
5600 None,
5601 &crate::profile::Profile::full(),
5602 None,
5603 Some(&kp),
5604 None,
5605 None, None, None, None, None, "test-session", );
5612 assert!(resp.error.is_none(), "MCP error: {:?}", resp.error);
5613 let text = resp.result.unwrap()["content"][0]["text"]
5614 .as_str()
5615 .unwrap()
5616 .to_string();
5617 let val: Value = serde_json::from_str(&text).unwrap();
5618 let reflection_id = val["id"]
5619 .as_str()
5620 .expect("reflect response must carry the new memory id")
5621 .to_string();
5622
5623 let mut stmt = conn
5629 .prepare(
5630 "SELECT target_id, attest_level, signature \
5631 FROM memory_links \
5632 WHERE source_id = ?1 AND relation = 'reflects_on' \
5633 ORDER BY created_at",
5634 )
5635 .unwrap();
5636 let rows: Vec<(String, String, Option<Vec<u8>>)> = stmt
5637 .query_map(rusqlite::params![&reflection_id], |r| {
5638 Ok((
5639 r.get::<_, String>(0)?,
5640 r.get::<_, String>(1)?,
5641 r.get::<_, Option<Vec<u8>>>(2)?,
5642 ))
5643 })
5644 .unwrap()
5645 .map(std::result::Result::unwrap)
5646 .collect();
5647 assert_eq!(
5648 rows.len(),
5649 source_ids.len(),
5650 "expected one reflects_on edge per source; got {rows:?}"
5651 );
5652 for (target_id, attest_level, signature) in &rows {
5653 assert_eq!(
5654 attest_level, "self_signed",
5655 "reflects_on edge to {target_id} must surface self_signed (got {attest_level})"
5656 );
5657 let sig_bytes = signature.as_ref().unwrap_or_else(|| {
5658 panic!("reflects_on edge to {target_id} must persist a signature blob")
5659 });
5660 assert_eq!(
5661 sig_bytes.len(),
5662 64,
5663 "reflects_on edge to {target_id} signature must be 64 bytes (got {})",
5664 sig_bytes.len()
5665 );
5666 }
5667 }
5668
5669 #[test]
5704 fn issue_1315_memory_reflect_wire_layer_preserves_caller_metadata() {
5705 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5706 let mut source_ids = Vec::new();
5710 for tag in ["src-a", "src-b", "src-c"] {
5711 let mem = Memory {
5712 id: uuid::Uuid::new_v4().to_string(),
5713 tier: Tier::Mid,
5714 namespace: "issue-1315-wire".into(),
5715 title: tag.into(),
5716 content: format!("body for {tag}"),
5717 tags: vec![],
5718 priority: 5,
5719 confidence: 1.0,
5720 source: "api".into(),
5721 access_count: 0,
5722 created_at: chrono::Utc::now().to_rfc3339(),
5723 updated_at: chrono::Utc::now().to_rfc3339(),
5724 last_accessed_at: None,
5725 expires_at: None,
5726 metadata: json!({}),
5727 reflection_depth: 0,
5728 memory_kind: crate::models::MemoryKind::Observation,
5729 entity_id: None,
5730 persona_version: None,
5731 citations: Vec::new(),
5732 source_uri: None,
5733 source_span: None,
5734 confidence_source: crate::models::ConfidenceSource::CallerProvided,
5735 confidence_signals: None,
5736 confidence_decayed_at: None,
5737 version: 1,
5738 };
5739 source_ids.push(db::insert(&conn, &mem).unwrap());
5740 }
5741
5742 let req = make_tools_call(
5748 "memory_reflect",
5749 json!({
5750 "source_ids": source_ids,
5751 "title": "issue-1315 wire-layer metadata pin",
5752 "content": "caller metadata.entity_id + probe must round-trip through tools/call",
5753 "namespace": "issue-1315-wire",
5754 "metadata": {
5755 "entity_id": "entity-1315-wire",
5756 "probe": "P2",
5757 },
5758 }),
5759 );
5760
5761 let resp = invoke_handle_request(&conn, &req);
5762 assert!(
5763 resp.error.is_none(),
5764 "expected ok rpc envelope; got error: {:?}",
5765 resp.error
5766 );
5767 let result = resp.result.expect("result envelope");
5770 assert!(
5771 result.get("isError").is_none_or(|v| v != true),
5772 "tools/call must not surface isError=true; result: {result}"
5773 );
5774 let text = result["content"][0]["text"]
5775 .as_str()
5776 .expect("content[0].text on memory_reflect ok envelope");
5777 let parsed: Value = serde_json::from_str(text).expect("reflect result text is JSON");
5778 let reflection_id = parsed["id"]
5779 .as_str()
5780 .expect("reflect result carries the new memory id")
5781 .to_string();
5782
5783 let (meta_str, mention): (String, Option<String>) = conn
5788 .query_row(
5789 "SELECT metadata, mentioned_entity_id FROM memories WHERE id = ?1",
5790 rusqlite::params![&reflection_id],
5791 |r| Ok((r.get(0)?, r.get(1)?)),
5792 )
5793 .expect("select reflection row");
5794 let meta: Value = serde_json::from_str(&meta_str).expect("metadata is JSON");
5795
5796 assert_eq!(
5798 meta.get("entity_id").and_then(Value::as_str),
5799 Some("entity-1315-wire"),
5800 "wire path must preserve metadata.entity_id; full metadata = {meta}"
5801 );
5802 assert_eq!(
5806 meta.get("probe").and_then(Value::as_str),
5807 Some("P2"),
5808 "wire path must preserve every caller-supplied metadata key; full metadata = {meta}"
5809 );
5810 assert_eq!(
5815 mention.as_deref(),
5816 Some("entity-1315-wire"),
5817 "mentioned_entity_id column must be populated from the wire-supplied metadata.entity_id"
5818 );
5819 assert!(
5823 meta.get("agent_id").is_some(),
5824 "system-generated agent_id must still be present"
5825 );
5826 assert!(
5827 meta.get("reflection_metadata").is_some(),
5828 "system-generated reflection_metadata block must still be present"
5829 );
5830 }
5831
5832 #[test]
5833 fn handle_link_error_missing_target_id() {
5834 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5835 let req = make_tools_call("memory_link", json!({"source_id": "x"}));
5836 let resp = invoke_handle_request(&conn, &req);
5837 let result = resp.result.unwrap();
5838 assert_eq!(result["isError"], true);
5839 }
5840
5841 #[test]
5842 fn handle_promote_error_unknown_id() {
5843 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5844 let req = make_tools_call(
5845 "memory_promote",
5846 json!({"id": "00000000-0000-0000-0000-000000000000"}),
5847 );
5848 let resp = invoke_handle_request(&conn, &req);
5849 let result = resp.result.unwrap();
5850 assert_eq!(result["isError"], true);
5851 }
5852
5853 #[test]
5854 fn handle_consolidate_error_missing_summary_keyword_tier() {
5855 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5857 let req = make_tools_call(
5858 "memory_consolidate",
5859 json!({"ids": ["a", "b"], "title": "t"}),
5860 );
5861 let resp = invoke_handle_request(&conn, &req);
5862 let result = resp.result.unwrap();
5863 assert_eq!(result["isError"], true);
5864 let msg = result["content"][0]["text"].as_str().unwrap();
5865 assert!(msg.contains("summary"));
5866 }
5867
5868 #[test]
5869 fn handle_capabilities_happy_returns_tier_struct() {
5870 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5871 let req = make_tools_call("memory_capabilities", json!({}));
5872 let resp = invoke_handle_request(&conn, &req);
5873 assert!(resp.error.is_none());
5874 let text = resp.result.unwrap()["content"][0]["text"]
5875 .as_str()
5876 .unwrap()
5877 .to_string();
5878 let val: Value = serde_json::from_str(&text).unwrap();
5879 assert!(val["tier"].is_string());
5880 assert!(val["features"].is_object());
5881 }
5882
5883 #[test]
5892 fn mcp_capabilities_v2_schema_includes_all_blocks() {
5893 let _gate = crate::config::lock_permissions_mode_for_test();
5897 crate::config::clear_permissions_mode_override_for_test();
5898 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5899 let req = make_tools_call("memory_capabilities", json!({"accept": "v2"}));
5900 let resp = invoke_handle_request(&conn, &req);
5901 let text = resp.result.unwrap()["content"][0]["text"]
5902 .as_str()
5903 .unwrap()
5904 .to_string();
5905 let val: Value = serde_json::from_str(&text).unwrap();
5906
5907 assert_eq!(val["schema_version"], "2", "schema_version bumped to 2");
5908
5909 assert!(val["permissions"].is_object(), "permissions block present");
5912 assert_eq!(val["permissions"]["mode"], "advisory");
5913 assert!(val["permissions"]["active_rules"].is_number());
5914 assert!(
5915 val["permissions"].get("rule_summary").is_none(),
5916 "v2 drops rule_summary (no per-rule serializer)"
5917 );
5918 assert_eq!(val["permissions"]["inheritance"], "enforced");
5922
5923 assert!(val["hooks"].is_object(), "hooks block present");
5925 assert!(val["hooks"]["registered_count"].is_number());
5926 assert!(
5927 val["hooks"].get("by_event").is_none(),
5928 "v2 drops hooks.by_event (no event registry)"
5929 );
5930
5931 assert!(val["compaction"].is_object(), "compaction block present");
5933 assert_eq!(val["compaction"]["planned"], true);
5934 assert_eq!(val["compaction"]["enabled"], false);
5935 assert_eq!(val["compaction"]["version"], "v0.8+");
5936 assert!(val["compaction"].get("interval_minutes").is_none());
5937 assert!(val["compaction"].get("last_run_at").is_none());
5938 assert!(val["compaction"].get("last_run_stats").is_none());
5939
5940 assert!(val["approval"].is_object(), "approval block present");
5943 assert!(val["approval"]["pending_requests"].is_number());
5944 assert!(
5945 val["approval"].get("subscribers").is_none(),
5946 "v2 drops approval.subscribers (no subscription API)"
5947 );
5948 assert!(
5949 val["approval"].get("default_timeout_seconds").is_none(),
5950 "v2 drops approval.default_timeout_seconds (no sweeper)"
5951 );
5952
5953 assert!(val["transcripts"].is_object(), "transcripts block present");
5961 assert_eq!(val["transcripts"]["planned"], false);
5962 assert_eq!(val["transcripts"]["enabled"], false);
5963 assert_eq!(val["transcripts"]["version"], env!("CARGO_PKG_VERSION"));
5964
5965 assert_eq!(val["features"]["memory_reflection"]["planned"], false);
5969 assert_eq!(val["features"]["memory_reflection"]["enabled"], true);
5970
5971 assert_eq!(val["features"]["recall_mode_active"], "disabled");
5974 assert_eq!(val["features"]["reranker_active"], "off");
5975 }
5976
5977 #[test]
5982 fn mcp_capabilities_v2_backwards_compatible() {
5983 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
5984 let req = make_tools_call("memory_capabilities", json!({}));
5985 let resp = invoke_handle_request(&conn, &req);
5986 let text = resp.result.unwrap()["content"][0]["text"]
5987 .as_str()
5988 .unwrap()
5989 .to_string();
5990 let val: Value = serde_json::from_str(&text).unwrap();
5991
5992 assert!(val["tier"].is_string(), "v1: tier preserved");
5994 assert!(val["version"].is_string(), "v1: version preserved");
5995 assert!(val["features"].is_object(), "v1: features preserved");
5996 assert!(val["models"].is_object(), "v1: models preserved");
5997
5998 assert!(val["features"]["keyword_search"].is_boolean());
6000 assert!(val["features"]["semantic_search"].is_boolean());
6001 assert!(val["features"]["embedder_loaded"].is_boolean());
6002 assert!(val["models"]["embedding"].is_string());
6003 assert!(val["models"]["llm"].is_string());
6004 assert!(val["models"]["cross_encoder"].is_string());
6005 }
6006
6007 #[test]
6011 fn mcp_capabilities_accept_v1_returns_legacy_shape() {
6012 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6013 let req = make_tools_call("memory_capabilities", json!({"accept": "v1"}));
6014 let resp = invoke_handle_request(&conn, &req);
6015 let text = resp.result.unwrap()["content"][0]["text"]
6016 .as_str()
6017 .unwrap()
6018 .to_string();
6019 let val: Value = serde_json::from_str(&text).unwrap();
6020
6021 assert_eq!(
6028 val.get("schema_version").and_then(Value::as_str),
6029 Some("1"),
6030 "Round-2 F13 — v1 must carry schema_version on the wire"
6031 );
6032 assert!(val.get("permissions").is_none());
6034 assert!(val.get("hooks").is_none());
6035 assert!(val.get("compaction").is_none());
6036 assert!(val.get("approval").is_none());
6037 assert!(val.get("transcripts").is_none());
6038 assert!(val["features"]["memory_reflection"].is_boolean());
6040 assert!(val["features"].get("recall_mode_active").is_none());
6042 assert!(val["features"].get("reranker_active").is_none());
6043 }
6044
6045 #[test]
6049 fn mcp_capabilities_pending_requests_reflects_db() {
6050 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6051 let payload = serde_json::json!({"foo": "bar"}).to_string();
6054 conn.execute(
6055 "INSERT INTO pending_actions (id, action_type, memory_id, namespace,
6056 payload, requested_by, requested_at, status)
6057 VALUES ('p-1', 'store', NULL, 'global', ?1, 'agent-1',
6058 '2026-04-27T00:00:00Z', 'pending')",
6059 rusqlite::params![payload],
6060 )
6061 .unwrap();
6062
6063 let req = make_tools_call("memory_capabilities", json!({}));
6064 let resp = invoke_handle_request(&conn, &req);
6065 let text = resp.result.unwrap()["content"][0]["text"]
6066 .as_str()
6067 .unwrap()
6068 .to_string();
6069 let val: Value = serde_json::from_str(&text).unwrap();
6070
6071 assert_eq!(
6072 val["approval"]["pending_requests"], 1,
6073 "pending_actions(status=pending) count surfaces live"
6074 );
6075 }
6076
6077 #[test]
6078 fn handle_subscribe_error_unregistered_agent() {
6079 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6083 let req = make_tools_call(
6084 "memory_subscribe",
6085 json!({"url": "https://example.com/hook", "secret": "mcp-sub-test-secret"}),
6086 );
6087 let resp = invoke_handle_request(&conn, &req);
6088 let result = resp.result.unwrap();
6089 assert_eq!(result["isError"], true);
6090 let msg = result["content"][0]["text"].as_str().unwrap();
6091 assert!(msg.contains("not registered"));
6092 }
6093
6094 #[test]
6099 fn test_jsonrpc_handles_well_formed_request() {
6100 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6101 let line = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
6102 let resp = dispatch_line(&conn, line).expect("expected response");
6103 assert!(resp.error.is_none());
6104 let result = resp.result.unwrap();
6105 assert!(result["tools"].is_array());
6106 }
6107
6108 #[test]
6109 fn test_jsonrpc_handles_malformed_json() {
6110 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6111 let resp = dispatch_line(&conn, "this is not json at all").expect("expected response");
6113 let err = resp.error.unwrap();
6114 assert_eq!(err.code, -32700);
6115 assert!(err.message.contains("parse error"));
6116 assert_eq!(resp.id, Value::Null);
6118 }
6119
6120 #[test]
6121 fn test_jsonrpc_handles_truncated_request() {
6122 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6123 let resp = dispatch_line(&conn, r#"{"jsonrpc":"2.0","id":1,"method":"#)
6125 .expect("expected response");
6126 let err = resp.error.unwrap();
6127 assert_eq!(err.code, -32700);
6128 }
6129
6130 #[test]
6131 fn test_jsonrpc_handles_two_requests_per_line() {
6132 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6137 let line = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"} {"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
6138 let resp = dispatch_line(&conn, line).expect("expected response");
6139 let err = resp.error.unwrap();
6140 assert_eq!(err.code, -32700);
6141 }
6142
6143 #[test]
6144 fn test_jsonrpc_handles_blank_line() {
6145 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6147 assert!(dispatch_line(&conn, "").is_none());
6148 assert!(dispatch_line(&conn, " \t ").is_none());
6149 }
6150
6151 #[test]
6152 fn test_jsonrpc_handles_notification_no_response() {
6153 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6156 let line = r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
6157 assert!(dispatch_line(&conn, line).is_none());
6158 let line_null = r#"{"jsonrpc":"2.0","id":null,"method":"notifications/initialized"}"#;
6160 assert!(dispatch_line(&conn, line_null).is_none());
6161 }
6162
6163 #[test]
6164 fn test_jsonrpc_handles_method_not_found() {
6165 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6167 let req = RpcRequest {
6168 jsonrpc: "2.0".into(),
6169 id: Some(json!(7)),
6170 method: "no/such/method".into(),
6171 params: json!({}),
6172 };
6173 let resp = invoke_handle_request(&conn, &req);
6174 let err = resp.error.unwrap();
6175 assert_eq!(err.code, -32601);
6176 assert!(err.message.contains("method not found"));
6177 }
6178
6179 #[test]
6180 fn test_jsonrpc_handles_invalid_params() {
6181 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6183 let req = RpcRequest {
6184 jsonrpc: "2.0".into(),
6185 id: Some(json!(8)),
6186 method: "tools/call".into(),
6187 params: json!({"arguments": {}}),
6188 };
6189 let resp = invoke_handle_request(&conn, &req);
6190 let err = resp.error.unwrap();
6191 assert_eq!(err.code, -32602);
6192 }
6193
6194 #[test]
6195 fn test_jsonrpc_handles_unknown_tool_returns_minus_32601() {
6196 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6198 let req = make_tools_call("memory_does_not_exist", json!({}));
6199 let resp = invoke_handle_request(&conn, &req);
6200 let err = resp.error.unwrap();
6201 assert_eq!(err.code, -32601);
6202 assert!(err.message.contains("memory_does_not_exist"));
6203 }
6204
6205 #[test]
6217 fn issue_1254_tool_name_leak_gated_behind_profile_hint_in_errors() {
6218 use crate::config::McpConfig;
6219 let tier_config = FeatureTier::Keyword.config();
6220 let resolved_ttl = crate::config::ResolvedTtl::default();
6221 let resolved_scoring = crate::config::ResolvedScoring::default();
6222 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6223
6224 let core_profile = crate::profile::Profile::core();
6233 assert!(
6234 !core_profile.loads("memory_atomise"),
6235 "test sentinel: memory_atomise must not be loaded under --profile core; \
6236 pick a different higher-profile tool if this changes"
6237 );
6238 let req = make_tools_call("memory_atomise", json!({}));
6239
6240 let resp_default = handle_request(
6244 &conn,
6245 std::path::Path::new(":memory:"),
6246 &req,
6247 None,
6248 None,
6249 None,
6250 &tier_config,
6251 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
6252 None,
6253 &resolved_ttl,
6254 &resolved_scoring,
6255 true,
6256 false,
6257 None,
6258 &core_profile,
6259 None,
6260 None,
6261 None,
6262 None,
6263 None,
6264 None,
6265 None,
6266 None, "test-session", );
6269 let err_default = resp_default
6270 .error
6271 .expect("tools/call against a non-loaded tool must error");
6272 assert_eq!(
6273 err_default.code, -32601,
6274 "method-not-found code unchanged across the hint posture"
6275 );
6276 assert!(
6278 err_default.message.starts_with("unknown tool: "),
6279 "#1254: default posture must return a uniform 'unknown tool: <name>' \
6280 error regardless of family membership; got: {}",
6281 err_default.message
6282 );
6283 assert!(
6284 err_default.message.contains("memory_atomise"),
6285 "the refused tool name is fine — the leak was the FAMILY name, not the \
6286 tool name (the client supplied that); got: {}",
6287 err_default.message
6288 );
6289 assert!(
6291 !err_default.message.contains("family"),
6292 "#1254: default posture must NOT leak family membership; got: {}",
6293 err_default.message
6294 );
6295 assert!(
6296 !err_default.message.contains("--profile"),
6297 "#1254: default posture must NOT advise which profile would load the tool; got: {}",
6298 err_default.message
6299 );
6300
6301 let cfg_with_hint = McpConfig {
6303 profile_hint_in_errors: true,
6304 ..McpConfig::default()
6305 };
6306 let resp_hint = handle_request(
6307 &conn,
6308 std::path::Path::new(":memory:"),
6309 &req,
6310 None,
6311 None,
6312 None,
6313 &tier_config,
6314 &crate::config::ResolvedModels::from_tier_preset(&tier_config),
6315 None,
6316 &resolved_ttl,
6317 &resolved_scoring,
6318 true,
6319 false,
6320 None,
6321 &core_profile,
6322 Some(&cfg_with_hint),
6323 None,
6324 None,
6325 None,
6326 None,
6327 None,
6328 None,
6329 None, "test-session", );
6332 let err_hint = resp_hint
6333 .error
6334 .expect("hint-enabled tools/call must still error on non-loaded tool");
6335 assert_eq!(err_hint.code, -32601);
6336 assert!(
6339 err_hint.message.contains("family"),
6340 "#1254: profile_hint_in_errors=true must surface the family hint; \
6341 got: {}",
6342 err_hint.message
6343 );
6344 assert!(
6345 err_hint.message.contains("--profile"),
6346 "#1254: profile_hint_in_errors=true must advise the operator on \
6347 how to load the tool; got: {}",
6348 err_hint.message
6349 );
6350 }
6351
6352 #[test]
6353 fn test_jsonrpc_rejects_wrong_version() {
6354 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6356 let req = RpcRequest {
6357 jsonrpc: "1.0".into(),
6358 id: Some(json!(1)),
6359 method: "tools/list".into(),
6360 params: json!({}),
6361 };
6362 let resp = invoke_handle_request(&conn, &req);
6363 let err = resp.error.unwrap();
6364 assert_eq!(err.code, -32600);
6365 }
6366
6367 #[test]
6368 fn test_jsonrpc_handles_initialize() {
6369 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6371 let req = RpcRequest {
6372 jsonrpc: "2.0".into(),
6373 id: Some(json!(1)),
6374 method: "initialize".into(),
6375 params: json!({"clientInfo": {"name": "test-client"}}),
6376 };
6377 let resp = invoke_handle_request(&conn, &req);
6378 assert!(resp.error.is_none());
6379 let result = resp.result.unwrap();
6380 assert_eq!(result["protocolVersion"], "2024-11-05");
6381 assert_eq!(result["serverInfo"]["name"], "ai-memory");
6382 }
6383
6384 #[test]
6398 fn test_auto_register_creates_top_level_namespace() {
6399 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6402 super::auto_register_path_hierarchy(&conn, "m9-top");
6403 let count: i64 = conn
6404 .query_row("SELECT COUNT(*) FROM namespace_meta", [], |r| r.get(0))
6405 .unwrap();
6406 assert_eq!(count, 0);
6407 }
6408
6409 #[test]
6410 fn test_auto_register_creates_nested_hierarchy() {
6411 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6419 let mem = Memory {
6423 id: uuid::Uuid::new_v4().to_string(),
6424 tier: Tier::Long,
6425 namespace: "m9-parent".into(),
6426 title: "parent standard".into(),
6427 content: "...".into(),
6428 tags: vec![],
6429 priority: 5,
6430 confidence: 1.0,
6431 source: "test".into(),
6432 access_count: 0,
6433 created_at: chrono::Utc::now().to_rfc3339(),
6434 updated_at: chrono::Utc::now().to_rfc3339(),
6435 last_accessed_at: None,
6436 expires_at: None,
6437 metadata: json!({}),
6438 reflection_depth: 0,
6439 memory_kind: crate::models::MemoryKind::Observation,
6440 entity_id: None,
6441 persona_version: None,
6442 citations: Vec::new(),
6443 source_uri: None,
6444 source_span: None,
6445 confidence_source: crate::models::ConfidenceSource::CallerProvided,
6446 confidence_signals: None,
6447 confidence_decayed_at: None,
6448 version: 1,
6449 };
6450 let std_id = db::insert(&conn, &mem).unwrap();
6451 db::set_namespace_standard(&conn, "m9-parent", &std_id, None).unwrap();
6452 let child_mem = Memory {
6454 id: uuid::Uuid::new_v4().to_string(),
6455 tier: Tier::Long,
6456 namespace: "repo/team/sub".into(),
6457 title: "child".into(),
6458 content: "...".into(),
6459 tags: vec![],
6460 priority: 5,
6461 confidence: 1.0,
6462 source: "test".into(),
6463 access_count: 0,
6464 created_at: chrono::Utc::now().to_rfc3339(),
6465 updated_at: chrono::Utc::now().to_rfc3339(),
6466 last_accessed_at: None,
6467 expires_at: None,
6468 metadata: json!({}),
6469 reflection_depth: 0,
6470 memory_kind: crate::models::MemoryKind::Observation,
6471 entity_id: None,
6472 persona_version: None,
6473 citations: Vec::new(),
6474 source_uri: None,
6475 source_span: None,
6476 confidence_source: crate::models::ConfidenceSource::CallerProvided,
6477 confidence_signals: None,
6478 confidence_decayed_at: None,
6479 version: 1,
6480 };
6481 let child_id = db::insert(&conn, &child_mem).unwrap();
6482 db::set_namespace_standard(&conn, "repo/team/sub", &child_id, None).unwrap();
6483 super::auto_register_path_hierarchy(&conn, "repo/team/sub");
6485 let id = db::get_namespace_standard(&conn, "repo/team/sub")
6487 .unwrap()
6488 .unwrap();
6489 assert_eq!(id, child_id);
6490 }
6491
6492 #[test]
6493 fn test_auto_register_idempotent() {
6494 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6497 super::auto_register_path_hierarchy(&conn, "m9-idem");
6498 super::auto_register_path_hierarchy(&conn, "m9-idem");
6499 let count: i64 = conn
6500 .query_row("SELECT COUNT(*) FROM namespace_meta", [], |r| r.get(0))
6501 .unwrap();
6502 assert_eq!(count, 0);
6503 }
6504
6505 #[test]
6506 fn test_auto_register_handles_empty_string_or_root() {
6507 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6510 super::auto_register_path_hierarchy(&conn, "");
6511 super::auto_register_path_hierarchy(&conn, "/");
6512 super::auto_register_path_hierarchy(&conn, "*");
6513 let count: i64 = conn
6515 .query_row("SELECT COUNT(*) FROM namespace_meta", [], |r| r.get(0))
6516 .unwrap();
6517 assert_eq!(count, 0);
6518 }
6519
6520 #[test]
6521 fn test_auto_register_skips_when_explicit_parent_set() {
6522 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6526 let parent_mem = Memory {
6528 id: uuid::Uuid::new_v4().to_string(),
6529 tier: Tier::Long,
6530 namespace: "m9-explicit-parent".into(),
6531 title: "p".into(),
6532 content: "c".into(),
6533 tags: vec![],
6534 priority: 5,
6535 confidence: 1.0,
6536 source: "test".into(),
6537 access_count: 0,
6538 created_at: chrono::Utc::now().to_rfc3339(),
6539 updated_at: chrono::Utc::now().to_rfc3339(),
6540 last_accessed_at: None,
6541 expires_at: None,
6542 metadata: json!({}),
6543 reflection_depth: 0,
6544 memory_kind: crate::models::MemoryKind::Observation,
6545 entity_id: None,
6546 persona_version: None,
6547 citations: Vec::new(),
6548 source_uri: None,
6549 source_span: None,
6550 confidence_source: crate::models::ConfidenceSource::CallerProvided,
6551 confidence_signals: None,
6552 confidence_decayed_at: None,
6553 version: 1,
6554 };
6555 let parent_id = db::insert(&conn, &parent_mem).unwrap();
6556 db::set_namespace_standard(&conn, "m9-explicit-parent", &parent_id, None).unwrap();
6557
6558 let child_mem = Memory {
6559 id: uuid::Uuid::new_v4().to_string(),
6560 tier: Tier::Long,
6561 namespace: "m9-explicit-child".into(),
6562 title: "c".into(),
6563 content: "c".into(),
6564 tags: vec![],
6565 priority: 5,
6566 confidence: 1.0,
6567 source: "test".into(),
6568 access_count: 0,
6569 created_at: chrono::Utc::now().to_rfc3339(),
6570 updated_at: chrono::Utc::now().to_rfc3339(),
6571 last_accessed_at: None,
6572 expires_at: None,
6573 metadata: json!({}),
6574 reflection_depth: 0,
6575 memory_kind: crate::models::MemoryKind::Observation,
6576 entity_id: None,
6577 persona_version: None,
6578 citations: Vec::new(),
6579 source_uri: None,
6580 source_span: None,
6581 confidence_source: crate::models::ConfidenceSource::CallerProvided,
6582 confidence_signals: None,
6583 confidence_decayed_at: None,
6584 version: 1,
6585 };
6586 let child_id = db::insert(&conn, &child_mem).unwrap();
6587 db::set_namespace_standard(
6588 &conn,
6589 "m9-explicit-child",
6590 &child_id,
6591 Some("m9-explicit-parent"),
6592 )
6593 .unwrap();
6594
6595 assert_eq!(
6597 db::get_namespace_parent(&conn, "m9-explicit-child"),
6598 Some("m9-explicit-parent".to_string())
6599 );
6600 super::auto_register_path_hierarchy(&conn, "m9-explicit-child");
6601 assert_eq!(
6603 db::get_namespace_parent(&conn, "m9-explicit-child"),
6604 Some("m9-explicit-parent".to_string())
6605 );
6606 }
6607
6608 fn make_recall_response(memories: Vec<Value>) -> Value {
6613 let count = memories.len();
6614 json!({
6615 "memories": memories,
6616 "count": count,
6617 "mode": "keyword",
6618 })
6619 }
6620
6621 fn seed_namespace_standard(
6622 conn: &rusqlite::Connection,
6623 namespace: &str,
6624 title: &str,
6625 ) -> String {
6626 let mem = Memory {
6627 id: uuid::Uuid::new_v4().to_string(),
6628 tier: Tier::Long,
6629 namespace: namespace.into(),
6630 title: title.into(),
6631 content: "policy text".into(),
6632 tags: vec!["_standard".into()],
6633 priority: 5,
6634 confidence: 1.0,
6635 source: "test".into(),
6636 access_count: 0,
6637 created_at: chrono::Utc::now().to_rfc3339(),
6638 updated_at: chrono::Utc::now().to_rfc3339(),
6639 last_accessed_at: None,
6640 expires_at: None,
6641 metadata: json!({}),
6642 reflection_depth: 0,
6643 memory_kind: crate::models::MemoryKind::Observation,
6644 entity_id: None,
6645 persona_version: None,
6646 citations: Vec::new(),
6647 source_uri: None,
6648 source_span: None,
6649 confidence_source: crate::models::ConfidenceSource::CallerProvided,
6650 confidence_signals: None,
6651 confidence_decayed_at: None,
6652 version: 1,
6653 };
6654 let id = db::insert(conn, &mem).unwrap();
6655 db::set_namespace_standard(conn, namespace, &id, None).unwrap();
6656 id
6657 }
6658
6659 #[test]
6660 fn test_inject_namespace_standard_attaches_when_present() {
6661 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6662 let std_id = seed_namespace_standard(&conn, "m9-inject-attach", "S");
6663 let mut resp = make_recall_response(vec![]);
6664 super::inject_namespace_standard(&conn, Some("m9-inject-attach"), &mut resp);
6665 assert!(resp["standard"].is_object(), "expected attached standard");
6666 assert_eq!(resp["standard"]["id"].as_str().unwrap(), std_id);
6667 }
6668
6669 #[test]
6670 fn test_inject_namespace_standard_skips_when_absent() {
6671 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6674 let mut resp = make_recall_response(vec![]);
6675 let before = resp.clone();
6676 super::inject_namespace_standard(&conn, Some("m9-inject-empty"), &mut resp);
6677 assert_eq!(resp, before);
6678 assert!(resp.get("standard").is_none());
6679 assert!(resp.get("standards").is_none());
6680 }
6681
6682 #[test]
6683 fn test_inject_namespace_standard_top_of_recall_response() {
6684 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6688 let std_id = seed_namespace_standard(&conn, "m9-inject-dedup", "S");
6689 let dup = json!({"id": std_id, "title": "S", "content": "policy text"});
6691 let other = json!({"id": "other-id", "title": "noise", "content": "x"});
6692 let mut resp = make_recall_response(vec![dup.clone(), other.clone()]);
6693 super::inject_namespace_standard(&conn, Some("m9-inject-dedup"), &mut resp);
6694 assert_eq!(resp["standard"]["id"].as_str().unwrap(), std_id);
6695 let memories = resp["memories"].as_array().unwrap();
6696 assert_eq!(memories.len(), 1);
6697 assert_eq!(memories[0]["id"], "other-id");
6698 assert_eq!(resp["count"], 1);
6699 }
6700
6701 #[test]
6702 fn test_inject_namespace_standard_preserves_other_response_fields() {
6703 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6705 seed_namespace_standard(&conn, "m9-inject-preserve", "S");
6706 let mut resp = json!({
6707 "memories": [],
6708 "count": 0,
6709 "mode": "hybrid",
6710 "diagnostics": {"latency_ms": 42},
6711 });
6712 super::inject_namespace_standard(&conn, Some("m9-inject-preserve"), &mut resp);
6713 assert_eq!(resp["mode"], "hybrid");
6714 assert_eq!(resp["diagnostics"]["latency_ms"], 42);
6715 assert!(resp["standard"].is_object());
6716 }
6717
6718 #[test]
6719 fn test_inject_namespace_standard_no_namespace_uses_global() {
6720 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6723 seed_namespace_standard(&conn, "*", "global standard");
6724 let mut resp = make_recall_response(vec![]);
6725 super::inject_namespace_standard(&conn, None, &mut resp);
6726 assert_eq!(resp["standard"]["title"], "global standard");
6727 }
6728
6729 #[test]
6730 fn test_inject_namespace_standard_multiple_levels_emits_array() {
6731 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6735 seed_namespace_standard(&conn, "*", "GLOBAL");
6736 seed_namespace_standard(&conn, "m9-multi", "LOCAL");
6737 let mut resp = make_recall_response(vec![]);
6738 super::inject_namespace_standard(&conn, Some("m9-multi"), &mut resp);
6739 assert!(resp["standards"].is_array());
6740 let arr = resp["standards"].as_array().unwrap();
6741 assert_eq!(arr.len(), 2);
6742 assert_eq!(arr[0]["title"], "GLOBAL");
6744 assert_eq!(arr[1]["title"], "LOCAL");
6745 assert!(resp.get("standard").is_none());
6746 }
6747
6748 #[test]
6773 fn handle_archive_list_returns_empty_when_no_archived() {
6774 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6775 let req = make_tools_call("memory_archive_list", json!({}));
6776 let resp = invoke_handle_request(&conn, &req);
6777 assert!(resp.error.is_none());
6778 let text = resp.result.unwrap()["content"][0]["text"]
6779 .as_str()
6780 .unwrap()
6781 .to_string();
6782 let val: Value = serde_json::from_str(&text).unwrap();
6783 assert_eq!(val["count"], 0);
6784 assert!(val["archived"].is_array());
6785 }
6786
6787 #[test]
6788 fn handle_archive_list_with_namespace_filter() {
6789 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6790 let req = make_tools_call(
6791 "memory_archive_list",
6792 json!({"namespace": "w12-archive", "limit": 5, "offset": 0}),
6793 );
6794 let resp = invoke_handle_request(&conn, &req);
6795 assert!(resp.error.is_none());
6796 }
6797
6798 #[test]
6799 fn handle_archive_restore_unknown_id_returns_error() {
6800 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6801 let req = make_tools_call(
6802 "memory_archive_restore",
6803 json!({"id": "00000000-0000-0000-0000-000000000000"}),
6804 );
6805 let resp = invoke_handle_request(&conn, &req);
6806 let result = resp.result.unwrap();
6807 assert_eq!(result["isError"], true);
6808 let msg = result["content"][0]["text"].as_str().unwrap();
6809 assert!(msg.contains("archive") || msg.contains("not found"));
6810 }
6811
6812 #[test]
6813 fn handle_archive_purge_with_older_than_zero() {
6814 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6817 let req = make_tools_call("memory_archive_purge", json!({"older_than_days": 0}));
6818 let resp = invoke_handle_request(&conn, &req);
6819 assert!(resp.error.is_none());
6820 let text = resp.result.unwrap()["content"][0]["text"]
6821 .as_str()
6822 .unwrap()
6823 .to_string();
6824 let val: Value = serde_json::from_str(&text).unwrap();
6825 assert!(val["purged"].is_u64() || val["purged"].is_i64());
6826 }
6827
6828 #[test]
6829 fn handle_archive_stats_returns_struct() {
6830 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6831 let req = make_tools_call("memory_archive_stats", json!({}));
6832 let resp = invoke_handle_request(&conn, &req);
6833 assert!(resp.error.is_none());
6834 let text = resp.result.unwrap()["content"][0]["text"]
6835 .as_str()
6836 .unwrap()
6837 .to_string();
6838 let val: Value = serde_json::from_str(&text).unwrap();
6839 assert!(val.is_object() || val.is_number() || val.is_array());
6841 }
6842
6843 #[test]
6844 fn handle_kg_timeline_unknown_source_returns_empty_events() {
6845 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6846 let req = make_tools_call(
6847 "memory_kg_timeline",
6848 json!({"source_id": "00000000-0000-0000-0000-000000000000"}),
6849 );
6850 let resp = invoke_handle_request(&conn, &req);
6851 assert!(resp.error.is_none());
6852 let text = resp.result.unwrap()["content"][0]["text"]
6853 .as_str()
6854 .unwrap()
6855 .to_string();
6856 let val: Value = serde_json::from_str(&text).unwrap();
6857 assert!(val["events"].is_array());
6858 assert_eq!(val["count"], 0);
6859 }
6860
6861 #[test]
6862 fn handle_kg_timeline_with_since_until_filters() {
6863 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6864 let req = make_tools_call(
6865 "memory_kg_timeline",
6866 json!({
6867 "source_id": "00000000-0000-0000-0000-000000000000",
6868 "since": "2024-01-01T00:00:00Z",
6869 "until": "2025-01-01T00:00:00Z",
6870 "limit": 50,
6871 }),
6872 );
6873 let resp = invoke_handle_request(&conn, &req);
6874 assert!(resp.error.is_none());
6875 }
6876
6877 #[test]
6878 fn handle_kg_timeline_invalid_since_returns_error() {
6879 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6880 let req = make_tools_call(
6881 "memory_kg_timeline",
6882 json!({
6883 "source_id": "00000000-0000-0000-0000-000000000000",
6884 "since": "this-is-not-a-timestamp",
6885 }),
6886 );
6887 let resp = invoke_handle_request(&conn, &req);
6888 let result = resp.result.unwrap();
6889 assert_eq!(result["isError"], true);
6890 }
6891
6892 #[test]
6893 fn handle_kg_invalidate_no_match_returns_found_false() {
6894 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6895 let req = make_tools_call(
6896 "memory_kg_invalidate",
6897 json!({
6898 "source_id": "00000000-0000-0000-0000-000000000000",
6899 "target_id": "11111111-1111-1111-1111-111111111111",
6900 "relation": "related_to",
6901 }),
6902 );
6903 let resp = invoke_handle_request(&conn, &req);
6904 assert!(resp.error.is_none());
6905 let text = resp.result.unwrap()["content"][0]["text"]
6906 .as_str()
6907 .unwrap()
6908 .to_string();
6909 let val: Value = serde_json::from_str(&text).unwrap();
6910 assert_eq!(val["found"], false);
6911 }
6912
6913 #[test]
6914 fn handle_kg_invalidate_with_explicit_valid_until() {
6915 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6919 let src = Memory {
6920 id: uuid::Uuid::new_v4().to_string(),
6921 tier: Tier::Long,
6922 namespace: "w12-kg".into(),
6923 title: "src".into(),
6924 content: "c".into(),
6925 tags: vec![],
6926 priority: 5,
6927 confidence: 1.0,
6928 source: "test".into(),
6929 access_count: 0,
6930 created_at: chrono::Utc::now().to_rfc3339(),
6931 updated_at: chrono::Utc::now().to_rfc3339(),
6932 last_accessed_at: None,
6933 expires_at: None,
6934 metadata: json!({}),
6935 reflection_depth: 0,
6936 memory_kind: crate::models::MemoryKind::Observation,
6937 entity_id: None,
6938 persona_version: None,
6939 citations: Vec::new(),
6940 source_uri: None,
6941 source_span: None,
6942 confidence_source: crate::models::ConfidenceSource::CallerProvided,
6943 confidence_signals: None,
6944 confidence_decayed_at: None,
6945 version: 1,
6946 };
6947 let mut tgt = src.clone();
6948 tgt.id = uuid::Uuid::new_v4().to_string();
6949 tgt.title = "tgt".into();
6950 let src_id = db::insert(&conn, &src).unwrap();
6951 let tgt_id = db::insert(&conn, &tgt).unwrap();
6952 db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
6953
6954 let req = make_tools_call(
6955 "memory_kg_invalidate",
6956 json!({
6957 "source_id": src_id,
6958 "target_id": tgt_id,
6959 "relation": "related_to",
6960 "valid_until": "2025-01-01T00:00:00Z",
6961 }),
6962 );
6963 let resp = invoke_handle_request(&conn, &req);
6964 assert!(resp.error.is_none());
6965 let text = resp.result.unwrap()["content"][0]["text"]
6966 .as_str()
6967 .unwrap()
6968 .to_string();
6969 let val: Value = serde_json::from_str(&text).unwrap();
6970 assert_eq!(val["found"], true);
6971 assert_eq!(val["valid_until"], "2025-01-01T00:00:00Z");
6972 }
6973
6974 #[test]
6975 fn handle_kg_invalidate_invalid_valid_until_format() {
6976 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6977 let req = make_tools_call(
6978 "memory_kg_invalidate",
6979 json!({
6980 "source_id": "00000000-0000-0000-0000-000000000000",
6981 "target_id": "11111111-1111-1111-1111-111111111111",
6982 "relation": "related_to",
6983 "valid_until": "not-a-date",
6984 }),
6985 );
6986 let resp = invoke_handle_request(&conn, &req);
6987 let result = resp.result.unwrap();
6988 assert_eq!(result["isError"], true);
6989 }
6990
6991 #[test]
6992 fn handle_kg_query_with_max_depth_and_filters() {
6993 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
6994 let req = make_tools_call(
6995 "memory_kg_query",
6996 json!({
6997 "source_id": "00000000-0000-0000-0000-000000000000",
6998 "max_depth": 2,
6999 "valid_at": "2025-01-01T00:00:00Z",
7000 "allowed_agents": ["agent-a", "agent-b"],
7001 "limit": 10,
7002 }),
7003 );
7004 let resp = invoke_handle_request(&conn, &req);
7005 assert!(resp.error.is_none());
7006 let text = resp.result.unwrap()["content"][0]["text"]
7007 .as_str()
7008 .unwrap()
7009 .to_string();
7010 let val: Value = serde_json::from_str(&text).unwrap();
7011 assert_eq!(val["max_depth"], 2);
7012 assert!(val["memories"].is_array());
7013 }
7014
7015 #[test]
7016 fn handle_kg_query_invalid_valid_at() {
7017 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7018 let req = make_tools_call(
7019 "memory_kg_query",
7020 json!({
7021 "source_id": "00000000-0000-0000-0000-000000000000",
7022 "valid_at": "garbage",
7023 }),
7024 );
7025 let resp = invoke_handle_request(&conn, &req);
7026 let result = resp.result.unwrap();
7027 assert_eq!(result["isError"], true);
7028 }
7029
7030 #[test]
7031 fn handle_kg_query_rejects_invalid_agent_id() {
7032 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7033 let req = make_tools_call(
7034 "memory_kg_query",
7035 json!({
7036 "source_id": "00000000-0000-0000-0000-000000000000",
7037 "allowed_agents": ["bad agent with spaces!!"],
7038 }),
7039 );
7040 let resp = invoke_handle_request(&conn, &req);
7041 let result = resp.result.unwrap();
7042 assert_eq!(result["isError"], true);
7043 }
7044
7045 #[test]
7046 fn handle_session_start_happy_returns_memories() {
7047 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7048 let mem = Memory {
7050 id: uuid::Uuid::new_v4().to_string(),
7051 tier: Tier::Long,
7052 namespace: "w12-session".into(),
7053 title: "seed".into(),
7054 content: "c".into(),
7055 tags: vec![],
7056 priority: 5,
7057 confidence: 1.0,
7058 source: "test".into(),
7059 access_count: 0,
7060 created_at: chrono::Utc::now().to_rfc3339(),
7061 updated_at: chrono::Utc::now().to_rfc3339(),
7062 last_accessed_at: None,
7063 expires_at: None,
7064 metadata: json!({}),
7065 reflection_depth: 0,
7066 memory_kind: crate::models::MemoryKind::Observation,
7067 entity_id: None,
7068 persona_version: None,
7069 citations: Vec::new(),
7070 source_uri: None,
7071 source_span: None,
7072 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7073 confidence_signals: None,
7074 confidence_decayed_at: None,
7075 version: 1,
7076 };
7077 db::insert(&conn, &mem).unwrap();
7078 let req = make_tools_call(
7079 "memory_session_start",
7080 json!({"namespace": "w12-session", "limit": 5, "format": "json"}),
7081 );
7082 let resp = invoke_handle_request(&conn, &req);
7083 assert!(resp.error.is_none());
7084 let text = resp.result.unwrap()["content"][0]["text"]
7085 .as_str()
7086 .unwrap()
7087 .to_string();
7088 let val: Value = serde_json::from_str(&text).unwrap();
7089 assert_eq!(val["mode"], "session_start");
7090 assert!(val["memories"].is_array());
7091 }
7092
7093 #[test]
7094 fn handle_session_start_empty_namespace_returns_zero() {
7095 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7096 let req = make_tools_call(
7097 "memory_session_start",
7098 json!({"namespace": "w12-empty-ns", "format": "json"}),
7099 );
7100 let resp = invoke_handle_request(&conn, &req);
7101 assert!(resp.error.is_none());
7102 let text = resp.result.unwrap()["content"][0]["text"]
7103 .as_str()
7104 .unwrap()
7105 .to_string();
7106 let val: Value = serde_json::from_str(&text).unwrap();
7107 assert_eq!(val["count"], 0);
7108 }
7109
7110 #[test]
7120 fn handle_session_start_rejects_invalid_namespace() {
7121 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7122 let req = make_tools_call(
7123 "memory_session_start",
7124 json!({"namespace": "foo bar", "format": "json"}),
7126 );
7127 let resp = invoke_handle_request(&conn, &req);
7128 assert!(resp.error.is_none(), "must not surface as RPC error");
7131 let result = resp.result.expect("ok_response present");
7132 assert_eq!(
7133 result.get("isError").and_then(|v| v.as_bool()),
7134 Some(true),
7135 "invalid namespace must return isError=true, got {result}"
7136 );
7137 let text = result["content"][0]["text"]
7138 .as_str()
7139 .unwrap_or_default()
7140 .to_string();
7141 assert!(
7142 text.to_lowercase().contains("namespace"),
7143 "error message should mention namespace, got: {text}"
7144 );
7145 }
7146
7147 #[test]
7148 fn handle_inbox_returns_empty_for_unregistered_caller() {
7149 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7150 let req = make_tools_call("memory_inbox", json!({"agent_id": "test-bot"}));
7151 let resp = invoke_handle_request(&conn, &req);
7152 assert!(resp.error.is_none());
7153 let text = resp.result.unwrap()["content"][0]["text"]
7154 .as_str()
7155 .unwrap()
7156 .to_string();
7157 let val: Value = serde_json::from_str(&text).unwrap();
7158 assert_eq!(val["agent_id"], "test-bot");
7159 assert!(val["namespace"].as_str().unwrap().starts_with("_messages/"));
7160 assert_eq!(val["count"], 0);
7161 }
7162
7163 #[test]
7164 fn handle_inbox_with_unread_only_filter() {
7165 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7166 let req = make_tools_call(
7167 "memory_inbox",
7168 json!({"agent_id": "test-bot", "unread_only": true, "limit": 10}),
7169 );
7170 let resp = invoke_handle_request(&conn, &req);
7171 assert!(resp.error.is_none());
7172 let text = resp.result.unwrap()["content"][0]["text"]
7173 .as_str()
7174 .unwrap()
7175 .to_string();
7176 let val: Value = serde_json::from_str(&text).unwrap();
7177 assert_eq!(val["unread_only"], true);
7178 }
7179
7180 #[test]
7181 fn handle_notify_happy_returns_message_id() {
7182 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7183 let req = make_tools_call(
7184 "memory_notify",
7185 json!({
7186 "target_agent_id": "alice",
7187 "title": "hello",
7188 "payload": "world",
7189 "tier": Tier::Mid.as_str(),
7190 "priority": 5,
7191 }),
7192 );
7193 let resp = invoke_handle_request(&conn, &req);
7194 assert!(resp.error.is_none());
7195 let text = resp.result.unwrap()["content"][0]["text"]
7196 .as_str()
7197 .unwrap()
7198 .to_string();
7199 let val: Value = serde_json::from_str(&text).unwrap();
7200 assert!(val["id"].is_string());
7201 assert_eq!(val["to"], "alice");
7202 assert_eq!(val["namespace"], "_messages/alice");
7203 }
7204
7205 #[test]
7206 fn handle_notify_invalid_tier_returns_error() {
7207 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7208 let req = make_tools_call(
7209 "memory_notify",
7210 json!({
7211 "target_agent_id": "bob",
7212 "title": "hi",
7213 "payload": "p",
7214 "tier": "bogus-tier",
7215 }),
7216 );
7217 let resp = invoke_handle_request(&conn, &req);
7218 let result = resp.result.unwrap();
7219 assert_eq!(result["isError"], true);
7220 let msg = result["content"][0]["text"].as_str().unwrap();
7221 assert!(msg.contains("invalid tier"));
7222 }
7223
7224 #[test]
7225 fn handle_agent_register_then_list() {
7226 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7227 let req = make_tools_call(
7229 "memory_agent_register",
7230 json!({
7231 "agent_id": "w12-bot",
7232 "agent_type": "ai:w12-bot",
7233 "capabilities": ["read", "write"],
7234 }),
7235 );
7236 let resp = invoke_handle_request(&conn, &req);
7237 assert!(resp.error.is_none());
7238 let text = resp.result.unwrap()["content"][0]["text"]
7239 .as_str()
7240 .unwrap()
7241 .to_string();
7242 let val: Value = serde_json::from_str(&text).unwrap();
7243 assert_eq!(val["registered"], true);
7244 let req2 = make_tools_call("memory_agent_list", json!({}));
7246 let resp2 = invoke_handle_request(&conn, &req2);
7247 assert!(resp2.error.is_none());
7248 let text2 = resp2.result.unwrap()["content"][0]["text"]
7249 .as_str()
7250 .unwrap()
7251 .to_string();
7252 let val2: Value = serde_json::from_str(&text2).unwrap();
7253 assert!(val2["count"].as_u64().unwrap() >= 1);
7254 }
7255
7256 #[test]
7257 fn handle_agent_register_invalid_type_rejects() {
7258 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7259 let req = make_tools_call(
7260 "memory_agent_register",
7261 json!({"agent_id": "w12-bot2", "agent_type": " not-allowed-type with spaces "}),
7262 );
7263 let resp = invoke_handle_request(&conn, &req);
7264 let result = resp.result.unwrap();
7265 assert_eq!(result["isError"], true);
7266 }
7267
7268 #[test]
7269 fn handle_namespace_set_get_clear_round_trip() {
7270 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7271 let mem = Memory {
7273 id: uuid::Uuid::new_v4().to_string(),
7274 tier: Tier::Long,
7275 namespace: "w12-ns".into(),
7276 title: "policy".into(),
7277 content: "be excellent".into(),
7278 tags: vec![],
7279 priority: 5,
7280 confidence: 1.0,
7281 source: "test".into(),
7282 access_count: 0,
7283 created_at: chrono::Utc::now().to_rfc3339(),
7284 updated_at: chrono::Utc::now().to_rfc3339(),
7285 last_accessed_at: None,
7286 expires_at: None,
7287 metadata: json!({}),
7288 reflection_depth: 0,
7289 memory_kind: crate::models::MemoryKind::Observation,
7290 entity_id: None,
7291 persona_version: None,
7292 citations: Vec::new(),
7293 source_uri: None,
7294 source_span: None,
7295 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7296 confidence_signals: None,
7297 confidence_decayed_at: None,
7298 version: 1,
7299 };
7300 let std_id = db::insert(&conn, &mem).unwrap();
7301
7302 let set_req = make_tools_call(
7304 "memory_namespace_set_standard",
7305 json!({"namespace": "w12-ns", "id": std_id.clone()}),
7306 );
7307 let set_resp = invoke_handle_request(&conn, &set_req);
7308 assert!(set_resp.error.is_none());
7309 let set_text = set_resp.result.unwrap()["content"][0]["text"]
7310 .as_str()
7311 .unwrap()
7312 .to_string();
7313 let set_val: Value = serde_json::from_str(&set_text).unwrap();
7314 assert_eq!(set_val["set"], true);
7315
7316 let get_req = make_tools_call(
7318 "memory_namespace_get_standard",
7319 json!({"namespace": "w12-ns"}),
7320 );
7321 let get_resp = invoke_handle_request(&conn, &get_req);
7322 assert!(get_resp.error.is_none());
7323 let get_text = get_resp.result.unwrap()["content"][0]["text"]
7324 .as_str()
7325 .unwrap()
7326 .to_string();
7327 let get_val: Value = serde_json::from_str(&get_text).unwrap();
7328 assert_eq!(get_val["standard_id"], std_id);
7329
7330 let clr_req = make_tools_call(
7332 "memory_namespace_clear_standard",
7333 json!({"namespace": "w12-ns"}),
7334 );
7335 let clr_resp = invoke_handle_request(&conn, &clr_req);
7336 assert!(clr_resp.error.is_none());
7337 let clr_text = clr_resp.result.unwrap()["content"][0]["text"]
7338 .as_str()
7339 .unwrap()
7340 .to_string();
7341 let clr_val: Value = serde_json::from_str(&clr_text).unwrap();
7342 assert_eq!(clr_val["cleared"], true);
7343 }
7344
7345 #[test]
7346 fn handle_namespace_get_standard_missing_returns_null() {
7347 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7348 let req = make_tools_call(
7349 "memory_namespace_get_standard",
7350 json!({"namespace": "w12-no-standard-here"}),
7351 );
7352 let resp = invoke_handle_request(&conn, &req);
7353 assert!(resp.error.is_none());
7354 let text = resp.result.unwrap()["content"][0]["text"]
7355 .as_str()
7356 .unwrap()
7357 .to_string();
7358 let val: Value = serde_json::from_str(&text).unwrap();
7359 assert!(val["standard_id"].is_null());
7360 }
7361
7362 #[test]
7363 fn handle_namespace_get_standard_inherit_returns_chain() {
7364 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7367 seed_namespace_standard(&conn, "*", "global rule");
7368 seed_namespace_standard(&conn, "w12-inh", "specific rule");
7369 let req = make_tools_call(
7370 "memory_namespace_get_standard",
7371 json!({"namespace": "w12-inh", "inherit": true}),
7372 );
7373 let resp = invoke_handle_request(&conn, &req);
7374 assert!(resp.error.is_none());
7375 let text = resp.result.unwrap()["content"][0]["text"]
7376 .as_str()
7377 .unwrap()
7378 .to_string();
7379 let val: Value = serde_json::from_str(&text).unwrap();
7380 assert!(val["chain"].is_array());
7381 assert!(val["standards"].is_array());
7382 assert!(val["count"].as_u64().unwrap() >= 1);
7383 }
7384
7385 #[test]
7386 fn handle_namespace_set_standard_with_invalid_governance_rejected() {
7387 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7388 let mem = Memory {
7389 id: uuid::Uuid::new_v4().to_string(),
7390 tier: Tier::Long,
7391 namespace: "w12-gov".into(),
7392 title: "p".into(),
7393 content: "c".into(),
7394 tags: vec![],
7395 priority: 5,
7396 confidence: 1.0,
7397 source: "test".into(),
7398 access_count: 0,
7399 created_at: chrono::Utc::now().to_rfc3339(),
7400 updated_at: chrono::Utc::now().to_rfc3339(),
7401 last_accessed_at: None,
7402 expires_at: None,
7403 metadata: json!({}),
7404 reflection_depth: 0,
7405 memory_kind: crate::models::MemoryKind::Observation,
7406 entity_id: None,
7407 persona_version: None,
7408 citations: Vec::new(),
7409 source_uri: None,
7410 source_span: None,
7411 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7412 confidence_signals: None,
7413 confidence_decayed_at: None,
7414 version: 1,
7415 };
7416 let id = db::insert(&conn, &mem).unwrap();
7417 let req = make_tools_call(
7418 "memory_namespace_set_standard",
7419 json!({
7420 "namespace": "w12-gov",
7421 "id": id,
7422 "governance": {"this": "is not a valid policy"},
7423 }),
7424 );
7425 let resp = invoke_handle_request(&conn, &req);
7426 let result = resp.result.unwrap();
7427 assert_eq!(result["isError"], true);
7428 let msg = result["content"][0]["text"].as_str().unwrap();
7429 assert!(msg.contains("invalid governance") || msg.contains("governance"));
7430 }
7431
7432 #[test]
7433 fn handle_namespace_set_standard_invalid_namespace_rejected() {
7434 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7435 let req = make_tools_call(
7436 "memory_namespace_set_standard",
7437 json!({"namespace": "bad ns with spaces!!", "id": "any"}),
7438 );
7439 let resp = invoke_handle_request(&conn, &req);
7440 let result = resp.result.unwrap();
7441 assert_eq!(result["isError"], true);
7442 }
7443
7444 #[test]
7445 fn handle_pending_list_happy_returns_array() {
7446 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7447 let req = make_tools_call(
7448 "memory_pending_list",
7449 json!({"status": "pending", "limit": 100}),
7450 );
7451 let resp = invoke_handle_request(&conn, &req);
7452 assert!(resp.error.is_none());
7453 let text = resp.result.unwrap()["content"][0]["text"]
7454 .as_str()
7455 .unwrap()
7456 .to_string();
7457 let val: Value = serde_json::from_str(&text).unwrap();
7458 assert!(val["pending"].is_array());
7459 assert!(val["count"].is_u64());
7460 }
7461
7462 #[test]
7463 fn handle_pending_approve_unknown_id_returns_error() {
7464 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7465 let req = make_tools_call(
7466 "memory_pending_approve",
7467 json!({"id": "00000000-0000-0000-0000-000000000000", "agent_id": "human:approver"}),
7468 );
7469 let resp = invoke_handle_request(&conn, &req);
7470 let result = resp.result.unwrap();
7471 assert!(result.is_object());
7474 }
7475
7476 #[test]
7477 fn handle_pending_reject_unknown_id_returns_not_found() {
7478 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7479 let req = make_tools_call(
7480 "memory_pending_reject",
7481 json!({"id": "00000000-0000-0000-0000-000000000000", "agent_id": "human:rejector"}),
7482 );
7483 let resp = invoke_handle_request(&conn, &req);
7484 let result = resp.result.unwrap();
7485 assert_eq!(result["isError"], true);
7486 let msg = result["content"][0]["text"].as_str().unwrap();
7487 assert!(msg.contains("not found") || msg.contains("already decided"));
7488 }
7489
7490 #[test]
7491 fn handle_gc_dry_run_returns_count_without_deleting() {
7492 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7493 let req = make_tools_call("memory_gc", json!({"dry_run": true}));
7494 let resp = invoke_handle_request(&conn, &req);
7495 assert!(resp.error.is_none());
7496 let text = resp.result.unwrap()["content"][0]["text"]
7497 .as_str()
7498 .unwrap()
7499 .to_string();
7500 let val: Value = serde_json::from_str(&text).unwrap();
7501 assert_eq!(val["dry_run"], true);
7502 assert!(val["collected"].is_u64() || val["collected"].is_i64());
7503 }
7504
7505 #[test]
7506 fn handle_gc_actual_run_returns_zero_on_empty_db() {
7507 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7508 let req = make_tools_call("memory_gc", json!({}));
7509 let resp = invoke_handle_request(&conn, &req);
7510 assert!(resp.error.is_none());
7511 let text = resp.result.unwrap()["content"][0]["text"]
7512 .as_str()
7513 .unwrap()
7514 .to_string();
7515 let val: Value = serde_json::from_str(&text).unwrap();
7516 assert_eq!(val["dry_run"], false);
7517 }
7518
7519 #[test]
7520 fn handle_forget_dry_run_with_filters() {
7521 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7522 let req = make_tools_call(
7523 "memory_forget",
7524 json!({"namespace": "w12-forget", "tier": Tier::Short.as_str(), "dry_run": true}),
7525 );
7526 let resp = invoke_handle_request(&conn, &req);
7527 assert!(resp.error.is_none());
7528 let text = resp.result.unwrap()["content"][0]["text"]
7529 .as_str()
7530 .unwrap()
7531 .to_string();
7532 let val: Value = serde_json::from_str(&text).unwrap();
7533 assert_eq!(val["dry_run"], true);
7534 }
7535
7536 #[test]
7537 fn handle_forget_actual_with_namespace() {
7538 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7539 let req = make_tools_call(
7540 "memory_forget",
7541 json!({"namespace": "w12-forget-actual", "dry_run": false}),
7542 );
7543 let resp = invoke_handle_request(&conn, &req);
7544 assert!(resp.error.is_none());
7545 }
7546
7547 #[test]
7548 fn handle_unsubscribe_unknown_returns_false() {
7549 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7552 let req = make_tools_call(
7553 "memory_unsubscribe",
7554 json!({"id": "00000000-0000-0000-0000-000000000000"}),
7555 );
7556 let resp = invoke_handle_request(&conn, &req);
7557 assert!(resp.error.is_none());
7558 let text = resp.result.unwrap()["content"][0]["text"]
7559 .as_str()
7560 .unwrap()
7561 .to_string();
7562 let val: Value = serde_json::from_str(&text).unwrap();
7563 assert!(
7565 val["removed"] == json!(false) || val["removed"] == json!(0),
7566 "unexpected removed value: {:?}",
7567 val["removed"]
7568 );
7569 }
7570
7571 #[test]
7572 fn handle_list_subscriptions_returns_array() {
7573 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7574 let req = make_tools_call("memory_list_subscriptions", json!({}));
7575 let resp = invoke_handle_request(&conn, &req);
7576 assert!(resp.error.is_none());
7577 }
7578
7579 #[test]
7580 fn handle_entity_register_happy() {
7581 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7582 let req = make_tools_call(
7583 "memory_entity_register",
7584 json!({
7585 "canonical_name": "Hugo Boss",
7586 "namespace": "w12-people",
7587 "aliases": ["HB", "Hugo"],
7588 }),
7589 );
7590 let resp = invoke_handle_request(&conn, &req);
7591 assert!(resp.error.is_none());
7592 let text = resp.result.unwrap()["content"][0]["text"]
7593 .as_str()
7594 .unwrap()
7595 .to_string();
7596 let val: Value = serde_json::from_str(&text).unwrap();
7597 assert!(val["entity_id"].is_string());
7598 assert_eq!(val["canonical_name"], "Hugo Boss");
7599 }
7600
7601 #[test]
7602 fn handle_entity_register_invalid_namespace() {
7603 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7604 let req = make_tools_call(
7605 "memory_entity_register",
7606 json!({"canonical_name": "X", "namespace": "INVALID NS!"}),
7607 );
7608 let resp = invoke_handle_request(&conn, &req);
7609 let result = resp.result.unwrap();
7610 assert_eq!(result["isError"], true);
7611 }
7612
7613 #[test]
7614 fn handle_entity_get_by_alias_not_found_returns_null() {
7615 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7616 let req = make_tools_call(
7617 "memory_entity_get_by_alias",
7618 json!({"alias": "no-such-alias", "namespace": "w12-people"}),
7619 );
7620 let resp = invoke_handle_request(&conn, &req);
7621 assert!(resp.error.is_none());
7622 let text = resp.result.unwrap()["content"][0]["text"]
7623 .as_str()
7624 .unwrap()
7625 .to_string();
7626 let val: Value = serde_json::from_str(&text).unwrap();
7627 assert_eq!(val["found"], false);
7628 }
7629
7630 #[test]
7631 fn handle_get_taxonomy_with_prefix_and_depth() {
7632 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7633 let req = make_tools_call(
7634 "memory_get_taxonomy",
7635 json!({"namespace_prefix": "w12-tax", "depth": 4, "limit": 100}),
7636 );
7637 let resp = invoke_handle_request(&conn, &req);
7638 assert!(resp.error.is_none());
7639 let text = resp.result.unwrap()["content"][0]["text"]
7640 .as_str()
7641 .unwrap()
7642 .to_string();
7643 let val: Value = serde_json::from_str(&text).unwrap();
7644 assert!(val["tree"].is_object() || val["tree"].is_array());
7645 }
7646
7647 #[test]
7648 fn handle_get_taxonomy_strips_trailing_slash() {
7649 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7650 let req = make_tools_call(
7651 "memory_get_taxonomy",
7652 json!({"namespace_prefix": "w12-tax/", "depth": 2}),
7653 );
7654 let resp = invoke_handle_request(&conn, &req);
7655 assert!(resp.error.is_none());
7657 }
7658
7659 #[test]
7660 fn handle_get_taxonomy_invalid_prefix_after_strip() {
7661 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7662 let req = make_tools_call(
7663 "memory_get_taxonomy",
7664 json!({"namespace_prefix": "BAD NS!"}),
7665 );
7666 let resp = invoke_handle_request(&conn, &req);
7667 let result = resp.result.unwrap();
7668 assert_eq!(result["isError"], true);
7669 }
7670
7671 #[test]
7672 fn handle_check_duplicate_no_embedder_errors() {
7673 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7676 let req = make_tools_call(
7677 "memory_check_duplicate",
7678 json!({"title": "T", "content": "C"}),
7679 );
7680 let resp = invoke_handle_request(&conn, &req);
7681 let result = resp.result.unwrap();
7682 assert_eq!(result["isError"], true);
7683 let msg = result["content"][0]["text"].as_str().unwrap();
7684 assert!(msg.contains("embedder") || msg.contains("semantic"));
7685 }
7686
7687 #[test]
7688 fn handle_expand_query_no_llm_errors() {
7689 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7690 let req = make_tools_call("memory_expand_query", json!({"query": "test"}));
7691 let resp = invoke_handle_request(&conn, &req);
7692 let result = resp.result.unwrap();
7693 assert_eq!(result["isError"], true);
7694 let msg = result["content"][0]["text"].as_str().unwrap();
7695 assert!(msg.contains("smart") || msg.contains("LLM") || msg.contains("Ollama"));
7696 }
7697
7698 #[test]
7699 fn handle_auto_tag_no_llm_errors() {
7700 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7701 let req = make_tools_call(
7702 "memory_auto_tag",
7703 json!({"id": "00000000-0000-0000-0000-000000000000"}),
7704 );
7705 let resp = invoke_handle_request(&conn, &req);
7706 let result = resp.result.unwrap();
7707 assert_eq!(result["isError"], true);
7708 }
7709
7710 #[test]
7711 fn handle_detect_contradiction_no_llm_errors() {
7712 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7713 let req = make_tools_call(
7714 "memory_detect_contradiction",
7715 json!({"id_a": "00000000-0000-0000-0000-000000000000", "id_b": "11111111-1111-1111-1111-111111111111"}),
7716 );
7717 let resp = invoke_handle_request(&conn, &req);
7718 let result = resp.result.unwrap();
7719 assert_eq!(result["isError"], true);
7720 }
7721
7722 #[test]
7723 fn handle_update_unknown_id_returns_not_found() {
7724 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7725 let req = make_tools_call(
7726 "memory_update",
7727 json!({
7728 "id": "00000000-0000-0000-0000-000000000000",
7729 "title": "new title",
7730 }),
7731 );
7732 let resp = invoke_handle_request(&conn, &req);
7733 let result = resp.result.unwrap();
7734 assert_eq!(result["isError"], true);
7735 let msg = result["content"][0]["text"].as_str().unwrap();
7736 assert!(msg.contains("not found"));
7737 }
7738
7739 #[test]
7740 fn handle_update_invalid_priority_rejected() {
7741 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7742 let mem = Memory {
7744 id: uuid::Uuid::new_v4().to_string(),
7745 tier: Tier::Long,
7746 namespace: "w12-update".into(),
7747 title: "t".into(),
7748 content: "c".into(),
7749 tags: vec![],
7750 priority: 5,
7751 confidence: 1.0,
7752 source: "test".into(),
7753 access_count: 0,
7754 created_at: chrono::Utc::now().to_rfc3339(),
7755 updated_at: chrono::Utc::now().to_rfc3339(),
7756 last_accessed_at: None,
7757 expires_at: None,
7758 metadata: json!({}),
7759 reflection_depth: 0,
7760 memory_kind: crate::models::MemoryKind::Observation,
7761 entity_id: None,
7762 persona_version: None,
7763 citations: Vec::new(),
7764 source_uri: None,
7765 source_span: None,
7766 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7767 confidence_signals: None,
7768 confidence_decayed_at: None,
7769 version: 1,
7770 };
7771 let id = db::insert(&conn, &mem).unwrap();
7772 let req = make_tools_call(
7773 "memory_update",
7774 json!({"id": id, "priority": 99_i64}), );
7776 let resp = invoke_handle_request(&conn, &req);
7777 let result = resp.result.unwrap();
7778 assert_eq!(result["isError"], true);
7779 }
7780
7781 #[test]
7782 fn handle_update_with_metadata_object_accepted() {
7783 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7786 let mem = Memory {
7787 id: uuid::Uuid::new_v4().to_string(),
7788 tier: Tier::Mid,
7789 namespace: "w12-meta".into(),
7790 title: "t".into(),
7791 content: "c".into(),
7792 tags: vec![],
7793 priority: 5,
7794 confidence: 1.0,
7795 source: "test".into(),
7796 access_count: 0,
7797 created_at: chrono::Utc::now().to_rfc3339(),
7798 updated_at: chrono::Utc::now().to_rfc3339(),
7799 last_accessed_at: None,
7800 expires_at: None,
7801 metadata: json!({}),
7802 reflection_depth: 0,
7803 memory_kind: crate::models::MemoryKind::Observation,
7804 entity_id: None,
7805 persona_version: None,
7806 citations: Vec::new(),
7807 source_uri: None,
7808 source_span: None,
7809 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7810 confidence_signals: None,
7811 confidence_decayed_at: None,
7812 version: 1,
7813 };
7814 let id = db::insert(&conn, &mem).unwrap();
7815 let req = make_tools_call(
7816 "memory_update",
7817 json!({
7818 "id": id,
7819 "metadata": {"custom": "field", "numbers": [1, 2, 3]},
7820 }),
7821 );
7822 let resp = invoke_handle_request(&conn, &req);
7823 assert!(resp.error.is_none());
7824 }
7825
7826 #[test]
7827 fn handle_get_links_unknown_id_returns_empty() {
7828 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7829 let req = make_tools_call(
7830 "memory_get_links",
7831 json!({"id": "00000000-0000-0000-0000-000000000000"}),
7832 );
7833 let resp = invoke_handle_request(&conn, &req);
7834 assert!(resp.error.is_none());
7835 let text = resp.result.unwrap()["content"][0]["text"]
7836 .as_str()
7837 .unwrap()
7838 .to_string();
7839 let val: Value = serde_json::from_str(&text).unwrap();
7840 assert!(val["links"].is_array());
7841 assert_eq!(val["count"], 0);
7842 }
7843
7844 #[test]
7845 fn handle_link_invalid_relation_rejected() {
7846 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7847 let req = make_tools_call(
7848 "memory_link",
7849 json!({
7850 "source_id": "00000000-0000-0000-0000-000000000000",
7851 "target_id": "11111111-1111-1111-1111-111111111111",
7852 "relation": "BADRELATIONNOTALLOWED",
7853 }),
7854 );
7855 let resp = invoke_handle_request(&conn, &req);
7856 let result = resp.result.unwrap();
7857 assert_eq!(result["isError"], true);
7858 }
7859
7860 #[test]
7861 fn handle_promote_to_namespace_with_explicit_target() {
7862 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7868 let mem = Memory {
7869 id: uuid::Uuid::new_v4().to_string(),
7870 tier: Tier::Mid,
7871 namespace: "w12-parent/w12-child".into(),
7872 title: "t".into(),
7873 content: "c".into(),
7874 tags: vec![],
7875 priority: 5,
7876 confidence: 1.0,
7877 source: "test".into(),
7878 access_count: 0,
7879 created_at: chrono::Utc::now().to_rfc3339(),
7880 updated_at: chrono::Utc::now().to_rfc3339(),
7881 last_accessed_at: None,
7882 expires_at: None,
7883 metadata: json!({}),
7884 reflection_depth: 0,
7885 memory_kind: crate::models::MemoryKind::Observation,
7886 entity_id: None,
7887 persona_version: None,
7888 citations: Vec::new(),
7889 source_uri: None,
7890 source_span: None,
7891 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7892 confidence_signals: None,
7893 confidence_decayed_at: None,
7894 version: 1,
7895 };
7896 let id = db::insert(&conn, &mem).unwrap();
7897 let req = make_tools_call(
7898 "memory_promote",
7899 json!({"id": id, "to_namespace": "w12-parent"}),
7900 );
7901 let resp = invoke_handle_request(&conn, &req);
7902 assert!(resp.error.is_none());
7903 let text = resp.result.unwrap()["content"][0]["text"]
7904 .as_str()
7905 .unwrap()
7906 .to_string();
7907 let val: Value = serde_json::from_str(&text).unwrap();
7908 assert_eq!(val["mode"], "vertical");
7909 assert!(val["clone_id"].is_string());
7910 }
7911
7912 #[test]
7913 fn handle_promote_invalid_to_namespace_rejected() {
7914 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7915 let mem = Memory {
7916 id: uuid::Uuid::new_v4().to_string(),
7917 tier: Tier::Mid,
7918 namespace: "w12-pm".into(),
7919 title: "t".into(),
7920 content: "c".into(),
7921 tags: vec![],
7922 priority: 5,
7923 confidence: 1.0,
7924 source: "test".into(),
7925 access_count: 0,
7926 created_at: chrono::Utc::now().to_rfc3339(),
7927 updated_at: chrono::Utc::now().to_rfc3339(),
7928 last_accessed_at: None,
7929 expires_at: None,
7930 metadata: json!({}),
7931 reflection_depth: 0,
7932 memory_kind: crate::models::MemoryKind::Observation,
7933 entity_id: None,
7934 persona_version: None,
7935 citations: Vec::new(),
7936 source_uri: None,
7937 source_span: None,
7938 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7939 confidence_signals: None,
7940 confidence_decayed_at: None,
7941 version: 1,
7942 };
7943 let id = db::insert(&conn, &mem).unwrap();
7944 let req = make_tools_call(
7945 "memory_promote",
7946 json!({"id": id, "to_namespace": "BAD NS WITH SPACES"}),
7947 );
7948 let resp = invoke_handle_request(&conn, &req);
7949 let result = resp.result.unwrap();
7950 assert_eq!(result["isError"], true);
7951 }
7952
7953 #[test]
7954 fn handle_consolidate_with_explicit_summary_no_llm() {
7955 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
7957 let mem_a = Memory {
7958 id: uuid::Uuid::new_v4().to_string(),
7959 tier: Tier::Mid,
7960 namespace: "w12-cons".into(),
7961 title: "a".into(),
7962 content: "alpha".into(),
7963 tags: vec![],
7964 priority: 5,
7965 confidence: 1.0,
7966 source: "test".into(),
7967 access_count: 0,
7968 created_at: chrono::Utc::now().to_rfc3339(),
7969 updated_at: chrono::Utc::now().to_rfc3339(),
7970 last_accessed_at: None,
7971 expires_at: None,
7972 metadata: json!({}),
7973 reflection_depth: 0,
7974 memory_kind: crate::models::MemoryKind::Observation,
7975 entity_id: None,
7976 persona_version: None,
7977 citations: Vec::new(),
7978 source_uri: None,
7979 source_span: None,
7980 confidence_source: crate::models::ConfidenceSource::CallerProvided,
7981 confidence_signals: None,
7982 confidence_decayed_at: None,
7983 version: 1,
7984 };
7985 let mut mem_b = mem_a.clone();
7986 mem_b.id = uuid::Uuid::new_v4().to_string();
7987 mem_b.title = "b".into();
7988 mem_b.content = "beta".into();
7989 let id_a = db::insert(&conn, &mem_a).unwrap();
7990 let id_b = db::insert(&conn, &mem_b).unwrap();
7991
7992 let req = make_tools_call(
7993 "memory_consolidate",
7994 json!({
7995 "ids": [id_a, id_b],
7996 "title": "merged",
7997 "summary": "merged summary",
7998 "namespace": "w12-cons",
7999 }),
8000 );
8001 let resp = invoke_handle_request(&conn, &req);
8002 assert!(resp.error.is_none());
8003 let text = resp.result.unwrap()["content"][0]["text"]
8004 .as_str()
8005 .unwrap()
8006 .to_string();
8007 let val: Value = serde_json::from_str(&text).unwrap();
8008 assert!(val["id"].is_string());
8009 assert_eq!(val["consolidated"], 2);
8010 }
8011
8012 #[test]
8013 fn handle_consolidate_non_string_id_rejected() {
8014 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8015 let req = make_tools_call(
8016 "memory_consolidate",
8017 json!({"ids": [42, "valid-id"], "title": "t", "summary": "s"}),
8018 );
8019 let resp = invoke_handle_request(&conn, &req);
8020 let result = resp.result.unwrap();
8021 assert_eq!(result["isError"], true);
8022 let msg = result["content"][0]["text"].as_str().unwrap();
8023 assert!(msg.contains("must be a string"));
8024 }
8025
8026 #[test]
8031 fn test_jsonrpc_handles_ping() {
8032 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8033 let req = RpcRequest {
8034 jsonrpc: "2.0".into(),
8035 id: Some(json!(1)),
8036 method: "ping".into(),
8037 params: json!({}),
8038 };
8039 let resp = invoke_handle_request(&conn, &req);
8040 assert!(resp.error.is_none());
8041 }
8042
8043 #[test]
8044 fn test_jsonrpc_handles_notifications_initialized() {
8045 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8048 let req = RpcRequest {
8049 jsonrpc: "2.0".into(),
8050 id: Some(json!(2)),
8051 method: "notifications/initialized".into(),
8052 params: json!({}),
8053 };
8054 let resp = invoke_handle_request(&conn, &req);
8055 assert!(resp.error.is_none());
8056 }
8057
8058 #[test]
8059 fn test_jsonrpc_prompts_list_returns_array() {
8060 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8061 let req = RpcRequest {
8062 jsonrpc: "2.0".into(),
8063 id: Some(json!(3)),
8064 method: "prompts/list".into(),
8065 params: json!({}),
8066 };
8067 let resp = invoke_handle_request(&conn, &req);
8068 assert!(resp.error.is_none());
8069 let result = resp.result.unwrap();
8070 assert!(result["prompts"].is_array());
8071 }
8072
8073 #[test]
8074 fn test_jsonrpc_prompts_get_known_name_returns_messages() {
8075 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8076 let req = RpcRequest {
8077 jsonrpc: "2.0".into(),
8078 id: Some(json!(4)),
8079 method: "prompts/get".into(),
8080 params: json!({"name": "recall-first"}),
8081 };
8082 let resp = invoke_handle_request(&conn, &req);
8083 assert!(resp.error.is_none());
8084 let result = resp.result.unwrap();
8085 assert!(result["messages"].is_array());
8086 }
8087
8088 #[test]
8089 fn test_jsonrpc_prompts_get_with_namespace_arg_includes_hint() {
8090 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8091 let req = RpcRequest {
8092 jsonrpc: "2.0".into(),
8093 id: Some(json!(5)),
8094 method: "prompts/get".into(),
8095 params: json!({"name": "recall-first", "arguments": {"namespace": "w12-test"}}),
8096 };
8097 let resp = invoke_handle_request(&conn, &req);
8098 assert!(resp.error.is_none());
8099 let result = resp.result.unwrap();
8100 let text = result["messages"][0]["content"]["text"].as_str().unwrap();
8101 assert!(text.contains("w12-test"));
8102 }
8103
8104 #[test]
8105 fn test_jsonrpc_prompts_get_unknown_name_returns_error() {
8106 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8107 let req = RpcRequest {
8108 jsonrpc: "2.0".into(),
8109 id: Some(json!(6)),
8110 method: "prompts/get".into(),
8111 params: json!({"name": "no-such-prompt"}),
8112 };
8113 let resp = invoke_handle_request(&conn, &req);
8114 let err = resp.error.unwrap();
8115 assert_eq!(err.code, -32602);
8116 }
8117
8118 #[test]
8119 fn test_jsonrpc_prompts_get_missing_name_returns_error() {
8120 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8121 let req = RpcRequest {
8122 jsonrpc: "2.0".into(),
8123 id: Some(json!(7)),
8124 method: "prompts/get".into(),
8125 params: json!({}),
8126 };
8127 let resp = invoke_handle_request(&conn, &req);
8128 let err = resp.error.unwrap();
8129 assert_eq!(err.code, -32602);
8130 }
8131
8132 #[test]
8133 fn test_jsonrpc_prompts_get_memory_workflow() {
8134 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8135 let req = RpcRequest {
8136 jsonrpc: "2.0".into(),
8137 id: Some(json!(8)),
8138 method: "prompts/get".into(),
8139 params: json!({"name": "memory-workflow"}),
8140 };
8141 let resp = invoke_handle_request(&conn, &req);
8142 assert!(resp.error.is_none());
8143 let result = resp.result.unwrap();
8144 assert!(result["messages"].is_array());
8145 }
8146
8147 #[test]
8148 fn test_jsonrpc_tools_call_empty_tool_name_rejected() {
8149 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8150 let req = RpcRequest {
8151 jsonrpc: "2.0".into(),
8152 id: Some(json!(9)),
8153 method: "tools/call".into(),
8154 params: json!({"name": ""}),
8155 };
8156 let resp = invoke_handle_request(&conn, &req);
8157 let err = resp.error.unwrap();
8158 assert_eq!(err.code, -32602);
8159 }
8160
8161 #[test]
8162 fn test_jsonrpc_tools_call_arguments_not_object_uses_empty() {
8163 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8168 let req = RpcRequest {
8169 jsonrpc: "2.0".into(),
8170 id: Some(json!(10)),
8171 method: "tools/call".into(),
8172 params: json!({"name": "memory_capabilities", "arguments": null}),
8173 };
8174 let resp = invoke_handle_request(&conn, &req);
8175 assert!(resp.error.is_none());
8177 }
8178
8179 #[test]
8180 fn test_jsonrpc_tools_call_unicode_in_args() {
8181 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8184 let req = make_tools_call(
8185 "memory_store",
8186 json!({"title": "тест", "content": "日本語 ✨", "namespace": "w12-unicode"}),
8187 );
8188 let resp = invoke_handle_request(&conn, &req);
8189 assert!(resp.error.is_none());
8190 }
8191
8192 #[test]
8193 fn test_jsonrpc_dispatch_line_with_id_zero_treated_as_request() {
8194 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8197 let line = r#"{"jsonrpc":"2.0","id":0,"method":"tools/list"}"#;
8198 let resp = dispatch_line(&conn, line);
8199 assert!(resp.is_some());
8200 }
8201
8202 #[test]
8203 fn test_jsonrpc_dispatch_line_string_id_passes_through() {
8204 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8205 let line = r#"{"jsonrpc":"2.0","id":"call-abc","method":"tools/list"}"#;
8206 let resp = dispatch_line(&conn, line).expect("expected response");
8207 assert_eq!(resp.id, json!("call-abc"));
8208 }
8209
8210 #[test]
8215 fn test_build_namespace_chain_global_only() {
8216 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8217 let chain = super::build_namespace_chain(&conn, "*");
8218 assert_eq!(chain, vec!["*".to_string()]);
8219 }
8220
8221 #[test]
8222 fn test_build_namespace_chain_simple_namespace() {
8223 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8225 let chain = super::build_namespace_chain(&conn, "w12-flat");
8226 assert!(chain.contains(&"*".to_string()));
8227 assert!(chain.contains(&"w12-flat".to_string()));
8228 }
8229
8230 #[test]
8231 fn test_build_namespace_chain_nested_yields_ancestors() {
8232 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8233 let chain = super::build_namespace_chain(&conn, "a/b/c");
8234 assert_eq!(chain.first().unwrap(), "*");
8236 assert!(chain.contains(&"a/b/c".to_string()));
8237 let pos_a = chain.iter().position(|s| s == "a").unwrap();
8239 let pos_ab = chain.iter().position(|s| s == "a/b").unwrap();
8240 let pos_abc = chain.iter().position(|s| s == "a/b/c").unwrap();
8241 assert!(pos_a < pos_ab && pos_ab < pos_abc);
8242 }
8243
8244 #[test]
8245 fn test_build_namespace_chain_with_explicit_parent() {
8246 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8249 let parent_mem = Memory {
8252 id: uuid::Uuid::new_v4().to_string(),
8253 tier: Tier::Long,
8254 namespace: "w12-explicit-grand".into(),
8255 title: "g".into(),
8256 content: "c".into(),
8257 tags: vec![],
8258 priority: 5,
8259 confidence: 1.0,
8260 source: "test".into(),
8261 access_count: 0,
8262 created_at: chrono::Utc::now().to_rfc3339(),
8263 updated_at: chrono::Utc::now().to_rfc3339(),
8264 last_accessed_at: None,
8265 expires_at: None,
8266 metadata: json!({}),
8267 reflection_depth: 0,
8268 memory_kind: crate::models::MemoryKind::Observation,
8269 entity_id: None,
8270 persona_version: None,
8271 citations: Vec::new(),
8272 source_uri: None,
8273 source_span: None,
8274 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8275 confidence_signals: None,
8276 confidence_decayed_at: None,
8277 version: 1,
8278 };
8279 let pid = db::insert(&conn, &parent_mem).unwrap();
8280 db::set_namespace_standard(&conn, "w12-explicit-grand", &pid, None).unwrap();
8281
8282 let mut child_mem = parent_mem.clone();
8283 child_mem.id = uuid::Uuid::new_v4().to_string();
8284 child_mem.namespace = "w12-explicit-leaf".into();
8285 let cid = db::insert(&conn, &child_mem).unwrap();
8286 db::set_namespace_standard(&conn, "w12-explicit-leaf", &cid, Some("w12-explicit-grand"))
8287 .unwrap();
8288
8289 let chain = super::build_namespace_chain(&conn, "w12-explicit-leaf");
8290 assert!(chain.contains(&"w12-explicit-grand".to_string()));
8292 assert!(chain.contains(&"w12-explicit-leaf".to_string()));
8293 }
8294
8295 #[test]
8300 fn test_extract_governance_default_when_metadata_absent() {
8301 let mem_val = json!({"id": "x"});
8302 let gov = super::extract_governance(&mem_val);
8303 assert!(gov.is_object() || gov.is_null());
8305 }
8306
8307 #[test]
8308 fn test_extract_governance_default_when_metadata_invalid() {
8309 let mem_val = json!({"metadata": {"governance": {"unknown": "policy"}}});
8311 let gov = super::extract_governance(&mem_val);
8312 assert!(gov.is_object());
8314 }
8315
8316 #[test]
8321 fn test_messages_namespace_for_plain_id() {
8322 assert_eq!(super::messages_namespace_for("alice"), "_messages/alice");
8323 }
8324
8325 #[test]
8326 fn test_messages_namespace_for_ai_prefixed_id() {
8327 let ns = super::messages_namespace_for("ai:claude@host:pid-1");
8328 assert!(ns.starts_with("_messages/"));
8329 assert!(ns.contains("ai:"));
8330 }
8331
8332 #[test]
8338 fn test_inject_namespace_standard_no_namespace_no_global() {
8339 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8341 let mut resp = make_recall_response(vec![]);
8342 let before = resp.clone();
8343 super::inject_namespace_standard(&conn, None, &mut resp);
8344 assert_eq!(resp, before);
8345 }
8346
8347 #[test]
8355 fn handle_promote_default_tier_to_long() {
8356 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8359 let mem = Memory {
8360 id: uuid::Uuid::new_v4().to_string(),
8361 tier: Tier::Mid,
8362 namespace: "w12-tier-promote".into(),
8363 title: "t".into(),
8364 content: "c".into(),
8365 tags: vec![],
8366 priority: 5,
8367 confidence: 1.0,
8368 source: "test".into(),
8369 access_count: 0,
8370 created_at: chrono::Utc::now().to_rfc3339(),
8371 updated_at: chrono::Utc::now().to_rfc3339(),
8372 last_accessed_at: None,
8373 expires_at: None,
8374 metadata: json!({}),
8375 reflection_depth: 0,
8376 memory_kind: crate::models::MemoryKind::Observation,
8377 entity_id: None,
8378 persona_version: None,
8379 citations: Vec::new(),
8380 source_uri: None,
8381 source_span: None,
8382 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8383 confidence_signals: None,
8384 confidence_decayed_at: None,
8385 version: 1,
8386 };
8387 let id = db::insert(&conn, &mem).unwrap();
8388 let req = make_tools_call("memory_promote", json!({"id": id}));
8389 let resp = invoke_handle_request(&conn, &req);
8390 assert!(resp.error.is_none());
8391 let text = resp.result.unwrap()["content"][0]["text"]
8392 .as_str()
8393 .unwrap()
8394 .to_string();
8395 let val: Value = serde_json::from_str(&text).unwrap();
8396 assert_eq!(val["promoted"], true);
8397 assert_eq!(val["mode"], "tier");
8398 assert_eq!(val["tier"], Tier::Long.as_str());
8399 }
8400
8401 #[test]
8402 fn handle_store_dedup_updates_existing() {
8403 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8406 let req1 = make_tools_call(
8407 "memory_store",
8408 json!({
8409 "title": "dup-title",
8410 "content": "first",
8411 "namespace": "w12-dedup",
8412 "tier": Tier::Mid.as_str(),
8413 }),
8414 );
8415 let resp1 = invoke_handle_request(&conn, &req1);
8416 assert!(resp1.error.is_none());
8417 let text1 = resp1.result.unwrap()["content"][0]["text"]
8418 .as_str()
8419 .unwrap()
8420 .to_string();
8421 let val1: Value = serde_json::from_str(&text1).unwrap();
8422 let id1 = val1["id"].as_str().unwrap().to_string();
8423
8424 let req2 = make_tools_call(
8425 "memory_store",
8426 json!({
8427 "title": "dup-title",
8428 "content": "second-update",
8429 "namespace": "w12-dedup",
8430 "tier": Tier::Long.as_str(),
8431 }),
8432 );
8433 let resp2 = invoke_handle_request(&conn, &req2);
8434 assert!(resp2.error.is_none());
8435 let text2 = resp2.result.unwrap()["content"][0]["text"]
8436 .as_str()
8437 .unwrap()
8438 .to_string();
8439 let val2: Value = serde_json::from_str(&text2).unwrap();
8440 assert_eq!(val2["id"], id1);
8441 assert_eq!(val2["duplicate"], true);
8442 assert_eq!(val2["action"], "updated existing memory");
8443 }
8444
8445 #[test]
8446 fn handle_subscribe_with_registered_agent_succeeds() {
8447 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8450 let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
8454 db::register_agent(&conn, &resolved, "human", &[]).unwrap();
8455 let req = make_tools_call(
8458 "memory_subscribe",
8459 json!({
8460 "url": "https://example.com/hook",
8461 "events": "memory_store,memory_delete",
8462 "namespace_filter": "w12-sub",
8463 "secret": "mcp-sub-test-secret",
8464 }),
8465 );
8466 let resp = invoke_handle_request(&conn, &req);
8467 assert!(resp.error.is_none());
8468 let text = resp.result.unwrap()["content"][0]["text"]
8469 .as_str()
8470 .unwrap()
8471 .to_string();
8472 let val: Value = serde_json::from_str(&text).unwrap();
8473 assert!(val["id"].is_string());
8474 assert_eq!(val["url"], "https://example.com/hook");
8475 }
8476
8477 #[test]
8478 fn handle_subscribe_invalid_url_after_registered() {
8479 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8484 let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
8485 db::register_agent(&conn, &resolved, "human", &[]).unwrap();
8486 let req = make_tools_call(
8487 "memory_subscribe",
8488 json!({"url": "not-a-url-at-all", "secret": "mcp-sub-test-secret"}),
8489 );
8490 let resp = invoke_handle_request(&conn, &req);
8491 let result = resp.result.unwrap();
8492 assert_eq!(result["isError"], true);
8493 }
8494
8495 #[test]
8496 fn handle_namespace_set_standard_with_valid_governance() {
8497 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8501 let mem = Memory {
8502 id: uuid::Uuid::new_v4().to_string(),
8503 tier: Tier::Long,
8504 namespace: "w12-gov-ok".into(),
8505 title: "p".into(),
8506 content: "c".into(),
8507 tags: vec![],
8508 priority: 5,
8509 confidence: 1.0,
8510 source: "test".into(),
8511 access_count: 0,
8512 created_at: chrono::Utc::now().to_rfc3339(),
8513 updated_at: chrono::Utc::now().to_rfc3339(),
8514 last_accessed_at: None,
8515 expires_at: None,
8516 metadata: json!({}),
8517 reflection_depth: 0,
8518 memory_kind: crate::models::MemoryKind::Observation,
8519 entity_id: None,
8520 persona_version: None,
8521 citations: Vec::new(),
8522 source_uri: None,
8523 source_span: None,
8524 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8525 confidence_signals: None,
8526 confidence_decayed_at: None,
8527 version: 1,
8528 };
8529 let id = db::insert(&conn, &mem).unwrap();
8530 let req = make_tools_call(
8531 "memory_namespace_set_standard",
8532 json!({
8533 "namespace": "w12-gov-ok",
8534 "id": id,
8535 "governance": {
8536 "write": "any",
8537 "promote": "any",
8538 "delete": "owner",
8539 "approver": "human",
8540 },
8541 }),
8542 );
8543 let resp = invoke_handle_request(&conn, &req);
8544 assert!(resp.error.is_none());
8545 let text = resp.result.unwrap()["content"][0]["text"]
8546 .as_str()
8547 .unwrap()
8548 .to_string();
8549 let val: Value = serde_json::from_str(&text).unwrap();
8550 assert_eq!(val["set"], true);
8551 assert!(val["governance"].is_object());
8552 }
8553
8554 #[test]
8555 fn handle_namespace_set_standard_with_parent() {
8556 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8557 let mem = Memory {
8558 id: uuid::Uuid::new_v4().to_string(),
8559 tier: Tier::Long,
8560 namespace: "w12-parent-ns".into(),
8561 title: "p".into(),
8562 content: "c".into(),
8563 tags: vec![],
8564 priority: 5,
8565 confidence: 1.0,
8566 source: "test".into(),
8567 access_count: 0,
8568 created_at: chrono::Utc::now().to_rfc3339(),
8569 updated_at: chrono::Utc::now().to_rfc3339(),
8570 last_accessed_at: None,
8571 expires_at: None,
8572 metadata: json!({}),
8573 reflection_depth: 0,
8574 memory_kind: crate::models::MemoryKind::Observation,
8575 entity_id: None,
8576 persona_version: None,
8577 citations: Vec::new(),
8578 source_uri: None,
8579 source_span: None,
8580 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8581 confidence_signals: None,
8582 confidence_decayed_at: None,
8583 version: 1,
8584 };
8585 let id = db::insert(&conn, &mem).unwrap();
8586 let req = make_tools_call(
8587 "memory_namespace_set_standard",
8588 json!({
8589 "namespace": "w12-parent-ns",
8590 "id": id,
8591 "parent": "w12-grand-ns",
8592 }),
8593 );
8594 let resp = invoke_handle_request(&conn, &req);
8595 assert!(resp.error.is_none());
8596 let text = resp.result.unwrap()["content"][0]["text"]
8597 .as_str()
8598 .unwrap()
8599 .to_string();
8600 let val: Value = serde_json::from_str(&text).unwrap();
8601 assert_eq!(val["parent"], "w12-grand-ns");
8602 }
8603
8604 #[test]
8605 fn handle_get_resolves_by_prefix_and_includes_links() {
8606 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8610 let mem = Memory {
8611 id: uuid::Uuid::new_v4().to_string(),
8612 tier: Tier::Long,
8613 namespace: "w12-prefix".into(),
8614 title: "T".into(),
8615 content: "C".into(),
8616 tags: vec![],
8617 priority: 5,
8618 confidence: 1.0,
8619 source: "test".into(),
8620 access_count: 0,
8621 created_at: chrono::Utc::now().to_rfc3339(),
8622 updated_at: chrono::Utc::now().to_rfc3339(),
8623 last_accessed_at: None,
8624 expires_at: None,
8625 metadata: json!({}),
8626 reflection_depth: 0,
8627 memory_kind: crate::models::MemoryKind::Observation,
8628 entity_id: None,
8629 persona_version: None,
8630 citations: Vec::new(),
8631 source_uri: None,
8632 source_span: None,
8633 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8634 confidence_signals: None,
8635 confidence_decayed_at: None,
8636 version: 1,
8637 };
8638 let id = db::insert(&conn, &mem).unwrap();
8639 let req = make_tools_call("memory_get", json!({"id": id}));
8640 let resp = invoke_handle_request(&conn, &req);
8641 assert!(resp.error.is_none());
8642 let text = resp.result.unwrap()["content"][0]["text"]
8643 .as_str()
8644 .unwrap()
8645 .to_string();
8646 let val: Value = serde_json::from_str(&text).unwrap();
8647 assert!(val["links"].is_array());
8648 assert_eq!(val["id"], id);
8649 }
8650
8651 #[test]
8652 fn handle_link_creates_link_between_existing_memories() {
8653 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8656 let src = Memory {
8657 id: uuid::Uuid::new_v4().to_string(),
8658 tier: Tier::Long,
8659 namespace: "w12-link".into(),
8660 title: "src".into(),
8661 content: "c".into(),
8662 tags: vec![],
8663 priority: 5,
8664 confidence: 1.0,
8665 source: "test".into(),
8666 access_count: 0,
8667 created_at: chrono::Utc::now().to_rfc3339(),
8668 updated_at: chrono::Utc::now().to_rfc3339(),
8669 last_accessed_at: None,
8670 expires_at: None,
8671 metadata: json!({}),
8672 reflection_depth: 0,
8673 memory_kind: crate::models::MemoryKind::Observation,
8674 entity_id: None,
8675 persona_version: None,
8676 citations: Vec::new(),
8677 source_uri: None,
8678 source_span: None,
8679 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8680 confidence_signals: None,
8681 confidence_decayed_at: None,
8682 version: 1,
8683 };
8684 let mut tgt = src.clone();
8685 tgt.id = uuid::Uuid::new_v4().to_string();
8686 tgt.title = "tgt".into();
8687 let src_id = db::insert(&conn, &src).unwrap();
8688 let tgt_id = db::insert(&conn, &tgt).unwrap();
8689 let req = make_tools_call(
8690 "memory_link",
8691 json!({
8692 "source_id": src_id,
8693 "target_id": tgt_id,
8694 "relation": "related_to",
8695 }),
8696 );
8697 let resp = invoke_handle_request(&conn, &req);
8698 assert!(resp.error.is_none());
8699 let text = resp.result.unwrap()["content"][0]["text"]
8700 .as_str()
8701 .unwrap()
8702 .to_string();
8703 let val: Value = serde_json::from_str(&text).unwrap();
8704 assert_eq!(val["linked"], true);
8705 }
8706
8707 #[test]
8708 fn handle_get_links_returns_outbound_and_inbound() {
8709 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8711 let src = Memory {
8712 id: uuid::Uuid::new_v4().to_string(),
8713 tier: Tier::Long,
8714 namespace: "w12-getlinks".into(),
8715 title: "src".into(),
8716 content: "c".into(),
8717 tags: vec![],
8718 priority: 5,
8719 confidence: 1.0,
8720 source: "test".into(),
8721 access_count: 0,
8722 created_at: chrono::Utc::now().to_rfc3339(),
8723 updated_at: chrono::Utc::now().to_rfc3339(),
8724 last_accessed_at: None,
8725 expires_at: None,
8726 metadata: json!({}),
8727 reflection_depth: 0,
8728 memory_kind: crate::models::MemoryKind::Observation,
8729 entity_id: None,
8730 persona_version: None,
8731 citations: Vec::new(),
8732 source_uri: None,
8733 source_span: None,
8734 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8735 confidence_signals: None,
8736 confidence_decayed_at: None,
8737 version: 1,
8738 };
8739 let mut tgt = src.clone();
8740 tgt.id = uuid::Uuid::new_v4().to_string();
8741 let src_id = db::insert(&conn, &src).unwrap();
8742 let tgt_id = db::insert(&conn, &tgt).unwrap();
8743 db::create_link(&conn, &src_id, &tgt_id, "supersedes").unwrap();
8744
8745 let req = make_tools_call("memory_get_links", json!({"id": src_id}));
8746 let resp = invoke_handle_request(&conn, &req);
8747 assert!(resp.error.is_none());
8748 let text = resp.result.unwrap()["content"][0]["text"]
8749 .as_str()
8750 .unwrap()
8751 .to_string();
8752 let val: Value = serde_json::from_str(&text).unwrap();
8753 assert!(val["count"].as_u64().unwrap() >= 1);
8754 }
8755
8756 #[test]
8757 fn handle_kg_timeline_with_seeded_link_returns_event() {
8758 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8759 let src = Memory {
8760 id: uuid::Uuid::new_v4().to_string(),
8761 tier: Tier::Long,
8762 namespace: "w12-tl".into(),
8763 title: "src".into(),
8764 content: "c".into(),
8765 tags: vec![],
8766 priority: 5,
8767 confidence: 1.0,
8768 source: "test".into(),
8769 access_count: 0,
8770 created_at: chrono::Utc::now().to_rfc3339(),
8771 updated_at: chrono::Utc::now().to_rfc3339(),
8772 last_accessed_at: None,
8773 expires_at: None,
8774 metadata: json!({}),
8775 reflection_depth: 0,
8776 memory_kind: crate::models::MemoryKind::Observation,
8777 entity_id: None,
8778 persona_version: None,
8779 citations: Vec::new(),
8780 source_uri: None,
8781 source_span: None,
8782 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8783 confidence_signals: None,
8784 confidence_decayed_at: None,
8785 version: 1,
8786 };
8787 let mut tgt = src.clone();
8788 tgt.id = uuid::Uuid::new_v4().to_string();
8789 tgt.title = "tgt".into();
8790 let src_id = db::insert(&conn, &src).unwrap();
8791 let tgt_id = db::insert(&conn, &tgt).unwrap();
8792 db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
8793
8794 let req = make_tools_call(
8795 "memory_kg_timeline",
8796 json!({"source_id": src_id, "limit": 10}),
8797 );
8798 let resp = invoke_handle_request(&conn, &req);
8799 assert!(resp.error.is_none());
8800 let text = resp.result.unwrap()["content"][0]["text"]
8801 .as_str()
8802 .unwrap()
8803 .to_string();
8804 let val: Value = serde_json::from_str(&text).unwrap();
8805 assert_eq!(val["count"], 1);
8806 let events = val["events"].as_array().unwrap();
8807 assert_eq!(events[0]["target_id"], tgt_id);
8808 }
8809
8810 #[test]
8816 fn handle_kg_query_by_source_uri_returns_roots() {
8817 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8818 let mk = |ns: &str, t: &str, uri: Option<&str>| Memory {
8820 id: uuid::Uuid::new_v4().to_string(),
8821 tier: Tier::Long,
8822 namespace: ns.into(),
8823 title: t.into(),
8824 content: "c".into(),
8825 tags: vec![],
8826 priority: 5,
8827 confidence: 1.0,
8828 source: "test".into(),
8829 access_count: 0,
8830 created_at: chrono::Utc::now().to_rfc3339(),
8831 updated_at: chrono::Utc::now().to_rfc3339(),
8832 last_accessed_at: None,
8833 expires_at: None,
8834 metadata: json!({}),
8835 reflection_depth: 0,
8836 memory_kind: crate::models::MemoryKind::Observation,
8837 entity_id: None,
8838 persona_version: None,
8839 citations: Vec::new(),
8840 source_uri: uri.map(str::to_string),
8841 source_span: None,
8842 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8843 confidence_signals: None,
8844 confidence_decayed_at: None,
8845 version: 1,
8846 };
8847 let uri = "doc:test-uplift/abc#section-1";
8848 db::insert(&conn, &mk("kg-uplift", "a", Some(uri))).unwrap();
8849 db::insert(&conn, &mk("kg-uplift", "b", Some(uri))).unwrap();
8850 db::insert(&conn, &mk("kg-uplift", "c", None)).unwrap();
8851
8852 let req = make_tools_call(
8853 "memory_kg_query",
8854 json!({"by_source_uri": uri, "namespace": "kg-uplift"}),
8855 );
8856 let resp = invoke_handle_request(&conn, &req);
8857 assert!(resp.error.is_none(), "{resp:?}");
8858 let text = resp.result.unwrap()["content"][0]["text"]
8859 .as_str()
8860 .unwrap()
8861 .to_string();
8862 let val: Value = serde_json::from_str(&text).unwrap();
8863 assert_eq!(val["by_source_uri"], uri);
8864 assert_eq!(val["count"], 2);
8865 let mems = val["memories"].as_array().unwrap();
8866 assert_eq!(mems.len(), 2);
8867 assert!(mems.iter().all(|m| m["depth"].as_u64() == Some(0)));
8869 }
8870
8871 #[test]
8872 fn handle_kg_query_by_source_uri_rejects_invalid_uri() {
8873 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8874 let req = make_tools_call("memory_kg_query", json!({"by_source_uri": " "}));
8877 let resp = invoke_handle_request(&conn, &req);
8878 let result = resp.result.unwrap();
8879 assert_eq!(result["isError"], true);
8880 let text = result["content"][0]["text"].as_str().unwrap();
8883 assert!(text.contains("source_id is required"));
8884 }
8885
8886 #[test]
8887 fn handle_kg_query_by_source_uri_validates_uri_shape() {
8888 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8889 let req = make_tools_call(
8892 "memory_kg_query",
8893 json!({"by_source_uri": "bad\u{0007}uri"}),
8894 );
8895 let resp = invoke_handle_request(&conn, &req);
8896 let result = resp.result.unwrap();
8897 assert_eq!(result["isError"], true);
8898 }
8899
8900 #[test]
8901 fn handle_kg_query_with_seeded_link_returns_node() {
8902 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8903 let src = Memory {
8904 id: uuid::Uuid::new_v4().to_string(),
8905 tier: Tier::Long,
8906 namespace: "w12-kgq".into(),
8907 title: "src".into(),
8908 content: "c".into(),
8909 tags: vec![],
8910 priority: 5,
8911 confidence: 1.0,
8912 source: "test".into(),
8913 access_count: 0,
8914 created_at: chrono::Utc::now().to_rfc3339(),
8915 updated_at: chrono::Utc::now().to_rfc3339(),
8916 last_accessed_at: None,
8917 expires_at: None,
8918 metadata: json!({}),
8919 reflection_depth: 0,
8920 memory_kind: crate::models::MemoryKind::Observation,
8921 entity_id: None,
8922 persona_version: None,
8923 citations: Vec::new(),
8924 source_uri: None,
8925 source_span: None,
8926 confidence_source: crate::models::ConfidenceSource::CallerProvided,
8927 confidence_signals: None,
8928 confidence_decayed_at: None,
8929 version: 1,
8930 };
8931 let mut tgt = src.clone();
8932 tgt.id = uuid::Uuid::new_v4().to_string();
8933 let src_id = db::insert(&conn, &src).unwrap();
8934 let tgt_id = db::insert(&conn, &tgt).unwrap();
8935 db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
8936
8937 let req = make_tools_call(
8938 "memory_kg_query",
8939 json!({"source_id": src_id, "max_depth": 1, "limit": 10}),
8940 );
8941 let resp = invoke_handle_request(&conn, &req);
8942 assert!(resp.error.is_none());
8943 let text = resp.result.unwrap()["content"][0]["text"]
8944 .as_str()
8945 .unwrap()
8946 .to_string();
8947 let val: Value = serde_json::from_str(&text).unwrap();
8948 assert!(val["count"].as_u64().unwrap() >= 1);
8949 assert!(val["paths"].is_array());
8950 }
8951
8952 #[test]
8953 fn handle_archive_list_with_pagination() {
8954 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8955 let req = make_tools_call("memory_archive_list", json!({"limit": 100, "offset": 50}));
8956 let resp = invoke_handle_request(&conn, &req);
8957 assert!(resp.error.is_none());
8958 }
8959
8960 #[test]
8961 fn handle_pending_list_with_status_filter() {
8962 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8963 for status in &["pending", "approved", "rejected"] {
8964 let req = make_tools_call(
8965 "memory_pending_list",
8966 json!({"status": status, "limit": 50}),
8967 );
8968 let resp = invoke_handle_request(&conn, &req);
8969 assert!(resp.error.is_none(), "failed for status={status}");
8970 }
8971 }
8972
8973 #[test]
8974 fn handle_pending_approve_with_seeded_pending_action() {
8975 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
8977 let pending_id = db::queue_pending_action(
8978 &conn,
8979 crate::models::GovernedAction::Promote,
8980 "w12-approve",
8981 None,
8982 "human:requestor",
8983 &json!({"id": "00000000-0000-0000-0000-000000000000"}),
8984 )
8985 .unwrap();
8986 let req = make_tools_call(
8987 "memory_pending_approve",
8988 json!({"id": pending_id, "agent_id": "human:approver"}),
8989 );
8990 let resp = invoke_handle_request(&conn, &req);
8991 let result = resp.result.unwrap();
8994 assert!(result.is_object());
8995 }
8996
8997 #[test]
8998 fn handle_pending_reject_with_seeded_pending_action() {
8999 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9000 let pending_id = db::queue_pending_action(
9001 &conn,
9002 crate::models::GovernedAction::Promote,
9003 "w12-reject",
9004 None,
9005 "human:requestor",
9006 &json!({"id": "00000000-0000-0000-0000-000000000000"}),
9007 )
9008 .unwrap();
9009 let req = make_tools_call(
9010 "memory_pending_reject",
9011 json!({"id": pending_id, "agent_id": "human:rejector"}),
9012 );
9013 let resp = invoke_handle_request(&conn, &req);
9014 assert!(resp.error.is_none());
9015 let text = resp.result.unwrap()["content"][0]["text"]
9016 .as_str()
9017 .unwrap()
9018 .to_string();
9019 let val: Value = serde_json::from_str(&text).unwrap();
9020 assert_eq!(val["rejected"], true);
9021 }
9022
9023 #[test]
9024 fn handle_session_start_toon_format_default() {
9025 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9028 let req = make_tools_call("memory_session_start", json!({"namespace": "w12-toon"}));
9029 let resp = invoke_handle_request(&conn, &req);
9030 assert!(resp.error.is_none());
9031 let result = resp.result.unwrap();
9033 assert!(result["content"][0]["text"].is_string());
9034 }
9035
9036 #[test]
9037 fn handle_search_explicit_toon_format() {
9038 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9039 let req = make_tools_call(
9040 "memory_search",
9041 json!({"query": "anything", "format": "toon"}),
9042 );
9043 let resp = invoke_handle_request(&conn, &req);
9044 assert!(resp.error.is_none());
9045 }
9046
9047 #[test]
9048 fn handle_recall_explicit_toon_format() {
9049 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9050 let req = make_tools_call("memory_recall", json!({"context": "ctx", "format": "toon"}));
9051 let resp = invoke_handle_request(&conn, &req);
9052 assert!(resp.error.is_none());
9053 }
9054
9055 #[test]
9056 fn handle_list_explicit_toon_compact_format() {
9057 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9058 let req = make_tools_call(
9059 "memory_list",
9060 json!({"namespace": "w12-toon-list", "format": "toon_compact"}),
9061 );
9062 let resp = invoke_handle_request(&conn, &req);
9063 assert!(resp.error.is_none());
9064 }
9065
9066 #[test]
9067 fn handle_search_with_namespace_and_tier_filters() {
9068 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9069 let req = make_tools_call(
9070 "memory_search",
9071 json!({
9072 "query": "test query",
9073 "namespace": "w12-search",
9074 "tier": Tier::Long.as_str(),
9075 "limit": 10,
9076 "agent_id": "ai:bot",
9077 "format": "json",
9078 }),
9079 );
9080 let resp = invoke_handle_request(&conn, &req);
9081 assert!(resp.error.is_none());
9082 }
9083
9084 #[test]
9085 fn handle_search_invalid_agent_id_rejected() {
9086 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9087 let req = make_tools_call(
9088 "memory_search",
9089 json!({"query": "x", "agent_id": "bad agent !!"}),
9090 );
9091 let resp = invoke_handle_request(&conn, &req);
9092 let result = resp.result.unwrap();
9093 assert_eq!(result["isError"], true);
9094 }
9095
9096 #[test]
9097 fn handle_search_invalid_as_agent_rejected() {
9098 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9099 let req = make_tools_call(
9100 "memory_search",
9101 json!({"query": "x", "as_agent": "BAD AS AGENT"}),
9102 );
9103 let resp = invoke_handle_request(&conn, &req);
9104 let result = resp.result.unwrap();
9105 assert_eq!(result["isError"], true);
9106 }
9107
9108 #[test]
9109 fn handle_recall_invalid_as_agent_rejected() {
9110 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9111 let req = make_tools_call(
9112 "memory_recall",
9113 json!({"context": "x", "as_agent": "INVALID NS"}),
9114 );
9115 let resp = invoke_handle_request(&conn, &req);
9116 let result = resp.result.unwrap();
9117 assert_eq!(result["isError"], true);
9118 }
9119
9120 #[test]
9121 fn handle_recall_with_context_tokens() {
9122 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9125 let req = make_tools_call(
9126 "memory_recall",
9127 json!({
9128 "context": "main",
9129 "context_tokens": ["recent", "tokens", "from", "convo"],
9130 "format": "json",
9131 }),
9132 );
9133 let resp = invoke_handle_request(&conn, &req);
9134 assert!(resp.error.is_none());
9135 }
9136
9137 #[test]
9138 fn handle_recall_with_budget_tokens_positive() {
9139 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9140 let req = make_tools_call(
9141 "memory_recall",
9142 json!({"context": "x", "budget_tokens": 1000, "format": "json"}),
9143 );
9144 let resp = invoke_handle_request(&conn, &req);
9145 assert!(resp.error.is_none());
9146 let text = resp.result.unwrap()["content"][0]["text"]
9147 .as_str()
9148 .unwrap()
9149 .to_string();
9150 let val: Value = serde_json::from_str(&text).unwrap();
9151 assert!(val["tokens_used"].is_u64() || val["tokens_used"].is_i64());
9152 assert_eq!(val["budget_tokens"], 1000);
9153 }
9154
9155 #[test]
9156 fn handle_recall_invalid_namespace_filter_passes_through() {
9157 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9160 let req = make_tools_call(
9161 "memory_recall",
9162 json!({
9163 "context": "x",
9164 "namespace": "w12-no-such-namespace",
9165 "format": "json",
9166 }),
9167 );
9168 let resp = invoke_handle_request(&conn, &req);
9169 assert!(resp.error.is_none());
9170 }
9171
9172 #[test]
9173 fn handle_list_with_tier_filter() {
9174 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9175 let req = make_tools_call(
9176 "memory_list",
9177 json!({
9178 "namespace": "w12-list-tier",
9179 "tier": Tier::Long.as_str(),
9180 "agent_id": "ai:bot",
9181 "limit": 25,
9182 "format": "json",
9183 }),
9184 );
9185 let resp = invoke_handle_request(&conn, &req);
9186 assert!(resp.error.is_none());
9187 }
9188
9189 #[test]
9190 fn handle_list_invalid_tier_treated_as_none() {
9191 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9195 let req = make_tools_call(
9196 "memory_list",
9197 json!({"namespace": "w12-list-bad-tier", "tier": "ULTRAMID", "format": "json"}),
9198 );
9199 let resp = invoke_handle_request(&conn, &req);
9200 assert!(resp.error.is_none());
9201 }
9202
9203 #[test]
9204 fn handle_get_taxonomy_invalid_depth_clamps_to_max() {
9205 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9208 let req = make_tools_call(
9209 "memory_get_taxonomy",
9210 json!({"depth": 100_000_u64, "limit": 50_000_u64}),
9211 );
9212 let resp = invoke_handle_request(&conn, &req);
9213 assert!(resp.error.is_none());
9214 }
9215
9216 #[test]
9217 fn handle_archive_purge_no_filter_purges_all() {
9218 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9219 let req = make_tools_call("memory_archive_purge", json!({}));
9220 let resp = invoke_handle_request(&conn, &req);
9221 assert!(resp.error.is_none());
9222 }
9223
9224 #[test]
9225 fn handle_check_duplicate_invalid_title_rejected() {
9226 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9229 let req = make_tools_call(
9230 "memory_check_duplicate",
9231 json!({"title": "", "content": "anything"}),
9232 );
9233 let resp = invoke_handle_request(&conn, &req);
9234 let result = resp.result.unwrap();
9235 assert_eq!(result["isError"], true);
9236 }
9237
9238 #[test]
9239 fn handle_check_duplicate_invalid_namespace_rejected() {
9240 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9241 let req = make_tools_call(
9242 "memory_check_duplicate",
9243 json!({"title": "T", "content": "C", "namespace": "BAD NS"}),
9244 );
9245 let resp = invoke_handle_request(&conn, &req);
9246 let result = resp.result.unwrap();
9247 assert_eq!(result["isError"], true);
9248 }
9249
9250 #[test]
9251 fn handle_entity_register_with_explicit_agent_id() {
9252 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9255 let req = make_tools_call(
9256 "memory_entity_register",
9257 json!({
9258 "canonical_name": "Org Alpha",
9259 "namespace": "w12-orgs",
9260 "aliases": ["alpha", "α"],
9261 "agent_id": "ai:bot",
9262 }),
9263 );
9264 let resp = invoke_handle_request(&conn, &req);
9265 assert!(resp.error.is_none());
9266 }
9267
9268 #[test]
9269 fn handle_entity_register_invalid_explicit_agent_id() {
9270 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9271 let req = make_tools_call(
9272 "memory_entity_register",
9273 json!({
9274 "canonical_name": "Org Beta",
9275 "namespace": "w12-orgs",
9276 "agent_id": "BAD AGENT !!",
9277 }),
9278 );
9279 let resp = invoke_handle_request(&conn, &req);
9280 let result = resp.result.unwrap();
9281 assert_eq!(result["isError"], true);
9282 }
9283
9284 #[test]
9285 fn handle_entity_get_by_alias_no_namespace() {
9286 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9288 let req = make_tools_call("memory_entity_get_by_alias", json!({"alias": "any-alias"}));
9289 let resp = invoke_handle_request(&conn, &req);
9290 assert!(resp.error.is_none());
9291 }
9292
9293 #[test]
9294 fn handle_inbox_with_message_seeded() {
9295 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9297 let notify = make_tools_call(
9298 "memory_notify",
9299 json!({
9300 "target_agent_id": "alice-w12",
9301 "title": "ping",
9302 "payload": "are you there?",
9303 "tier": Tier::Short.as_str(),
9304 }),
9305 );
9306 let _ = invoke_handle_request(&conn, ¬ify);
9307 let inbox = make_tools_call(
9308 "memory_inbox",
9309 json!({"agent_id": "alice-w12", "limit": 10}),
9310 );
9311 let resp = invoke_handle_request(&conn, &inbox);
9312 assert!(resp.error.is_none());
9313 let text = resp.result.unwrap()["content"][0]["text"]
9314 .as_str()
9315 .unwrap()
9316 .to_string();
9317 let val: Value = serde_json::from_str(&text).unwrap();
9318 assert!(val["count"].as_u64().unwrap() >= 1);
9319 assert_eq!(val["agent_id"], "alice-w12");
9320 }
9321
9322 #[test]
9323 fn handle_consolidate_succeeds_when_source_was_standard() {
9324 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9330 let mem_a = Memory {
9331 id: uuid::Uuid::new_v4().to_string(),
9332 tier: Tier::Long,
9333 namespace: "w12-cons-warn".into(),
9334 title: "a".into(),
9335 content: "alpha".into(),
9336 tags: vec![],
9337 priority: 5,
9338 confidence: 1.0,
9339 source: "test".into(),
9340 access_count: 0,
9341 created_at: chrono::Utc::now().to_rfc3339(),
9342 updated_at: chrono::Utc::now().to_rfc3339(),
9343 last_accessed_at: None,
9344 expires_at: None,
9345 metadata: json!({}),
9346 reflection_depth: 0,
9347 memory_kind: crate::models::MemoryKind::Observation,
9348 entity_id: None,
9349 persona_version: None,
9350 citations: Vec::new(),
9351 source_uri: None,
9352 source_span: None,
9353 confidence_source: crate::models::ConfidenceSource::CallerProvided,
9354 confidence_signals: None,
9355 confidence_decayed_at: None,
9356 version: 1,
9357 };
9358 let mut mem_b = mem_a.clone();
9359 mem_b.id = uuid::Uuid::new_v4().to_string();
9360 mem_b.title = "b".into();
9361 mem_b.content = "beta".into();
9362 let id_a = db::insert(&conn, &mem_a).unwrap();
9363 let id_b = db::insert(&conn, &mem_b).unwrap();
9364 db::set_namespace_standard(&conn, "w12-cons-warn", &id_a, None).unwrap();
9366
9367 let req = make_tools_call(
9368 "memory_consolidate",
9369 json!({
9370 "ids": [id_a, id_b],
9371 "title": "merged-warn",
9372 "summary": "merged summary",
9373 "namespace": "w12-cons-warn",
9374 }),
9375 );
9376 let resp = invoke_handle_request(&conn, &req);
9377 assert!(resp.error.is_none());
9378 let text = resp.result.unwrap()["content"][0]["text"]
9379 .as_str()
9380 .unwrap()
9381 .to_string();
9382 let val: Value = serde_json::from_str(&text).unwrap();
9383 assert!(val["id"].is_string());
9384 assert_eq!(val["consolidated"], 2);
9385 }
9386
9387 #[test]
9388 fn handle_update_clears_expires_with_empty_string() {
9389 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9392 let mem = Memory {
9393 id: uuid::Uuid::new_v4().to_string(),
9394 tier: Tier::Short,
9395 namespace: "w12-clear-exp".into(),
9396 title: "t".into(),
9397 content: "c".into(),
9398 tags: vec![],
9399 priority: 5,
9400 confidence: 1.0,
9401 source: "test".into(),
9402 access_count: 0,
9403 created_at: chrono::Utc::now().to_rfc3339(),
9404 updated_at: chrono::Utc::now().to_rfc3339(),
9405 last_accessed_at: None,
9406 expires_at: Some(chrono::Utc::now().to_rfc3339()),
9407 metadata: json!({}),
9408 reflection_depth: 0,
9409 memory_kind: crate::models::MemoryKind::Observation,
9410 entity_id: None,
9411 persona_version: None,
9412 citations: Vec::new(),
9413 source_uri: None,
9414 source_span: None,
9415 confidence_source: crate::models::ConfidenceSource::CallerProvided,
9416 confidence_signals: None,
9417 confidence_decayed_at: None,
9418 version: 1,
9419 };
9420 let id = db::insert(&conn, &mem).unwrap();
9421 let req = make_tools_call("memory_update", json!({"id": id, "expires_at": ""}));
9422 let resp = invoke_handle_request(&conn, &req);
9423 let result = resp.result.unwrap();
9426 assert!(result.is_object());
9429 }
9430
9431 #[test]
9432 fn handle_update_change_namespace() {
9433 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9434 let mem = Memory {
9435 id: uuid::Uuid::new_v4().to_string(),
9436 tier: Tier::Mid,
9437 namespace: "w12-update-ns".into(),
9438 title: "t".into(),
9439 content: "c".into(),
9440 tags: vec![],
9441 priority: 5,
9442 confidence: 1.0,
9443 source: "test".into(),
9444 access_count: 0,
9445 created_at: chrono::Utc::now().to_rfc3339(),
9446 updated_at: chrono::Utc::now().to_rfc3339(),
9447 last_accessed_at: None,
9448 expires_at: None,
9449 metadata: json!({}),
9450 reflection_depth: 0,
9451 memory_kind: crate::models::MemoryKind::Observation,
9452 entity_id: None,
9453 persona_version: None,
9454 citations: Vec::new(),
9455 source_uri: None,
9456 source_span: None,
9457 confidence_source: crate::models::ConfidenceSource::CallerProvided,
9458 confidence_signals: None,
9459 confidence_decayed_at: None,
9460 version: 1,
9461 };
9462 let id = db::insert(&conn, &mem).unwrap();
9463 let req = make_tools_call(
9464 "memory_update",
9465 json!({
9466 "id": id,
9467 "namespace": "w12-update-ns-new",
9468 "tags": ["a", "b"],
9469 "title": "new-title",
9470 "content": "new-content",
9471 "tier": Tier::Long.as_str(),
9472 "priority": 8_i64,
9473 "confidence": 0.9_f64,
9474 }),
9475 );
9476 let resp = invoke_handle_request(&conn, &req);
9477 assert!(resp.error.is_none());
9478 }
9479
9480 #[test]
9481 fn handle_delete_with_prefix_id_lookup() {
9482 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9484 let mem = Memory {
9485 id: uuid::Uuid::new_v4().to_string(),
9486 tier: Tier::Mid,
9487 namespace: "w12-delete-prefix".into(),
9488 title: "t".into(),
9489 content: "c".into(),
9490 tags: vec![],
9491 priority: 5,
9492 confidence: 1.0,
9493 source: "test".into(),
9494 access_count: 0,
9495 created_at: chrono::Utc::now().to_rfc3339(),
9496 updated_at: chrono::Utc::now().to_rfc3339(),
9497 last_accessed_at: None,
9498 expires_at: None,
9499 metadata: json!({}),
9500 reflection_depth: 0,
9501 memory_kind: crate::models::MemoryKind::Observation,
9502 entity_id: None,
9503 persona_version: None,
9504 citations: Vec::new(),
9505 source_uri: None,
9506 source_span: None,
9507 confidence_source: crate::models::ConfidenceSource::CallerProvided,
9508 confidence_signals: None,
9509 confidence_decayed_at: None,
9510 version: 1,
9511 };
9512 let id = db::insert(&conn, &mem).unwrap();
9513 let req = make_tools_call("memory_delete", json!({"id": id}));
9514 let resp = invoke_handle_request(&conn, &req);
9515 assert!(resp.error.is_none());
9516 let text = resp.result.unwrap()["content"][0]["text"]
9517 .as_str()
9518 .unwrap()
9519 .to_string();
9520 let val: Value = serde_json::from_str(&text).unwrap();
9521 assert_eq!(val["deleted"], true);
9522 }
9523
9524 #[test]
9525 fn handle_unsubscribe_after_subscribe_removes_row() {
9526 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9529 let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
9530 db::register_agent(&conn, &resolved, "human", &[]).unwrap();
9531 let sub = make_tools_call(
9532 "memory_subscribe",
9533 json!({"url": "https://example.com/hook2", "secret": "mcp-sub-test-secret"}),
9534 );
9535 let sub_resp = invoke_handle_request(&conn, &sub);
9536 let sub_text = sub_resp.result.unwrap()["content"][0]["text"]
9537 .as_str()
9538 .unwrap()
9539 .to_string();
9540 let sub_val: Value = serde_json::from_str(&sub_text).unwrap();
9541 let id = sub_val["id"].as_str().unwrap().to_string();
9542
9543 let unsub = make_tools_call("memory_unsubscribe", json!({"id": id}));
9544 let unsub_resp = invoke_handle_request(&conn, &unsub);
9545 assert!(unsub_resp.error.is_none());
9546 let unsub_text = unsub_resp.result.unwrap()["content"][0]["text"]
9547 .as_str()
9548 .unwrap()
9549 .to_string();
9550 let unsub_val: Value = serde_json::from_str(&unsub_text).unwrap();
9551 assert!(
9552 unsub_val["removed"] == json!(true) || unsub_val["removed"] == json!(1),
9553 "unexpected removed value: {:?}",
9554 unsub_val["removed"]
9555 );
9556 }
9557
9558 #[test]
9559 fn handle_list_subscriptions_after_subscribe_returns_one() {
9560 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9562 let resolved = crate::identity::resolve_agent_id(None, None).unwrap();
9563 db::register_agent(&conn, &resolved, "human", &[]).unwrap();
9564 let sub = make_tools_call(
9565 "memory_subscribe",
9566 json!({"url": "https://example.com/listed", "secret": "mcp-sub-test-secret"}),
9567 );
9568 let _ = invoke_handle_request(&conn, &sub);
9569 let req = make_tools_call("memory_list_subscriptions", json!({}));
9570 let resp = invoke_handle_request(&conn, &req);
9571 assert!(resp.error.is_none());
9572 let text = resp.result.unwrap()["content"][0]["text"]
9573 .as_str()
9574 .unwrap()
9575 .to_string();
9576 let val: Value = serde_json::from_str(&text).unwrap();
9577 assert!(val.get("subscriptions").is_some() || val.get("count").is_some() || val.is_array());
9580 }
9581
9582 #[test]
9583 fn test_inject_namespace_standard_dedup_keeps_originals_order() {
9584 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9587 let std_id = seed_namespace_standard(&conn, "w12-order", "S");
9588 let mems = vec![
9589 json!({"id": "first", "title": "f"}),
9590 json!({"id": std_id, "title": "S"}),
9591 json!({"id": "third", "title": "t"}),
9592 ];
9593 let mut resp = make_recall_response(mems);
9594 super::inject_namespace_standard(&conn, Some("w12-order"), &mut resp);
9595 let memories = resp["memories"].as_array().unwrap();
9596 assert_eq!(memories.len(), 2);
9597 assert_eq!(memories[0]["id"], "first");
9598 assert_eq!(memories[1]["id"], "third");
9599 }
9600
9601 fn i4_insert_test_memory(conn: &rusqlite::Connection, id: &str) {
9615 let now = chrono::Utc::now().to_rfc3339();
9616 conn.execute(
9629 "INSERT INTO memories (
9630 id, tier, namespace, title, content, created_at, updated_at, metadata
9631 ) VALUES (?1, 'short', 'team/eng', ?2, 'body', ?3, ?3, '{\"scope\":\"public\"}')",
9632 rusqlite::params![id, format!("title-{id}"), now],
9633 )
9634 .unwrap();
9635 }
9636
9637 fn i4_decode_response_payload(resp: &RpcResponse) -> Value {
9640 let text = resp
9641 .result
9642 .as_ref()
9643 .expect("expected ok response")
9644 .get("content")
9645 .and_then(|c| c.get(0))
9646 .and_then(|c| c.get("text"))
9647 .and_then(Value::as_str)
9648 .expect("response wrapper must have content[0].text");
9649 serde_json::from_str(text).expect("response payload must be JSON")
9650 }
9651
9652 #[test]
9658 fn i4_replay_no_links_returns_empty_array() {
9659 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9660 i4_insert_test_memory(&conn, "mem-empty");
9661
9662 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-empty"}));
9663 let resp = invoke_handle_request(&conn, &req);
9664 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
9665
9666 let payload = i4_decode_response_payload(&resp);
9667 assert_eq!(payload["memory_id"], "mem-empty");
9668 assert_eq!(payload["count"], 0);
9669 let transcripts = payload["transcripts"].as_array().unwrap();
9670 assert!(transcripts.is_empty());
9671 }
9672
9673 #[test]
9678 fn i4_replay_single_transcript_returns_content_and_metadata() {
9679 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9680 i4_insert_test_memory(&conn, "mem-single");
9681 let body = "the canonical conversation that produced this memory";
9682 let t = crate::transcripts::store(&conn, "team/eng", body, None).unwrap();
9683 crate::transcripts::link_transcript(&conn, "mem-single", &t.id, Some(2), Some(20)).unwrap();
9684
9685 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-single"}));
9686 let resp = invoke_handle_request(&conn, &req);
9687 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
9688
9689 let payload = i4_decode_response_payload(&resp);
9690 assert_eq!(payload["memory_id"], "mem-single");
9691 assert_eq!(payload["count"], 1);
9692 let transcripts = payload["transcripts"].as_array().unwrap();
9693 assert_eq!(transcripts.len(), 1);
9694 let entry = &transcripts[0];
9695 assert_eq!(entry["id"], t.id);
9696 assert_eq!(entry["content"], body);
9697 assert_eq!(entry["span_start"], 2);
9698 assert_eq!(entry["span_end"], 20);
9699 assert_eq!(entry["original_size"].as_i64().unwrap(), body.len() as i64);
9700 assert!(entry["compressed_size"].as_i64().unwrap() > 0);
9703 assert!(entry["created_at"].is_string());
9704 assert!(entry.get("truncated").is_none());
9706 }
9707
9708 #[test]
9713 fn i4_replay_multiple_transcripts_chronological_order() {
9714 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9715 i4_insert_test_memory(&conn, "mem-multi");
9716
9717 let older = crate::transcripts::store(&conn, "team/eng", "older body", None).unwrap();
9720 let backdate =
9721 (chrono::Utc::now() - chrono::Duration::seconds(crate::SECS_PER_HOUR)).to_rfc3339();
9722 conn.execute(
9723 "UPDATE memory_transcripts SET created_at = ?1 WHERE id = ?2",
9724 rusqlite::params![backdate, older.id],
9725 )
9726 .unwrap();
9727
9728 let newer = crate::transcripts::store(&conn, "team/eng", "newer body", None).unwrap();
9729
9730 crate::transcripts::link_transcript(&conn, "mem-multi", &newer.id, None, None).unwrap();
9734 crate::transcripts::link_transcript(&conn, "mem-multi", &older.id, None, None).unwrap();
9735
9736 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-multi"}));
9737 let resp = invoke_handle_request(&conn, &req);
9738 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
9739
9740 let payload = i4_decode_response_payload(&resp);
9741 let transcripts = payload["transcripts"].as_array().unwrap();
9742 assert_eq!(transcripts.len(), 2);
9743 assert_eq!(transcripts[0]["id"], older.id);
9745 assert_eq!(transcripts[0]["content"], "older body");
9746 assert_eq!(transcripts[1]["id"], newer.id);
9747 assert_eq!(transcripts[1]["content"], "newer body");
9748 }
9749
9750 #[test]
9755 fn i4_replay_large_transcript_truncates_when_verbose_false() {
9756 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9757 i4_insert_test_memory(&conn, "mem-large");
9758
9759 let body: String = "abcdefghij".repeat(20_000); assert!(body.len() > REPLAY_VERBOSE_THRESHOLD_BYTES as usize);
9762 let t = crate::transcripts::store(&conn, "team/eng", &body, None).unwrap();
9763 crate::transcripts::link_transcript(&conn, "mem-large", &t.id, None, None).unwrap();
9764
9765 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-large"}));
9766 let resp = invoke_handle_request(&conn, &req);
9767 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
9768
9769 let payload = i4_decode_response_payload(&resp);
9770 let transcripts = payload["transcripts"].as_array().unwrap();
9771 assert_eq!(transcripts.len(), 1);
9772 let entry = &transcripts[0];
9773 assert_eq!(entry["truncated"], true);
9774 assert!(
9775 entry.get("content").is_none(),
9776 "content must be OMITTED when truncated; got: {entry}"
9777 );
9778 assert_eq!(entry["original_size"].as_i64().unwrap(), body.len() as i64);
9781 assert!(entry["compressed_size"].as_i64().unwrap() > 0);
9782 }
9783
9784 #[test]
9788 fn i4_replay_large_transcript_returns_content_when_verbose_true() {
9789 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9790 i4_insert_test_memory(&conn, "mem-large-verbose");
9791
9792 let body: String = "abcdefghij".repeat(20_000);
9793 let t = crate::transcripts::store(&conn, "team/eng", &body, None).unwrap();
9794 crate::transcripts::link_transcript(&conn, "mem-large-verbose", &t.id, None, None).unwrap();
9795
9796 let req = make_tools_call(
9797 "memory_replay",
9798 json!({"memory_id": "mem-large-verbose", "verbose": true}),
9799 );
9800 let resp = invoke_handle_request(&conn, &req);
9801 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
9802
9803 let payload = i4_decode_response_payload(&resp);
9804 let transcripts = payload["transcripts"].as_array().unwrap();
9805 assert_eq!(transcripts.len(), 1);
9806 let entry = &transcripts[0];
9807 assert!(
9808 entry.get("truncated").is_none(),
9809 "verbose=true must NOT set truncated"
9810 );
9811 assert_eq!(entry["content"].as_str().unwrap(), body);
9812 }
9813
9814 #[test]
9819 fn i4_replay_missing_memory_id_yields_handler_error() {
9820 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9821 let req = make_tools_call("memory_replay", json!({}));
9822 let resp = invoke_handle_request(&conn, &req);
9823 assert!(
9828 resp.error.is_none(),
9829 "expected handler-level error, not RPC error"
9830 );
9831 let result = resp.result.expect("must surface a result envelope");
9832 assert_eq!(result["isError"], true);
9833 }
9834
9835 #[test]
9840 fn i4_replay_skips_dangling_transcript_link() {
9841 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9842 i4_insert_test_memory(&conn, "mem-dangling");
9843
9844 let live = crate::transcripts::store(&conn, "team/eng", "live body", None).unwrap();
9845 let pruned = crate::transcripts::store(&conn, "team/eng", "pruned body", None).unwrap();
9846 crate::transcripts::link_transcript(&conn, "mem-dangling", &live.id, None, None).unwrap();
9847 crate::transcripts::link_transcript(&conn, "mem-dangling", &pruned.id, None, None).unwrap();
9848
9849 conn.execute("PRAGMA foreign_keys = OFF", []).unwrap();
9854 conn.execute(
9855 "DELETE FROM memory_transcripts WHERE id = ?1",
9856 rusqlite::params![pruned.id],
9857 )
9858 .unwrap();
9859 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
9860
9861 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-dangling"}));
9862 let resp = invoke_handle_request(&conn, &req);
9863 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
9864
9865 let payload = i4_decode_response_payload(&resp);
9866 let transcripts = payload["transcripts"].as_array().unwrap();
9867 assert_eq!(
9868 transcripts.len(),
9869 1,
9870 "only the live transcript should appear; pruned id is silently dropped"
9871 );
9872 assert_eq!(transcripts[0]["id"], live.id);
9873 }
9874
9875 fn reflect_test_seed_source(
9891 conn: &rusqlite::Connection,
9892 namespace: &str,
9893 title: &str,
9894 depth: i32,
9895 ) -> String {
9896 let now = chrono::Utc::now().to_rfc3339();
9897 let mem = Memory {
9898 id: uuid::Uuid::new_v4().to_string(),
9899 tier: Tier::Mid,
9900 namespace: namespace.to_string(),
9901 title: title.to_string(),
9902 content: format!("seed body for {title}"),
9903 tags: vec!["reflect-test".to_string()],
9904 priority: 5,
9905 confidence: 1.0,
9906 source: "test".to_string(),
9907 access_count: 0,
9908 created_at: now.clone(),
9909 updated_at: now,
9910 last_accessed_at: None,
9911 expires_at: None,
9912 metadata: json!({"agent_id": "test-agent-reflect"}),
9913 reflection_depth: depth,
9914 memory_kind: crate::models::MemoryKind::Observation,
9915 entity_id: None,
9916 persona_version: None,
9917 citations: Vec::new(),
9918 source_uri: None,
9919 source_span: None,
9920 confidence_source: crate::models::ConfidenceSource::CallerProvided,
9921 confidence_signals: None,
9922 confidence_decayed_at: None,
9923 version: 1,
9924 };
9925 db::insert(conn, &mem).unwrap()
9926 }
9927
9928 fn reflect_test_seed_governance(
9933 conn: &rusqlite::Connection,
9934 namespace: &str,
9935 governance: Value,
9936 ) {
9937 let now = chrono::Utc::now().to_rfc3339();
9938 let metadata = json!({
9939 "agent_id": "test-agent-reflect",
9940 "governance": governance,
9941 });
9942 let standard = Memory {
9943 id: uuid::Uuid::new_v4().to_string(),
9944 tier: Tier::Long,
9945 namespace: format!("_standards-{namespace}"),
9946 title: format!("standard for {namespace}"),
9947 content: "reflect-test policy".to_string(),
9948 tags: vec![],
9949 priority: 9,
9950 confidence: 1.0,
9951 source: "test".to_string(),
9952 access_count: 0,
9953 created_at: now.clone(),
9954 updated_at: now,
9955 last_accessed_at: None,
9956 expires_at: None,
9957 metadata,
9958 reflection_depth: 0,
9959 memory_kind: crate::models::MemoryKind::Observation,
9960 entity_id: None,
9961 persona_version: None,
9962 citations: Vec::new(),
9963 source_uri: None,
9964 source_span: None,
9965 confidence_source: crate::models::ConfidenceSource::CallerProvided,
9966 confidence_signals: None,
9967 confidence_decayed_at: None,
9968 version: 1,
9969 };
9970 let std_id = db::insert(conn, &standard).unwrap();
9971 db::set_namespace_standard(conn, namespace, &std_id, None).unwrap();
9972 }
9973
9974 #[test]
9977 fn handle_reflect_happy_path_single_source_returns_envelope() {
9978 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
9979 let src = reflect_test_seed_source(&conn, "team/reflect-a", "src-1", 0);
9980 let req = make_tools_call(
9981 "memory_reflect",
9982 json!({
9983 "source_ids": [src],
9984 "title": "pattern: alpha",
9985 "content": "synthesised reflection content",
9986 "namespace": "team/reflect-a",
9987 "tier": Tier::Mid.as_str(),
9988 "priority": 7,
9989 "confidence": 0.9,
9990 "tags": ["reflection", "alpha"],
9991 }),
9992 );
9993 let resp = invoke_handle_request(&conn, &req);
9994 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
9995 let payload = i4_decode_response_payload(&resp);
9996 assert!(payload["id"].is_string());
9997 assert_eq!(payload["reflection_depth"], 1);
9998 assert_eq!(payload["namespace"], "team/reflect-a");
9999 let reflects_on = payload["reflects_on"].as_array().unwrap();
10000 assert_eq!(reflects_on.len(), 1);
10001 }
10002
10003 #[test]
10004 fn handle_reflect_happy_path_metadata_object_is_accepted() {
10005 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10009 let src = reflect_test_seed_source(&conn, "team/reflect-meta", "src-1", 0);
10010 let req = make_tools_call(
10011 "memory_reflect",
10012 json!({
10013 "source_ids": [src],
10014 "title": "with metadata",
10015 "content": "body",
10016 "metadata": {"custom_field": "abc"},
10017 }),
10018 );
10019 let resp = invoke_handle_request(&conn, &req);
10020 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10021 let payload = i4_decode_response_payload(&resp);
10022 assert!(payload["id"].is_string());
10023 }
10024
10025 #[test]
10026 fn handle_reflect_omitted_namespace_defaults_to_first_source_namespace() {
10027 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10031 let src = reflect_test_seed_source(&conn, "team/reflect-defns", "src-1", 0);
10032 let req = make_tools_call(
10033 "memory_reflect",
10034 json!({
10035 "source_ids": [src],
10036 "title": "defaulted namespace",
10037 "content": "body",
10038 }),
10040 );
10041 let resp = invoke_handle_request(&conn, &req);
10042 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10043 let payload = i4_decode_response_payload(&resp);
10044 assert_eq!(payload["namespace"], "team/reflect-defns");
10045 }
10046
10047 #[test]
10048 fn handle_reflect_explicit_agent_id_is_honoured() {
10049 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10051 let src = reflect_test_seed_source(&conn, "team/reflect-aid", "src-1", 0);
10052 let req = make_tools_call(
10053 "memory_reflect",
10054 json!({
10055 "source_ids": [src],
10056 "title": "agent override",
10057 "content": "body",
10058 "namespace": "team/reflect-aid",
10059 "agent_id": "ai:explicit-agent",
10060 }),
10061 );
10062 let resp = invoke_handle_request(&conn, &req);
10063 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10064 let payload = i4_decode_response_payload(&resp);
10065 let new_id = payload["id"].as_str().unwrap();
10066 let stored = db::get(&conn, new_id).unwrap().unwrap();
10067 assert_eq!(
10068 stored.metadata["agent_id"].as_str(),
10069 Some("ai:explicit-agent")
10070 );
10071 }
10072
10073 #[test]
10074 fn handle_reflect_agent_id_from_metadata_blob_is_honoured() {
10075 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10078 let src = reflect_test_seed_source(&conn, "team/reflect-mid", "src-1", 0);
10079 let req = make_tools_call(
10080 "memory_reflect",
10081 json!({
10082 "source_ids": [src],
10083 "title": "agent from metadata",
10084 "content": "body",
10085 "namespace": "team/reflect-mid",
10086 "metadata": {"agent_id": "ai:meta-agent"},
10087 }),
10088 );
10089 let resp = invoke_handle_request(&conn, &req);
10090 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10091 let payload = i4_decode_response_payload(&resp);
10092 let new_id = payload["id"].as_str().unwrap();
10093 let stored = db::get(&conn, new_id).unwrap().unwrap();
10094 assert_eq!(stored.metadata["agent_id"].as_str(), Some("ai:meta-agent"));
10095 }
10096
10097 #[test]
10100 fn handle_reflect_missing_source_ids_returns_error() {
10101 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10102 let req = make_tools_call("memory_reflect", json!({"title": "t", "content": "c"}));
10103 let resp = invoke_handle_request(&conn, &req);
10104 let result = resp.result.unwrap();
10105 assert_eq!(result["isError"], true);
10106 let text = result["content"][0]["text"].as_str().unwrap();
10107 assert!(text.contains("source_ids"), "got {text}");
10108 }
10109
10110 #[test]
10111 fn handle_reflect_empty_source_ids_returns_error() {
10112 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10113 let req = make_tools_call(
10114 "memory_reflect",
10115 json!({"source_ids": [], "title": "t", "content": "c"}),
10116 );
10117 let resp = invoke_handle_request(&conn, &req);
10118 let result = resp.result.unwrap();
10119 assert_eq!(result["isError"], true);
10120 let text = result["content"][0]["text"].as_str().unwrap();
10121 assert!(text.contains("cannot be empty"), "got {text}");
10122 }
10123
10124 #[test]
10125 fn handle_reflect_non_string_source_id_returns_error() {
10126 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10129 let req = make_tools_call(
10130 "memory_reflect",
10131 json!({
10132 "source_ids": ["valid-id", 42, "another"],
10133 "title": "t",
10134 "content": "c",
10135 }),
10136 );
10137 let resp = invoke_handle_request(&conn, &req);
10138 let result = resp.result.unwrap();
10139 assert_eq!(result["isError"], true);
10140 let text = result["content"][0]["text"].as_str().unwrap();
10141 assert!(text.contains("source_ids[1]"), "got {text}");
10142 }
10143
10144 #[test]
10145 fn handle_reflect_missing_title_returns_error() {
10146 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10147 let src = reflect_test_seed_source(&conn, "team/r-mt", "src", 0);
10148 let req = make_tools_call(
10149 "memory_reflect",
10150 json!({"source_ids": [src], "content": "c"}),
10151 );
10152 let resp = invoke_handle_request(&conn, &req);
10153 let result = resp.result.unwrap();
10154 assert_eq!(result["isError"], true);
10155 let text = result["content"][0]["text"].as_str().unwrap();
10156 assert!(text.contains("title"), "got {text}");
10157 }
10158
10159 #[test]
10160 fn handle_reflect_missing_content_returns_error() {
10161 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10162 let src = reflect_test_seed_source(&conn, "team/r-mc", "src", 0);
10163 let req = make_tools_call("memory_reflect", json!({"source_ids": [src], "title": "t"}));
10164 let resp = invoke_handle_request(&conn, &req);
10165 let result = resp.result.unwrap();
10166 assert_eq!(result["isError"], true);
10167 let text = result["content"][0]["text"].as_str().unwrap();
10168 assert!(text.contains("content"), "got {text}");
10169 }
10170
10171 #[test]
10172 fn handle_reflect_invalid_tier_returns_error() {
10173 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10174 let src = reflect_test_seed_source(&conn, "team/r-tier", "src", 0);
10175 let req = make_tools_call(
10176 "memory_reflect",
10177 json!({
10178 "source_ids": [src],
10179 "title": "t",
10180 "content": "c",
10181 "tier": "ephemeral",
10182 }),
10183 );
10184 let resp = invoke_handle_request(&conn, &req);
10185 let result = resp.result.unwrap();
10186 assert_eq!(result["isError"], true);
10187 let text = result["content"][0]["text"].as_str().unwrap();
10188 assert!(text.contains("invalid tier"), "got {text}");
10189 }
10190
10191 #[test]
10194 fn handle_reflect_source_not_found_returns_error_string() {
10195 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10196 let req = make_tools_call(
10197 "memory_reflect",
10198 json!({
10199 "source_ids": ["nonexistent-id"],
10200 "title": "t",
10201 "content": "c",
10202 "namespace": "team/r-nf",
10203 }),
10204 );
10205 let resp = invoke_handle_request(&conn, &req);
10206 let result = resp.result.unwrap();
10207 assert_eq!(result["isError"], true);
10208 let text = result["content"][0]["text"].as_str().unwrap();
10209 assert!(text.contains("source memory not found"), "got {text}",);
10210 assert!(text.contains("nonexistent-id"), "got {text}");
10211 }
10212
10213 #[test]
10214 fn handle_reflect_depth_exceeded_returns_typed_error() {
10215 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10227 reflect_test_seed_governance(
10228 &conn,
10229 "team/r-depth",
10230 json!({
10231 "write": "any",
10232 "max_reflection_depth": 1,
10233 }),
10234 );
10235 let s1 = reflect_test_seed_source(&conn, "team/r-depth", "src-1", 1);
10236 let req = make_tools_call(
10237 "memory_reflect",
10238 json!({
10239 "source_ids": [s1],
10240 "title": "would be depth 2",
10241 "content": "body",
10242 "namespace": "team/r-depth",
10243 }),
10244 );
10245 let resp = invoke_handle_request(&conn, &req);
10246 let result = resp.result.unwrap();
10247 assert_eq!(result["isError"], true);
10248 let text = result["content"][0]["text"].as_str().unwrap();
10249 assert!(
10250 text.contains("REFLECTION_DEPTH_EXCEEDED"),
10251 "expected typed error prefix; got {text}",
10252 );
10253 assert!(text.contains("depth 2"), "got {text}");
10254 assert!(text.contains("max_reflection_depth 1"), "got {text}",);
10255 assert!(text.contains("namespace='team/r-depth'"), "got {text}",);
10256 }
10257
10258 #[test]
10261 fn handle_reflect_approval_gate_queues_pending_above_threshold() {
10262 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10267 reflect_test_seed_governance(
10268 &conn,
10269 "team/r-approve",
10270 json!({"require_approval_above_depth": 1}),
10271 );
10272 let s1 = reflect_test_seed_source(&conn, "team/r-approve", "src-1", 1);
10273 let req = make_tools_call(
10274 "memory_reflect",
10275 json!({
10276 "source_ids": [s1],
10277 "title": "would need approval",
10278 "content": "body",
10279 "namespace": "team/r-approve",
10280 }),
10281 );
10282 let resp = invoke_handle_request(&conn, &req);
10283 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10284 let payload = i4_decode_response_payload(&resp);
10285 assert_eq!(payload["status"], "pending");
10286 assert!(payload["pending_id"].is_string());
10287 assert_eq!(payload["action"], "reflect");
10288 assert_eq!(payload["namespace"], "team/r-approve");
10289 assert_eq!(payload["proposed_depth"], 2);
10290 assert_eq!(payload["require_approval_above_depth"], 1);
10291 }
10292
10293 #[test]
10294 fn handle_reflect_approval_gate_under_threshold_proceeds() {
10295 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10298 reflect_test_seed_governance(
10299 &conn,
10300 "team/r-under",
10301 json!({"require_approval_above_depth": 5}),
10302 );
10303 let s1 = reflect_test_seed_source(&conn, "team/r-under", "src-1", 0);
10304 let req = make_tools_call(
10305 "memory_reflect",
10306 json!({
10307 "source_ids": [s1],
10308 "title": "under threshold",
10309 "content": "body",
10310 "namespace": "team/r-under",
10311 }),
10312 );
10313 let resp = invoke_handle_request(&conn, &req);
10314 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10315 let payload = i4_decode_response_payload(&resp);
10316 assert!(payload["status"].as_str() != Some("pending"));
10318 assert!(payload["id"].is_string());
10319 assert_eq!(payload["reflection_depth"], 1);
10320 }
10321
10322 #[test]
10348 fn handle_check_duplicate_missing_title_returns_error() {
10349 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10350 let req = make_tools_call("memory_check_duplicate", json!({"content": "c"}));
10351 let resp = invoke_handle_request(&conn, &req);
10352 let result = resp.result.unwrap();
10353 assert_eq!(result["isError"], true);
10354 let text = result["content"][0]["text"].as_str().unwrap();
10355 assert!(text.contains("title"), "got {text}");
10356 }
10357
10358 #[test]
10359 fn handle_check_duplicate_missing_content_returns_error() {
10360 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10361 let req = make_tools_call("memory_check_duplicate", json!({"title": "t"}));
10362 let resp = invoke_handle_request(&conn, &req);
10363 let result = resp.result.unwrap();
10364 assert_eq!(result["isError"], true);
10365 let text = result["content"][0]["text"].as_str().unwrap();
10366 assert!(text.contains("content"), "got {text}");
10367 }
10368
10369 #[test]
10370 fn handle_check_duplicate_no_embedder_returns_error() {
10371 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10375 let req = make_tools_call(
10376 "memory_check_duplicate",
10377 json!({
10378 "title": "duplicate-check",
10379 "content": "body",
10380 "namespace": "team/dup",
10381 "threshold": 0.85,
10382 }),
10383 );
10384 let resp = invoke_handle_request(&conn, &req);
10385 let result = resp.result.unwrap();
10386 assert_eq!(result["isError"], true);
10387 let text = result["content"][0]["text"].as_str().unwrap();
10388 assert!(
10389 text.contains("requires the embedder"),
10390 "expected embedder-required error; got {text}",
10391 );
10392 }
10393
10394 #[test]
10395 fn handle_check_duplicate_whitespace_only_namespace_is_filtered() {
10396 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10403 let req = make_tools_call(
10404 "memory_check_duplicate",
10405 json!({
10406 "title": "t",
10407 "content": "c",
10408 "namespace": " ",
10409 }),
10410 );
10411 let resp = invoke_handle_request(&conn, &req);
10412 let result = resp.result.unwrap();
10413 assert_eq!(result["isError"], true);
10414 let text = result["content"][0]["text"].as_str().unwrap();
10415 assert!(
10421 text.contains("requires the embedder"),
10422 "expected fallthrough to embedder gate; got {text}",
10423 );
10424 }
10425
10426 #[test]
10427 fn handle_check_duplicate_explicit_threshold_is_accepted() {
10428 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10434 let req = make_tools_call(
10435 "memory_check_duplicate",
10436 json!({
10437 "title": "t",
10438 "content": "c",
10439 "threshold": 0.92,
10440 }),
10441 );
10442 let resp = invoke_handle_request(&conn, &req);
10443 let result = resp.result.unwrap();
10444 assert_eq!(result["isError"], true);
10445 let text = result["content"][0]["text"].as_str().unwrap();
10446 assert!(text.contains("requires the embedder"), "got {text}");
10447 }
10448
10449 #[test]
10450 fn handle_quota_status_with_agent_id_returns_single_envelope() {
10451 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10455 let req = make_tools_call("memory_quota_status", json!({"agent_id": "agent-status-a"}));
10456 let resp = invoke_handle_request(&conn, &req);
10457 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10458 let payload = i4_decode_response_payload(&resp);
10459 assert_eq!(payload["agent_id"], "agent-status-a");
10460 assert!(payload["quota"].is_object(), "expected quota object");
10462 assert!(payload["count"].is_null());
10464 assert!(payload["quotas"].is_null());
10465 }
10466
10467 #[test]
10468 fn handle_quota_status_without_agent_id_returns_list_envelope() {
10469 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10471 let _ = invoke_handle_request(
10474 &conn,
10475 &make_tools_call("memory_quota_status", json!({"agent_id": "agent-a"})),
10476 );
10477 let _ = invoke_handle_request(
10478 &conn,
10479 &make_tools_call("memory_quota_status", json!({"agent_id": "agent-b"})),
10480 );
10481 let req = make_tools_call("memory_quota_status", json!({}));
10482 let resp = invoke_handle_request(&conn, &req);
10483 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10484 let payload = i4_decode_response_payload(&resp);
10485 assert!(payload["count"].is_number());
10487 assert!(payload["quotas"].is_array());
10488 assert!(payload["count"].as_u64().unwrap() >= 2);
10489 assert!(payload["agent_id"].is_null());
10491 assert!(payload["quota"].is_null());
10492 }
10493
10494 #[test]
10506 fn handle_entity_register_missing_namespace_returns_error() {
10507 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10509 let req = make_tools_call("memory_entity_register", json!({"canonical_name": "Pluto"}));
10510 let resp = invoke_handle_request(&conn, &req);
10511 let result = resp.result.unwrap();
10512 assert_eq!(result["isError"], true);
10513 let text = result["content"][0]["text"].as_str().unwrap();
10514 assert!(text.contains("namespace"), "got {text}");
10515 }
10516
10517 #[test]
10518 fn handle_entity_register_metadata_object_is_accepted() {
10519 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10521 let req = make_tools_call(
10522 "memory_entity_register",
10523 json!({
10524 "canonical_name": "Charon",
10525 "namespace": "team/dwarf",
10526 "metadata": {"orbit": "outer"},
10527 }),
10528 );
10529 let resp = invoke_handle_request(&conn, &req);
10530 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10531 let payload = i4_decode_response_payload(&resp);
10532 assert!(payload["entity_id"].is_string());
10533 assert_eq!(payload["canonical_name"], "Charon");
10534 assert_eq!(payload["namespace"], "team/dwarf");
10535 }
10536
10537 #[test]
10538 fn handle_kg_invalidate_missing_target_id_returns_error() {
10539 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10541 let req = make_tools_call(
10542 "memory_kg_invalidate",
10543 json!({"source_id": "abc", "relation": "related_to"}),
10544 );
10545 let resp = invoke_handle_request(&conn, &req);
10546 let result = resp.result.unwrap();
10547 assert_eq!(result["isError"], true);
10548 let text = result["content"][0]["text"].as_str().unwrap();
10549 assert!(text.contains("target_id"), "got {text}");
10550 }
10551
10552 #[test]
10553 fn handle_kg_invalidate_missing_relation_returns_error() {
10554 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10556 let req = make_tools_call(
10557 "memory_kg_invalidate",
10558 json!({"source_id": "abc", "target_id": "def"}),
10559 );
10560 let resp = invoke_handle_request(&conn, &req);
10561 let result = resp.result.unwrap();
10562 assert_eq!(result["isError"], true);
10563 let text = result["content"][0]["text"].as_str().unwrap();
10564 assert!(text.contains("relation"), "got {text}");
10565 }
10566
10567 #[test]
10568 fn handle_reflect_approval_gate_uses_default_namespace() {
10569 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10572 reflect_test_seed_governance(
10573 &conn,
10574 "team/r-defgate",
10575 json!({"require_approval_above_depth": 0}),
10576 );
10577 let s1 = reflect_test_seed_source(&conn, "team/r-defgate", "src", 0);
10578 let req = make_tools_call(
10579 "memory_reflect",
10580 json!({
10581 "source_ids": [s1],
10582 "title": "default-namespace gate",
10583 "content": "body",
10584 }),
10586 );
10587 let resp = invoke_handle_request(&conn, &req);
10588 assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
10589 let payload = i4_decode_response_payload(&resp);
10590 assert_eq!(payload["status"], "pending");
10591 assert_eq!(payload["namespace"], "team/r-defgate");
10592 assert_eq!(payload["proposed_depth"], 1);
10593 }
10594
10595 #[test]
10612 fn handle_kg_invalidate_missing_source_id_returns_error() {
10613 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10614 let req = make_tools_call(
10615 "memory_kg_invalidate",
10616 json!({"target_id": "11111111-1111-1111-1111-111111111111", "relation": "related_to"}),
10617 );
10618 let resp = invoke_handle_request(&conn, &req);
10619 let result = resp.result.unwrap();
10620 assert_eq!(result["isError"], true);
10621 let text = result["content"][0]["text"].as_str().unwrap();
10622 assert!(text.contains("source_id"), "got {text}");
10623 }
10624
10625 #[test]
10630 fn handle_kg_invalidate_malformed_source_id_rejected() {
10631 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10632 let req = make_tools_call(
10633 "memory_kg_invalidate",
10634 json!({
10635 "source_id": "abc\u{0000}def",
10637 "target_id": "11111111-1111-1111-1111-111111111111",
10638 "relation": "related_to",
10639 }),
10640 );
10641 let resp = invoke_handle_request(&conn, &req);
10642 let result = resp.result.unwrap();
10643 assert_eq!(result["isError"], true);
10644 }
10645
10646 #[test]
10656 fn handle_kg_invalidate_orphan_link_uses_global_namespace_fallback() {
10657 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10658 let src = Memory {
10659 id: uuid::Uuid::new_v4().to_string(),
10660 tier: Tier::Long,
10661 namespace: "w12-orphan".into(),
10662 title: "orphan-src".into(),
10663 content: "c".into(),
10664 tags: vec![],
10665 priority: 5,
10666 confidence: 1.0,
10667 source: "test".into(),
10668 access_count: 0,
10669 created_at: chrono::Utc::now().to_rfc3339(),
10670 updated_at: chrono::Utc::now().to_rfc3339(),
10671 last_accessed_at: None,
10672 expires_at: None,
10673 metadata: json!({}),
10674 reflection_depth: 0,
10675 memory_kind: crate::models::MemoryKind::Observation,
10676 entity_id: None,
10677 persona_version: None,
10678 citations: Vec::new(),
10679 source_uri: None,
10680 source_span: None,
10681 confidence_source: crate::models::ConfidenceSource::CallerProvided,
10682 confidence_signals: None,
10683 confidence_decayed_at: None,
10684 version: 1,
10685 };
10686 let mut tgt = src.clone();
10687 tgt.id = uuid::Uuid::new_v4().to_string();
10688 tgt.title = "orphan-tgt".into();
10689 let src_id = db::insert(&conn, &src).unwrap();
10690 let tgt_id = db::insert(&conn, &tgt).unwrap();
10691 db::create_link(&conn, &src_id, &tgt_id, "related_to").unwrap();
10692
10693 conn.pragma_update(None, "foreign_keys", false).unwrap();
10697 conn.execute(
10698 "DELETE FROM memories WHERE id = ?1",
10699 rusqlite::params![&src_id],
10700 )
10701 .unwrap();
10702 conn.pragma_update(None, "foreign_keys", true).unwrap();
10703
10704 let req = make_tools_call(
10705 "memory_kg_invalidate",
10706 json!({
10707 "source_id": src_id,
10708 "target_id": tgt_id,
10709 "relation": "related_to",
10710 }),
10711 );
10712 let resp = invoke_handle_request(&conn, &req);
10713 assert!(resp.error.is_none(), "unexpected err: {:?}", resp.error);
10714 let text = resp.result.unwrap()["content"][0]["text"]
10715 .as_str()
10716 .unwrap()
10717 .to_string();
10718 let val: Value = serde_json::from_str(&text).unwrap();
10719 assert_eq!(val["found"], true);
10722 }
10723
10724 #[test]
10730 fn handle_kg_timeline_invalid_until_returns_error() {
10731 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10732 let req = make_tools_call(
10733 "memory_kg_timeline",
10734 json!({
10735 "source_id": "00000000-0000-0000-0000-000000000000",
10736 "until": "not-a-timestamp",
10737 }),
10738 );
10739 let resp = invoke_handle_request(&conn, &req);
10740 let result = resp.result.unwrap();
10741 assert_eq!(result["isError"], true);
10742 }
10743
10744 #[test]
10747 fn handle_kg_timeline_missing_source_id_returns_error() {
10748 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10749 let req = make_tools_call("memory_kg_timeline", json!({}));
10750 let resp = invoke_handle_request(&conn, &req);
10751 let result = resp.result.unwrap();
10752 assert_eq!(result["isError"], true);
10753 let text = result["content"][0]["text"].as_str().unwrap();
10754 assert!(text.contains("source_id"), "got {text}");
10755 }
10756
10757 #[test]
10763 fn handle_find_paths_missing_source_id_returns_error() {
10764 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10765 let req = make_tools_call(
10766 "memory_find_paths",
10767 json!({"target_id": "11111111-1111-1111-1111-111111111111"}),
10768 );
10769 let resp = invoke_handle_request(&conn, &req);
10770 let result = resp.result.unwrap();
10771 assert_eq!(result["isError"], true);
10772 let text = result["content"][0]["text"].as_str().unwrap();
10773 assert!(text.contains("source_id"), "got {text}");
10774 }
10775
10776 #[test]
10778 fn handle_find_paths_missing_target_id_returns_error() {
10779 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10780 let req = make_tools_call(
10781 "memory_find_paths",
10782 json!({"source_id": "00000000-0000-0000-0000-000000000000"}),
10783 );
10784 let resp = invoke_handle_request(&conn, &req);
10785 let result = resp.result.unwrap();
10786 assert_eq!(result["isError"], true);
10787 let text = result["content"][0]["text"].as_str().unwrap();
10788 assert!(text.contains("target_id"), "got {text}");
10789 }
10790
10791 #[test]
10794 fn handle_find_paths_invalid_source_id_rejected() {
10795 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10796 let req = make_tools_call(
10797 "memory_find_paths",
10798 json!({
10799 "source_id": "abc\u{0000}def",
10801 "target_id": "11111111-1111-1111-1111-111111111111",
10802 }),
10803 );
10804 let resp = invoke_handle_request(&conn, &req);
10805 let result = resp.result.unwrap();
10806 assert_eq!(result["isError"], true);
10807 }
10808
10809 #[test]
10813 fn handle_find_paths_happy_path_with_explicit_depth_and_limit() {
10814 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10815 let mk = |title: &str| Memory {
10817 id: uuid::Uuid::new_v4().to_string(),
10818 tier: Tier::Long,
10819 namespace: "w12-fp".into(),
10820 title: title.into(),
10821 content: "x".into(),
10822 tags: vec![],
10823 priority: 5,
10824 confidence: 1.0,
10825 source: "test".into(),
10826 access_count: 0,
10827 created_at: chrono::Utc::now().to_rfc3339(),
10828 updated_at: chrono::Utc::now().to_rfc3339(),
10829 last_accessed_at: None,
10830 expires_at: None,
10831 metadata: json!({}),
10832 reflection_depth: 0,
10833 memory_kind: crate::models::MemoryKind::Observation,
10834 entity_id: None,
10835 persona_version: None,
10836 citations: Vec::new(),
10837 source_uri: None,
10838 source_span: None,
10839 confidence_source: crate::models::ConfidenceSource::CallerProvided,
10840 confidence_signals: None,
10841 confidence_decayed_at: None,
10842 version: 1,
10843 };
10844 let a = db::insert(&conn, &mk("a")).unwrap();
10845 let b = db::insert(&conn, &mk("b")).unwrap();
10846 let c = db::insert(&conn, &mk("c")).unwrap();
10847 db::create_link(&conn, &a, &b, "related_to").unwrap();
10848 db::create_link(&conn, &b, &c, "related_to").unwrap();
10849
10850 let req = make_tools_call(
10851 "memory_find_paths",
10852 json!({
10853 "source_id": a,
10854 "target_id": c,
10855 "max_depth": 5_u64,
10856 "max_results": 10_u64,
10857 }),
10858 );
10859 let resp = invoke_handle_request(&conn, &req);
10860 assert!(resp.error.is_none(), "err: {:?}", resp.error);
10861 let text = resp.result.unwrap()["content"][0]["text"]
10862 .as_str()
10863 .unwrap()
10864 .to_string();
10865 let val: Value = serde_json::from_str(&text).unwrap();
10866 assert!(val["count"].as_u64().unwrap() >= 1, "got {val}");
10867 let paths = val["paths"].as_array().unwrap();
10868 assert!(!paths.is_empty());
10869 }
10870
10871 #[test]
10875 fn handle_find_paths_zero_depth_surfaces_db_error_verbatim() {
10876 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10877 let req = make_tools_call(
10878 "memory_find_paths",
10879 json!({
10880 "source_id": "00000000-0000-0000-0000-000000000000",
10881 "target_id": "11111111-1111-1111-1111-111111111111",
10882 "max_depth": 0_u64,
10883 }),
10884 );
10885 let resp = invoke_handle_request(&conn, &req);
10886 let result = resp.result.unwrap();
10887 assert_eq!(result["isError"], true);
10888 let text = result["content"][0]["text"].as_str().unwrap();
10889 assert!(text.contains("max_depth"), "got {text}");
10890 }
10891
10892 #[test]
10896 fn handle_find_paths_excessive_depth_surfaces_max_error() {
10897 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10898 let req = make_tools_call(
10899 "memory_find_paths",
10900 json!({
10901 "source_id": "00000000-0000-0000-0000-000000000000",
10902 "target_id": "11111111-1111-1111-1111-111111111111",
10903 "max_depth": 1_000_000_u64,
10904 }),
10905 );
10906 let resp = invoke_handle_request(&conn, &req);
10907 let result = resp.result.unwrap();
10908 assert_eq!(result["isError"], true);
10909 let text = result["content"][0]["text"].as_str().unwrap();
10910 assert!(
10911 text.contains("max_depth") || text.contains("FIND_PATHS_MAX_DEPTH"),
10912 "got {text}"
10913 );
10914 }
10915
10916 #[test]
10920 fn handle_find_paths_include_invalidated_true_round_trip() {
10921 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10922 let mem = Memory {
10925 id: uuid::Uuid::new_v4().to_string(),
10926 tier: Tier::Long,
10927 namespace: "w12-fp-inv".into(),
10928 title: "solo".into(),
10929 content: "c".into(),
10930 tags: vec![],
10931 priority: 5,
10932 confidence: 1.0,
10933 source: "test".into(),
10934 access_count: 0,
10935 created_at: chrono::Utc::now().to_rfc3339(),
10936 updated_at: chrono::Utc::now().to_rfc3339(),
10937 last_accessed_at: None,
10938 expires_at: None,
10939 metadata: json!({}),
10940 reflection_depth: 0,
10941 memory_kind: crate::models::MemoryKind::Observation,
10942 entity_id: None,
10943 persona_version: None,
10944 citations: Vec::new(),
10945 source_uri: None,
10946 source_span: None,
10947 confidence_source: crate::models::ConfidenceSource::CallerProvided,
10948 confidence_signals: None,
10949 confidence_decayed_at: None,
10950 version: 1,
10951 };
10952 let id = db::insert(&conn, &mem).unwrap();
10953 let req = make_tools_call(
10954 "memory_find_paths",
10955 json!({
10956 "source_id": id,
10957 "target_id": id,
10958 "include_invalidated": true,
10959 }),
10960 );
10961 let resp = invoke_handle_request(&conn, &req);
10962 assert!(resp.error.is_none(), "err: {:?}", resp.error);
10963 let text = resp.result.unwrap()["content"][0]["text"]
10964 .as_str()
10965 .unwrap()
10966 .to_string();
10967 let val: Value = serde_json::from_str(&text).unwrap();
10968 assert!(val["count"].as_u64().unwrap() >= 1);
10969 }
10970
10971 #[test]
10975 fn handle_entity_get_by_alias_missing_alias_returns_error() {
10976 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10977 let req = make_tools_call("memory_entity_get_by_alias", json!({}));
10978 let resp = invoke_handle_request(&conn, &req);
10979 let result = resp.result.unwrap();
10980 assert_eq!(result["isError"], true);
10981 let text = result["content"][0]["text"].as_str().unwrap();
10982 assert!(text.contains("alias"), "got {text}");
10983 }
10984
10985 #[test]
10988 fn handle_entity_get_by_alias_invalid_namespace_rejected() {
10989 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
10990 let req = make_tools_call(
10991 "memory_entity_get_by_alias",
10992 json!({"alias": "any", "namespace": "BAD NS"}),
10993 );
10994 let resp = invoke_handle_request(&conn, &req);
10995 let result = resp.result.unwrap();
10996 assert_eq!(result["isError"], true);
10997 }
10998
10999 #[test]
11004 fn handle_entity_get_by_alias_registered_alias_resolves() {
11005 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11006 let reg = make_tools_call(
11009 "memory_entity_register",
11010 json!({
11011 "canonical_name": "Acme Inc",
11012 "namespace": "w12-entities",
11013 "aliases": ["Acme", "ACME"],
11014 }),
11015 );
11016 let reg_resp = invoke_handle_request(&conn, ®);
11017 assert!(
11018 reg_resp.error.is_none(),
11019 "entity_register err: {:?}",
11020 reg_resp.error
11021 );
11022
11023 let req = make_tools_call(
11024 "memory_entity_get_by_alias",
11025 json!({"alias": "Acme", "namespace": "w12-entities"}),
11026 );
11027 let resp = invoke_handle_request(&conn, &req);
11028 assert!(resp.error.is_none(), "err: {:?}", resp.error);
11029 let text = resp.result.unwrap()["content"][0]["text"]
11030 .as_str()
11031 .unwrap()
11032 .to_string();
11033 let val: Value = serde_json::from_str(&text).unwrap();
11034 assert_eq!(val["found"], true, "got {val}");
11035 assert_eq!(val["canonical_name"], "Acme Inc");
11036 assert_eq!(val["namespace"], "w12-entities");
11037 assert!(val["aliases"].is_array());
11038 }
11039
11040 #[test]
11045 fn handle_entity_get_by_alias_whitespace_only_namespace_treated_as_none() {
11046 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11047 let req = make_tools_call(
11048 "memory_entity_get_by_alias",
11049 json!({"alias": "x", "namespace": " "}),
11050 );
11051 let resp = invoke_handle_request(&conn, &req);
11052 assert!(resp.error.is_none(), "err: {:?}", resp.error);
11053 }
11054
11055 #[test]
11060 fn handle_verify_missing_required_args_returns_error() {
11061 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11062 let req = make_tools_call("memory_verify", json!({}));
11063 let resp = invoke_handle_request(&conn, &req);
11064 let result = resp.result.unwrap();
11065 assert_eq!(result["isError"], true);
11066 let text = result["content"][0]["text"].as_str().unwrap();
11067 assert!(
11068 text.contains("link_id") || text.contains("source_id"),
11069 "got {text}"
11070 );
11071 }
11072
11073 #[test]
11075 fn handle_verify_source_id_without_target_id_returns_error() {
11076 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11077 let req = make_tools_call(
11078 "memory_verify",
11079 json!({"source_id": "00000000-0000-0000-0000-000000000000"}),
11080 );
11081 let resp = invoke_handle_request(&conn, &req);
11082 let result = resp.result.unwrap();
11083 assert_eq!(result["isError"], true);
11084 }
11085
11086 #[test]
11089 fn handle_verify_malformed_link_id_returns_error() {
11090 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11091 let req = make_tools_call("memory_verify", json!({"link_id": "totally-bad-shape"}));
11092 let resp = invoke_handle_request(&conn, &req);
11093 let result = resp.result.unwrap();
11094 assert_eq!(result["isError"], true);
11095 let text = result["content"][0]["text"].as_str().unwrap();
11096 assert!(text.contains("link_id"), "got {text}");
11097 }
11098
11099 #[test]
11102 fn handle_verify_invalid_link_rejected_by_validator() {
11103 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11104 let req = make_tools_call(
11105 "memory_verify",
11106 json!({
11107 "source_id": "not a uuid",
11108 "target_id": "11111111-1111-1111-1111-111111111111",
11109 "relation": "related_to",
11110 }),
11111 );
11112 let resp = invoke_handle_request(&conn, &req);
11113 let result = resp.result.unwrap();
11114 assert_eq!(result["isError"], true);
11115 }
11116
11117 #[test]
11121 fn handle_verify_missing_link_returns_not_found_error() {
11122 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11123 let src = Memory {
11126 id: uuid::Uuid::new_v4().to_string(),
11127 tier: Tier::Long,
11128 namespace: "w12-vfn".into(),
11129 title: "src".into(),
11130 content: "c".into(),
11131 tags: vec![],
11132 priority: 5,
11133 confidence: 1.0,
11134 source: "test".into(),
11135 access_count: 0,
11136 created_at: chrono::Utc::now().to_rfc3339(),
11137 updated_at: chrono::Utc::now().to_rfc3339(),
11138 last_accessed_at: None,
11139 expires_at: None,
11140 metadata: json!({}),
11141 reflection_depth: 0,
11142 memory_kind: crate::models::MemoryKind::Observation,
11143 entity_id: None,
11144 persona_version: None,
11145 citations: Vec::new(),
11146 source_uri: None,
11147 source_span: None,
11148 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11149 confidence_signals: None,
11150 confidence_decayed_at: None,
11151 version: 1,
11152 };
11153 let mut tgt = src.clone();
11154 tgt.id = uuid::Uuid::new_v4().to_string();
11155 tgt.title = "tgt".into();
11156 let src_id = db::insert(&conn, &src).unwrap();
11157 let tgt_id = db::insert(&conn, &tgt).unwrap();
11158 let req = make_tools_call(
11159 "memory_verify",
11160 json!({
11161 "source_id": src_id,
11162 "target_id": tgt_id,
11163 "relation": "related_to",
11164 }),
11165 );
11166 let resp = invoke_handle_request(&conn, &req);
11167 let result = resp.result.unwrap();
11168 assert_eq!(result["isError"], true);
11169 let text = result["content"][0]["text"].as_str().unwrap();
11170 assert!(text.contains("link not found"), "got {text}");
11171 }
11172
11173 #[test]
11179 fn handle_verify_unsigned_link_reports_unsigned_and_null_fields() {
11180 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11181 let src = Memory {
11182 id: uuid::Uuid::new_v4().to_string(),
11183 tier: Tier::Long,
11184 namespace: "w12-vu".into(),
11185 title: "src".into(),
11186 content: "c".into(),
11187 tags: vec![],
11188 priority: 5,
11189 confidence: 1.0,
11190 source: "test".into(),
11191 access_count: 0,
11192 created_at: chrono::Utc::now().to_rfc3339(),
11193 updated_at: chrono::Utc::now().to_rfc3339(),
11194 last_accessed_at: None,
11195 expires_at: None,
11196 metadata: json!({}),
11197 reflection_depth: 0,
11198 memory_kind: crate::models::MemoryKind::Observation,
11199 entity_id: None,
11200 persona_version: None,
11201 citations: Vec::new(),
11202 source_uri: None,
11203 source_span: None,
11204 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11205 confidence_signals: None,
11206 confidence_decayed_at: None,
11207 version: 1,
11208 };
11209 let mut tgt = src.clone();
11210 tgt.id = uuid::Uuid::new_v4().to_string();
11211 tgt.title = "tgt".into();
11212 let src_id = db::insert(&conn, &src).unwrap();
11213 let tgt_id = db::insert(&conn, &tgt).unwrap();
11214
11215 let attest = db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", None)
11219 .expect("create_link_signed (unsigned)");
11220 assert_eq!(attest, "unsigned");
11221
11222 let req = make_tools_call(
11223 "memory_verify",
11224 json!({
11225 "source_id": src_id,
11226 "target_id": tgt_id,
11227 "relation": "related_to",
11228 }),
11229 );
11230 let resp = invoke_handle_request(&conn, &req);
11231 assert!(resp.error.is_none(), "err: {:?}", resp.error);
11232 let text = resp.result.unwrap()["content"][0]["text"]
11233 .as_str()
11234 .unwrap()
11235 .to_string();
11236 let val: Value = serde_json::from_str(&text).unwrap();
11237 assert_eq!(val["signature_verified"], false);
11238 assert_eq!(val["attest_level"], "unsigned");
11239 assert!(val["signed_by"].is_null());
11240 assert!(val["signed_at"].is_null());
11241 }
11242
11243 #[test]
11247 fn handle_verify_link_id_composite_form_resolves_same_row() {
11248 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11249 let src = Memory {
11250 id: uuid::Uuid::new_v4().to_string(),
11251 tier: Tier::Long,
11252 namespace: "w12-vc".into(),
11253 title: "src".into(),
11254 content: "c".into(),
11255 tags: vec![],
11256 priority: 5,
11257 confidence: 1.0,
11258 source: "test".into(),
11259 access_count: 0,
11260 created_at: chrono::Utc::now().to_rfc3339(),
11261 updated_at: chrono::Utc::now().to_rfc3339(),
11262 last_accessed_at: None,
11263 expires_at: None,
11264 metadata: json!({}),
11265 reflection_depth: 0,
11266 memory_kind: crate::models::MemoryKind::Observation,
11267 entity_id: None,
11268 persona_version: None,
11269 citations: Vec::new(),
11270 source_uri: None,
11271 source_span: None,
11272 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11273 confidence_signals: None,
11274 confidence_decayed_at: None,
11275 version: 1,
11276 };
11277 let mut tgt = src.clone();
11278 tgt.id = uuid::Uuid::new_v4().to_string();
11279 tgt.title = "tgt".into();
11280 let src_id = db::insert(&conn, &src).unwrap();
11281 let tgt_id = db::insert(&conn, &tgt).unwrap();
11282 db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", None)
11283 .expect("create_link_signed");
11284
11285 let composite = format!("{src_id}--related_to-->{tgt_id}");
11286 let req = make_tools_call("memory_verify", json!({"link_id": composite}));
11287 let resp = invoke_handle_request(&conn, &req);
11288 assert!(resp.error.is_none(), "err: {:?}", resp.error);
11289 let text = resp.result.unwrap()["content"][0]["text"]
11290 .as_str()
11291 .unwrap()
11292 .to_string();
11293 let val: Value = serde_json::from_str(&text).unwrap();
11294 assert_eq!(val["signature_verified"], false);
11295 assert_eq!(val["attest_level"], "unsigned");
11296 }
11297
11298 fn verify_key_env_guard() -> &'static std::sync::Mutex<()> {
11304 crate::identity::keypair::key_dir_env_lock()
11305 }
11306
11307 #[test]
11316 fn handle_verify_signed_link_without_local_pubkey_reports_stored_attest_and_unverified() {
11317 let _g = verify_key_env_guard()
11320 .lock()
11321 .unwrap_or_else(std::sync::PoisonError::into_inner);
11322 let prev = std::env::var("AI_MEMORY_KEY_DIR").ok();
11323 let tmp = tempfile::TempDir::new().expect("tempdir");
11324 unsafe {
11326 std::env::set_var("AI_MEMORY_KEY_DIR", tmp.path());
11327 }
11328
11329 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11330 let src = Memory {
11331 id: uuid::Uuid::new_v4().to_string(),
11332 tier: Tier::Long,
11333 namespace: "w12-vnk".into(),
11334 title: "src".into(),
11335 content: "c".into(),
11336 tags: vec![],
11337 priority: 5,
11338 confidence: 1.0,
11339 source: "test".into(),
11340 access_count: 0,
11341 created_at: chrono::Utc::now().to_rfc3339(),
11342 updated_at: chrono::Utc::now().to_rfc3339(),
11343 last_accessed_at: None,
11344 expires_at: None,
11345 metadata: json!({}),
11346 reflection_depth: 0,
11347 memory_kind: crate::models::MemoryKind::Observation,
11348 entity_id: None,
11349 persona_version: None,
11350 citations: Vec::new(),
11351 source_uri: None,
11352 source_span: None,
11353 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11354 confidence_signals: None,
11355 confidence_decayed_at: None,
11356 version: 1,
11357 };
11358 let mut tgt = src.clone();
11359 tgt.id = uuid::Uuid::new_v4().to_string();
11360 tgt.title = "tgt".into();
11361 let src_id = db::insert(&conn, &src).unwrap();
11362 let tgt_id = db::insert(&conn, &tgt).unwrap();
11363
11364 let alice = crate::identity::keypair::generate("alice").unwrap();
11369 let attest = db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", Some(&alice))
11370 .expect("create_link_signed");
11371 assert_eq!(attest, "self_signed");
11372
11373 let req = make_tools_call(
11374 "memory_verify",
11375 json!({
11376 "source_id": src_id,
11377 "target_id": tgt_id,
11378 "relation": "related_to",
11379 }),
11380 );
11381 let resp = invoke_handle_request(&conn, &req);
11382 assert!(resp.error.is_none(), "err: {:?}", resp.error);
11383 let text = resp.result.unwrap()["content"][0]["text"]
11384 .as_str()
11385 .unwrap()
11386 .to_string();
11387 let val: Value = serde_json::from_str(&text).unwrap();
11388 assert_eq!(val["signature_verified"], false);
11389 assert_eq!(val["attest_level"], "self_signed");
11392
11393 unsafe {
11395 match prev {
11396 Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
11397 None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
11398 }
11399 }
11400 }
11401
11402 #[test]
11410 fn handle_verify_self_signed_link_verifies_and_populates_signed_fields() {
11411 let _g = verify_key_env_guard()
11412 .lock()
11413 .unwrap_or_else(std::sync::PoisonError::into_inner);
11414 let prev = std::env::var("AI_MEMORY_KEY_DIR").ok();
11415 let key_tmp = tempfile::TempDir::new().expect("key tempdir");
11416 unsafe {
11418 std::env::set_var("AI_MEMORY_KEY_DIR", key_tmp.path());
11419 }
11420
11421 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11422 let src = Memory {
11423 id: uuid::Uuid::new_v4().to_string(),
11424 tier: Tier::Long,
11425 namespace: "w12-vss".into(),
11426 title: "src".into(),
11427 content: "c".into(),
11428 tags: vec![],
11429 priority: 5,
11430 confidence: 1.0,
11431 source: "test".into(),
11432 access_count: 0,
11433 created_at: chrono::Utc::now().to_rfc3339(),
11434 updated_at: chrono::Utc::now().to_rfc3339(),
11435 last_accessed_at: None,
11436 expires_at: None,
11437 metadata: json!({}),
11438 reflection_depth: 0,
11439 memory_kind: crate::models::MemoryKind::Observation,
11440 entity_id: None,
11441 persona_version: None,
11442 citations: Vec::new(),
11443 source_uri: None,
11444 source_span: None,
11445 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11446 confidence_signals: None,
11447 confidence_decayed_at: None,
11448 version: 1,
11449 };
11450 let mut tgt = src.clone();
11451 tgt.id = uuid::Uuid::new_v4().to_string();
11452 tgt.title = "tgt".into();
11453 let src_id = db::insert(&conn, &src).unwrap();
11454 let tgt_id = db::insert(&conn, &tgt).unwrap();
11455
11456 let alice = crate::identity::keypair::generate("alice").unwrap();
11459 crate::identity::keypair::save(&alice, key_tmp.path()).unwrap();
11460 let attest = db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", Some(&alice))
11461 .expect("create_link_signed");
11462 assert_eq!(attest, "self_signed");
11463
11464 let req = make_tools_call(
11465 "memory_verify",
11466 json!({
11467 "source_id": src_id,
11468 "target_id": tgt_id,
11469 "relation": "related_to",
11470 }),
11471 );
11472 let resp = invoke_handle_request(&conn, &req);
11473 assert!(resp.error.is_none(), "err: {:?}", resp.error);
11474 let text = resp.result.unwrap()["content"][0]["text"]
11475 .as_str()
11476 .unwrap()
11477 .to_string();
11478 let val: Value = serde_json::from_str(&text).unwrap();
11479 assert_eq!(val["signature_verified"], true, "got {val}");
11480 assert_eq!(val["attest_level"], "self_signed");
11481 assert_eq!(val["signed_by"], "alice");
11482 assert!(
11483 val["signed_at"].is_string(),
11484 "signed_at must be RFC3339 string, got {:?}",
11485 val["signed_at"]
11486 );
11487
11488 unsafe {
11490 match prev {
11491 Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
11492 None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
11493 }
11494 }
11495 }
11496
11497 #[test]
11503 fn handle_verify_tampered_signature_returns_false_and_unsigned() {
11504 let _g = verify_key_env_guard()
11505 .lock()
11506 .unwrap_or_else(std::sync::PoisonError::into_inner);
11507 let prev = std::env::var("AI_MEMORY_KEY_DIR").ok();
11508 let key_tmp = tempfile::TempDir::new().expect("key tempdir");
11509 unsafe {
11511 std::env::set_var("AI_MEMORY_KEY_DIR", key_tmp.path());
11512 }
11513
11514 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11515 let src = Memory {
11516 id: uuid::Uuid::new_v4().to_string(),
11517 tier: Tier::Long,
11518 namespace: "w12-vts".into(),
11519 title: "src".into(),
11520 content: "c".into(),
11521 tags: vec![],
11522 priority: 5,
11523 confidence: 1.0,
11524 source: "test".into(),
11525 access_count: 0,
11526 created_at: chrono::Utc::now().to_rfc3339(),
11527 updated_at: chrono::Utc::now().to_rfc3339(),
11528 last_accessed_at: None,
11529 expires_at: None,
11530 metadata: json!({}),
11531 reflection_depth: 0,
11532 memory_kind: crate::models::MemoryKind::Observation,
11533 entity_id: None,
11534 persona_version: None,
11535 citations: Vec::new(),
11536 source_uri: None,
11537 source_span: None,
11538 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11539 confidence_signals: None,
11540 confidence_decayed_at: None,
11541 version: 1,
11542 };
11543 let mut tgt = src.clone();
11544 tgt.id = uuid::Uuid::new_v4().to_string();
11545 tgt.title = "tgt".into();
11546 let src_id = db::insert(&conn, &src).unwrap();
11547 let tgt_id = db::insert(&conn, &tgt).unwrap();
11548
11549 let alice = crate::identity::keypair::generate("alice").unwrap();
11550 crate::identity::keypair::save(&alice, key_tmp.path()).unwrap();
11551 db::create_link_signed(&conn, &src_id, &tgt_id, "related_to", Some(&alice))
11552 .expect("create_link_signed");
11553
11554 let original_sig: Vec<u8> = conn
11556 .query_row(
11557 "SELECT signature FROM memory_links \
11558 WHERE source_id = ?1 AND target_id = ?2",
11559 rusqlite::params![&src_id, &tgt_id],
11560 |row| row.get::<_, Vec<u8>>(0),
11561 )
11562 .expect("read signature");
11563 assert_eq!(original_sig.len(), 64);
11564 let mut tampered = original_sig.clone();
11565 tampered[0] ^= 0xFF;
11566 conn.execute(
11567 "UPDATE memory_links SET signature = ?3 \
11568 WHERE source_id = ?1 AND target_id = ?2",
11569 rusqlite::params![&src_id, &tgt_id, &tampered],
11570 )
11571 .unwrap();
11572
11573 let req = make_tools_call(
11574 "memory_verify",
11575 json!({
11576 "source_id": src_id,
11577 "target_id": tgt_id,
11578 "relation": "related_to",
11579 }),
11580 );
11581 let resp = invoke_handle_request(&conn, &req);
11582 assert!(resp.error.is_none(), "err: {:?}", resp.error);
11583 let text = resp.result.unwrap()["content"][0]["text"]
11584 .as_str()
11585 .unwrap()
11586 .to_string();
11587 let val: Value = serde_json::from_str(&text).unwrap();
11588 assert_eq!(val["signature_verified"], false, "got {val}");
11589 assert_eq!(val["attest_level"], "unsigned");
11590 assert!(val["signed_by"].is_null());
11591 assert!(val["signed_at"].is_null());
11592
11593 unsafe {
11595 match prev {
11596 Some(v) => std::env::set_var("AI_MEMORY_KEY_DIR", v),
11597 None => std::env::remove_var("AI_MEMORY_KEY_DIR"),
11598 }
11599 }
11600 }
11601
11602 fn chunkc_seed_memory(
11617 conn: &rusqlite::Connection,
11618 namespace: &str,
11619 title: &str,
11620 tier: Tier,
11621 ) -> String {
11622 let now = chrono::Utc::now().to_rfc3339();
11623 let mem = Memory {
11624 id: uuid::Uuid::new_v4().to_string(),
11625 tier,
11626 namespace: namespace.to_string(),
11627 title: title.to_string(),
11628 content: format!("body for {title}"),
11629 tags: vec!["chunkc".to_string()],
11630 priority: 5,
11631 confidence: 1.0,
11632 source: "test".to_string(),
11633 access_count: 0,
11634 created_at: now.clone(),
11635 updated_at: now,
11636 last_accessed_at: None,
11637 expires_at: None,
11638 metadata: json!({"agent_id": "test-agent-chunkc", "scope": "public"}),
11642 reflection_depth: 0,
11643 memory_kind: crate::models::MemoryKind::Observation,
11644 entity_id: None,
11645 persona_version: None,
11646 citations: Vec::new(),
11647 source_uri: None,
11648 source_span: None,
11649 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11650 confidence_signals: None,
11651 confidence_decayed_at: None,
11652 version: 1,
11653 };
11654 db::insert(conn, &mem).unwrap()
11655 }
11656
11657 fn seed_family_owned(
11663 conn: &rusqlite::Connection,
11664 namespace: &str,
11665 family: &str,
11666 owner: &str,
11667 scope: &str,
11668 ) -> String {
11669 let now = chrono::Utc::now().to_rfc3339();
11670 let mem = Memory {
11671 id: uuid::Uuid::new_v4().to_string(),
11672 tier: Tier::Mid,
11673 namespace: namespace.to_string(),
11674 title: format!("{family}-{owner}"),
11675 content: format!("owned by {owner}"),
11676 tags: vec![],
11677 priority: 5,
11678 confidence: 1.0,
11679 source: "test".to_string(),
11680 access_count: 0,
11681 created_at: now.clone(),
11682 updated_at: now,
11683 last_accessed_at: None,
11684 expires_at: None,
11685 metadata: json!({"family": family, "agent_id": owner, "scope": scope}),
11686 reflection_depth: 0,
11687 memory_kind: crate::models::MemoryKind::Observation,
11688 entity_id: None,
11689 persona_version: None,
11690 citations: Vec::new(),
11691 source_uri: None,
11692 source_span: None,
11693 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11694 confidence_signals: None,
11695 confidence_decayed_at: None,
11696 version: 1,
11697 };
11698 db::insert(conn, &mem).unwrap()
11699 }
11700
11701 #[test]
11702 fn load_family_filters_other_agents_private_1555() {
11703 let (owner_a, owner_b) = ("alice", "bob");
11706 let (ns, fam) = ("shared-fam-ns", "core");
11707 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11708 let a_id = seed_family_owned(&conn, ns, fam, owner_a, "private");
11709 let b_id = seed_family_owned(&conn, ns, fam, owner_b, "private");
11710 let q = json!({"family": fam, "namespace": ns, "k": 100});
11711 let ids = |resp: &Value| -> Vec<String> {
11712 resp["memories"]
11713 .as_array()
11714 .unwrap()
11715 .iter()
11716 .map(|m| m["id"].as_str().unwrap().to_string())
11717 .collect()
11718 };
11719 let r_b = crate::mcp::handle_load_family(&conn, &q, Some(owner_b)).unwrap();
11721 let b_ids = ids(&r_b);
11722 assert!(b_ids.contains(&b_id), "{owner_b} sees own row");
11723 assert!(
11724 !b_ids.contains(&a_id),
11725 "{owner_a}'s private row filtered for {owner_b}"
11726 );
11727 assert_eq!(
11728 r_b["count"].as_u64(),
11729 Some(1),
11730 "count recomputed post-filter"
11731 );
11732 let r_a = crate::mcp::handle_load_family(&conn, &q, Some(owner_a)).unwrap();
11734 assert!(ids(&r_a).contains(&a_id));
11735 let r_all = crate::mcp::handle_load_family(&conn, &q, None).unwrap();
11737 assert_eq!(
11738 r_all["count"].as_u64(),
11739 Some(2),
11740 "None == trust-all (legacy)"
11741 );
11742 }
11743
11744 #[test]
11745 fn smart_load_filters_other_agents_private_1555() {
11746 let (owner_a, owner_b) = ("alice", "bob");
11747 let (ns, fam) = ("shared-fam-ns2", "core");
11748 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11749 let a_id = seed_family_owned(&conn, ns, fam, owner_a, "private");
11750 let b_id = seed_family_owned(&conn, ns, fam, owner_b, "collective");
11751 let resp = crate::mcp::handle_smart_load(
11753 &conn,
11754 &json!({"intent": "", "namespace": ns, "k": 100}),
11755 None,
11756 Some(owner_b),
11757 )
11758 .unwrap();
11759 let ids: Vec<String> = resp["memories"]
11760 .as_array()
11761 .unwrap()
11762 .iter()
11763 .map(|m| m["id"].as_str().unwrap().to_string())
11764 .collect();
11765 assert!(
11766 !ids.contains(&a_id),
11767 "{owner_a}'s private row filtered via smart_load for {owner_b}"
11768 );
11769 assert!(
11770 ids.contains(&b_id),
11771 "{owner_b}'s own collective row is visible"
11772 );
11773 }
11774
11775 fn chunkc_seed_family_memory(
11776 conn: &rusqlite::Connection,
11777 namespace: &str,
11778 family: &str,
11779 ) -> String {
11780 let now = chrono::Utc::now().to_rfc3339();
11781 let mem = Memory {
11782 id: uuid::Uuid::new_v4().to_string(),
11783 tier: Tier::Mid,
11784 namespace: namespace.to_string(),
11785 title: format!("{family}-mem"),
11786 content: format!("seeded for {family}"),
11787 tags: vec![],
11788 priority: 5,
11789 confidence: 1.0,
11790 source: "test".to_string(),
11791 access_count: 0,
11792 created_at: now.clone(),
11793 updated_at: now,
11794 last_accessed_at: None,
11795 expires_at: None,
11796 metadata: json!({"family": family, "agent_id": "test-agent-chunkc"}),
11797 reflection_depth: 0,
11798 memory_kind: crate::models::MemoryKind::Observation,
11799 entity_id: None,
11800 persona_version: None,
11801 citations: Vec::new(),
11802 source_uri: None,
11803 source_span: None,
11804 confidence_source: crate::models::ConfidenceSource::CallerProvided,
11805 confidence_signals: None,
11806 confidence_decayed_at: None,
11807 version: 1,
11808 };
11809 db::insert(conn, &mem).unwrap()
11810 }
11811
11812 fn chunkc_lock_perms() -> std::sync::MutexGuard<'static, ()> {
11816 let g = crate::config::lock_permissions_mode_for_test();
11817 crate::config::clear_permissions_mode_override_for_test();
11818 crate::permissions::clear_active_permission_rules_for_test();
11819 g
11820 }
11821
11822 #[test]
11826 fn chunkc_archive_list_then_restore_round_trip() {
11827 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11828 let id = chunkc_seed_memory(&conn, "chunkc-archroot", "archived-mem", Tier::Mid);
11829 db::forget(
11831 &conn,
11832 Some("chunkc-archroot"),
11833 None,
11834 None,
11835 true, )
11837 .unwrap();
11838
11839 let list_req = make_tools_call(
11841 "memory_archive_list",
11842 json!({"namespace": "chunkc-archroot", "limit": 10}),
11843 );
11844 let resp = invoke_handle_request(&conn, &list_req);
11845 let payload = i4_decode_response_payload(&resp);
11846 assert!(payload["count"].as_u64().unwrap() >= 1);
11847
11848 let restore_req = make_tools_call("memory_archive_restore", json!({"id": id}));
11850 let resp = invoke_handle_request(&conn, &restore_req);
11851 assert!(resp.error.is_none());
11852 let payload = i4_decode_response_payload(&resp);
11853 assert_eq!(payload["restored"], true);
11854 assert_eq!(payload["id"], id);
11855 }
11856
11857 #[test]
11859 fn chunkc_archive_restore_invalid_id_returns_validation_error() {
11860 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11861 let req = make_tools_call(
11862 "memory_archive_restore",
11863 json!({"id": "bad id with spaces!"}),
11864 );
11865 let resp = invoke_handle_request(&conn, &req);
11866 let result = resp.result.unwrap();
11867 assert_eq!(result["isError"], true);
11868 }
11869
11870 #[test]
11872 fn chunkc_archive_restore_missing_id_returns_error() {
11873 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11874 let req = make_tools_call("memory_archive_restore", json!({}));
11875 let resp = invoke_handle_request(&conn, &req);
11876 let result = resp.result.unwrap();
11877 assert_eq!(result["isError"], true);
11878 let msg = result["content"][0]["text"].as_str().unwrap();
11879 assert!(msg.contains("id"));
11880 }
11881
11882 #[test]
11888 fn chunkc_archive_purge_denied_by_permission_rule() {
11889 let _gate = chunkc_lock_perms();
11890 crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
11891 namespace_pattern: crate::DEFAULT_NAMESPACE.to_string(),
11892 op: "memory_archive".to_string(),
11893 agent_pattern: "chunkc-archdeny-*".to_string(),
11896 decision: crate::permissions::RuleDecision::Deny,
11897 reason: Some("chunkc: archive denied".to_string()),
11898 }]);
11899 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11900 let req = make_tools_call(
11901 "memory_archive_purge",
11902 json!({
11903 "older_than_days": 0,
11904 "agent_id": "chunkc-archdeny-bot",
11905 }),
11906 );
11907 let resp = invoke_handle_request(&conn, &req);
11908 let result = resp.result.unwrap();
11909 assert_eq!(result["isError"], true);
11910 let msg = result["content"][0]["text"].as_str().unwrap();
11911 assert!(msg.contains("archive denied") || msg.contains("denied"));
11912 crate::permissions::clear_active_permission_rules_for_test();
11913 }
11914
11915 #[test]
11918 fn chunkc_archive_purge_ask_returns_pending_payload() {
11919 let _gate = chunkc_lock_perms();
11920 crate::config::override_active_permissions_mode_for_test(
11921 crate::config::PermissionsMode::Advisory,
11922 );
11923 crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
11924 namespace_pattern: crate::DEFAULT_NAMESPACE.to_string(),
11925 op: "memory_archive".to_string(),
11926 agent_pattern: "chunkc-archask-*".to_string(),
11927 decision: crate::permissions::RuleDecision::Ask,
11928 reason: Some("chunkc: confirm purge".to_string()),
11929 }]);
11930 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11931 let req = make_tools_call(
11932 "memory_archive_purge",
11933 json!({
11934 "older_than_days": 0,
11935 "agent_id": "chunkc-archask-bot",
11936 }),
11937 );
11938 let resp = invoke_handle_request(&conn, &req);
11939 assert!(resp.error.is_none());
11940 let payload = i4_decode_response_payload(&resp);
11941 assert_eq!(payload["status"], "ask");
11942 assert_eq!(payload["action"], "archive");
11943 crate::permissions::clear_active_permission_rules_for_test();
11944 }
11945
11946 #[test]
11950 fn chunkc_stats_returns_struct() {
11951 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11952 let _ = chunkc_seed_memory(&conn, "chunkc-stats", "title", Tier::Mid);
11953 let req = make_tools_call("memory_stats", json!({}));
11954 let resp = invoke_handle_request(&conn, &req);
11955 assert!(resp.error.is_none());
11956 let payload = i4_decode_response_payload(&resp);
11957 assert!(payload.is_object());
11959 }
11960
11961 #[test]
11963 fn chunkc_forget_pattern_filter_actual_run_deletes() {
11964 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11965 let _ = chunkc_seed_memory(&conn, "chunkc-pat", "abc-xyz", Tier::Mid);
11966 let _ = chunkc_seed_memory(&conn, "chunkc-pat", "def-xyz", Tier::Mid);
11967 let _ = chunkc_seed_memory(&conn, "chunkc-pat", "qqq-only", Tier::Mid);
11968 let req = make_tools_call(
11969 "memory_forget",
11970 json!({
11971 "namespace": "chunkc-pat",
11972 "pattern": "xyz",
11973 "dry_run": false,
11974 }),
11975 );
11976 let resp = invoke_handle_request(&conn, &req);
11977 assert!(resp.error.is_none());
11978 let payload = i4_decode_response_payload(&resp);
11979 assert!(payload["deleted"].as_u64().unwrap() >= 2);
11980 }
11981
11982 #[test]
11985 fn chunkc_forget_dry_run_pattern_with_tier_filter() {
11986 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
11987 let _ = chunkc_seed_memory(&conn, "chunkc-mix", "a-short", Tier::Short);
11988 let _ = chunkc_seed_memory(&conn, "chunkc-mix", "a-long", Tier::Long);
11989 let req = make_tools_call(
11990 "memory_forget",
11991 json!({
11992 "namespace": "chunkc-mix",
11993 "pattern": "a-",
11994 "tier": Tier::Short.as_str(),
11995 "dry_run": true,
11996 }),
11997 );
11998 let resp = invoke_handle_request(&conn, &req);
11999 assert!(resp.error.is_none());
12000 let payload = i4_decode_response_payload(&resp);
12001 assert_eq!(payload["dry_run"], true);
12002 assert_eq!(payload["would_delete"].as_u64().unwrap(), 1);
12003 }
12004
12005 #[test]
12011 fn chunkc_search_with_all_optional_filters() {
12012 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12013 let _ = chunkc_seed_memory(&conn, "chunkc-search-ns", "needle target", Tier::Long);
12014 let req = make_tools_call(
12015 "memory_search",
12016 json!({
12017 "query": "needle",
12018 "namespace": "chunkc-search-ns",
12019 "tier": Tier::Long.as_str(),
12020 "limit": 5,
12021 "agent_id": "test-agent-chunkc",
12022 "format": "json",
12023 }),
12024 );
12025 let resp = invoke_handle_request(&conn, &req);
12026 assert!(resp.error.is_none());
12027 let payload = i4_decode_response_payload(&resp);
12028 assert!(payload["results"].is_array());
12029 }
12030
12031 #[test]
12033 fn chunkc_search_invalid_agent_id_returns_error() {
12034 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12035 let req = make_tools_call(
12036 "memory_search",
12037 json!({"query": "x", "agent_id": "bad agent with spaces!"}),
12038 );
12039 let resp = invoke_handle_request(&conn, &req);
12040 let result = resp.result.unwrap();
12041 assert_eq!(result["isError"], true);
12042 }
12043
12044 #[test]
12046 fn chunkc_search_invalid_as_agent_returns_error() {
12047 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12048 let req = make_tools_call(
12049 "memory_search",
12050 json!({"query": "x", "as_agent": "bad agent with spaces!"}),
12051 );
12052 let resp = invoke_handle_request(&conn, &req);
12053 let result = resp.result.unwrap();
12054 assert_eq!(result["isError"], true);
12055 }
12056
12057 #[test]
12061 fn chunkc_namespace_set_standard_invalid_parent_rejected() {
12062 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12063 let id = chunkc_seed_memory(&conn, "chunkc-ns-bad-parent", "p", Tier::Long);
12064 let req = make_tools_call(
12065 "memory_namespace_set_standard",
12066 json!({
12067 "namespace": "chunkc-ns-bad-parent",
12068 "id": id,
12069 "parent": "bad parent with spaces!!",
12070 }),
12071 );
12072 let resp = invoke_handle_request(&conn, &req);
12073 let result = resp.result.unwrap();
12074 assert_eq!(result["isError"], true);
12075 }
12076
12077 #[test]
12080 fn chunkc_namespace_set_standard_missing_memory_with_governance() {
12081 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12082 let req = make_tools_call(
12083 "memory_namespace_set_standard",
12084 json!({
12085 "namespace": "chunkc-ns-missing",
12086 "id": "00000000-0000-0000-0000-000000000000",
12087 "governance": {
12088 "write": "any",
12089 "promote": "any",
12090 "delete": "owner",
12091 "approver": "human",
12092 },
12093 }),
12094 );
12095 let resp = invoke_handle_request(&conn, &req);
12096 let result = resp.result.unwrap();
12097 assert_eq!(result["isError"], true);
12098 let msg = result["content"][0]["text"].as_str().unwrap();
12099 assert!(msg.contains("not found"));
12100 }
12101
12102 #[test]
12105 fn chunkc_namespace_get_standard_inherit_no_chain_returns_zero() {
12106 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12107 let req = make_tools_call(
12108 "memory_namespace_get_standard",
12109 json!({
12110 "namespace": "chunkc-ns-empty/deep",
12111 "inherit": true,
12112 }),
12113 );
12114 let resp = invoke_handle_request(&conn, &req);
12115 assert!(resp.error.is_none());
12116 let payload = i4_decode_response_payload(&resp);
12117 assert_eq!(payload["count"], 0);
12118 assert!(payload["chain"].is_array());
12119 assert!(payload["standards"].is_array());
12120 }
12121
12122 #[test]
12128 fn chunkc_namespace_get_standard_dangling_returns_warning() {
12129 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12130 let id = chunkc_seed_memory(&conn, "chunkc-ns-dangling", "std", Tier::Long);
12131 db::set_namespace_standard(&conn, "chunkc-ns-dangling", &id, None).unwrap();
12132 conn.execute("DELETE FROM memories WHERE id = ?1", rusqlite::params![id])
12135 .unwrap();
12136 let req = make_tools_call(
12137 "memory_namespace_get_standard",
12138 json!({"namespace": "chunkc-ns-dangling"}),
12139 );
12140 let resp = invoke_handle_request(&conn, &req);
12141 assert!(resp.error.is_none());
12142 let payload = i4_decode_response_payload(&resp);
12143 assert!(
12144 payload["warning"].as_str().is_some(),
12145 "expected dangling warning; got: {payload}"
12146 );
12147 }
12148
12149 #[test]
12152 fn chunkc_extract_governance_default_when_missing() {
12153 let mem_val = json!({"id": "x", "metadata": {"agent_id": "a"}});
12154 let gov = super::namespace::extract_governance(&mem_val);
12155 assert!(gov.is_object());
12156 }
12157
12158 #[test]
12161 fn chunkc_extract_governance_default_when_no_metadata() {
12162 let mem_val = json!({"id": "x"});
12163 let gov = super::namespace::extract_governance(&mem_val);
12164 assert!(gov.is_object());
12167 }
12168
12169 #[test]
12172 fn chunkc_extract_governance_default_when_governance_invalid() {
12173 let mem_val = json!({"id": "x", "metadata": {"governance": "not-an-object"}});
12174 let gov = super::namespace::extract_governance(&mem_val);
12175 assert!(gov.is_object());
12176 }
12177
12178 #[test]
12183 fn chunkc_namespace_set_standard_non_object_metadata_becomes_object() {
12184 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12185 let id = chunkc_seed_memory(&conn, "chunkc-ns-nullmeta", "p", Tier::Long);
12187 conn.execute(
12188 "UPDATE memories SET metadata = 'null' WHERE id = ?1",
12189 rusqlite::params![id],
12190 )
12191 .unwrap();
12192 let req = make_tools_call(
12193 "memory_namespace_set_standard",
12194 json!({
12195 "namespace": "chunkc-ns-nullmeta",
12196 "id": id,
12197 "governance": {
12198 "write": "any",
12199 "promote": "any",
12200 "delete": "owner",
12201 "approver": "human",
12202 },
12203 }),
12204 );
12205 let resp = invoke_handle_request(&conn, &req);
12206 assert!(resp.error.is_none());
12207 let payload = i4_decode_response_payload(&resp);
12208 assert_eq!(payload["set"], true);
12209 }
12210
12211 #[test]
12222 fn chunkc_auto_register_path_hierarchy_finds_ancestor_parent() {
12223 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12224 let cwd = match std::env::current_dir() {
12231 Ok(c) => c,
12232 Err(_) => return,
12233 };
12234 let home = match dirs::home_dir() {
12235 Some(h) => h,
12236 None => return,
12237 };
12238 if !cwd.starts_with(&home) || cwd == home {
12240 return;
12241 }
12242 let mut ancestor = cwd.parent();
12244 let mut matched_dir: Option<String> = None;
12245 while let Some(d) = ancestor {
12246 if d == home || !d.starts_with(&home) {
12247 break;
12248 }
12249 if let Some(name) = d.file_name().and_then(|n| n.to_str()) {
12250 matched_dir = Some(name.to_string());
12251 }
12252 ancestor = d.parent();
12253 }
12254 let parent_dir_name = match matched_dir {
12255 Some(n) if !n.is_empty() => n,
12256 _ => return,
12257 };
12258 let parent_id = chunkc_seed_memory(&conn, &parent_dir_name, "ancestor-std", Tier::Long);
12260 db::set_namespace_standard(&conn, &parent_dir_name, &parent_id, None).unwrap();
12261 let child_id = chunkc_seed_memory(&conn, "chunkc-autoreg-leaf", "leaf", Tier::Long);
12263 db::set_namespace_standard(&conn, "chunkc-autoreg-leaf", &child_id, None).unwrap();
12264 super::auto_register_path_hierarchy(&conn, "chunkc-autoreg-leaf");
12266 let parent = db::get_namespace_parent(&conn, "chunkc-autoreg-leaf");
12268 assert!(
12272 parent.is_some(),
12273 "auto_register must have populated parent_namespace from a matching ancestor"
12274 );
12275 }
12276
12277 #[test]
12280 fn chunkc_namespace_clear_standard_idempotent() {
12281 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12282 let req = make_tools_call(
12283 "memory_namespace_clear_standard",
12284 json!({"namespace": "chunkc-ns-noop"}),
12285 );
12286 let resp = invoke_handle_request(&conn, &req);
12287 assert!(resp.error.is_none());
12288 let payload = i4_decode_response_payload(&resp);
12289 assert_eq!(payload["cleared"], false);
12290 }
12291
12292 #[test]
12296 fn chunkc_promote_missing_id_returns_error() {
12297 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12298 let req = make_tools_call("memory_promote", json!({}));
12299 let resp = invoke_handle_request(&conn, &req);
12300 let result = resp.result.unwrap();
12301 assert_eq!(result["isError"], true);
12302 }
12303
12304 #[test]
12307 fn chunkc_promote_resolves_by_prefix() {
12308 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12309 let id = chunkc_seed_memory(&conn, "chunkc-pfx", "p", Tier::Mid);
12310 let prefix = &id[..8];
12311 let req = make_tools_call("memory_promote", json!({"id": prefix}));
12312 let resp = invoke_handle_request(&conn, &req);
12313 assert!(resp.error.is_none(), "got: {:?}", resp.error);
12314 let payload = i4_decode_response_payload(&resp);
12315 assert_eq!(payload["promoted"], true);
12316 assert_eq!(payload["mode"], "tier");
12317 }
12318
12319 #[test]
12323 fn chunkc_consolidate_missing_ids_returns_error() {
12324 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12325 let req = make_tools_call("memory_consolidate", json!({"title": "t", "summary": "s"}));
12326 let resp = invoke_handle_request(&conn, &req);
12327 let result = resp.result.unwrap();
12328 assert_eq!(result["isError"], true);
12329 let msg = result["content"][0]["text"].as_str().unwrap();
12330 assert!(msg.contains("ids"));
12331 }
12332
12333 #[test]
12335 fn chunkc_consolidate_missing_title_returns_error() {
12336 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12337 let req = make_tools_call("memory_consolidate", json!({"ids": ["a"], "summary": "s"}));
12338 let resp = invoke_handle_request(&conn, &req);
12339 let result = resp.result.unwrap();
12340 assert_eq!(result["isError"], true);
12341 let msg = result["content"][0]["text"].as_str().unwrap();
12342 assert!(msg.contains("title"));
12343 }
12344
12345 #[test]
12347 fn chunkc_consolidate_invalid_id_format_rejected() {
12348 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12349 let req = make_tools_call(
12350 "memory_consolidate",
12351 json!({
12352 "ids": ["bad id with spaces!"],
12353 "title": "t",
12354 "summary": "s",
12355 }),
12356 );
12357 let resp = invoke_handle_request(&conn, &req);
12358 let result = resp.result.unwrap();
12359 assert_eq!(result["isError"], true);
12360 }
12361
12362 #[test]
12364 fn chunkc_consolidate_denied_by_permission_rule() {
12365 let _gate = chunkc_lock_perms();
12366 crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
12367 namespace_pattern: "chunkc-cons-deny/**".to_string(),
12368 op: "memory_consolidate".to_string(),
12369 agent_pattern: "*".to_string(),
12370 decision: crate::permissions::RuleDecision::Deny,
12371 reason: Some("chunkc: consolidate denied".to_string()),
12372 }]);
12373 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12374 let id_a = chunkc_seed_memory(&conn, "chunkc-cons-deny/a", "a", Tier::Mid);
12375 let id_b = chunkc_seed_memory(&conn, "chunkc-cons-deny/a", "b", Tier::Mid);
12376 let req = make_tools_call(
12377 "memory_consolidate",
12378 json!({
12379 "ids": [id_a, id_b],
12380 "title": "merged",
12381 "summary": "summary",
12382 "namespace": "chunkc-cons-deny/a",
12383 }),
12384 );
12385 let resp = invoke_handle_request(&conn, &req);
12386 let result = resp.result.unwrap();
12387 assert_eq!(result["isError"], true);
12388 crate::permissions::clear_active_permission_rules_for_test();
12389 }
12390
12391 #[test]
12393 fn chunkc_consolidate_ask_returns_pending_payload() {
12394 let _gate = chunkc_lock_perms();
12395 crate::config::override_active_permissions_mode_for_test(
12396 crate::config::PermissionsMode::Advisory,
12397 );
12398 crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
12399 namespace_pattern: "chunkc-cons-ask/**".to_string(),
12400 op: "memory_consolidate".to_string(),
12401 agent_pattern: "*".to_string(),
12402 decision: crate::permissions::RuleDecision::Ask,
12403 reason: Some("chunkc: confirm consolidate".to_string()),
12404 }]);
12405 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12406 let id_a = chunkc_seed_memory(&conn, "chunkc-cons-ask/a", "a", Tier::Mid);
12407 let id_b = chunkc_seed_memory(&conn, "chunkc-cons-ask/a", "b", Tier::Mid);
12408 let req = make_tools_call(
12409 "memory_consolidate",
12410 json!({
12411 "ids": [id_a, id_b],
12412 "title": "merged",
12413 "summary": "summary",
12414 "namespace": "chunkc-cons-ask/a",
12415 }),
12416 );
12417 let resp = invoke_handle_request(&conn, &req);
12418 assert!(resp.error.is_none());
12419 let payload = i4_decode_response_payload(&resp);
12420 assert_eq!(payload["status"], "ask");
12421 assert_eq!(payload["action"], "consolidate");
12422 crate::permissions::clear_active_permission_rules_for_test();
12423 }
12424
12425 #[test]
12430 fn chunkc_consolidate_handler_embedder_branch_writes_embedding() {
12431 use crate::embeddings::Embed;
12432 use crate::embeddings::test_support::MockEmbedder;
12433 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12434 let id_a = chunkc_seed_memory(&conn, "chunkc-cons-emb", "a", Tier::Mid);
12435 let id_b = chunkc_seed_memory(&conn, "chunkc-cons-emb", "b", Tier::Mid);
12436 let embedder = MockEmbedder::new_local().unwrap();
12437 let res = super::consolidate::handle_consolidate(
12438 &conn,
12439 std::path::Path::new(":memory:"),
12440 &json!({
12441 "ids": [id_a, id_b],
12442 "title": "merged-embed",
12443 "summary": "merged summary text",
12444 "namespace": "chunkc-cons-emb",
12445 }),
12446 None, Some(&embedder as &dyn Embed), None, Some("test-mcp-client"), )
12451 .expect("consolidate handler must succeed");
12452 let new_id = res["id"].as_str().unwrap();
12453 let emb = db::get_embedding(&conn, new_id).unwrap();
12455 assert!(emb.is_some(), "embedder branch must store embedding");
12456 }
12457
12458 #[tokio::test(flavor = "multi_thread")]
12464 async fn chunkc_consolidate_llm_path_missing_source_id() {
12465 use wiremock::matchers::{method, path as wpath};
12466 use wiremock::{Mock, MockServer, ResponseTemplate};
12467 let server = MockServer::start().await;
12468 Mock::given(method("GET"))
12471 .and(wpath("/api/tags"))
12472 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
12473 "models": [{"name": "test-model"}]
12474 })))
12475 .mount(&server)
12476 .await;
12477 let uri = server.uri();
12478 let _outcome: () = tokio::task::spawn_blocking(move || {
12479 let llm = crate::llm::OllamaClient::new_with_url(&uri, "test-model").unwrap();
12480 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12481 let res = super::consolidate::handle_consolidate(
12483 &conn,
12484 std::path::Path::new(":memory:"),
12485 &json!({
12486 "ids": ["00000000-0000-0000-0000-000000000000"],
12487 "title": "t",
12488 "namespace": "chunkc-cons-miss",
12489 }),
12490 Some(&llm),
12491 None,
12492 None,
12493 None,
12494 );
12495 let err = res.unwrap_err();
12496 assert!(err.contains("memory not found"), "got: {err}");
12497 })
12498 .await
12499 .unwrap();
12500 }
12501
12502 #[tokio::test(flavor = "multi_thread")]
12506 async fn chunkc_consolidate_llm_path_synthesises_summary() {
12507 use wiremock::matchers::{method, path as wpath};
12508 use wiremock::{Mock, MockServer, ResponseTemplate};
12509 let server = MockServer::start().await;
12510 Mock::given(method("GET"))
12511 .and(wpath("/api/tags"))
12512 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
12513 "models": [{"name": "test-model"}]
12514 })))
12515 .mount(&server)
12516 .await;
12517 Mock::given(method("POST"))
12521 .and(wpath("/api/chat"))
12522 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
12523 "message": {"role": "assistant", "content": "synthesised consolidated summary"}
12524 })))
12525 .mount(&server)
12526 .await;
12527 let uri = server.uri();
12528 let _outcome: () = tokio::task::spawn_blocking(move || {
12529 let llm = crate::llm::OllamaClient::new_with_url(&uri, "test-model").unwrap();
12530 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12531 let id_a = chunkc_seed_memory(&conn, "chunkc-cons-llm", "a", Tier::Mid);
12532 let id_b = chunkc_seed_memory(&conn, "chunkc-cons-llm", "b", Tier::Mid);
12533 let res = super::consolidate::handle_consolidate(
12534 &conn,
12535 std::path::Path::new(":memory:"),
12536 &json!({
12537 "ids": [id_a, id_b],
12538 "title": "merged-llm",
12539 "namespace": "chunkc-cons-llm",
12541 }),
12542 Some(&llm),
12543 None,
12544 None,
12545 None,
12546 )
12547 .expect("LLM consolidate must succeed");
12548 assert!(res["auto_summary"] == json!(true));
12549 assert!(
12550 res["summary_preview"]
12551 .as_str()
12552 .unwrap()
12553 .contains("synthesised")
12554 );
12555 })
12556 .await
12557 .unwrap();
12558 }
12559
12560 #[test]
12564 fn chunkc_replay_invalid_memory_id_returns_validation_error() {
12565 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12566 let req = make_tools_call("memory_replay", json!({"memory_id": " "}));
12569 let resp = invoke_handle_request(&conn, &req);
12570 let result = resp.result.unwrap();
12571 assert_eq!(result["isError"], true);
12572 }
12573
12574 #[test]
12576 fn chunkc_replay_missing_memory_id_returns_error() {
12577 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12578 let req = make_tools_call("memory_replay", json!({}));
12579 let resp = invoke_handle_request(&conn, &req);
12580 let result = resp.result.unwrap();
12581 assert_eq!(result["isError"], true);
12582 let msg = result["content"][0]["text"].as_str().unwrap();
12583 assert!(msg.contains("memory_id"));
12584 }
12585
12586 #[test]
12589 fn chunkc_replay_dangling_transcript_link_silently_dropped() {
12590 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12591 i4_insert_test_memory(&conn, "mem-dangle");
12592 let t = crate::transcripts::store(&conn, "team/eng", "dangling body", None).unwrap();
12593 crate::transcripts::link_transcript(&conn, "mem-dangle", &t.id, None, None).unwrap();
12594 conn.execute(
12596 "DELETE FROM memory_transcripts WHERE id = ?1",
12597 rusqlite::params![t.id],
12598 )
12599 .unwrap();
12600 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-dangle"}));
12601 let resp = invoke_handle_request(&conn, &req);
12602 assert!(resp.error.is_none());
12603 let payload = i4_decode_response_payload(&resp);
12604 assert_eq!(payload["count"], 0);
12605 }
12606
12607 #[test]
12612 fn chunkc_replay_denied_by_permission_rule() {
12613 let _gate = chunkc_lock_perms();
12614 crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
12615 namespace_pattern: "team/eng-denyrule".to_string(),
12616 op: "memory_replay".to_string(),
12617 agent_pattern: "*".to_string(),
12618 decision: crate::permissions::RuleDecision::Deny,
12619 reason: Some("chunkc: replay denied".to_string()),
12620 }]);
12621 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12622 let now = chrono::Utc::now().to_rfc3339();
12625 conn.execute(
12628 "INSERT INTO memories (id, tier, namespace, title, content, created_at, updated_at, metadata)
12629 VALUES (?1, 'short', 'team/eng-denyrule', ?2, 'body', ?3, ?3, '{\"scope\":\"public\"}')",
12630 rusqlite::params!["mem-deny-uniq", "title-mem-deny-uniq", now],
12631 )
12632 .unwrap();
12633 let t = crate::transcripts::store(&conn, "team/eng-denyrule", "denied body", None).unwrap();
12634 crate::transcripts::link_transcript(&conn, "mem-deny-uniq", &t.id, None, None).unwrap();
12635 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-deny-uniq"}));
12636 let resp = invoke_handle_request(&conn, &req);
12637 let result = resp.result.unwrap();
12638 assert_eq!(result["isError"], true);
12639 let msg = result["content"][0]["text"].as_str().unwrap();
12640 assert!(msg.contains("replay denied") || msg.contains("denied"));
12641 crate::permissions::clear_active_permission_rules_for_test();
12642 }
12643
12644 #[test]
12647 fn chunkc_replay_ask_returns_pending_payload() {
12648 let _gate = chunkc_lock_perms();
12649 crate::config::override_active_permissions_mode_for_test(
12650 crate::config::PermissionsMode::Advisory,
12651 );
12652 crate::permissions::set_active_permission_rules(vec![crate::permissions::PermissionRule {
12653 namespace_pattern: "team/eng-askrule".to_string(),
12654 op: "memory_replay".to_string(),
12655 agent_pattern: "*".to_string(),
12656 decision: crate::permissions::RuleDecision::Ask,
12657 reason: Some("chunkc: confirm replay".to_string()),
12658 }]);
12659 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12660 let now = chrono::Utc::now().to_rfc3339();
12661 conn.execute(
12664 "INSERT INTO memories (id, tier, namespace, title, content, created_at, updated_at, metadata)
12665 VALUES (?1, 'short', 'team/eng-askrule', ?2, 'body', ?3, ?3, '{\"scope\":\"public\"}')",
12666 rusqlite::params!["mem-ask-uniq", "title-mem-ask-uniq", now],
12667 )
12668 .unwrap();
12669 let t = crate::transcripts::store(&conn, "team/eng-askrule", "ask body", None).unwrap();
12670 crate::transcripts::link_transcript(&conn, "mem-ask-uniq", &t.id, None, None).unwrap();
12671 let req = make_tools_call("memory_replay", json!({"memory_id": "mem-ask-uniq"}));
12672 let resp = invoke_handle_request(&conn, &req);
12673 assert!(resp.error.is_none());
12674 let payload = i4_decode_response_payload(&resp);
12675 assert_eq!(payload["status"], "ask");
12676 assert_eq!(payload["action"], "replay");
12677 crate::permissions::clear_active_permission_rules_for_test();
12678 }
12679
12680 #[test]
12687 fn chunkc_capabilities_v3_dispatch_returns_summary_block() {
12688 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12689 let req = make_tools_call("memory_capabilities", json!({"accept": "v3"}));
12690 let resp = invoke_handle_request(&conn, &req);
12691 assert!(resp.error.is_none());
12692 let payload = i4_decode_response_payload(&resp);
12693 assert!(payload["summary"].as_str().is_some());
12694 assert!(payload["to_describe_to_user"].as_str().is_some());
12695 assert!(payload["tools"].is_array());
12696 }
12697
12698 #[test]
12701 fn chunkc_capabilities_v3_with_verbose_and_schema_overlays_tools() {
12702 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12703 let req = make_tools_call(
12704 "memory_capabilities",
12705 json!({
12706 "accept": "v3",
12707 "verbose": true,
12708 "include_schema": true,
12709 }),
12710 );
12711 let resp = invoke_handle_request(&conn, &req);
12712 assert!(resp.error.is_none());
12713 let payload = i4_decode_response_payload(&resp);
12714 let tools = payload["tools"].as_array().unwrap();
12716 assert!(
12717 tools.iter().any(|t| t.get("inputSchema").is_some()),
12718 "verbose+include_schema must overlay inputSchema"
12719 );
12720 assert!(
12721 tools.iter().any(|t| t.get("docstring").is_some()),
12722 "verbose must overlay docstring"
12723 );
12724 }
12725
12726 #[test]
12729 fn chunkc_overlay_tool_payloads_noop_when_both_flags_false() {
12730 let mut obj = serde_json::Map::new();
12731 obj.insert("tools".to_string(), json!([]));
12732 let before = obj.clone();
12733 crate::mcp::overlay_tool_payloads(&mut obj, &crate::profile::Profile::core(), false, false);
12734 assert_eq!(obj, before, "no-op when neither flag set");
12735 }
12736
12737 #[test]
12740 fn chunkc_overlay_tool_payloads_synthesises_v2_tool_payloads() {
12741 let mut obj = serde_json::Map::new();
12742 obj.insert("schema_version".to_string(), json!("2"));
12743 crate::mcp::overlay_tool_payloads(
12744 &mut obj,
12745 &crate::profile::Profile::core(),
12746 true, true, );
12749 let payloads = obj.get("tool_payloads").and_then(Value::as_array).unwrap();
12750 assert!(
12751 !payloads.is_empty(),
12752 "tool_payloads must be synthesised for v2-shape"
12753 );
12754 }
12755
12756 #[test]
12758 fn chunkc_effective_tier_label_all_four_arms() {
12759 use crate::mcp::effective_tier_label;
12760 assert_eq!(effective_tier_label(true, true, true), "autonomous");
12761 assert_eq!(effective_tier_label(true, true, false), "smart");
12762 assert_eq!(effective_tier_label(false, true, false), "semantic");
12763 assert_eq!(effective_tier_label(false, false, false), "keyword");
12764 }
12765
12766 #[test]
12769 fn chunkc_format_rule_summary_renders_each_approver_variant() {
12770 use crate::mcp::format_rule_summary;
12771 use crate::models::{ApproverType, GovernanceLevel, GovernancePolicy};
12772
12773 let mut p = GovernancePolicy::default();
12774 p.core.write = GovernanceLevel::Any;
12775 p.core.promote = GovernanceLevel::Any;
12776 p.core.delete = GovernanceLevel::Owner;
12777 p.core.approver = ApproverType::Human;
12778 p.core.inherit = true;
12779 let s = format_rule_summary("alpha/eng", &p);
12780 assert!(s.contains("alpha/eng"));
12781 assert!(s.contains("approver=human"));
12782 assert!(s.contains("inherit=true"));
12783
12784 p.core.approver = ApproverType::Agent("ops-bot".to_string());
12785 let s = format_rule_summary("alpha/eng", &p);
12786 assert!(s.contains("approver=agent:ops-bot"));
12787
12788 p.core.approver = ApproverType::Consensus(3);
12789 let s = format_rule_summary("alpha/eng", &p);
12790 assert!(s.contains("approver=consensus:3"));
12791 }
12792
12793 #[test]
12795 fn chunkc_capabilities_accept_parse_all_variants() {
12796 use crate::mcp::CapabilitiesAccept;
12797 assert_eq!(CapabilitiesAccept::parse("v1"), CapabilitiesAccept::V1);
12798 assert_eq!(CapabilitiesAccept::parse("1"), CapabilitiesAccept::V1);
12799 assert_eq!(CapabilitiesAccept::parse("v2"), CapabilitiesAccept::V2);
12800 assert_eq!(CapabilitiesAccept::parse("2"), CapabilitiesAccept::V2);
12801 assert_eq!(CapabilitiesAccept::parse("v3"), CapabilitiesAccept::V3);
12802 assert_eq!(CapabilitiesAccept::parse("3"), CapabilitiesAccept::V3);
12803 assert_eq!(CapabilitiesAccept::parse(""), CapabilitiesAccept::V3);
12805 assert_eq!(CapabilitiesAccept::parse("garbage"), CapabilitiesAccept::V3);
12806 assert_eq!(CapabilitiesAccept::parse(" V2 "), CapabilitiesAccept::V2);
12808 }
12809
12810 #[test]
12813 fn chunkc_handle_capabilities_with_conn_rejects_v3() {
12814 let tier = crate::config::FeatureTier::Keyword.config();
12815 let res = crate::mcp::handle_capabilities_with_conn(
12816 &tier,
12817 &crate::config::ResolvedModels::from_tier_preset(&tier),
12818 None,
12819 false,
12820 None,
12821 crate::mcp::CapabilitiesAccept::V3,
12822 );
12823 let err = res.unwrap_err();
12824 assert!(err.contains("handle_capabilities_with_conn_v3"));
12825 }
12826
12827 #[test]
12829 fn chunkc_handle_capabilities_with_conn_v1_returns_legacy_shape() {
12830 let tier = crate::config::FeatureTier::Keyword.config();
12831 let v = crate::mcp::handle_capabilities_with_conn(
12832 &tier,
12833 &crate::config::ResolvedModels::from_tier_preset(&tier),
12834 None,
12835 false,
12836 None,
12837 crate::mcp::CapabilitiesAccept::V1,
12838 )
12839 .unwrap();
12840 assert!(v.is_object());
12842 }
12843
12844 #[test]
12849 fn chunkc_smart_load_f14_control_intents_13_of_13() {
12850 use crate::mcp::handle_smart_load;
12851 let cases: &[(&str, &str)] = &[
12855 ("recall and search for stored memories", "core"),
12857 (
12859 "delete and forget the stale memories then promote the survivors",
12860 "lifecycle",
12861 ),
12862 ("I'm about to debug a flaky test", "graph"),
12864 ("query the knowledge graph for entity timeline", "graph"),
12866 ("approve the pending governance review", "governance"),
12868 (
12870 "consolidate duplicate memories that contradict each other",
12871 "power",
12872 ),
12873 ("restore an archived backup of old memories", "archive"),
12875 ("register a new agent and start a session", "meta"),
12877 ("send a notification to another agent", "other"),
12879 ("expand a query and find related memories", "power"),
12881 ("call memory_notify on the other agent", "other"),
12883 ("audit the namespace permission policy rules", "governance"),
12885 ("migrate and rotate the stale records", "lifecycle"),
12887 ];
12888 for (intent, expected) in cases {
12889 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12890 for fam in [
12891 "core",
12892 "lifecycle",
12893 "graph",
12894 "governance",
12895 "power",
12896 "meta",
12897 "archive",
12898 "other",
12899 ] {
12900 let _ = chunkc_seed_family_memory(&conn, "ns", fam);
12901 }
12902 let resp = handle_smart_load(&conn, &json!({"intent": intent}), None, None)
12903 .expect("smart_load must succeed");
12904 assert_eq!(
12905 resp["chosen_family"], *expected,
12906 "F14 control intent {intent:?} expected {expected}; got: {resp}"
12907 );
12908 assert_eq!(resp["chosen_family_source"], "keyword");
12909 }
12910 }
12911
12912 #[test]
12915 fn chunkc_load_family_k_zero_clamps_to_one() {
12916 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12917 let _ = chunkc_seed_family_memory(&conn, "ns", "core");
12918 let _ = chunkc_seed_family_memory(&conn, "ns", "core");
12919 let resp = crate::mcp::handle_load_family(
12920 &conn,
12921 &json!({"family": "core", "namespace": "ns", "k": 0}),
12922 None,
12923 )
12924 .expect("must succeed");
12925 assert_eq!(resp["k"], 1);
12926 assert_eq!(resp["count"], 1);
12927 }
12928
12929 #[test]
12931 fn chunkc_load_family_invalid_namespace_rejected() {
12932 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12933 let err = crate::mcp::handle_load_family(
12934 &conn,
12935 &json!({"family": "core", "namespace": "bad ns with spaces!"}),
12936 None,
12937 )
12938 .unwrap_err();
12939 assert!(err.contains("namespace") || err.contains("invalid"));
12940 }
12941
12942 #[test]
12945 fn chunkc_load_family_expired_rows_are_filtered_out() {
12946 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12947 let _ = chunkc_seed_family_memory(&conn, "ns-exp", "core");
12949 let now = chrono::Utc::now().to_rfc3339();
12951 let past = (chrono::Utc::now() - chrono::Duration::hours(2)).to_rfc3339();
12952 let stale = Memory {
12953 id: uuid::Uuid::new_v4().to_string(),
12954 tier: Tier::Short,
12955 namespace: "ns-exp".to_string(),
12956 title: "stale".to_string(),
12957 content: "stale".to_string(),
12958 tags: vec![],
12959 priority: 5,
12960 confidence: 1.0,
12961 source: "test".to_string(),
12962 access_count: 0,
12963 created_at: now.clone(),
12964 updated_at: now,
12965 last_accessed_at: None,
12966 expires_at: Some(past),
12967 metadata: json!({"family": "core"}),
12968 reflection_depth: 0,
12969 memory_kind: crate::models::MemoryKind::Observation,
12970 entity_id: None,
12971 persona_version: None,
12972 citations: Vec::new(),
12973 source_uri: None,
12974 source_span: None,
12975 confidence_source: crate::models::ConfidenceSource::CallerProvided,
12976 confidence_signals: None,
12977 confidence_decayed_at: None,
12978 version: 1,
12979 };
12980 db::insert(&conn, &stale).unwrap();
12981 let resp = crate::mcp::handle_load_family(
12982 &conn,
12983 &json!({"family": "core", "namespace": "ns-exp"}),
12984 None,
12985 )
12986 .expect("must succeed");
12987 assert_eq!(resp["count"], 1, "expired row must be filtered");
12988 }
12989
12990 #[test]
12994 fn chunkc_smart_load_embedder_path_reports_embedder_source() {
12995 use crate::embeddings::Embed;
12996 use crate::embeddings::test_support::MockEmbedder;
12997 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
12998 for fam in [
12999 "core",
13000 "lifecycle",
13001 "graph",
13002 "governance",
13003 "power",
13004 "meta",
13005 "archive",
13006 "other",
13007 ] {
13008 let _ = chunkc_seed_family_memory(&conn, "ns", fam);
13009 }
13010 let embedder = MockEmbedder::new_local().unwrap();
13011 let resp = crate::mcp::handle_smart_load(
13014 &conn,
13015 &json!({"intent": "blortzfribblequx zarflargle"}),
13016 Some(&embedder as &dyn Embed),
13017 None,
13018 )
13019 .expect("smart_load must succeed");
13020 assert!(resp["chosen_family"].is_string());
13025 assert!(resp["score"].is_number());
13026 }
13027
13028 #[test]
13031 fn chunkc_build_capabilities_summary_each_named_profile() {
13032 use crate::mcp::build_capabilities_summary;
13033 use crate::profile::Profile;
13034 for p in [
13035 Profile::core(),
13036 Profile::graph(),
13037 Profile::admin(),
13038 Profile::power(),
13039 Profile::full(),
13040 ] {
13041 let s = build_capabilities_summary(&p);
13042 assert!(s.contains("memory tools"));
13043 assert!(s.contains("memory_load_family"));
13044 assert!(s.contains("memory_smart_load"));
13045 }
13046 let custom = Profile::parse("core,archive").unwrap();
13048 let s = build_capabilities_summary(&custom);
13049 assert!(s.contains("memory tools"));
13050 assert!(s.contains("core") && s.contains("archive"));
13052 }
13053
13054 #[test]
13057 fn chunkc_build_capabilities_describe_to_user_both_branches() {
13058 use crate::mcp::build_capabilities_describe_to_user;
13059 use crate::profile::Profile;
13060 let s_full = build_capabilities_describe_to_user(&Profile::full());
13061 assert!(s_full.contains("all"));
13062 let s_core = build_capabilities_describe_to_user(&Profile::core());
13063 assert!(s_core.contains("memory tool"));
13064 assert!(s_core.contains("tools") || s_core.contains("tool"));
13070 }
13071
13072 #[test]
13074 fn chunkc_build_capabilities_tools_with_allowlist_denying_agent() {
13075 use crate::config::McpConfig;
13076 use crate::mcp::build_capabilities_tools;
13077 use crate::profile::Profile;
13078 use std::collections::HashMap;
13079 let mut allowlist = HashMap::new();
13080 allowlist.insert("alice".to_string(), vec!["core".to_string()]);
13081 let cfg = McpConfig {
13082 allowlist: Some(allowlist),
13083 ..McpConfig::default()
13084 };
13085 let tools = build_capabilities_tools(&Profile::full(), Some(&cfg), Some("alice"));
13086 let core_entry = tools.iter().find(|t| t.family == "core").unwrap();
13089 assert!(core_entry.callable_now);
13090 let non_core = tools.iter().find(|t| t.family != "core").unwrap();
13091 assert!(!non_core.callable_now);
13092 }
13093
13094 #[test]
13095 fn issue_1673_n13_unknown_caller_does_not_falsely_deny_callable_now() {
13096 use crate::config::McpConfig;
13101 use crate::mcp::build_capabilities_tools;
13102 use crate::profile::Profile;
13103 use std::collections::HashMap;
13104 let mut allowlist = HashMap::new();
13105 allowlist.insert("alice".to_string(), vec!["core".to_string()]);
13106 let cfg = McpConfig {
13107 allowlist: Some(allowlist),
13108 ..McpConfig::default()
13109 };
13110 let tools = build_capabilities_tools(&Profile::full(), Some(&cfg), None);
13111 for t in tools.iter().filter(|t| t.loaded) {
13114 assert!(
13115 t.callable_now,
13116 "loaded tool {} must be callable_now for an unknown caller",
13117 t.name
13118 );
13119 }
13120 }
13121
13122 #[test]
13125 fn chunkc_build_agent_permitted_families_empty_allowlist_returns_none() {
13126 use crate::config::McpConfig;
13127 use crate::mcp::build_agent_permitted_families;
13128 use std::collections::HashMap;
13129 let cfg = McpConfig {
13130 allowlist: Some(HashMap::new()),
13131 ..McpConfig::default()
13132 };
13133 assert_eq!(
13134 build_agent_permitted_families(Some(&cfg), Some("alice")),
13135 None
13136 );
13137 }
13138
13139 #[test]
13142 fn chunkc_build_agent_permitted_families_populated_allowlist() {
13143 use crate::config::McpConfig;
13144 use crate::mcp::build_agent_permitted_families;
13145 use std::collections::HashMap;
13146 let mut allowlist = HashMap::new();
13147 allowlist.insert(
13148 "alice".to_string(),
13149 vec!["core".to_string(), "graph".to_string()],
13150 );
13151 let cfg = McpConfig {
13152 allowlist: Some(allowlist),
13153 ..McpConfig::default()
13154 };
13155 let perm = build_agent_permitted_families(Some(&cfg), Some("alice")).unwrap();
13156 assert!(perm.contains(&"core".to_string()));
13157 assert!(perm.contains(&"graph".to_string()));
13158 }
13159
13160 #[test]
13163 fn chunkc_build_capabilities_summary_drives_all_label_arms() {
13164 use crate::mcp::build_capabilities_summary;
13165 use crate::profile::Profile;
13166 let labels = [
13168 Profile::full(),
13169 Profile::core(),
13170 Profile::graph(),
13171 Profile::admin(),
13172 Profile::power(),
13173 ];
13174 for p in labels {
13175 let s = build_capabilities_summary(&p);
13176 assert!(s.contains("memory tools"));
13177 }
13178 let custom = Profile::parse("core,graph,archive").unwrap();
13180 let s = build_capabilities_summary(&custom);
13181 assert!(s.contains("core,graph,archive") || s.contains("core") && s.contains("graph"));
13182 }
13183
13184 #[test]
13189 fn chunkc_handle_capabilities_with_conn_v3_full_overlay() {
13190 use crate::config::{FeatureTier, McpConfig};
13191 use crate::harness::Harness;
13192 use crate::mcp::handle_capabilities_with_conn_v3;
13193 use crate::profile::Profile;
13194 use std::collections::HashMap;
13195 let tier = FeatureTier::Keyword.config();
13196 let mut allowlist = HashMap::new();
13197 allowlist.insert("alice".to_string(), vec!["core".to_string()]);
13198 let cfg = McpConfig {
13199 allowlist: Some(allowlist),
13200 ..McpConfig::default()
13201 };
13202 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13203 let harness = Harness::detect("claude-code");
13205 let v = handle_capabilities_with_conn_v3(
13206 &tier,
13207 &crate::config::ResolvedModels::from_tier_preset(&tier),
13208 None,
13209 false,
13210 Some(&conn),
13211 &Profile::core(),
13212 Some(&cfg),
13213 Some("alice"),
13214 Some(&harness),
13215 )
13216 .unwrap();
13217 assert_eq!(v["schema_version"], "3");
13218 assert!(v["summary"].as_str().is_some());
13219 assert!(v["to_describe_to_user"].as_str().is_some());
13220 assert!(v["tools"].is_array());
13221 assert!(v["agent_permitted_families"].is_array());
13222 }
13223
13224 #[test]
13227 fn chunkc_handle_capabilities_with_conn_v2_db_count_overlay() {
13228 use crate::config::FeatureTier;
13229 use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
13230 let tier = FeatureTier::Keyword.config();
13231 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13232 let v = handle_capabilities_with_conn(
13233 &tier,
13234 &crate::config::ResolvedModels::from_tier_preset(&tier),
13235 None,
13236 false,
13237 Some(&conn),
13238 CapabilitiesAccept::V2,
13239 )
13240 .unwrap();
13241 assert_eq!(v["schema_version"], "2");
13242 assert!(v["permissions"]["active_rules"].as_u64().is_some());
13243 assert!(v["hooks"]["registered_count"].as_u64().is_some());
13244 assert!(v["approval"]["pending_requests"].as_u64().is_some());
13245 }
13246
13247 #[test]
13253 fn chunkc_promote_governance_pending() {
13254 use crate::models::{ApproverType, CorePolicy, GovernanceLevel, GovernancePolicy};
13255 let _gate = chunkc_lock_perms();
13256 crate::config::override_active_permissions_mode_for_test(
13257 crate::config::PermissionsMode::Enforce,
13258 );
13259 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13260 let id = chunkc_seed_memory(&conn, "chunkc-prom-pend", "p", Tier::Mid);
13261 let std_id = {
13263 let mem = Memory {
13264 id: uuid::Uuid::new_v4().to_string(),
13265 tier: Tier::Long,
13266 namespace: "chunkc-prom-pend".into(),
13267 title: "std".into(),
13268 content: "policy".into(),
13269 tags: vec![],
13270 priority: 9,
13271 confidence: 1.0,
13272 source: "test".into(),
13273 access_count: 0,
13274 created_at: chrono::Utc::now().to_rfc3339(),
13275 updated_at: chrono::Utc::now().to_rfc3339(),
13276 last_accessed_at: None,
13277 expires_at: None,
13278 metadata: json!({
13279 "governance": GovernancePolicy {
13280 core: CorePolicy {
13281 write: GovernanceLevel::Any,
13282 promote: GovernanceLevel::Approve,
13283 delete: GovernanceLevel::Any,
13284 approver: ApproverType::Human,
13285 inherit: false,
13286 max_reflection_depth: None,
13287 },
13288 ..Default::default()
13289 }
13290 }),
13291 reflection_depth: 0,
13292 memory_kind: crate::models::MemoryKind::Observation,
13293 entity_id: None,
13294 persona_version: None,
13295 citations: Vec::new(),
13296 source_uri: None,
13297 source_span: None,
13298 confidence_source: crate::models::ConfidenceSource::CallerProvided,
13299 confidence_signals: None,
13300 confidence_decayed_at: None,
13301 version: 1,
13302 };
13303 db::insert(&conn, &mem).unwrap()
13304 };
13305 db::set_namespace_standard(&conn, "chunkc-prom-pend", &std_id, None).unwrap();
13306 let req = make_tools_call("memory_promote", json!({"id": id}));
13307 let resp = invoke_handle_request(&conn, &req);
13308 assert!(resp.error.is_none());
13309 let payload = i4_decode_response_payload(&resp);
13310 assert_eq!(payload["status"], "pending");
13311 assert_eq!(payload["action"], "promote");
13312 assert_eq!(payload["memory_id"], id);
13313 crate::permissions::clear_active_permission_rules_for_test();
13314 }
13315
13316 #[test]
13320 fn chunkc_promote_governance_denied() {
13321 use crate::models::{ApproverType, CorePolicy, GovernanceLevel, GovernancePolicy};
13322 let _gate = chunkc_lock_perms();
13323 crate::config::override_active_permissions_mode_for_test(
13324 crate::config::PermissionsMode::Enforce,
13325 );
13326 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13327 let id = chunkc_seed_memory(&conn, "chunkc-prom-deny", "p", Tier::Mid);
13328 let std_id = {
13332 let mem = Memory {
13333 id: uuid::Uuid::new_v4().to_string(),
13334 tier: Tier::Long,
13335 namespace: "chunkc-prom-deny".into(),
13336 title: "std".into(),
13337 content: "policy".into(),
13338 tags: vec![],
13339 priority: 9,
13340 confidence: 1.0,
13341 source: "test".into(),
13342 access_count: 0,
13343 created_at: chrono::Utc::now().to_rfc3339(),
13344 updated_at: chrono::Utc::now().to_rfc3339(),
13345 last_accessed_at: None,
13346 expires_at: None,
13347 metadata: json!({
13348 "governance": GovernancePolicy {
13349 core: CorePolicy {
13350 write: GovernanceLevel::Any,
13351 promote: GovernanceLevel::Owner,
13352 delete: GovernanceLevel::Any,
13353 approver: ApproverType::Agent("not-me".to_string()),
13354 inherit: false,
13355 max_reflection_depth: None,
13356 },
13357 ..Default::default()
13358 }
13359 }),
13360 reflection_depth: 0,
13361 memory_kind: crate::models::MemoryKind::Observation,
13362 entity_id: None,
13363 persona_version: None,
13364 citations: Vec::new(),
13365 source_uri: None,
13366 source_span: None,
13367 confidence_source: crate::models::ConfidenceSource::CallerProvided,
13368 confidence_signals: None,
13369 confidence_decayed_at: None,
13370 version: 1,
13371 };
13372 db::insert(&conn, &mem).unwrap()
13373 };
13374 db::set_namespace_standard(&conn, "chunkc-prom-deny", &std_id, None).unwrap();
13375 let req = make_tools_call(
13376 "memory_promote",
13377 json!({"id": id, "agent_id": "calling-agent"}),
13378 );
13379 let resp = invoke_handle_request(&conn, &req);
13380 let result = resp.result.unwrap();
13381 assert!(result.is_object());
13385 crate::permissions::clear_active_permission_rules_for_test();
13386 }
13387
13388 #[test]
13394 fn chunkc_consolidate_vector_index_branch_inserts_new_id() {
13395 use crate::embeddings::Embed;
13396 use crate::embeddings::test_support::MockEmbedder;
13397 use crate::hnsw::VectorIndex;
13398 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13399 let id_a = chunkc_seed_memory(&conn, "chunkc-cons-vidx", "a", Tier::Mid);
13400 let id_b = chunkc_seed_memory(&conn, "chunkc-cons-vidx", "b", Tier::Mid);
13401 let embedder = MockEmbedder::new_local().unwrap();
13402 let index = VectorIndex::empty();
13405 index.insert(id_a.clone(), embedder.embed("a").unwrap());
13406 index.insert(id_b.clone(), embedder.embed("b").unwrap());
13407 let res = super::consolidate::handle_consolidate(
13408 &conn,
13409 std::path::Path::new(":memory:"),
13410 &json!({
13411 "ids": [id_a, id_b],
13412 "title": "merged-vidx",
13413 "summary": "vidx summary",
13414 "namespace": "chunkc-cons-vidx",
13415 }),
13416 None,
13417 Some(&embedder as &dyn Embed),
13418 Some(&index),
13419 None,
13420 )
13421 .expect("must succeed");
13422 let new_id = res["id"].as_str().unwrap();
13427 let emb = db::get_embedding(&conn, new_id).unwrap();
13428 assert!(emb.is_some());
13429 }
13430
13431 #[test]
13438 fn chunkc_consolidate_iterates_namespace_standard_check() {
13439 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13440 let id_a = chunkc_seed_memory(&conn, "chunkc-cons-warn", "a", Tier::Long);
13441 let id_b = chunkc_seed_memory(&conn, "chunkc-cons-warn", "b", Tier::Mid);
13442 db::set_namespace_standard(&conn, "chunkc-cons-warn", &id_a, None).unwrap();
13443 let req = make_tools_call(
13444 "memory_consolidate",
13445 json!({
13446 "ids": [id_a, id_b],
13447 "title": "merged-warn",
13448 "summary": "warn summary",
13449 "namespace": "chunkc-cons-warn",
13450 }),
13451 );
13452 let resp = invoke_handle_request(&conn, &req);
13453 assert!(resp.error.is_none());
13454 let payload = i4_decode_response_payload(&resp);
13455 assert_eq!(payload["consolidated"], 2);
13456 }
13457
13458 #[test]
13463 fn chunkc_archive_list_returns_inserted_rows() {
13464 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13465 let _ = chunkc_seed_memory(&conn, "chunkc-archlist", "row-one", Tier::Mid);
13466 let _ = chunkc_seed_memory(&conn, "chunkc-archlist", "row-two", Tier::Mid);
13467 db::forget(&conn, Some("chunkc-archlist"), None, None, true).unwrap();
13468 let req = make_tools_call(
13469 "memory_archive_list",
13470 json!({"namespace": "chunkc-archlist", "limit": 50}),
13471 );
13472 let resp = invoke_handle_request(&conn, &req);
13473 assert!(resp.error.is_none());
13474 let payload = i4_decode_response_payload(&resp);
13475 assert!(payload["count"].as_u64().unwrap() >= 2);
13476 let archived = payload["archived"].as_array().unwrap();
13477 assert!(!archived.is_empty());
13478 }
13479
13480 #[test]
13485 fn chunkc_replay_verbose_true_small_transcript_inlines_content() {
13486 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13487 i4_insert_test_memory(&conn, "mem-vsmall");
13488 let body = "tiny body that fits well below the threshold";
13489 let t = crate::transcripts::store(&conn, "team/eng", body, None).unwrap();
13490 crate::transcripts::link_transcript(&conn, "mem-vsmall", &t.id, Some(0), Some(10)).unwrap();
13491 let req = make_tools_call(
13492 "memory_replay",
13493 json!({"memory_id": "mem-vsmall", "verbose": true}),
13494 );
13495 let resp = invoke_handle_request(&conn, &req);
13496 assert!(resp.error.is_none());
13497 let payload = i4_decode_response_payload(&resp);
13498 let transcripts = payload["transcripts"].as_array().unwrap();
13499 assert_eq!(transcripts[0]["content"].as_str().unwrap(), body);
13500 }
13501
13502 #[test]
13505 fn chunkc_replay_with_explicit_agent_id_resolves() {
13506 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13507 i4_insert_test_memory(&conn, "mem-explicit-agent");
13508 let t = crate::transcripts::store(&conn, "team/eng", "body", None).unwrap();
13509 crate::transcripts::link_transcript(&conn, "mem-explicit-agent", &t.id, None, None)
13510 .unwrap();
13511 let req = make_tools_call(
13512 "memory_replay",
13513 json!({"memory_id": "mem-explicit-agent", "agent_id": "agent-explicit"}),
13514 );
13515 let resp = invoke_handle_request(&conn, &req);
13516 assert!(resp.error.is_none());
13517 let payload = i4_decode_response_payload(&resp);
13518 assert_eq!(payload["count"], 1);
13519 }
13520
13521 #[test]
13526 fn chunkc_namespace_inherit_chain_surfaces_governance() {
13527 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13528 let parent_id = chunkc_seed_memory(&conn, "chunkc-inh-parent", "p", Tier::Long);
13531 db::set_namespace_standard(&conn, "chunkc-inh-parent", &parent_id, None).unwrap();
13532 let leaf_id = chunkc_seed_memory(&conn, "chunkc-inh-parent/leaf", "l", Tier::Long);
13533 db::set_namespace_standard(
13534 &conn,
13535 "chunkc-inh-parent/leaf",
13536 &leaf_id,
13537 Some("chunkc-inh-parent"),
13538 )
13539 .unwrap();
13540 let req = make_tools_call(
13541 "memory_namespace_get_standard",
13542 json!({
13543 "namespace": "chunkc-inh-parent/leaf",
13544 "inherit": true,
13545 }),
13546 );
13547 let resp = invoke_handle_request(&conn, &req);
13548 assert!(resp.error.is_none());
13549 let payload = i4_decode_response_payload(&resp);
13550 assert!(payload["count"].as_u64().unwrap() >= 1);
13551 let standards = payload["standards"].as_array().unwrap();
13552 for entry in standards {
13553 assert!(entry["governance"].is_object());
13554 }
13555 }
13556
13557 #[test]
13560 fn chunkc_handle_capabilities_with_conn_v2_reranker_none() {
13561 use crate::config::FeatureTier;
13562 use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
13563 let tier = FeatureTier::Keyword.config();
13564 let v = handle_capabilities_with_conn(
13565 &tier,
13566 &crate::config::ResolvedModels::from_tier_preset(&tier),
13567 None, false,
13569 None,
13570 CapabilitiesAccept::V2,
13571 )
13572 .unwrap();
13573 assert_eq!(v["features"]["reranker_active"], "off");
13574 }
13575
13576 #[test]
13579 fn chunkc_handle_capabilities_with_conn_reranker_lexical_fallback() {
13580 use crate::config::FeatureTier;
13581 use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
13582 use crate::reranker::{BatchedReranker, CrossEncoder};
13583 let tier = FeatureTier::Keyword.config();
13584 let lexical = BatchedReranker::new(CrossEncoder::new());
13587 let v = handle_capabilities_with_conn(
13588 &tier,
13589 &crate::config::ResolvedModels::from_tier_preset(&tier),
13590 Some(&lexical),
13591 false,
13592 None,
13593 CapabilitiesAccept::V2,
13594 )
13595 .unwrap();
13596 assert_eq!(v["features"]["reranker_active"], "lexical_fallback");
13597 assert_eq!(v["features"]["cross_encoder_reranking"], false);
13598 }
13599
13600 #[test]
13603 fn chunkc_compute_recall_mode_hybrid_when_embedder_loaded() {
13604 use crate::config::FeatureTier;
13605 use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
13606 let tier = FeatureTier::Semantic.config();
13607 let v = handle_capabilities_with_conn(
13608 &tier,
13609 &crate::config::ResolvedModels::from_tier_preset(&tier),
13610 None,
13611 true, None,
13613 CapabilitiesAccept::V2,
13614 )
13615 .unwrap();
13616 assert_eq!(v["features"]["recall_mode_active"], "hybrid");
13617 }
13618
13619 #[test]
13622 fn chunkc_compute_recall_mode_degraded_when_embedder_not_loaded() {
13623 use crate::config::FeatureTier;
13624 use crate::mcp::{CapabilitiesAccept, handle_capabilities_with_conn};
13625 let tier = FeatureTier::Semantic.config();
13626 let v = handle_capabilities_with_conn(
13627 &tier,
13628 &crate::config::ResolvedModels::from_tier_preset(&tier),
13629 None,
13630 false, None,
13632 CapabilitiesAccept::V2,
13633 )
13634 .unwrap();
13635 assert_eq!(v["features"]["recall_mode_active"], "degraded");
13636 }
13637
13638 #[test]
13643 fn chunkc_overlay_tool_payloads_handles_malformed_tool_entries() {
13644 let mut obj = serde_json::Map::new();
13645 obj.insert(
13646 "tools".to_string(),
13647 json!([
13648 "not-an-object", {"family": "x"}, {"name": "no_such_tool"}, {"name": "memory_capabilities"}, ]),
13653 );
13654 crate::mcp::overlay_tool_payloads(&mut obj, &crate::profile::Profile::core(), true, true);
13655 let tools = obj.get("tools").and_then(Value::as_array).unwrap();
13658 let real = tools
13659 .iter()
13660 .find(|t| t.get("name").and_then(Value::as_str) == Some("memory_capabilities"))
13661 .unwrap();
13662 assert!(real.get("inputSchema").is_some());
13663 assert!(real.get("docstring").is_some());
13664 }
13665
13666 #[test]
13671 fn chunkc_smart_load_keyword_veto_overrides_embedder() {
13672 use crate::embeddings::Embed;
13673 use crate::embeddings::test_support::MockEmbedder;
13674 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13675 for fam in [
13676 "core",
13677 "lifecycle",
13678 "graph",
13679 "governance",
13680 "power",
13681 "meta",
13682 "archive",
13683 "other",
13684 ] {
13685 let _ = chunkc_seed_family_memory(&conn, "ns", fam);
13686 }
13687 let embedder = MockEmbedder::new_local().unwrap();
13688 let resp = crate::mcp::handle_smart_load(
13689 &conn,
13690 &json!({"intent": "call memory_notify on the other agent"}),
13691 Some(&embedder as &dyn Embed),
13692 None,
13693 )
13694 .expect("must succeed");
13695 assert_eq!(resp["chosen_family"], "other");
13696 }
13699
13700 #[test]
13706 fn chunkc_smart_load_empty_intent_routes_to_core_fallback() {
13707 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13708 for fam in ["core", "lifecycle"] {
13709 let _ = chunkc_seed_family_memory(&conn, "ns-empty", fam);
13710 }
13711 let resp = crate::mcp::handle_smart_load(
13712 &conn,
13713 &json!({"intent": " ", "namespace": "ns-empty", "k": 5}),
13714 None,
13715 None,
13716 )
13717 .expect("smart_load must succeed on whitespace intent");
13718 assert_eq!(resp["chosen_family"], "core");
13719 assert_eq!(resp["chosen_family_source"], "fallback");
13720 assert_eq!(resp["intent"], "");
13721 assert_eq!(resp["namespace"], "ns-empty");
13723 assert_eq!(resp["k"], 5);
13724 }
13725
13726 #[test]
13730 fn chunkc_smart_load_punctuation_only_intent_keyword_fallback() {
13731 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13732 let _ = chunkc_seed_family_memory(&conn, "ns-punct", "core");
13733 let resp =
13734 crate::mcp::handle_smart_load(&conn, &json!({"intent": "!!!---???"}), None, None)
13735 .expect("smart_load must succeed on punctuation-only intent");
13736 assert_eq!(resp["chosen_family"], "core");
13737 assert_eq!(resp["chosen_family_source"], "fallback");
13738 }
13739
13740 #[test]
13743 fn chunkc_smart_load_failing_embedder_falls_back_to_keyword() {
13744 use crate::embeddings::Embed;
13745
13746 struct FailingEmbedder;
13747 impl Embed for FailingEmbedder {
13748 fn embed(&self, _: &str) -> anyhow::Result<Vec<f32>> {
13749 Err(anyhow::anyhow!("simulated embedder failure"))
13750 }
13751 fn embed_batch(&self, _: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
13752 Err(anyhow::anyhow!("simulated embedder batch failure"))
13753 }
13754 }
13755
13756 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13757 for fam in [
13758 "core",
13759 "lifecycle",
13760 "graph",
13761 "governance",
13762 "power",
13763 "meta",
13764 "archive",
13765 "other",
13766 ] {
13767 let _ = chunkc_seed_family_memory(&conn, "ns-fail-emb", fam);
13768 }
13769 let embedder = FailingEmbedder;
13770 let resp = crate::mcp::handle_smart_load(
13771 &conn,
13772 &json!({"intent": "delete and forget stale memories"}),
13773 Some(&embedder as &dyn Embed),
13774 None,
13775 )
13776 .expect("smart_load must succeed even when embedder fails");
13777 assert_eq!(resp["chosen_family"], "lifecycle");
13780 assert_eq!(resp["chosen_family_source"], "keyword");
13781 }
13782
13783 #[test]
13786 fn chunkc_load_family_k_above_cap_clamps_to_100() {
13787 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13788 let _ = chunkc_seed_family_memory(&conn, "ns-cap", "core");
13789 let resp = crate::mcp::handle_load_family(
13790 &conn,
13791 &json!({"family": "core", "namespace": "ns-cap", "k": 5_000}),
13792 None,
13793 )
13794 .expect("must succeed");
13795 assert_eq!(resp["k"], 100);
13796 }
13797
13798 #[test]
13801 fn chunkc_load_family_unknown_family_rejected() {
13802 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13803 let err = crate::mcp::handle_load_family(&conn, &json!({"family": "not-a-family"}), None)
13804 .unwrap_err();
13805 assert!(
13806 err.to_lowercase().contains("family") || err.to_lowercase().contains("unknown"),
13807 "expected an UnknownFamily diagnostic, got: {err}"
13808 );
13809 }
13810
13811 #[test]
13813 fn chunkc_load_family_missing_family_rejected() {
13814 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13815 let err = crate::mcp::handle_load_family(&conn, &json!({"k": 5}), None).unwrap_err();
13816 assert!(err.contains("family"));
13817 }
13818
13819 #[test]
13825 fn chunkc_namespace_set_standard_with_governance_merges_metadata() {
13826 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13827 let id = chunkc_seed_memory(&conn, "chunkc-gov-set", "p", Tier::Long);
13828 let req = make_tools_call(
13829 "memory_namespace_set_standard",
13830 json!({
13831 "namespace": "chunkc-gov-set",
13832 "id": id,
13833 "governance": {
13834 "policy": "auto",
13835 },
13836 }),
13837 );
13838 let resp = invoke_handle_request(&conn, &req);
13839 assert!(
13840 resp.error.is_none(),
13841 "happy-path governance merge failed: {:?}",
13842 resp.error
13843 );
13844 }
13845
13846 #[test]
13849 fn chunkc_extract_governance_returns_parsed_policy_when_valid() {
13850 let policy = crate::models::GovernancePolicy::default();
13853 let policy_val = serde_json::to_value(&policy).unwrap();
13854 let mem_val = json!({
13855 "metadata": {
13856 "governance": policy_val,
13857 }
13858 });
13859 let gov = super::namespace::extract_governance(&mem_val);
13860 assert!(
13861 gov.is_object(),
13862 "expected parsed governance object, got {gov}"
13863 );
13864 }
13865
13866 #[test]
13870 fn chunkc_replay_truncates_large_transcript_when_not_verbose() {
13871 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13872 let memory_id = chunkc_seed_memory(&conn, "chunkc-replay-big", "m", Tier::Long);
13873 let big_content = "x".repeat(150 * 1024);
13875 let transcript = crate::transcripts::store(&conn, "chunkc-replay-big", &big_content, None)
13876 .expect("store transcript");
13877 crate::transcripts::link_transcript(&conn, &memory_id, &transcript.id, None, None)
13878 .expect("link transcript");
13879 let req = make_tools_call(
13880 "memory_replay",
13881 json!({"memory_id": memory_id, "verbose": false}),
13882 );
13883 let resp = invoke_handle_request(&conn, &req);
13884 assert!(
13885 resp.error.is_none(),
13886 "replay returned error: {:?}",
13887 resp.error
13888 );
13889 let payload = i4_decode_response_payload(&resp);
13890 let transcripts = payload["transcripts"]
13891 .as_array()
13892 .expect("transcripts array");
13893 assert_eq!(transcripts.len(), 1);
13894 assert_eq!(transcripts[0]["truncated"], true);
13895 assert!(transcripts[0].get("content").is_none());
13896 }
13897
13898 #[test]
13902 fn chunkc_forget_invalid_tier_string_silently_dropped() {
13903 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13904 let _ = chunkc_seed_memory(&conn, "chunkc-forget-tier", "v1", Tier::Mid);
13905 let req = make_tools_call(
13906 "memory_forget",
13907 json!({
13908 "namespace": "chunkc-forget-tier",
13909 "tier": "not-a-tier",
13910 "dry_run": true,
13911 }),
13912 );
13913 let resp = invoke_handle_request(&conn, &req);
13914 assert!(resp.error.is_none());
13915 let payload = i4_decode_response_payload(&resp);
13916 assert_eq!(payload["dry_run"], true);
13917 assert!(payload["would_delete"].as_u64().unwrap() >= 1);
13919 }
13920
13921 #[test]
13925 fn chunkc_archive_restore_success_returns_restored_true() {
13926 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13927 let id = chunkc_seed_memory(&conn, "chunkc-restore-ok", "rmem", Tier::Mid);
13928 db::forget(&conn, Some("chunkc-restore-ok"), None, None, true).unwrap();
13930 let req = make_tools_call("memory_archive_restore", json!({"id": id}));
13931 let resp = invoke_handle_request(&conn, &req);
13932 assert!(
13933 resp.error.is_none(),
13934 "restore should succeed: {:?}",
13935 resp.error
13936 );
13937 let payload = i4_decode_response_payload(&resp);
13938 assert_eq!(payload["restored"], true);
13939 }
13940
13941 #[test]
13944 fn chunkc_archive_purge_allowed_returns_purged_count() {
13945 let _gate = chunkc_lock_perms();
13946 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13947 let _ = chunkc_seed_memory(&conn, "chunkc-purge-ok", "victim", Tier::Mid);
13949 db::forget(&conn, Some("chunkc-purge-ok"), None, None, true).unwrap();
13950 crate::permissions::clear_active_permission_rules_for_test();
13952 let req = make_tools_call("memory_archive_purge", json!({}));
13955 let resp = invoke_handle_request(&conn, &req);
13956 assert!(
13957 resp.error.is_none(),
13958 "purge happy path failed: {:?}",
13959 resp.error
13960 );
13961 let payload = i4_decode_response_payload(&resp);
13962 assert!(payload["purged"].as_u64().is_some());
13963 }
13964
13965 #[test]
13967 fn chunkc_archive_gc_real_run_invokes_db_gc() {
13968 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
13969 let past = (chrono::Utc::now() - chrono::Duration::hours(2)).to_rfc3339();
13971 let mem = Memory {
13972 id: uuid::Uuid::new_v4().to_string(),
13973 tier: Tier::Short,
13974 namespace: "chunkc-gc-real".to_string(),
13975 title: "stale".to_string(),
13976 content: "stale".to_string(),
13977 tags: vec![],
13978 priority: 5,
13979 confidence: 1.0,
13980 source: "test".to_string(),
13981 access_count: 0,
13982 created_at: chrono::Utc::now().to_rfc3339(),
13983 updated_at: chrono::Utc::now().to_rfc3339(),
13984 last_accessed_at: None,
13985 expires_at: Some(past),
13986 metadata: json!({}),
13987 reflection_depth: 0,
13988 memory_kind: crate::models::MemoryKind::Observation,
13989 entity_id: None,
13990 persona_version: None,
13991 citations: Vec::new(),
13992 source_uri: None,
13993 source_span: None,
13994 confidence_source: crate::models::ConfidenceSource::CallerProvided,
13995 confidence_signals: None,
13996 confidence_decayed_at: None,
13997 version: 1,
13998 };
13999 db::insert(&conn, &mem).unwrap();
14000 let req = make_tools_call("memory_gc", json!({"dry_run": false}));
14001 let resp = invoke_handle_request(&conn, &req);
14002 assert!(resp.error.is_none(), "gc real run failed: {:?}", resp.error);
14003 let payload = i4_decode_response_payload(&resp);
14004 assert_eq!(payload["dry_run"], false);
14005 }
14006
14007 #[test]
14011 fn chunkc_archive_gc_dry_run_returns_count() {
14012 let conn = db::open(std::path::Path::new(":memory:")).unwrap();
14013 let past = (chrono::Utc::now() - chrono::Duration::hours(2)).to_rfc3339();
14014 let mem = Memory {
14015 id: uuid::Uuid::new_v4().to_string(),
14016 tier: Tier::Short,
14017 namespace: "chunkc-gc-dry".to_string(),
14018 title: "stale".to_string(),
14019 content: "stale".to_string(),
14020 tags: vec![],
14021 priority: 5,
14022 confidence: 1.0,
14023 source: "test".to_string(),
14024 access_count: 0,
14025 created_at: chrono::Utc::now().to_rfc3339(),
14026 updated_at: chrono::Utc::now().to_rfc3339(),
14027 last_accessed_at: None,
14028 expires_at: Some(past),
14029 metadata: json!({}),
14030 reflection_depth: 0,
14031 memory_kind: crate::models::MemoryKind::Observation,
14032 entity_id: None,
14033 persona_version: None,
14034 citations: Vec::new(),
14035 source_uri: None,
14036 source_span: None,
14037 confidence_source: crate::models::ConfidenceSource::CallerProvided,
14038 confidence_signals: None,
14039 confidence_decayed_at: None,
14040 version: 1,
14041 };
14042 db::insert(&conn, &mem).unwrap();
14043 let req = make_tools_call("memory_gc", json!({"dry_run": true}));
14044 let resp = invoke_handle_request(&conn, &req);
14045 assert!(resp.error.is_none(), "gc dry-run failed: {:?}", resp.error);
14046 let payload = i4_decode_response_payload(&resp);
14047 assert_eq!(payload["dry_run"], true);
14048 assert!(payload["collected"].as_u64().unwrap() >= 1);
14049 }
14050
14051 #[test]
14062 fn every_registered_tool_has_dispatch_arm_1050() {
14063 for tool in crate::mcp::registry::registered_tools() {
14064 let name = tool.name;
14065 assert!(
14066 super::lookup_dispatch(name).is_some(),
14067 "tool '{name}' is registered in registered_tools() but has no \
14068 dispatch arm in TOOL_DISPATCH_TABLE — clients calling \
14069 tools/call '{name}' will get -32601 unknown tool (#1050)"
14070 );
14071 }
14072 }
14073
14074 #[test]
14075 fn every_dispatch_arm_has_registered_tool_1050() {
14076 let registered: std::collections::HashSet<&str> = crate::mcp::registry::registered_tools()
14077 .iter()
14078 .map(|t| t.name)
14079 .collect();
14080 for (name, _f) in super::TOOL_DISPATCH_TABLE {
14081 assert!(
14082 registered.contains(*name),
14083 "dispatch arm '{name}' exists in TOOL_DISPATCH_TABLE but is not \
14084 registered in registered_tools() — orphan dispatch wrapper (#1050)"
14085 );
14086 }
14087 }
14088
14089 #[test]
14104 fn mcp_line_length_cap_1249_const_invariants() {
14105 assert!(
14106 super::MCP_MAX_LINE_BYTES > 1_000_000,
14107 "16 MiB cap must comfortably exceed largest realistic MCP request"
14108 );
14109 assert!(
14110 super::MCP_MAX_DRAIN_BYTES > super::MCP_MAX_LINE_BYTES,
14111 "drain ceiling must exceed line cap so overrun handling can drain to next \\n"
14112 );
14113 assert!(
14114 super::MCP_MAX_LINE_BYTES <= 64 * 1024 * 1024,
14115 "16 MiB upper bound — bigger caps make OOM-vector peers viable again"
14116 );
14117 }
14118
14119 #[test]
14124 fn mcp_line_length_cap_1249_read_until_take_overrun() {
14125 use std::io::{BufRead, Read};
14126 let cap: usize = 1024 * 1024;
14128 let payload = vec![b'A'; cap + 8192]; let mut src = std::io::Cursor::new(payload);
14130 let mut buf: Vec<u8> = Vec::new();
14131 let n = (&mut src)
14132 .take((cap as u64) + 1)
14133 .read_until(b'\n', &mut buf)
14134 .expect("read_until succeeds against in-memory cursor");
14135 assert_eq!(n, cap + 1, "should stop at the take() cap");
14137 assert_ne!(
14138 buf.last(),
14139 Some(&b'\n'),
14140 "buf must NOT end in \\n when the cap was hit — that's the overrun signal"
14141 );
14142 let mut scratch = [0u8; 64];
14145 let m = src.read(&mut scratch).expect("further reads succeed");
14146 assert!(
14147 m > 0,
14148 "underlying stream still has bytes for the drain path"
14149 );
14150 }
14151
14152 #[test]
14155 fn mcp_line_length_cap_1249_clean_newline_termination() {
14156 use std::io::BufRead;
14157 let mut payload: Vec<u8> = vec![b'X'; 4096];
14158 payload.push(b'\n');
14159 payload.extend_from_slice(b"second line\n");
14160 let mut src = std::io::Cursor::new(payload);
14161 let mut buf: Vec<u8> = Vec::new();
14162 let n = (&mut src)
14163 .take((super::MCP_MAX_LINE_BYTES as u64) + 1)
14164 .read_until(b'\n', &mut buf)
14165 .expect("read_until OK");
14166 assert_eq!(n, 4097);
14167 assert_eq!(buf.last(), Some(&b'\n'));
14168 buf.clear();
14170 let m = (&mut src)
14171 .take((super::MCP_MAX_LINE_BYTES as u64) + 1)
14172 .read_until(b'\n', &mut buf)
14173 .expect("read_until OK");
14174 assert_eq!(m, "second line\n".len());
14175 assert_eq!(buf.last(), Some(&b'\n'));
14176 }
14177}
14178
14179#[cfg(test)]
14184mod backfill_resilience_1595_tests {
14185 use super::*;
14186 use crate::models::{Memory, Tier};
14187
14188 const POISON_MARKER: &str = "poison-row-marker-1595";
14192
14193 fn seed(conn: &rusqlite::Connection, title: &str, content: &str) -> String {
14194 let now = chrono::Utc::now().to_rfc3339();
14195 let mem = Memory {
14196 id: uuid::Uuid::new_v4().to_string(),
14197 tier: Tier::Long,
14198 namespace: "bf-1595".to_string(),
14199 title: title.to_string(),
14200 content: content.to_string(),
14201 tags: vec![],
14202 priority: 5,
14203 confidence: 1.0,
14204 source: "test".to_string(),
14205 access_count: 0,
14206 created_at: now.clone(),
14207 updated_at: now,
14208 last_accessed_at: None,
14209 expires_at: None,
14210 metadata: serde_json::json!({}),
14211 reflection_depth: 0,
14212 memory_kind: crate::models::MemoryKind::Observation,
14213 entity_id: None,
14214 persona_version: None,
14215 citations: Vec::new(),
14216 source_uri: None,
14217 source_span: None,
14218 confidence_source: crate::models::ConfidenceSource::CallerProvided,
14219 confidence_signals: None,
14220 confidence_decayed_at: None,
14221 version: 1,
14222 };
14223 db::insert(conn, &mem).unwrap()
14224 }
14225
14226 struct PoisonEmbedder;
14231 impl Embed for PoisonEmbedder {
14232 fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>> {
14233 if text.contains(POISON_MARKER) {
14234 anyhow::bail!("test: the input length exceeds the context length");
14235 }
14236 Ok(vec![0.5_f32; 4])
14237 }
14238 }
14239
14240 struct RecordingEmbedder {
14244 seen_lens: std::sync::Mutex<Vec<usize>>,
14245 }
14246 impl Embed for RecordingEmbedder {
14247 fn embed(&self, text: &str) -> anyhow::Result<Vec<f32>> {
14248 self.seen_lens.lock().unwrap().push(text.len());
14249 Ok(vec![0.5_f32; 4])
14250 }
14251 }
14252
14253 #[test]
14258 fn backfill_skips_poison_row_and_continues_1595() {
14259 let mut conn = db::open(std::path::Path::new(":memory:")).unwrap();
14260 for i in 0..2 {
14261 seed(&conn, &format!("ok-head-{i}"), "plain healthy content");
14262 }
14263 seed(&conn, "poison", POISON_MARKER);
14264 for i in 0..2 {
14265 seed(&conn, &format!("ok-tail-{i}"), "plain healthy content");
14266 }
14267
14268 let ok = run_embedding_backfill_with_batch_size(&mut conn, &PoisonEmbedder, 2)
14269 .expect("sweep must not error");
14270 assert_eq!(ok, 4, "all healthy rows backfilled");
14271
14272 let remaining = db::get_unembedded_ids(&conn).unwrap();
14273 assert_eq!(remaining.len(), 1, "exactly skipped=1 (the poison row)");
14274 assert!(
14275 remaining[0].2.contains(POISON_MARKER),
14276 "the surviving unembedded row is the poison row"
14277 );
14278 }
14279
14280 #[test]
14284 fn backfill_oversize_row_skipped_client_side_1595() {
14285 let mut conn = db::open(std::path::Path::new(":memory:")).unwrap();
14286 seed(&conn, "small-a", "fits fine");
14287 seed(
14288 &conn,
14289 "huge",
14290 &"a".repeat(crate::embeddings::EMBED_MAX_BYTES + 1),
14291 );
14292 seed(&conn, "small-b", "also fits");
14293
14294 let emb = RecordingEmbedder {
14295 seen_lens: std::sync::Mutex::new(Vec::new()),
14296 };
14297 let ok = run_embedding_backfill_with_batch_size(&mut conn, &emb, 10)
14298 .expect("sweep must not error");
14299 assert_eq!(ok, 2, "both small rows backfilled");
14300
14301 let remaining = db::get_unembedded_ids(&conn).unwrap();
14302 assert_eq!(remaining.len(), 1, "oversize row skipped, not embedded");
14303 assert_eq!(remaining[0].1, "huge");
14304
14305 let lens = emb.seen_lens.lock().unwrap();
14306 assert!(
14307 lens.iter()
14308 .all(|&l| l <= crate::embeddings::EMBED_MAX_BYTES),
14309 "oversize text must never be sent to the embedder, seen lens: {lens:?}"
14310 );
14311 }
14312
14313 #[test]
14316 fn embed_rows_with_fallback_batch_fault_recovers_per_row_1595() {
14317 struct BatchFailsRowsWork;
14318 impl Embed for BatchFailsRowsWork {
14319 fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
14320 Ok(vec![0.25_f32; 3])
14321 }
14322 fn embed_batch(&self, _texts: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
14323 anyhow::bail!("test: synthetic chunk-level failure")
14324 }
14325 }
14326 let rows: Vec<(String, String, String)> = (0..3)
14327 .map(|i| (format!("id-{i}"), format!("t-{i}"), format!("c-{i}")))
14328 .collect();
14329 let out = embed_rows_with_fallback(&BatchFailsRowsWork, &rows);
14330 assert_eq!(out.entries.len(), 3);
14331 assert!(out.skipped.is_empty());
14332 assert_eq!(out.entries[0].0, "id-0");
14333 }
14334
14335 #[test]
14338 fn embed_rows_with_fallback_misaligned_batch_recovers_per_row_1595() {
14339 struct MisalignedEmbedder;
14340 impl Embed for MisalignedEmbedder {
14341 fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
14342 Ok(vec![0.75_f32; 3])
14343 }
14344 fn embed_batch(&self, _texts: &[&str]) -> anyhow::Result<Vec<Vec<f32>>> {
14345 Ok(vec![vec![0.1_f32; 3]])
14346 }
14347 }
14348 let rows: Vec<(String, String, String)> = (0..2)
14349 .map(|i| (format!("id-{i}"), format!("t-{i}"), format!("c-{i}")))
14350 .collect();
14351 let out = embed_rows_with_fallback(&MisalignedEmbedder, &rows);
14352 assert_eq!(out.entries.len(), 2, "per-row fallback recovers both");
14353 assert!(out.skipped.is_empty());
14354 assert!(
14355 out.entries.iter().all(|(_, v)| v.len() == 3),
14356 "vectors come from the per-row path, not the misaligned batch"
14357 );
14358 }
14359
14360 #[test]
14363 fn embed_rows_with_fallback_reports_per_row_skips_1595() {
14364 let rows = vec![
14365 ("id-ok".to_string(), "t".to_string(), "fine".to_string()),
14366 (
14367 "id-bad".to_string(),
14368 "t".to_string(),
14369 POISON_MARKER.to_string(),
14370 ),
14371 ];
14372 let out = embed_rows_with_fallback(&PoisonEmbedder, &rows);
14373 assert_eq!(out.entries.len(), 1);
14374 assert_eq!(out.entries[0].0, "id-ok");
14375 assert_eq!(out.skipped.len(), 1);
14376 assert_eq!(out.skipped[0].0, "id-bad");
14377 assert!(
14378 out.skipped[0].1.contains("context length"),
14379 "skip reason carries the embedder error: {}",
14380 out.skipped[0].1
14381 );
14382 }
14383
14384 #[test]
14386 fn embed_rows_with_fallback_empty_rows_is_noop_1595() {
14387 struct PanickingEmbedder;
14388 impl Embed for PanickingEmbedder {
14389 fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
14390 unreachable!("must not be called for an empty chunk")
14391 }
14392 }
14393 let out = embed_rows_with_fallback(&PanickingEmbedder, &[]);
14394 assert!(out.entries.is_empty());
14395 assert!(out.skipped.is_empty());
14396 }
14397
14398 #[test]
14402 fn backfill_write_fault_falls_back_per_row_1595() {
14403 struct EightDimEmbedder;
14404 impl Embed for EightDimEmbedder {
14405 fn embed(&self, _text: &str) -> anyhow::Result<Vec<f32>> {
14406 Ok(vec![0.5_f32; 8])
14407 }
14408 }
14409 let mut conn = db::open(std::path::Path::new(":memory:")).unwrap();
14410 let est = seed(&conn, "established", "already embedded");
14412 db::set_embedding(&conn, &est, &[0.1, 0.2, 0.3, 0.4]).unwrap();
14413 seed(&conn, "new-a", "needs embedding");
14414 seed(&conn, "new-b", "needs embedding");
14415
14416 let ok = run_embedding_backfill_with_batch_size(&mut conn, &EightDimEmbedder, 10)
14417 .expect("write faults must not propagate");
14418 assert_eq!(ok, 0, "dim-mismatched rows cannot land");
14419 assert_eq!(
14420 db::get_unembedded_ids(&conn).unwrap().len(),
14421 2,
14422 "both rows skipped (left for the next sweep), sweep terminated"
14423 );
14424 }
14425}