1mod definitions;
13mod handlers;
14
15use crate::{Error, Result};
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use std::collections::HashMap;
19
20pub struct ToolRegistry {
22 tools: HashMap<String, ToolDefinition>,
24}
25
26impl ToolRegistry {
27 #[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 tools.insert("subcog_prompts".to_string(), definitions::prompts_tool());
66 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 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 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 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 tools.insert("subcog_graph".to_string(), definitions::graph_tool());
118
119 tools.insert("subcog_init".to_string(), definitions::init_tool());
121
122 tools.insert(
124 "subcog_templates".to_string(),
125 definitions::templates_tool(),
126 );
127 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 #[cfg(feature = "group-scope")]
151 {
152 tools.insert("subcog_groups".to_string(), definitions::groups_tool());
154 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 #[must_use]
190 pub fn list_tools(&self) -> Vec<&ToolDefinition> {
191 self.tools.values().collect()
192 }
193
194 #[must_use]
196 pub fn get_tool(&self, name: &str) -> Option<&ToolDefinition> {
197 self.tools.get(name)
198 }
199
200 pub fn execute(&self, name: &str, arguments: Value) -> Result<ToolResult> {
206 #[cfg(feature = "group-scope")]
208 {
209 let group_result = match name {
210 "subcog_groups" => Some(handlers::execute_groups(arguments.clone())),
212 "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 "subcog_prompts" => handlers::execute_prompts(arguments),
246 "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 "subcog_get" => handlers::execute_get(arguments),
254 "subcog_delete" => handlers::execute_delete(arguments),
255 "subcog_update" => handlers::execute_update(arguments),
256 "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 "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 "subcog_graph" => handlers::execute_graph(arguments),
271 "subcog_init" => handlers::execute_init(arguments),
273 "subcog_templates" => handlers::execute_templates(arguments),
275 "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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct ToolDefinition {
315 pub name: String,
317 pub description: String,
319 pub input_schema: Value,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ToolResult {
326 pub content: Vec<ToolContent>,
328 #[serde(default)]
330 pub is_error: bool,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(tag = "type", rename_all = "lowercase")]
336pub enum ToolContent {
337 Text {
339 text: String,
341 },
342 Image {
344 data: String,
346 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 #[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 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 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 let values = HashMap::new();
589 let missing = find_missing_required_variables(&variables, &values);
590 assert_eq!(missing, vec!["required_var"]);
591
592 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 #[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 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 let result = registry.execute(
624 "subcog_capture",
625 serde_json::json!({
626 "content": "test content",
627 "namespace": 12345, }),
629 );
630
631 assert!(result.is_err());
633 }
634
635 #[test]
636 fn test_error_response_missing_required_argument() {
637 let registry = ToolRegistry::new();
638
639 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 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 let Ok(tool_result) = result else {
663 return;
665 };
666
667 if tool_result.is_error {
669 return;
670 }
671
672 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 let result = registry.execute(
689 "prompt_save",
690 serde_json::json!({
691 "name": "test-prompt",
692 }),
693 );
694
695 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 let Ok(tool_result) = result else {
718 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 let result = registry.execute(
745 "prompt_run",
746 serde_json::json!({
747 "name": "nonexistent-prompt-12345",
748 }),
749 );
750
751 let is_error = match &result {
753 Err(_) => true,
754 Ok(tool_result) => tool_result.is_error,
755 };
756
757 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 let result = registry.execute(
770 "subcog_recall",
771 serde_json::json!({
772 "query": "",
773 "limit": 10,
774 }),
775 );
776
777 if let Err(e) = result {
779 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 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 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 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 assert!(text.contains("version") || text.contains("Version"));
824 assert!(text.len() > 10);
826 }
827 }
828
829 #[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 let Ok(tool_result) = result else {
890 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 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 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 }),
952 );
953
954 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 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 #[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 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 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 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 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 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 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 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 let Ok(tool_result) = result else {
1139 return; };
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 let Ok(tool_result) = result else {
1165 return; };
1167
1168 assert!(!tool_result.is_error);
1170 if let ToolContent::Text { text } = &tool_result.content[0] {
1171 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 let Ok(tool_result) = result else {
1192 return;
1193 };
1194
1195 assert!(!tool_result.is_error);
1197 assert!(!tool_result.content.is_empty());
1198 }
1199}