Skip to main content

subcog/mcp/tools/
mod.rs

1//! MCP tool implementations.
2//!
3//! Provides tool handlers for the Model Context Protocol.
4//!
5//! # Module Structure
6//!
7//! - [`definitions`]: Tool schema definitions (JSON Schema for input validation)
8//! - [`handlers`]: Tool execution logic
9//!   - [`handlers::core`]: Core memory operations (capture, recall, sync, etc.)
10//!   - [`handlers::prompts`]: Prompt management operations (save, list, run, etc.)
11
12mod definitions;
13mod handlers;
14
15use crate::{Error, Result};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::collections::HashMap;
19
20/// Registry of MCP tools.
21pub struct ToolRegistry {
22    /// Available tools.
23    tools: HashMap<String, ToolDefinition>,
24}
25
26impl ToolRegistry {
27    /// Creates a new tool registry with all subcog tools.
28    #[must_use]
29    #[allow(clippy::too_many_lines)]
30    pub fn new() -> Self {
31        let mut tools = HashMap::new();
32
33        tools.insert("subcog_capture".to_string(), definitions::capture_tool());
34        tools.insert("subcog_recall".to_string(), definitions::recall_tool());
35        tools.insert("subcog_status".to_string(), definitions::status_tool());
36        tools.insert(
37            "prompt_understanding".to_string(),
38            definitions::prompt_understanding_tool(),
39        );
40        tools.insert(
41            "subcog_namespaces".to_string(),
42            definitions::namespaces_tool(),
43        );
44        tools.insert(
45            "subcog_consolidate".to_string(),
46            definitions::consolidate_tool(),
47        );
48        tools.insert(
49            "subcog_get_summary".to_string(),
50            definitions::get_summary_tool(),
51        );
52        tools.insert("subcog_enrich".to_string(), definitions::enrich_tool());
53        tools.insert("subcog_sync".to_string(), definitions::sync_tool());
54        tools.insert("subcog_reindex".to_string(), definitions::reindex_tool());
55        tools.insert(
56            "subcog_gdpr_export".to_string(),
57            definitions::gdpr_export_tool(),
58        );
59        tools.insert(
60            "prompt_understanding".to_string(),
61            definitions::prompt_understanding_tool(),
62        );
63
64        // Consolidated prompt management tool
65        tools.insert("subcog_prompts".to_string(), definitions::prompts_tool());
66        // Legacy prompt management tools (for backward compatibility)
67        tools.insert("prompt_save".to_string(), definitions::prompt_save_tool());
68        tools.insert("prompt_list".to_string(), definitions::prompt_list_tool());
69        tools.insert("prompt_get".to_string(), definitions::prompt_get_tool());
70        tools.insert("prompt_run".to_string(), definitions::prompt_run_tool());
71        tools.insert(
72            "prompt_delete".to_string(),
73            definitions::prompt_delete_tool(),
74        );
75
76        // Core CRUD tools (industry parity: Mem0, Zep, LangMem)
77        tools.insert("subcog_get".to_string(), definitions::get_tool());
78        tools.insert("subcog_delete".to_string(), definitions::delete_tool());
79        tools.insert("subcog_update".to_string(), definitions::update_tool());
80
81        // Mem0 parity tools: list, delete_all, restore, history
82        tools.insert("subcog_list".to_string(), definitions::list_tool());
83        tools.insert(
84            "subcog_delete_all".to_string(),
85            definitions::delete_all_tool(),
86        );
87        tools.insert("subcog_restore".to_string(), definitions::restore_tool());
88        tools.insert("subcog_history".to_string(), definitions::history_tool());
89
90        // Knowledge graph tools
91        tools.insert("subcog_entities".to_string(), definitions::entities_tool());
92        tools.insert(
93            "subcog_relationships".to_string(),
94            definitions::relationships_tool(),
95        );
96        tools.insert(
97            "subcog_graph_query".to_string(),
98            definitions::graph_query_tool(),
99        );
100        tools.insert(
101            "subcog_extract_entities".to_string(),
102            definitions::extract_entities_tool(),
103        );
104        tools.insert(
105            "subcog_entity_merge".to_string(),
106            definitions::entity_merge_tool(),
107        );
108        tools.insert(
109            "subcog_relationship_infer".to_string(),
110            definitions::relationship_infer_tool(),
111        );
112        tools.insert(
113            "subcog_graph_visualize".to_string(),
114            definitions::graph_visualize_tool(),
115        );
116        // Consolidated graph tool
117        tools.insert("subcog_graph".to_string(), definitions::graph_tool());
118
119        // Session initialization tool
120        tools.insert("subcog_init".to_string(), definitions::init_tool());
121
122        // Consolidated context template tool
123        tools.insert(
124            "subcog_templates".to_string(),
125            definitions::templates_tool(),
126        );
127        // Legacy context template tools (for backward compatibility)
128        tools.insert(
129            "context_template_save".to_string(),
130            definitions::context_template_save_tool(),
131        );
132        tools.insert(
133            "context_template_list".to_string(),
134            definitions::context_template_list_tool(),
135        );
136        tools.insert(
137            "context_template_get".to_string(),
138            definitions::context_template_get_tool(),
139        );
140        tools.insert(
141            "context_template_render".to_string(),
142            definitions::context_template_render_tool(),
143        );
144        tools.insert(
145            "context_template_delete".to_string(),
146            definitions::context_template_delete_tool(),
147        );
148
149        // Group management tools (feature-gated)
150        #[cfg(feature = "group-scope")]
151        {
152            // Consolidated group management tool
153            tools.insert("subcog_groups".to_string(), definitions::groups_tool());
154            // Legacy group management tools (for backward compatibility)
155            tools.insert(
156                "subcog_group_create".to_string(),
157                definitions::group_create_tool(),
158            );
159            tools.insert(
160                "subcog_group_list".to_string(),
161                definitions::group_list_tool(),
162            );
163            tools.insert(
164                "subcog_group_get".to_string(),
165                definitions::group_get_tool(),
166            );
167            tools.insert(
168                "subcog_group_add_member".to_string(),
169                definitions::group_add_member_tool(),
170            );
171            tools.insert(
172                "subcog_group_remove_member".to_string(),
173                definitions::group_remove_member_tool(),
174            );
175            tools.insert(
176                "subcog_group_update_role".to_string(),
177                definitions::group_update_role_tool(),
178            );
179            tools.insert(
180                "subcog_group_delete".to_string(),
181                definitions::group_delete_tool(),
182            );
183        }
184
185        Self { tools }
186    }
187
188    /// Returns all tool definitions.
189    #[must_use]
190    pub fn list_tools(&self) -> Vec<&ToolDefinition> {
191        self.tools.values().collect()
192    }
193
194    /// Gets a tool definition by name.
195    #[must_use]
196    pub fn get_tool(&self, name: &str) -> Option<&ToolDefinition> {
197        self.tools.get(name)
198    }
199
200    /// Executes a tool with the given arguments.
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if the tool execution fails.
205    pub fn execute(&self, name: &str, arguments: Value) -> Result<ToolResult> {
206        // Handle group management tools (feature-gated)
207        #[cfg(feature = "group-scope")]
208        {
209            let group_result = match name {
210                // Consolidated group management tool
211                "subcog_groups" => Some(handlers::execute_groups(arguments.clone())),
212                // Legacy group management tools
213                "subcog_group_create" => Some(handlers::execute_group_create(arguments.clone())),
214                "subcog_group_list" => Some(handlers::execute_group_list(arguments.clone())),
215                "subcog_group_get" => Some(handlers::execute_group_get(arguments.clone())),
216                "subcog_group_add_member" => {
217                    Some(handlers::execute_group_add_member(arguments.clone()))
218                },
219                "subcog_group_remove_member" => {
220                    Some(handlers::execute_group_remove_member(arguments.clone()))
221                },
222                "subcog_group_update_role" => {
223                    Some(handlers::execute_group_update_role(arguments.clone()))
224                },
225                "subcog_group_delete" => Some(handlers::execute_group_delete(arguments.clone())),
226                _ => None,
227            };
228            if let Some(result) = group_result {
229                return result;
230            }
231        }
232
233        let result = match name {
234            "subcog_capture" => handlers::execute_capture(arguments),
235            "subcog_recall" => handlers::execute_recall(arguments),
236            "subcog_status" => handlers::execute_status(arguments),
237            "prompt_understanding" => handlers::execute_prompt_understanding(arguments),
238            "subcog_namespaces" => handlers::execute_namespaces(arguments),
239            "subcog_consolidate" => handlers::execute_consolidate(arguments),
240            "subcog_get_summary" => handlers::execute_get_summary(arguments),
241            "subcog_enrich" => handlers::execute_enrich(arguments),
242            "subcog_reindex" => handlers::execute_reindex(arguments),
243            "subcog_gdpr_export" => handlers::execute_gdpr_export(arguments),
244            // Consolidated prompt management tool
245            "subcog_prompts" => handlers::execute_prompts(arguments),
246            // Legacy prompt management tools
247            "prompt_save" => handlers::execute_prompt_save(arguments),
248            "prompt_list" => handlers::execute_prompt_list(arguments),
249            "prompt_get" => handlers::execute_prompt_get(arguments),
250            "prompt_run" => handlers::execute_prompt_run(arguments),
251            "prompt_delete" => handlers::execute_prompt_delete(arguments),
252            // Core CRUD tools (industry parity: Mem0, Zep, LangMem)
253            "subcog_get" => handlers::execute_get(arguments),
254            "subcog_delete" => handlers::execute_delete(arguments),
255            "subcog_update" => handlers::execute_update(arguments),
256            // Mem0 parity tools: list, delete_all, restore, history
257            "subcog_list" => handlers::execute_list(arguments),
258            "subcog_delete_all" => handlers::execute_delete_all(arguments),
259            "subcog_restore" => handlers::execute_restore(arguments),
260            "subcog_history" => handlers::execute_history(arguments),
261            // Knowledge graph tools
262            "subcog_entities" => handlers::execute_entities(arguments),
263            "subcog_relationships" => handlers::execute_relationships(arguments),
264            "subcog_graph_query" => handlers::execute_graph_query(arguments),
265            "subcog_extract_entities" => handlers::execute_extract_entities(arguments),
266            "subcog_entity_merge" => handlers::execute_entity_merge(arguments),
267            "subcog_relationship_infer" => handlers::execute_relationship_infer(arguments),
268            "subcog_graph_visualize" => handlers::execute_graph_visualize(arguments),
269            // Consolidated graph tool
270            "subcog_graph" => handlers::execute_graph(arguments),
271            // Session initialization
272            "subcog_init" => handlers::execute_init(arguments),
273            // Consolidated context template tool
274            "subcog_templates" => handlers::execute_templates(arguments),
275            // Legacy context template tools
276            "context_template_save" => handlers::execute_context_template_save(arguments),
277            "context_template_list" => handlers::execute_context_template_list(arguments),
278            "context_template_get" => handlers::execute_context_template_get(arguments),
279            "context_template_render" => handlers::execute_context_template_render(arguments),
280            "context_template_delete" => handlers::execute_context_template_delete(arguments),
281            _ => Err(Error::InvalidInput(format!("Unknown tool: {name}"))),
282        }?;
283
284        // Append hint for uninitialized sessions (except for init/prompt_understanding tools)
285        let skip_hint = matches!(
286            name,
287            "subcog_init" | "prompt_understanding" | "subcog_status"
288        );
289
290        if !skip_hint && super::session::should_show_hint() {
291            // Append hint to the result
292            let mut new_content = result.content;
293            if let Some(ToolContent::Text { text }) = new_content.last_mut() {
294                text.push_str(super::session::get_hint_message());
295            }
296            return Ok(ToolResult {
297                content: new_content,
298                is_error: result.is_error,
299            });
300        }
301
302        Ok(result)
303    }
304}
305
306impl Default for ToolRegistry {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312/// Definition of an MCP tool.
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct ToolDefinition {
315    /// Tool name.
316    pub name: String,
317    /// Tool description.
318    pub description: String,
319    /// JSON Schema for input validation.
320    pub input_schema: Value,
321}
322
323/// Result of a tool execution.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ToolResult {
326    /// Content returned by the tool.
327    pub content: Vec<ToolContent>,
328    /// Whether the result represents an error.
329    #[serde(default)]
330    pub is_error: bool,
331}
332
333/// Content types that can be returned by tools.
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(tag = "type", rename_all = "lowercase")]
336pub enum ToolContent {
337    /// Text content.
338    Text {
339        /// The text content.
340        text: String,
341    },
342    /// Image content (base64 encoded).
343    Image {
344        /// Base64-encoded image data.
345        data: String,
346        /// MIME type of the image.
347        mime_type: String,
348    },
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use crate::mcp::tool_types::{
355        domain_scope_to_display, find_missing_required_variables, format_variable_info,
356        parse_domain_scope, parse_namespace, parse_search_mode, truncate,
357    };
358    use crate::models::{Namespace, SearchMode};
359    use crate::storage::index::DomainScope;
360
361    #[test]
362    fn test_tool_registry_creation() {
363        let registry = ToolRegistry::new();
364        let tools = registry.list_tools();
365
366        assert!(!tools.is_empty());
367        assert!(registry.get_tool("subcog_capture").is_some());
368        assert!(registry.get_tool("subcog_recall").is_some());
369        assert!(registry.get_tool("subcog_status").is_some());
370        assert!(registry.get_tool("subcog_namespaces").is_some());
371        assert!(registry.get_tool("subcog_consolidate").is_some());
372        assert!(registry.get_tool("subcog_get_summary").is_some());
373    }
374
375    #[test]
376    fn test_tool_definitions() {
377        let registry = ToolRegistry::new();
378
379        let capture = registry.get_tool("subcog_capture").unwrap();
380        assert!(capture.description.contains("memory"));
381        assert!(
382            capture.input_schema["required"]
383                .as_array()
384                .unwrap()
385                .contains(&serde_json::json!("content"))
386        );
387    }
388
389    #[test]
390    fn test_execute_namespaces() {
391        let registry = ToolRegistry::new();
392        let result = registry
393            .execute("subcog_namespaces", serde_json::json!({}))
394            .unwrap();
395
396        assert!(!result.is_error);
397        assert!(!result.content.is_empty());
398
399        if let ToolContent::Text { text } = &result.content[0] {
400            assert!(text.contains("decisions"));
401            assert!(text.contains("patterns"));
402        }
403    }
404
405    #[test]
406    fn test_execute_status() {
407        let registry = ToolRegistry::new();
408        let result = registry
409            .execute("subcog_status", serde_json::json!({}))
410            .unwrap();
411
412        assert!(!result.is_error);
413        if let ToolContent::Text { text } = &result.content[0] {
414            assert!(text.contains("version"));
415        }
416    }
417
418    #[test]
419    fn test_execute_unknown_tool() {
420        let registry = ToolRegistry::new();
421        let result = registry.execute("unknown_tool", serde_json::json!({}));
422
423        assert!(result.is_err());
424    }
425
426    #[test]
427    fn test_parse_namespace() {
428        assert_eq!(parse_namespace("decisions"), Namespace::Decisions);
429        assert_eq!(parse_namespace("PATTERNS"), Namespace::Patterns);
430        assert_eq!(parse_namespace("tech-debt"), Namespace::TechDebt);
431    }
432
433    #[test]
434    fn test_parse_search_mode() {
435        assert_eq!(parse_search_mode("vector"), SearchMode::Vector);
436        assert_eq!(parse_search_mode("TEXT"), SearchMode::Text);
437        assert_eq!(parse_search_mode("hybrid"), SearchMode::Hybrid);
438        assert_eq!(parse_search_mode("unknown"), SearchMode::Hybrid);
439    }
440
441    #[test]
442    fn test_truncate() {
443        assert_eq!(truncate("short", 10), "short");
444        assert_eq!(truncate("this is a long string", 10), "this is...");
445    }
446
447    // ============================================================================
448    // Prompt Tool Tests
449    // ============================================================================
450
451    #[test]
452    fn test_prompt_tools_registered() {
453        let registry = ToolRegistry::new();
454
455        assert!(registry.get_tool("prompt_understanding").is_some());
456        assert!(registry.get_tool("prompt_save").is_some());
457        assert!(registry.get_tool("prompt_list").is_some());
458        assert!(registry.get_tool("prompt_get").is_some());
459        assert!(registry.get_tool("prompt_run").is_some());
460        assert!(registry.get_tool("prompt_delete").is_some());
461    }
462
463    #[test]
464    fn test_prompt_save_tool_schema() {
465        let registry = ToolRegistry::new();
466        let tool = registry.get_tool("prompt_save").unwrap();
467
468        assert!(tool.description.contains("Save"));
469        assert!(tool.input_schema["properties"]["name"].is_object());
470        assert!(tool.input_schema["properties"]["content"].is_object());
471        assert!(tool.input_schema["properties"]["file_path"].is_object());
472        assert!(tool.input_schema["properties"]["domain"].is_object());
473        assert!(tool.input_schema["properties"]["variables"].is_object());
474    }
475
476    #[test]
477    fn test_prompt_list_tool_schema() {
478        let registry = ToolRegistry::new();
479        let tool = registry.get_tool("prompt_list").unwrap();
480
481        assert!(tool.description.contains("List"));
482        assert!(tool.input_schema["properties"]["domain"].is_object());
483        assert!(tool.input_schema["properties"]["tags"].is_object());
484        assert!(tool.input_schema["properties"]["name_pattern"].is_object());
485    }
486
487    #[test]
488    fn test_prompt_get_tool_schema() {
489        let registry = ToolRegistry::new();
490        let tool = registry.get_tool("prompt_get").unwrap();
491
492        assert!(tool.description.contains("Get"));
493        let required = tool.input_schema["required"].as_array().unwrap();
494        assert!(required.contains(&serde_json::json!("name")));
495    }
496
497    #[test]
498    fn test_prompt_run_tool_schema() {
499        let registry = ToolRegistry::new();
500        let tool = registry.get_tool("prompt_run").unwrap();
501
502        assert!(tool.description.contains("Run"));
503        assert!(tool.input_schema["properties"]["variables"].is_object());
504    }
505
506    #[test]
507    fn test_prompt_delete_tool_schema() {
508        let registry = ToolRegistry::new();
509        let tool = registry.get_tool("prompt_delete").unwrap();
510
511        assert!(tool.description.contains("Delete"));
512        let required = tool.input_schema["required"].as_array().unwrap();
513        assert!(required.contains(&serde_json::json!("name")));
514        assert!(required.contains(&serde_json::json!("domain")));
515    }
516
517    #[test]
518    fn test_parse_domain_scope() {
519        assert_eq!(parse_domain_scope(Some("project")), DomainScope::Project);
520        assert_eq!(parse_domain_scope(Some("PROJECT")), DomainScope::Project);
521        assert_eq!(parse_domain_scope(Some("user")), DomainScope::User);
522        assert_eq!(parse_domain_scope(Some("org")), DomainScope::Org);
523        assert_eq!(parse_domain_scope(None), DomainScope::Project);
524        assert_eq!(parse_domain_scope(Some("unknown")), DomainScope::Project);
525    }
526
527    #[test]
528    fn test_domain_scope_to_display() {
529        assert_eq!(domain_scope_to_display(DomainScope::Project), "project");
530        assert_eq!(domain_scope_to_display(DomainScope::User), "user");
531        assert_eq!(domain_scope_to_display(DomainScope::Org), "org");
532    }
533
534    #[test]
535    fn test_format_variable_info() {
536        use crate::models::PromptVariable;
537
538        // Required variable with description and default
539        let var = PromptVariable {
540            name: "name".to_string(),
541            description: Some("User name".to_string()),
542            default: Some("World".to_string()),
543            required: true,
544        };
545        let info = format_variable_info(&var);
546        assert!(info.contains("**{{name}}**"));
547        assert!(info.contains("User name"));
548        assert!(info.contains("World"));
549        assert!(!info.contains("[optional]"));
550
551        // Optional variable
552        let var = PromptVariable {
553            name: "extra".to_string(),
554            description: None,
555            default: None,
556            required: false,
557        };
558        let info = format_variable_info(&var);
559        assert!(info.contains("[optional]"));
560    }
561
562    #[test]
563    fn test_find_missing_required_variables() {
564        use crate::models::PromptVariable;
565
566        let variables = vec![
567            PromptVariable {
568                name: "required_var".to_string(),
569                description: None,
570                default: None,
571                required: true,
572            },
573            PromptVariable {
574                name: "optional_var".to_string(),
575                description: None,
576                default: None,
577                required: false,
578            },
579            PromptVariable {
580                name: "with_default".to_string(),
581                description: None,
582                default: Some("default_value".to_string()),
583                required: true,
584            },
585        ];
586
587        // No values provided - only required_var should be missing
588        let values = HashMap::new();
589        let missing = find_missing_required_variables(&variables, &values);
590        assert_eq!(missing, vec!["required_var"]);
591
592        // With required_var provided - nothing missing
593        let mut values = HashMap::new();
594        values.insert("required_var".to_string(), "value".to_string());
595        let missing = find_missing_required_variables(&variables, &values);
596        assert!(missing.is_empty());
597    }
598
599    // ============================================================================
600    // Error Response Format Validation Tests
601    // ============================================================================
602
603    #[test]
604    fn test_error_response_unknown_tool() {
605        let registry = ToolRegistry::new();
606        let result = registry.execute("nonexistent_tool", serde_json::json!({}));
607
608        assert!(result.is_err());
609        let err = result.unwrap_err();
610        // Verify error message format
611        let msg = err.to_string();
612        assert!(
613            msg.contains("Unknown tool") || msg.contains("nonexistent_tool"),
614            "Error should mention unknown tool: {msg}"
615        );
616    }
617
618    #[test]
619    fn test_error_response_invalid_json_arguments() {
620        let registry = ToolRegistry::new();
621
622        // Invalid argument type for subcog_capture - namespace should be string
623        let result = registry.execute(
624            "subcog_capture",
625            serde_json::json!({
626                "content": "test content",
627                "namespace": 12345,  // Invalid: should be string
628            }),
629        );
630
631        // Should return error due to deserialization failure
632        assert!(result.is_err());
633    }
634
635    #[test]
636    fn test_error_response_missing_required_argument() {
637        let registry = ToolRegistry::new();
638
639        // Missing required 'content' field for subcog_capture
640        let result = registry.execute(
641            "subcog_capture",
642            serde_json::json!({
643                "namespace": "decisions",
644            }),
645        );
646
647        assert!(result.is_err());
648    }
649
650    #[test]
651    fn test_error_response_prompt_get_not_found() {
652        // This test verifies that prompt_get returns proper error for missing prompts
653        let registry = ToolRegistry::new();
654        let result = registry.execute(
655            "prompt_get",
656            serde_json::json!({
657                "name": "nonexistent-prompt-that-does-not-exist-12345",
658            }),
659        );
660
661        // Result might be error or a tool result with is_error=true
662        let Ok(tool_result) = result else {
663            // Error propagated - expected behavior
664            return;
665        };
666
667        // If is_error is true, that's expected
668        if tool_result.is_error {
669            return;
670        }
671
672        // Otherwise, content should indicate "not found" or "error"
673        let ToolContent::Text { text } = &tool_result.content[0] else {
674            return;
675        };
676
677        assert!(
678            text.to_lowercase().contains("not found") || text.to_lowercase().contains("error"),
679            "Expected 'not found' or 'error' in response: {text}"
680        );
681    }
682
683    #[test]
684    fn test_error_response_prompt_save_missing_content() {
685        let registry = ToolRegistry::new();
686
687        // Missing both content and file_path
688        let result = registry.execute(
689            "prompt_save",
690            serde_json::json!({
691                "name": "test-prompt",
692            }),
693        );
694
695        // Should fail - either content or file_path required
696        assert!(result.is_err());
697        let err = result.unwrap_err();
698        let msg = err.to_string();
699        assert!(
700            msg.contains("content") || msg.contains("file_path"),
701            "Error should mention missing content/file_path: {msg}"
702        );
703    }
704
705    #[test]
706    fn test_error_response_prompt_delete_not_found() {
707        let registry = ToolRegistry::new();
708        let result = registry.execute(
709            "prompt_delete",
710            serde_json::json!({
711                "name": "nonexistent-prompt-12345",
712                "domain": "project",
713            }),
714        );
715
716        // Result should indicate not found (either as error or is_error=true)
717        let Ok(tool_result) = result else {
718            // Error propagated - expected behavior
719            return;
720        };
721
722        if !tool_result.is_error {
723            return;
724        }
725
726        let ToolContent::Text { text } = &tool_result.content[0] else {
727            return;
728        };
729
730        assert!(
731            text.to_lowercase().contains("not found")
732                || text.to_lowercase().contains("error")
733                || text.to_lowercase().contains("failed"),
734            "Error response should indicate failure: {text}"
735        );
736    }
737
738    #[test]
739    fn test_error_response_prompt_run_missing_variables() {
740        let registry = ToolRegistry::new();
741
742        // Try to run a prompt without providing required variables
743        // First need a prompt that exists with required variables
744        let result = registry.execute(
745            "prompt_run",
746            serde_json::json!({
747                "name": "nonexistent-prompt-12345",
748            }),
749        );
750
751        // Should fail - prompt doesn't exist (either as Err or is_error=true)
752        let is_error = match &result {
753            Err(_) => true,
754            Ok(tool_result) => tool_result.is_error,
755        };
756
757        // Either outcome indicates proper error handling
758        assert!(
759            is_error || result.is_ok(),
760            "Should handle missing prompt gracefully"
761        );
762    }
763
764    #[test]
765    fn test_error_response_recall_invalid_filter() {
766        let registry = ToolRegistry::new();
767
768        // Valid recall with empty query should work but return no results
769        let result = registry.execute(
770            "subcog_recall",
771            serde_json::json!({
772                "query": "",
773                "limit": 10,
774            }),
775        );
776
777        // Empty query might return error or empty results
778        if let Err(e) = result {
779            // If error, should mention the query issue
780            let msg = e.to_string();
781            assert!(
782                msg.contains("query") || msg.contains("empty") || msg.contains("required"),
783                "Error should be descriptive: {msg}"
784            );
785            return;
786        }
787
788        // Otherwise, check tool result has content
789        let tool_result = result.expect("checked above");
790        assert!(!tool_result.content.is_empty());
791    }
792
793    #[test]
794    fn test_tool_result_content_format() {
795        let registry = ToolRegistry::new();
796
797        // Test that successful results have proper content format
798        let result = registry.execute("subcog_namespaces", serde_json::json!({}));
799        assert!(result.is_ok());
800
801        let tool_result = result.expect("checked above");
802        assert!(!tool_result.is_error);
803        assert!(!tool_result.content.is_empty());
804
805        // Content should be Text type
806        let ToolContent::Text { text } = &tool_result.content[0] else {
807            unreachable!("subcog_namespaces always returns Text content");
808        };
809        assert!(!text.is_empty());
810    }
811
812    #[test]
813    fn test_status_tool_returns_structured_info() {
814        let registry = ToolRegistry::new();
815        let result = registry
816            .execute("subcog_status", serde_json::json!({}))
817            .unwrap();
818
819        assert!(!result.is_error);
820
821        if let ToolContent::Text { text } = &result.content[0] {
822            // Should contain version info
823            assert!(text.contains("version") || text.contains("Version"));
824            // Should be human readable
825            assert!(text.len() > 10);
826        }
827    }
828
829    // ============================================================================
830    // Core CRUD Tool Tests (Industry Parity: Mem0, Zep, LangMem)
831    // ============================================================================
832
833    #[test]
834    fn test_crud_tools_registered() {
835        let registry = ToolRegistry::new();
836
837        assert!(registry.get_tool("subcog_get").is_some());
838        assert!(registry.get_tool("subcog_delete").is_some());
839        assert!(registry.get_tool("subcog_update").is_some());
840    }
841
842    #[test]
843    fn test_get_tool_schema() {
844        let registry = ToolRegistry::new();
845        let tool = registry.get_tool("subcog_get").unwrap();
846
847        assert!(tool.description.contains("Get"));
848        assert!(tool.input_schema["properties"]["memory_id"].is_object());
849        let required = tool.input_schema["required"].as_array().unwrap();
850        assert!(required.contains(&serde_json::json!("memory_id")));
851    }
852
853    #[test]
854    fn test_delete_tool_schema() {
855        let registry = ToolRegistry::new();
856        let tool = registry.get_tool("subcog_delete").unwrap();
857
858        assert!(tool.description.contains("Delete"));
859        assert!(tool.input_schema["properties"]["memory_id"].is_object());
860        assert!(tool.input_schema["properties"]["hard"].is_object());
861        let required = tool.input_schema["required"].as_array().unwrap();
862        assert!(required.contains(&serde_json::json!("memory_id")));
863    }
864
865    #[test]
866    fn test_update_tool_schema() {
867        let registry = ToolRegistry::new();
868        let tool = registry.get_tool("subcog_update").unwrap();
869
870        assert!(tool.description.contains("Update"));
871        assert!(tool.input_schema["properties"]["memory_id"].is_object());
872        assert!(tool.input_schema["properties"]["content"].is_object());
873        assert!(tool.input_schema["properties"]["tags"].is_object());
874        let required = tool.input_schema["required"].as_array().unwrap();
875        assert!(required.contains(&serde_json::json!("memory_id")));
876    }
877
878    #[test]
879    fn test_get_nonexistent_memory() {
880        let registry = ToolRegistry::new();
881        let result = registry.execute(
882            "subcog_get",
883            serde_json::json!({
884                "memory_id": "nonexistent-memory-12345"
885            }),
886        );
887
888        // Should succeed but return is_error=true for not found
889        let Ok(tool_result) = result else {
890            // Also acceptable if it returns Err
891            return;
892        };
893
894        assert!(tool_result.is_error);
895        if let ToolContent::Text { text } = &tool_result.content[0] {
896            assert!(text.to_lowercase().contains("not found"));
897        }
898    }
899
900    #[test]
901    fn test_delete_nonexistent_memory() {
902        let registry = ToolRegistry::new();
903        let result = registry.execute(
904            "subcog_delete",
905            serde_json::json!({
906                "memory_id": "nonexistent-memory-12345"
907            }),
908        );
909
910        // Should succeed but return is_error=true for not found
911        let Ok(tool_result) = result else {
912            return;
913        };
914
915        assert!(tool_result.is_error);
916        if let ToolContent::Text { text } = &tool_result.content[0] {
917            assert!(text.to_lowercase().contains("not found"));
918        }
919    }
920
921    #[test]
922    fn test_update_nonexistent_memory() {
923        let registry = ToolRegistry::new();
924        let result = registry.execute(
925            "subcog_update",
926            serde_json::json!({
927                "memory_id": "nonexistent-memory-12345",
928                "content": "new content"
929            }),
930        );
931
932        // Should succeed but return is_error=true for not found
933        let Ok(tool_result) = result else {
934            return;
935        };
936
937        assert!(tool_result.is_error);
938        if let ToolContent::Text { text } = &tool_result.content[0] {
939            assert!(text.to_lowercase().contains("not found"));
940        }
941    }
942
943    #[test]
944    fn test_update_requires_at_least_one_field() {
945        let registry = ToolRegistry::new();
946        let result = registry.execute(
947            "subcog_update",
948            serde_json::json!({
949                "memory_id": "some-memory-id"
950                // No content or tags provided
951            }),
952        );
953
954        // Should fail - at least one of content or tags is required
955        assert!(result.is_err());
956        let err = result.unwrap_err();
957        let msg = err.to_string();
958        assert!(
959            msg.contains("content") || msg.contains("tags"),
960            "Error should mention missing fields: {msg}"
961        );
962    }
963
964    #[test]
965    fn test_delete_hard_flag_defaults_to_false() {
966        // Verify schema shows default is false
967        let registry = ToolRegistry::new();
968        let tool = registry.get_tool("subcog_delete").unwrap();
969
970        let hard_schema = &tool.input_schema["properties"]["hard"];
971        assert_eq!(hard_schema["default"], serde_json::json!(false));
972    }
973
974    // ============================================================================
975    // Mem0 Parity Tool Tests (list, delete_all, restore, history)
976    // ============================================================================
977
978    #[test]
979    fn test_mem0_parity_tools_registered() {
980        let registry = ToolRegistry::new();
981
982        assert!(registry.get_tool("subcog_list").is_some());
983        assert!(registry.get_tool("subcog_delete_all").is_some());
984        assert!(registry.get_tool("subcog_restore").is_some());
985        assert!(registry.get_tool("subcog_history").is_some());
986    }
987
988    #[test]
989    fn test_list_tool_schema() {
990        let registry = ToolRegistry::new();
991        let tool = registry.get_tool("subcog_list").unwrap();
992
993        assert!(tool.description.contains("List"));
994        assert!(tool.input_schema["properties"]["filter"].is_object());
995        assert!(tool.input_schema["properties"]["limit"].is_object());
996        assert!(tool.input_schema["properties"]["offset"].is_object());
997        assert!(tool.input_schema["properties"]["user_id"].is_object());
998        assert!(tool.input_schema["properties"]["agent_id"].is_object());
999        // No required fields - all optional
1000        let required = tool.input_schema.get("required");
1001        assert!(required.is_none() || required.unwrap().as_array().unwrap().is_empty());
1002    }
1003
1004    #[test]
1005    fn test_delete_all_tool_schema() {
1006        let registry = ToolRegistry::new();
1007        let tool = registry.get_tool("subcog_delete_all").unwrap();
1008
1009        assert!(tool.description.to_lowercase().contains("delete"));
1010        assert!(tool.input_schema["properties"]["filter"].is_object());
1011        assert!(tool.input_schema["properties"]["dry_run"].is_object());
1012        assert!(tool.input_schema["properties"]["hard"].is_object());
1013        assert!(tool.input_schema["properties"]["user_id"].is_object());
1014        // dry_run defaults to true for safety
1015        assert_eq!(
1016            tool.input_schema["properties"]["dry_run"]["default"],
1017            serde_json::json!(true)
1018        );
1019    }
1020
1021    #[test]
1022    fn test_restore_tool_schema() {
1023        let registry = ToolRegistry::new();
1024        let tool = registry.get_tool("subcog_restore").unwrap();
1025
1026        assert!(tool.description.contains("Restore"));
1027        assert!(tool.input_schema["properties"]["memory_id"].is_object());
1028        let required = tool.input_schema["required"].as_array().unwrap();
1029        assert!(required.contains(&serde_json::json!("memory_id")));
1030    }
1031
1032    #[test]
1033    fn test_history_tool_schema() {
1034        let registry = ToolRegistry::new();
1035        let tool = registry.get_tool("subcog_history").unwrap();
1036
1037        assert!(tool.description.contains("history") || tool.description.contains("History"));
1038        assert!(tool.input_schema["properties"]["memory_id"].is_object());
1039        assert!(tool.input_schema["properties"]["limit"].is_object());
1040        let required = tool.input_schema["required"].as_array().unwrap();
1041        assert!(required.contains(&serde_json::json!("memory_id")));
1042    }
1043
1044    #[test]
1045    fn test_list_with_empty_args() {
1046        let registry = ToolRegistry::new();
1047        let result = registry.execute("subcog_list", serde_json::json!({}));
1048
1049        // Should succeed with empty args (returns all memories or empty list)
1050        assert!(result.is_ok());
1051        let tool_result = result.unwrap();
1052        assert!(!tool_result.content.is_empty());
1053    }
1054
1055    #[test]
1056    fn test_list_with_pagination() {
1057        let registry = ToolRegistry::new();
1058        let result = registry.execute(
1059            "subcog_list",
1060            serde_json::json!({
1061                "limit": 10,
1062                "offset": 0
1063            }),
1064        );
1065
1066        assert!(result.is_ok());
1067        let tool_result = result.unwrap();
1068        assert!(!tool_result.content.is_empty());
1069    }
1070
1071    #[test]
1072    fn test_list_with_user_scoping() {
1073        let registry = ToolRegistry::new();
1074        let result = registry.execute(
1075            "subcog_list",
1076            serde_json::json!({
1077                "user_id": "test-user-123",
1078                "limit": 5
1079            }),
1080        );
1081
1082        assert!(result.is_ok());
1083        let tool_result = result.unwrap();
1084        assert!(!tool_result.content.is_empty());
1085    }
1086
1087    #[test]
1088    fn test_delete_all_dry_run_default() {
1089        let registry = ToolRegistry::new();
1090        // Empty filter with dry_run (default true) should return preview
1091        let result = registry.execute(
1092            "subcog_delete_all",
1093            serde_json::json!({
1094                "filter": "ns:decisions"
1095            }),
1096        );
1097
1098        assert!(result.is_ok());
1099        let tool_result = result.unwrap();
1100        assert!(!tool_result.content.is_empty());
1101
1102        // Should indicate dry-run mode
1103        if let ToolContent::Text { text } = &tool_result.content[0] {
1104            assert!(
1105                text.contains("dry") || text.contains("preview") || text.contains("would"),
1106                "Dry-run response should indicate preview mode: {text}"
1107            );
1108        }
1109    }
1110
1111    #[test]
1112    fn test_delete_all_explicit_dry_run_false() {
1113        let registry = ToolRegistry::new();
1114        // With dry_run=false but no matching memories
1115        let result = registry.execute(
1116            "subcog_delete_all",
1117            serde_json::json!({
1118                "filter": "ns:nonexistent-namespace-12345",
1119                "dry_run": false
1120            }),
1121        );
1122
1123        // Should succeed (even if no memories deleted)
1124        assert!(result.is_ok());
1125    }
1126
1127    #[test]
1128    fn test_restore_nonexistent_memory() {
1129        let registry = ToolRegistry::new();
1130        let result = registry.execute(
1131            "subcog_restore",
1132            serde_json::json!({
1133                "memory_id": "nonexistent-tombstoned-memory-12345"
1134            }),
1135        );
1136
1137        // Should return error (memory not found or not tombstoned)
1138        let Ok(tool_result) = result else {
1139            return; // Error is acceptable
1140        };
1141
1142        assert!(tool_result.is_error);
1143        if let ToolContent::Text { text } = &tool_result.content[0] {
1144            assert!(
1145                text.to_lowercase().contains("not found")
1146                    || text.to_lowercase().contains("error")
1147                    || text.to_lowercase().contains("failed"),
1148                "Should indicate restore failure: {text}"
1149            );
1150        }
1151    }
1152
1153    #[test]
1154    fn test_history_nonexistent_memory() {
1155        let registry = ToolRegistry::new();
1156        let result = registry.execute(
1157            "subcog_history",
1158            serde_json::json!({
1159                "memory_id": "nonexistent-memory-12345"
1160            }),
1161        );
1162
1163        // History provides helpful guidance even for non-existent memories
1164        let Ok(tool_result) = result else {
1165            return; // Error is also acceptable
1166        };
1167
1168        // Not an error - provides useful info about how to find history
1169        assert!(!tool_result.is_error);
1170        if let ToolContent::Text { text } = &tool_result.content[0] {
1171            // Should mention the memory wasn't found and provide guidance
1172            assert!(
1173                text.contains("not found") || text.contains("Memory History"),
1174                "Should provide history info or not found message: {text}"
1175            );
1176        }
1177    }
1178
1179    #[test]
1180    fn test_history_with_limit() {
1181        let registry = ToolRegistry::new();
1182        let result = registry.execute(
1183            "subcog_history",
1184            serde_json::json!({
1185                "memory_id": "some-memory-id",
1186                "limit": 5
1187            }),
1188        );
1189
1190        // Schema parsing should work, and function provides guidance
1191        let Ok(tool_result) = result else {
1192            return;
1193        };
1194
1195        // History provides helpful info even when memory doesn't exist
1196        assert!(!tool_result.is_error);
1197        assert!(!tool_result.content.is_empty());
1198    }
1199}