1use super::artifacts::{ArtifactStore, ArtifactStoreLimits, ToolArtifact};
7use super::types::{Tool, ToolContext, ToolOutput};
8use super::ToolResult;
9use super::{
10 merge_tool_output_artifact_metadata, truncate_tool_output_with_artifact, ToolOutputArtifact,
11};
12use crate::llm::ToolDefinition;
13use crate::trace::{InMemoryTraceSink, TraceEvent, TraceSink};
14use anyhow::Result;
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::sync::{Arc, RwLock};
18
19pub struct ToolRegistry {
21 tools: RwLock<HashMap<String, Arc<dyn Tool>>>,
22 builtins: RwLock<std::collections::HashSet<String>>,
24 context: RwLock<ToolContext>,
25 artifact_store: ArtifactStore,
26 trace_sink: RwLock<Arc<dyn TraceSink>>,
27}
28
29impl ToolRegistry {
30 pub fn new(workspace: PathBuf) -> Self {
32 Self::with_artifact_limits(workspace, ArtifactStoreLimits::default())
33 }
34
35 pub fn with_artifact_limits(workspace: PathBuf, artifact_limits: ArtifactStoreLimits) -> Self {
37 Self::with_artifact_limits_and_workspace_services(
38 workspace.clone(),
39 artifact_limits,
40 crate::workspace::WorkspaceServices::local(workspace),
41 )
42 }
43
44 pub fn with_artifact_limits_and_workspace_services(
46 workspace: PathBuf,
47 artifact_limits: ArtifactStoreLimits,
48 workspace_services: Arc<crate::workspace::WorkspaceServices>,
49 ) -> Self {
50 let context = ToolContext::new(workspace).with_workspace_services(workspace_services);
51 Self {
52 tools: RwLock::new(HashMap::new()),
53 builtins: RwLock::new(std::collections::HashSet::new()),
54 context: RwLock::new(context),
55 artifact_store: ArtifactStore::with_limits(artifact_limits),
56 trace_sink: RwLock::new(Arc::new(InMemoryTraceSink::default())),
57 }
58 }
59
60 pub fn register_builtin(&self, tool: Arc<dyn Tool>) {
62 let name = tool.name().to_string();
63 let mut tools = self.tools.write().unwrap();
64 let mut builtins = self.builtins.write().unwrap();
65 tracing::debug!("Registering builtin tool: {}", name);
66 tools.insert(name.clone(), tool);
67 builtins.insert(name);
68 }
69
70 pub fn register(&self, tool: Arc<dyn Tool>) {
75 let name = tool.name().to_string();
76 let mut tools = self.tools.write().unwrap();
80 let builtins = self.builtins.read().unwrap();
81 if builtins.contains(&name) {
82 tracing::warn!(
83 "Rejected registration of tool '{}': cannot shadow builtin",
84 name
85 );
86 return;
87 }
88 tracing::debug!("Registering tool: {}", name);
89 tools.insert(name, tool);
90 }
91
92 pub(crate) fn register_with_shadow(
99 &self,
100 tool: Arc<dyn Tool>,
101 ) -> (bool, Option<Arc<dyn Tool>>) {
102 let name = tool.name().to_string();
103 let mut tools = self.tools.write().unwrap();
104 let builtins = self.builtins.read().unwrap();
105 if builtins.contains(&name) {
106 tracing::warn!(
107 "Rejected registration of tool '{}': cannot shadow builtin",
108 name
109 );
110 return (false, None);
111 }
112 tracing::debug!("Registering owned dynamic tool: {}", name);
113 (true, tools.insert(name, tool))
114 }
115
116 pub(crate) fn restore_if_same(
122 &self,
123 name: &str,
124 expected: &Arc<dyn Tool>,
125 replacement: Option<Arc<dyn Tool>>,
126 ) -> bool {
127 let mut tools = self.tools.write().unwrap();
128 let Some(current) = tools.get(name) else {
129 return false;
130 };
131 if !Arc::ptr_eq(current, expected) {
132 return false;
133 }
134
135 match replacement {
136 Some(tool) => {
137 tools.insert(name.to_string(), tool);
138 }
139 None => {
140 tools.remove(name);
141 }
142 }
143 true
144 }
145
146 pub(crate) fn register_if_absent(&self, tool: Arc<dyn Tool>) -> bool {
148 let name = tool.name().to_string();
149 let mut tools = self.tools.write().unwrap();
150 if tools.contains_key(&name) {
151 return false;
152 }
153 tracing::debug!("Registering previously absent dynamic tool: {}", name);
154 tools.insert(name, tool);
155 true
156 }
157
158 pub fn unregister(&self, name: &str) -> bool {
162 let mut tools = self.tools.write().unwrap();
163 let builtins = self.builtins.read().unwrap();
164 if builtins.contains(name) {
165 tracing::warn!(
166 "Rejected unregister of tool '{}': builtin tools cannot be removed through dynamic unregister",
167 name
168 );
169 return false;
170 }
171 tracing::debug!("Unregistering tool: {}", name);
172 tools.remove(name).is_some()
173 }
174
175 pub fn unregister_by_prefix(&self, prefix: &str) {
177 let mut tools = self.tools.write().unwrap();
178 let builtins = self.builtins.read().unwrap();
179 tools.retain(|name, _| builtins.contains(name) || !name.starts_with(prefix));
180 tracing::debug!("Unregistered tools with prefix: {}", prefix);
181 }
182
183 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
185 let tools = self.tools.read().unwrap();
186 tools.get(name).cloned()
187 }
188
189 pub fn contains(&self, name: &str) -> bool {
191 let tools = self.tools.read().unwrap();
192 tools.contains_key(name)
193 }
194
195 pub fn definitions(&self) -> Vec<ToolDefinition> {
197 let tools = self.tools.read().unwrap();
198 let mut definitions = tools
199 .values()
200 .map(|tool| ToolDefinition {
201 name: tool.name().to_string(),
202 description: tool.description().to_string(),
203 parameters: tool.parameters(),
204 })
205 .collect::<Vec<_>>();
206 definitions.sort_by(|a, b| a.name.cmp(&b.name));
207 definitions
208 }
209
210 pub fn list(&self) -> Vec<String> {
212 let tools = self.tools.read().unwrap();
213 let mut names = tools.keys().cloned().collect::<Vec<_>>();
214 names.sort();
215 names
216 }
217
218 pub fn len(&self) -> usize {
220 let tools = self.tools.read().unwrap();
221 tools.len()
222 }
223
224 pub fn is_empty(&self) -> bool {
226 self.len() == 0
227 }
228
229 pub fn context(&self) -> ToolContext {
231 self.context.read().unwrap().clone()
232 }
233
234 pub fn artifact_store(&self) -> ArtifactStore {
236 self.artifact_store.clone()
237 }
238
239 pub fn get_artifact(&self, artifact_uri: &str) -> Option<ToolArtifact> {
241 self.artifact_store.get(artifact_uri)
242 }
243
244 pub fn set_trace_sink(&self, sink: Arc<dyn TraceSink>) {
246 *self.trace_sink.write().unwrap() = sink;
247 }
248
249 pub fn trace_sink(&self) -> Arc<dyn TraceSink> {
251 Arc::clone(&self.trace_sink.read().unwrap())
252 }
253
254 pub fn set_search_config(&self, config: crate::config::SearchConfig) {
256 let mut ctx = self.context.write().unwrap();
257 *ctx = ctx.clone().with_search_config(config);
258 }
259
260 pub fn set_sandbox(&self, sandbox: std::sync::Arc<dyn crate::sandbox::BashSandbox>) {
263 let mut ctx = self.context.write().unwrap();
264 *ctx = ctx.clone().with_sandbox(sandbox);
265 }
266
267 pub fn set_command_env(&self, env: Arc<HashMap<String, String>>) {
270 let mut ctx = self.context.write().unwrap();
271 *ctx = ctx.clone().with_command_env(env);
272 }
273
274 pub async fn execute(&self, name: &str, args: &serde_json::Value) -> Result<ToolResult> {
280 let ctx = self.context();
281 self.execute_with_context(name, args, &ctx).await
282 }
283
284 pub async fn execute_with_context(
289 &self,
290 name: &str,
291 args: &serde_json::Value,
292 ctx: &ToolContext,
293 ) -> Result<ToolResult> {
294 let start = std::time::Instant::now();
295
296 let tool = self.get(name);
297
298 let result = match tool {
299 Some(tool) => {
300 let mut output = tool.execute(args, ctx).await?;
301 let original_content = output.content.clone();
302 let truncated = truncate_tool_output_with_artifact(name, &output.content);
303 output.content = truncated.content;
304 if let Some(artifact) = truncated.artifact {
305 self.store_tool_artifact(name, &original_content, &artifact);
306 output.metadata = Some(merge_tool_output_artifact_metadata(
307 output.metadata,
308 &artifact,
309 ));
310 }
311 Ok(ToolResult {
312 name: name.to_string(),
313 output: output.content,
314 exit_code: if output.success { 0 } else { 1 },
315 metadata: output.metadata,
316 images: output.images,
317 error_kind: output.error_kind,
318 })
319 }
320 None => Ok(ToolResult::error(name, format!("Unknown tool: {}", name))),
321 };
322
323 if let Ok(ref r) = result {
324 crate::telemetry::record_tool_result(r.exit_code, start.elapsed());
325 self.record_trace_event(name, r, start.elapsed());
326 }
327
328 result
329 }
330
331 pub async fn execute_raw(
333 &self,
334 name: &str,
335 args: &serde_json::Value,
336 ) -> Result<Option<ToolOutput>> {
337 let ctx = self.context();
338 self.execute_raw_with_context(name, args, &ctx).await
339 }
340
341 pub async fn execute_raw_with_context(
343 &self,
344 name: &str,
345 args: &serde_json::Value,
346 ctx: &ToolContext,
347 ) -> Result<Option<ToolOutput>> {
348 let tool = self.get(name);
349
350 match tool {
351 Some(tool) => {
352 let mut output = tool.execute(args, ctx).await?;
353 let original_content = output.content.clone();
354 let truncated = truncate_tool_output_with_artifact(name, &output.content);
355 output.content = truncated.content;
356 if let Some(artifact) = truncated.artifact {
357 self.store_tool_artifact(name, &original_content, &artifact);
358 output.metadata = Some(merge_tool_output_artifact_metadata(
359 output.metadata,
360 &artifact,
361 ));
362 }
363 Ok(Some(output))
364 }
365 None => Ok(None),
366 }
367 }
368
369 fn store_tool_artifact(&self, tool_name: &str, content: &str, artifact: &ToolOutputArtifact) {
370 self.artifact_store.put(ToolArtifact {
371 artifact_id: artifact.artifact_id.clone(),
372 artifact_uri: artifact.artifact_uri.clone(),
373 tool_name: tool_name.to_string(),
374 content: content.to_string(),
375 original_bytes: artifact.original_bytes,
376 shown_bytes: artifact.shown_bytes,
377 });
378 }
379
380 fn record_trace_event(&self, name: &str, result: &ToolResult, duration: std::time::Duration) {
381 let sink = self.trace_sink();
382 sink.record(TraceEvent::tool_execution(
383 name,
384 result.exit_code == 0,
385 result.exit_code,
386 duration,
387 result.output.len(),
388 result.metadata.as_ref(),
389 ));
390
391 if name == "program" {
392 sink.record(TraceEvent::program_execution(
393 name,
394 result.exit_code == 0,
395 result.exit_code,
396 duration,
397 result.output.len(),
398 result.metadata.as_ref(),
399 ));
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::trace::{InMemoryTraceSink, TraceEventKind};
408 use async_trait::async_trait;
409
410 struct MockTool {
411 name: String,
412 }
413
414 #[async_trait]
415 impl Tool for MockTool {
416 fn name(&self) -> &str {
417 &self.name
418 }
419
420 fn description(&self) -> &str {
421 "A mock tool for testing"
422 }
423
424 fn parameters(&self) -> serde_json::Value {
425 serde_json::json!({
426 "type": "object",
427 "additionalProperties": false,
428 "properties": {},
429 "required": []
430 })
431 }
432
433 async fn execute(
434 &self,
435 _args: &serde_json::Value,
436 _ctx: &ToolContext,
437 ) -> Result<ToolOutput> {
438 Ok(ToolOutput::success("mock output"))
439 }
440 }
441
442 #[test]
443 fn test_registry_register_and_get() {
444 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
445
446 let tool = Arc::new(MockTool {
447 name: "test".to_string(),
448 });
449 registry.register(tool);
450
451 assert!(registry.contains("test"));
452 assert!(!registry.contains("nonexistent"));
453
454 let retrieved = registry.get("test");
455 assert!(retrieved.is_some());
456 assert_eq!(retrieved.unwrap().name(), "test");
457 }
458
459 #[test]
460 fn test_registry_unregister() {
461 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
462
463 let tool = Arc::new(MockTool {
464 name: "test".to_string(),
465 });
466 registry.register(tool);
467
468 assert!(registry.contains("test"));
469 assert!(registry.unregister("test"));
470 assert!(!registry.contains("test"));
471 assert!(!registry.unregister("test")); }
473
474 #[test]
475 fn test_registry_unregister_preserves_builtins() {
476 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
477 registry.register_builtin(Arc::new(MockTool {
478 name: "read".to_string(),
479 }));
480
481 assert!(!registry.unregister("read"));
482 assert!(registry.contains("read"));
483 }
484
485 #[test]
486 fn test_registry_unregister_by_prefix_preserves_builtins() {
487 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
488 registry.register_builtin(Arc::new(MockTool {
489 name: "mcp__builtin".to_string(),
490 }));
491 registry.register(Arc::new(MockTool {
492 name: "mcp__dynamic".to_string(),
493 }));
494
495 registry.unregister_by_prefix("mcp__");
496
497 assert!(registry.contains("mcp__builtin"));
498 assert!(!registry.contains("mcp__dynamic"));
499 }
500
501 #[test]
502 fn concurrent_owned_registration_cannot_overwrite_builtin() {
503 for iteration in 0..32 {
504 let registry = Arc::new(ToolRegistry::new(PathBuf::from("/tmp")));
505 let name = format!("atomic_builtin_{iteration}");
506 let builtin: Arc<dyn Tool> = Arc::new(MockTool { name: name.clone() });
507 let dynamic: Arc<dyn Tool> = Arc::new(MockTool { name: name.clone() });
508 let barrier = Arc::new(std::sync::Barrier::new(3));
509
510 let builtin_registry = Arc::clone(®istry);
511 let builtin_tool = Arc::clone(&builtin);
512 let builtin_barrier = Arc::clone(&barrier);
513 let builtin_thread = std::thread::spawn(move || {
514 builtin_barrier.wait();
515 builtin_registry.register_builtin(builtin_tool);
516 });
517
518 let dynamic_registry = Arc::clone(®istry);
519 let dynamic_barrier = Arc::clone(&barrier);
520 let dynamic_thread = std::thread::spawn(move || {
521 dynamic_barrier.wait();
522 dynamic_registry.register_with_shadow(dynamic);
523 });
524
525 barrier.wait();
526 builtin_thread.join().unwrap();
527 dynamic_thread.join().unwrap();
528
529 let current = registry.get(&name).unwrap();
530 assert!(Arc::ptr_eq(¤t, &builtin));
531 assert!(!registry.unregister(&name));
532 }
533 }
534
535 #[test]
536 fn test_registry_definitions() {
537 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
538
539 registry.register(Arc::new(MockTool {
540 name: "tool2".to_string(),
541 }));
542 registry.register(Arc::new(MockTool {
543 name: "tool1".to_string(),
544 }));
545
546 let definitions = registry.definitions();
547 assert_eq!(definitions.len(), 2);
548 let names: Vec<&str> = definitions
549 .iter()
550 .map(|definition| definition.name.as_str())
551 .collect();
552 assert_eq!(names, vec!["tool1", "tool2"]);
553 }
554
555 #[tokio::test]
556 async fn test_registry_execute() {
557 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
558
559 registry.register(Arc::new(MockTool {
560 name: "test".to_string(),
561 }));
562
563 let result = registry
564 .execute("test", &serde_json::json!({}))
565 .await
566 .unwrap();
567 assert_eq!(result.exit_code, 0);
568 assert_eq!(result.output, "mock output");
569 }
570
571 #[tokio::test]
572 async fn test_registry_execute_unknown() {
573 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
574
575 let result = registry
576 .execute("unknown", &serde_json::json!({}))
577 .await
578 .unwrap();
579 assert_eq!(result.exit_code, 1);
580 assert!(result.output.contains("Unknown tool"));
581 }
582
583 #[tokio::test]
584 async fn test_registry_execute_with_context_success() {
585 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
586 let ctx = ToolContext::new(PathBuf::from("/tmp"));
587 let trace_sink = InMemoryTraceSink::default();
588 registry.set_trace_sink(Arc::new(trace_sink.clone()));
589
590 registry.register(Arc::new(MockTool {
591 name: "my_tool".to_string(),
592 }));
593
594 let result = registry
595 .execute_with_context("my_tool", &serde_json::json!({}), &ctx)
596 .await
597 .unwrap();
598 assert_eq!(result.name, "my_tool");
599 assert_eq!(result.exit_code, 0);
600 assert_eq!(result.output, "mock output");
601
602 let events = trace_sink.events();
603 assert_eq!(events.len(), 1);
604 assert_eq!(events[0].kind, TraceEventKind::ToolExecution);
605 assert_eq!(events[0].name, "my_tool");
606 assert!(events[0].success);
607 assert_eq!(events[0].output_bytes, "mock output".len());
608 }
609
610 #[tokio::test]
611 async fn test_registry_execute_with_context_unknown_tool() {
612 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
613 let ctx = ToolContext::new(PathBuf::from("/tmp"));
614
615 let result = registry
616 .execute_with_context("nonexistent", &serde_json::json!({}), &ctx)
617 .await
618 .unwrap();
619 assert_eq!(result.exit_code, 1);
620 assert!(result.output.contains("Unknown tool: nonexistent"));
621 }
622
623 struct FailingTool;
624
625 #[async_trait]
626 impl Tool for FailingTool {
627 fn name(&self) -> &str {
628 "failing"
629 }
630
631 fn description(&self) -> &str {
632 "A tool that returns failure"
633 }
634
635 fn parameters(&self) -> serde_json::Value {
636 serde_json::json!({
637 "type": "object",
638 "additionalProperties": false,
639 "properties": {},
640 "required": []
641 })
642 }
643
644 async fn execute(
645 &self,
646 _args: &serde_json::Value,
647 _ctx: &ToolContext,
648 ) -> Result<ToolOutput> {
649 Ok(ToolOutput::error("something went wrong"))
650 }
651 }
652
653 #[tokio::test]
654 async fn test_registry_execute_failing_tool() {
655 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
656 registry.register(Arc::new(FailingTool));
657
658 let result = registry
659 .execute("failing", &serde_json::json!({}))
660 .await
661 .unwrap();
662 assert_eq!(result.exit_code, 1);
663 assert_eq!(result.output, "something went wrong");
664 }
665
666 struct LargeOutputTool;
667
668 #[async_trait]
669 impl Tool for LargeOutputTool {
670 fn name(&self) -> &str {
671 "large_output"
672 }
673
674 fn description(&self) -> &str {
675 "A tool that returns more than the maximum output size"
676 }
677
678 fn parameters(&self) -> serde_json::Value {
679 serde_json::json!({
680 "type": "object",
681 "additionalProperties": false,
682 "properties": {},
683 "required": []
684 })
685 }
686
687 async fn execute(
688 &self,
689 _args: &serde_json::Value,
690 _ctx: &ToolContext,
691 ) -> Result<ToolOutput> {
692 Ok(ToolOutput::success(
693 "x".repeat(super::super::MAX_OUTPUT_SIZE + 1),
694 ))
695 }
696 }
697
698 #[tokio::test]
699 async fn test_registry_truncates_large_tool_output() {
700 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
701 let trace_sink = InMemoryTraceSink::default();
702 registry.set_trace_sink(Arc::new(trace_sink.clone()));
703 registry.register(Arc::new(LargeOutputTool));
704
705 let result = registry
706 .execute("large_output", &serde_json::json!({}))
707 .await
708 .unwrap();
709
710 assert_eq!(result.exit_code, 0);
711 assert!(result.output.contains("[tool output truncated:"));
712 assert!(result
713 .output
714 .contains("Full output artifact: a3s://tool-output/large_output/"));
715 assert!(result.output.len() < super::super::MAX_OUTPUT_SIZE + 512);
716 let metadata = result.metadata.expect("artifact metadata");
717 assert_eq!(
718 metadata["artifact"]["original_bytes"],
719 serde_json::json!(super::super::MAX_OUTPUT_SIZE + 1)
720 );
721 assert_eq!(
722 metadata["artifact"]["shown_bytes"],
723 serde_json::json!(super::super::MAX_OUTPUT_SIZE)
724 );
725 assert!(metadata["artifact"]["artifact_id"]
726 .as_str()
727 .unwrap()
728 .starts_with("tool-output:large_output:"));
729 assert!(metadata["artifact"]["artifact_uri"]
730 .as_str()
731 .unwrap()
732 .starts_with("a3s://tool-output/large_output/"));
733
734 let artifact_uri = metadata["artifact"]["artifact_uri"].as_str().unwrap();
735 let artifact = registry
736 .get_artifact(artifact_uri)
737 .expect("full output artifact");
738 assert_eq!(artifact.tool_name, "large_output");
739 assert_eq!(artifact.original_bytes, super::super::MAX_OUTPUT_SIZE + 1);
740 assert_eq!(artifact.shown_bytes, super::super::MAX_OUTPUT_SIZE);
741 assert_eq!(
742 artifact.content,
743 "x".repeat(super::super::MAX_OUTPUT_SIZE + 1)
744 );
745
746 let events = trace_sink.events();
747 assert_eq!(events.len(), 1);
748 assert_eq!(events[0].artifact_uris, vec![artifact_uri]);
749 }
750
751 #[tokio::test]
752 async fn test_registry_execute_raw_success() {
753 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
754 registry.register(Arc::new(MockTool {
755 name: "raw_test".to_string(),
756 }));
757
758 let output = registry
759 .execute_raw("raw_test", &serde_json::json!({}))
760 .await
761 .unwrap();
762 assert!(output.is_some());
763 let output = output.unwrap();
764 assert!(output.success);
765 assert_eq!(output.content, "mock output");
766 }
767
768 #[tokio::test]
769 async fn test_registry_execute_raw_stores_truncated_artifact() {
770 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
771 registry.register(Arc::new(LargeOutputTool));
772
773 let output = registry
774 .execute_raw("large_output", &serde_json::json!({}))
775 .await
776 .unwrap()
777 .expect("raw output");
778
779 assert!(output.content.contains("[tool output truncated:"));
780 let metadata = output.metadata.expect("artifact metadata");
781 let artifact_uri = metadata["artifact"]["artifact_uri"].as_str().unwrap();
782 let artifact = registry
783 .get_artifact(artifact_uri)
784 .expect("full output artifact");
785 assert_eq!(artifact.tool_name, "large_output");
786 assert_eq!(artifact.content.len(), super::super::MAX_OUTPUT_SIZE + 1);
787 }
788
789 #[tokio::test]
790 async fn test_registry_execute_raw_unknown() {
791 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
792
793 let output = registry
794 .execute_raw("missing", &serde_json::json!({}))
795 .await
796 .unwrap();
797 assert!(output.is_none());
798 }
799
800 #[test]
801 fn test_registry_list() {
802 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
803 registry.register(Arc::new(MockTool {
804 name: "beta".to_string(),
805 }));
806 registry.register(Arc::new(MockTool {
807 name: "alpha".to_string(),
808 }));
809
810 let names = registry.list();
811 assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
812 }
813
814 #[test]
815 fn test_registry_len_and_is_empty() {
816 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
817 assert!(registry.is_empty());
818 assert_eq!(registry.len(), 0);
819
820 registry.register(Arc::new(MockTool {
821 name: "t".to_string(),
822 }));
823 assert!(!registry.is_empty());
824 assert_eq!(registry.len(), 1);
825 }
826
827 #[test]
828 fn test_registry_replace_tool() {
829 let registry = ToolRegistry::new(PathBuf::from("/tmp"));
830 registry.register(Arc::new(MockTool {
831 name: "dup".to_string(),
832 }));
833 registry.register(Arc::new(MockTool {
834 name: "dup".to_string(),
835 }));
836 assert_eq!(registry.len(), 1);
838 }
839}