Skip to main content

prism_mcp_rs/core/
completion_handlers.rs

1//! Practical completion handlers for common MCP use cases
2//!
3//! Module provides ready-to-use completion handlers that servers can use
4//! to provide smart autocompletion for prompts, tools, and resources.
5//! These handlers implement the CompletionHandler trait and can be easily
6//! integrated into any MCP server.
7//!
8//! # Features
9//! - File path completion for resource URIs
10//! - Prompt argument completion with fuzzy matching
11//! - Tool parameter completion based on schema
12//! - Database-backed completion with caching
13//! - Custom completion with validation
14
15use crate::core::{
16    completion::{CompletionContext, CompletionHandler},
17    error::{McpError, McpResult},
18};
19use crate::protocol::messages::{CompletionArgument, CompletionReference};
20use async_trait::async_trait;
21use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23use tokio::fs;
24
25/// File system path completion handler
26///
27/// Provides completion suggestions for file system paths. This is useful
28/// for resource URIs that represent files and directories.
29///
30/// # Features
31/// - Directory traversal with proper permissions handling
32/// - File extension filtering
33/// - Hidden file inclusion control
34/// - Path normalization and validation
35///
36/// # Example
37/// ```rust
38/// use prism_mcp_rs::core::completion_handlers::FileSystemCompletionHandler;
39///
40/// let handler = FileSystemCompletionHandler::new("/home/user")
41/// .with_extensions(vec!["txt", "md", "json"])
42/// .include_hidden_files(false);
43/// ```
44pub struct FileSystemCompletionHandler {
45    /// Base directory for completions
46    base_path: PathBuf,
47    /// Allowed file extensions (None means all)
48    allowed_extensions: Option<Vec<String>>,
49    /// Whether to include hidden files/directories
50    include_hidden: bool,
51    /// Maximum number of suggestions to return
52    max_suggestions: usize,
53    /// Maximum directory depth to traverse
54    max_depth: usize,
55}
56
57impl FileSystemCompletionHandler {
58    /// Create a new file system completion handler
59    ///
60    /// # Arguments
61    /// * `base_path` - Base directory for file completions
62    pub fn new<P: AsRef<Path>>(base_path: P) -> Self {
63        Self {
64            base_path: base_path.as_ref().to_path_buf(),
65            allowed_extensions: None,
66            include_hidden: false,
67            max_suggestions: 20,
68            max_depth: 5,
69        }
70    }
71
72    /// Set allowed file extensions
73    ///
74    /// # Arguments
75    /// * `extensions` - List of allowed extensions (without dots)
76    pub fn with_extensions(mut self, extensions: Vec<&str>) -> Self {
77        self.allowed_extensions = Some(extensions.into_iter().map(|s| s.to_string()).collect());
78        self
79    }
80
81    /// Set whether to include hidden files
82    pub fn include_hidden_files(mut self, include: bool) -> Self {
83        self.include_hidden = include;
84        self
85    }
86
87    /// Set maximum number of suggestions
88    pub fn max_suggestions(mut self, max: usize) -> Self {
89        self.max_suggestions = max;
90        self
91    }
92
93    /// Set maximum directory depth
94    pub fn max_depth(mut self, depth: usize) -> Self {
95        self.max_depth = depth;
96        self
97    }
98
99    /// Check if a file should be included based on extension filter
100    fn should_include_file(&self, path: &Path) -> bool {
101        if let Some(ref extensions) = self.allowed_extensions {
102            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
103                return extensions.contains(&ext.to_string());
104            }
105            return false;
106        }
107        true
108    }
109
110    /// Check if a file/directory should be included based on hidden status
111    fn should_include_hidden(&self, path: &Path) -> bool {
112        if !self.include_hidden {
113            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
114                return !name.starts_with('.');
115            }
116        }
117        true
118    }
119
120    /// Get completions for a given path prefix
121    async fn get_path_completions(&self, prefix: &str) -> McpResult<Vec<String>> {
122        let mut suggestions = Vec::new();
123
124        // Resolve the target directory
125        let search_path = if prefix.is_empty() {
126            self.base_path.clone()
127        } else {
128            let prefix_path = Path::new(prefix);
129            if prefix_path.is_absolute() {
130                prefix_path.to_path_buf()
131            } else {
132                self.base_path.join(prefix_path)
133            }
134        };
135
136        // If the path doesn't exist or isn't accessible, try parent directory
137        let (dir_to_search, partial_name) = if search_path.exists() && search_path.is_dir() {
138            (search_path, String::new())
139        } else {
140            let parent = search_path.parent().unwrap_or(&self.base_path);
141            let partial = search_path
142                .file_name()
143                .and_then(|n| n.to_str())
144                .unwrap_or("")
145                .to_string();
146            (parent.to_path_buf(), partial)
147        };
148
149        // Read directory entries
150        match fs::read_dir(&dir_to_search).await {
151            Ok(mut entries) => {
152                while let Some(entry) = entries.next_entry().await.map_err(McpError::io)? {
153                    let path = entry.path();
154                    let file_name = path
155                        .file_name()
156                        .and_then(|n| n.to_str())
157                        .unwrap_or("")
158                        .to_string();
159
160                    // Skip if doesn't match partial name
161                    if !partial_name.is_empty() && !file_name.starts_with(&partial_name) {
162                        continue;
163                    }
164
165                    // Skip hidden files if not included
166                    if !self.should_include_hidden(&path) {
167                        continue;
168                    }
169
170                    // For files, check extension filter
171                    if path.is_file() && !self.should_include_file(&path) {
172                        continue;
173                    }
174
175                    // Create the completion suggestion
176                    let relative_path = path
177                        .strip_prefix(&self.base_path)
178                        .unwrap_or(&path)
179                        .to_string_lossy()
180                        .to_string();
181
182                    // Add trailing slash for directories
183                    let suggestion = if path.is_dir() {
184                        format!("{relative_path}/")
185                    } else {
186                        relative_path
187                    };
188
189                    suggestions.push(suggestion);
190
191                    if suggestions.len() >= self.max_suggestions {
192                        break;
193                    }
194                }
195            }
196            Err(e) => {
197                tracing::warn!("Failed to read directory {:?}: {}", dir_to_search, e);
198                return Ok(vec![]);
199            }
200        }
201
202        // Sort suggestions for consistent ordering
203        suggestions.sort();
204        Ok(suggestions)
205    }
206}
207
208#[async_trait]
209impl CompletionHandler for FileSystemCompletionHandler {
210    async fn complete(
211        &self,
212        reference: &CompletionReference,
213        argument: &CompletionArgument,
214        _context: Option<&CompletionContext>,
215    ) -> McpResult<Vec<String>> {
216        if let CompletionReference::Resource { uri } = reference {
217            // Handle file://URIs
218            if uri.starts_with("file://") {
219                let _path_part = uri.strip_prefix("file://").unwrap_or("");
220                return self.get_path_completions(&argument.value).await;
221            }
222
223            // For other URI schemes, provide basic path completion
224            if argument.name == "path" || argument.name == "filename" || argument.name == "uri" {
225                return self.get_path_completions(&argument.value).await;
226            }
227        }
228
229        Ok(vec![])
230    }
231}
232
233/// Fuzzy string completion handler
234///
235/// Provides fuzzy matching completion for string-based arguments.
236/// Useful for prompt names, tool names, and other predefined lists.
237///
238/// # Features
239/// - Fuzzy matching with customizable similarity threshold
240/// - Case-insensitive matching
241/// - Substring and prefix matching
242/// - Custom scoring algorithm
243///
244/// # Example
245/// ```rust
246/// use prism_mcp_rs::core::completion_handlers::FuzzyCompletionHandler;
247///
248/// let handler = FuzzyCompletionHandler::new(vec![
249/// "analyze_data", "analyze_text", "create_report", "generate_summary"
250/// ]).threshold(0.6);
251/// ```
252pub struct FuzzyCompletionHandler {
253    /// Available options for completion
254    options: Vec<String>,
255    /// Minimum similarity threshold (0.0 to 1.0)
256    threshold: f64,
257    /// Maximum number of suggestions
258    max_suggestions: usize,
259    /// Whether to use case-insensitive matching
260    case_insensitive: bool,
261}
262
263impl FuzzyCompletionHandler {
264    /// Create a new fuzzy completion handler
265    ///
266    /// # Arguments
267    /// * `options` - List of available completion options
268    pub fn new<S: AsRef<str>>(options: Vec<S>) -> Self {
269        Self {
270            options: options
271                .into_iter()
272                .map(|s| s.as_ref().to_string())
273                .collect(),
274            threshold: 0.4,
275            max_suggestions: 10,
276            case_insensitive: true,
277        }
278    }
279
280    /// Set similarity threshold (0.0 to 1.0)
281    pub fn threshold(mut self, threshold: f64) -> Self {
282        self.threshold = threshold.clamp(0.0, 1.0);
283        self
284    }
285
286    /// Set maximum number of suggestions
287    pub fn max_suggestions(mut self, max: usize) -> Self {
288        self.max_suggestions = max;
289        self
290    }
291
292    /// Set case sensitivity
293    pub fn case_sensitive(mut self, sensitive: bool) -> Self {
294        self.case_insensitive = !sensitive;
295        self
296    }
297
298    /// Calculate similarity between two strings using Jaro-Winkler-like algorithm
299    fn similarity(&self, a: &str, b: &str) -> f64 {
300        let a = if self.case_insensitive {
301            a.to_lowercase()
302        } else {
303            a.to_string()
304        };
305        let b = if self.case_insensitive {
306            b.to_lowercase()
307        } else {
308            b.to_string()
309        };
310
311        if a == b {
312            return 1.0;
313        }
314
315        if a.is_empty() || b.is_empty() {
316            return 0.0;
317        }
318
319        // Check for exact prefix match (high score)
320        if b.starts_with(&a) {
321            return 0.9 + (a.len() as f64 / b.len() as f64) * 0.1;
322        }
323
324        // Check for substring match
325        if b.contains(&a) {
326            return 0.7 + (a.len() as f64 / b.len() as f64) * 0.2;
327        }
328
329        // Simple character overlap ratio
330        let mut matches = 0;
331        let a_chars: Vec<char> = a.chars().collect();
332        let b_chars: Vec<char> = b.chars().collect();
333
334        for ac in &a_chars {
335            if b_chars.contains(ac) {
336                matches += 1;
337            }
338        }
339
340        matches as f64 / a_chars.len().max(b_chars.len()) as f64
341    }
342
343    /// Get fuzzy completions for the given input
344    fn get_fuzzy_completions(&self, input: &str) -> Vec<String> {
345        let mut scored_options: Vec<(f64, String)> = self
346            .options
347            .iter()
348            .map(|option| {
349                let score = self.similarity(input, option);
350                (score, option.clone())
351            })
352            .filter(|(score, _)| *score >= self.threshold)
353            .collect();
354
355        // Sort by score (descending)
356        scored_options.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
357
358        // Return top suggestions
359        scored_options
360            .into_iter()
361            .take(self.max_suggestions)
362            .map(|(_, option)| option)
363            .collect()
364    }
365}
366
367#[async_trait]
368impl CompletionHandler for FuzzyCompletionHandler {
369    async fn complete(
370        &self,
371        _reference: &CompletionReference,
372        argument: &CompletionArgument,
373        _context: Option<&CompletionContext>,
374    ) -> McpResult<Vec<String>> {
375        Ok(self.get_fuzzy_completions(&argument.value))
376    }
377}
378
379/// Schema-based completion handler
380///
381/// Provides completion suggestions based on JSON schema definitions.
382/// Useful for tool parameters that have enum constraints or specific patterns.
383///
384/// # Features
385/// - Enum value completion
386/// - Pattern-based completion
387/// - Type-aware suggestions
388/// - Format-specific completions (email, date, etc.)
389///
390/// # Example
391/// ```rust
392/// use prism_mcp_rs::core::completion_handlers::SchemaCompletionHandler;
393/// use serde_json::json;
394///
395/// let schema = json!({
396/// "type": "object",
397/// "properties": {
398/// "priority": {
399/// "type": "string",
400/// "enum": ["low", "medium", "high"]
401/// }
402/// }
403/// });
404///
405/// let handler = SchemaCompletionHandler::new(schema);
406/// ```
407pub struct SchemaCompletionHandler {
408    /// JSON schema for parameter validation and completion
409    schema: serde_json::Value,
410    /// Custom completion mappings
411    custom_completions: HashMap<String, Vec<String>>,
412}
413
414impl SchemaCompletionHandler {
415    /// Create a new schema-based completion handler
416    ///
417    /// # Arguments
418    /// * `schema` - JSON schema defining the parameter structure
419    pub fn new(schema: serde_json::Value) -> Self {
420        Self {
421            schema,
422            custom_completions: HashMap::new(),
423        }
424    }
425
426    /// Add custom completion values for a specific parameter
427    ///
428    /// # Arguments
429    /// * `parameter_name` - Name of the parameter
430    /// * `values` - List of completion values
431    pub fn add_custom_completions<S: AsRef<str>>(
432        mut self,
433        parameter_name: S,
434        values: Vec<S>,
435    ) -> Self {
436        let values: Vec<String> = values.into_iter().map(|s| s.as_ref().to_string()).collect();
437        self.custom_completions
438            .insert(parameter_name.as_ref().to_string(), values);
439        self
440    }
441
442    /// Extract enum values from schema property
443    fn get_enum_values(&self, property: &serde_json::Value) -> Vec<String> {
444        if let Some(enum_array) = property.get("enum").and_then(|e| e.as_array()) {
445            return enum_array
446                .iter()
447                .filter_map(|v| v.as_str())
448                .map(|s| s.to_string())
449                .collect();
450        }
451        vec![]
452    }
453
454    /// Get format-based suggestions
455    fn get_format_suggestions(&self, format: &str, current_value: &str) -> Vec<String> {
456        match format {
457            "email" => {
458                if current_value.is_empty() {
459                    vec!["user@example.com".to_string()]
460                } else if !current_value.contains('@') {
461                    vec![format!("{}@example.com", current_value)]
462                } else {
463                    vec![]
464                }
465            }
466            "date" => {
467                if current_value.is_empty() {
468                    vec!["2025-01-01".to_string()]
469                } else {
470                    vec![]
471                }
472            }
473            "time" => {
474                if current_value.is_empty() {
475                    vec!["12:00:00".to_string()]
476                } else {
477                    vec![]
478                }
479            }
480            "uri" => {
481                if current_value.is_empty() {
482                    vec!["https://example.com".to_string()]
483                } else if !current_value.contains("://") {
484                    vec![
485                        format!("https://{}", current_value),
486                        format!("http://{}", current_value),
487                        format!("file://{}", current_value),
488                    ]
489                } else {
490                    vec![]
491                }
492            }
493            _ => vec![],
494        }
495    }
496
497    /// Get completions for a parameter based on schema
498    fn get_parameter_completions(&self, parameter_name: &str, current_value: &str) -> Vec<String> {
499        // Check custom completions first
500        if let Some(custom) = self.custom_completions.get(parameter_name) {
501            return custom
502                .iter()
503                .filter(|value| value.starts_with(current_value))
504                .cloned()
505                .collect();
506        }
507
508        // Check schema properties
509        if let Some(properties) = self.schema.get("properties").and_then(|p| p.as_object()) {
510            if let Some(property) = properties.get(parameter_name) {
511                // Handle enum values
512                let enum_values = self.get_enum_values(property);
513                if !enum_values.is_empty() {
514                    return enum_values
515                        .into_iter()
516                        .filter(|value| value.starts_with(current_value))
517                        .collect();
518                }
519
520                // Handle format-based suggestions
521                if let Some(format) = property.get("format").and_then(|f| f.as_str()) {
522                    return self.get_format_suggestions(format, current_value);
523                }
524
525                // Handle type-based suggestions
526                if let Some(type_str) = property.get("type").and_then(|t| t.as_str()) {
527                    match type_str {
528                        "boolean" => {
529                            return vec!["true".to_string(), "false".to_string()]
530                                .into_iter()
531                                .filter(|value| value.starts_with(current_value))
532                                .collect();
533                        }
534                        "number" | "integer" if current_value.is_empty() => {
535                            return vec!["0".to_string(), "1".to_string(), "10".to_string()];
536                        }
537                        _ => {}
538                    }
539                }
540            }
541        }
542
543        vec![]
544    }
545}
546
547#[async_trait]
548impl CompletionHandler for SchemaCompletionHandler {
549    async fn complete(
550        &self,
551        _reference: &CompletionReference,
552        argument: &CompletionArgument,
553        _context: Option<&CompletionContext>,
554    ) -> McpResult<Vec<String>> {
555        Ok(self.get_parameter_completions(&argument.name, &argument.value))
556    }
557}
558
559/// Composite completion handler that combines multiple handlers
560///
561/// This handler allows you to combine different completion strategies
562/// and provides a unified interface for complex completion scenarios.
563///
564/// # Example
565/// ```rust
566/// use prism_mcp_rs::core::completion_handlers::{
567/// CompositeCompletionHandler, FuzzyCompletionHandler, FileSystemCompletionHandler
568/// };
569///
570/// let composite = CompositeCompletionHandler::new()
571/// .add_handler("files", FileSystemCompletionHandler::new("/home/user"))
572/// .add_handler("prompts", FuzzyCompletionHandler::new(vec!["analyze", "create", "generate"]));
573/// ```
574pub struct CompositeCompletionHandler {
575    /// Named completion handlers
576    handlers: HashMap<String, Box<dyn CompletionHandler>>,
577    /// Default handler to use when no specific handler matches
578    default_handler: Option<Box<dyn CompletionHandler>>,
579}
580
581impl CompositeCompletionHandler {
582    /// Create a new composite completion handler
583    pub fn new() -> Self {
584        Self {
585            handlers: HashMap::new(),
586            default_handler: None,
587        }
588    }
589
590    /// Add a named completion handler
591    ///
592    /// # Arguments
593    /// * `name` - Identifier for this handler (used for routing)
594    /// * `handler` - The completion handler implementation
595    pub fn add_handler<H>(mut self, name: &str, handler: H) -> Self
596    where
597        H: CompletionHandler + 'static,
598    {
599        self.handlers.insert(name.to_string(), Box::new(handler));
600        self
601    }
602
603    /// Set the default handler for unmatched requests
604    ///
605    /// # Arguments
606    /// * `handler` - Default completion handler
607    pub fn with_default<H>(mut self, handler: H) -> Self
608    where
609        H: CompletionHandler + 'static,
610    {
611        self.default_handler = Some(Box::new(handler));
612        self
613    }
614
615    /// Determine which handler to use based on the reference and argument
616    fn select_handler(
617        &self,
618        reference: &CompletionReference,
619        argument: &CompletionArgument,
620    ) -> Option<&dyn CompletionHandler> {
621        // Strategy 1: Match by reference type + argument name
622        let handler_key = match reference {
623            CompletionReference::Prompt { .. } => {
624                if argument.name == "name" {
625                    Some("prompts".to_string())
626                } else {
627                    Some(format!("prompt_{}", argument.name))
628                }
629            }
630            CompletionReference::Resource { .. } => {
631                if argument.name == "uri" || argument.name == "path" {
632                    Some("files".to_string())
633                } else {
634                    Some(format!("resource_{}", argument.name))
635                }
636            }
637            CompletionReference::Tool { name } => Some(format!("tool_{}_{}", name, argument.name)),
638        };
639
640        // Try specific handler first
641        if let Some(key) = handler_key {
642            if let Some(handler) = self.handlers.get(&key) {
643                return Some(handler.as_ref());
644            }
645        }
646
647        // Try generic handlers
648        match reference {
649            CompletionReference::Prompt { .. } => self.handlers.get("prompts").map(|h| h.as_ref()),
650            CompletionReference::Resource { .. } => {
651                self.handlers.get("resources").map(|h| h.as_ref())
652            }
653            CompletionReference::Tool { .. } => self.handlers.get("tools").map(|h| h.as_ref()),
654        }
655        .or_else(|| self.default_handler.as_ref().map(|h| h.as_ref()))
656    }
657}
658
659impl Default for CompositeCompletionHandler {
660    fn default() -> Self {
661        Self::new()
662    }
663}
664
665#[async_trait]
666impl CompletionHandler for CompositeCompletionHandler {
667    async fn complete(
668        &self,
669        reference: &CompletionReference,
670        argument: &CompletionArgument,
671        context: Option<&CompletionContext>,
672    ) -> McpResult<Vec<String>> {
673        if let Some(handler) = self.select_handler(reference, argument) {
674            handler.complete(reference, argument, context).await
675        } else {
676            Ok(vec![])
677        }
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684    use tempfile::TempDir;
685    use tokio::fs::File;
686
687    #[tokio::test]
688    async fn test_filesystem_completion() {
689        let temp_dir = TempDir::new().unwrap();
690        let temp_path = temp_dir.path();
691
692        // Create test files
693        File::create(temp_path.join("test.txt")).await.unwrap();
694        File::create(temp_path.join("example.md")).await.unwrap();
695        tokio::fs::create_dir(temp_path.join("subdir"))
696            .await
697            .unwrap();
698
699        let handler =
700            FileSystemCompletionHandler::new(temp_path).with_extensions(vec!["txt", "md"]);
701
702        let reference = CompletionReference::Resource {
703            uri: "file:///test".to_string(),
704        };
705        let argument = CompletionArgument {
706            name: "path".to_string(),
707            value: "".to_string(),
708        };
709
710        let completions = handler.complete(&reference, &argument, None).await.unwrap();
711
712        // Should include both files and the directory
713        assert!(completions.len() >= 2);
714        assert!(completions.iter().any(|c| c.contains("test.txt")));
715        assert!(completions.iter().any(|c| c.contains("example.md")));
716    }
717
718    #[tokio::test]
719    async fn test_fuzzy_completion() {
720        let handler = FuzzyCompletionHandler::new(vec![
721            "analyze_data",
722            "analyze_text",
723            "create_report",
724            "generate_summary",
725        ])
726        .threshold(0.3);
727
728        let reference = CompletionReference::Prompt {
729            name: "test".to_string(),
730        };
731        let argument = CompletionArgument {
732            name: "name".to_string(),
733            value: "ana".to_string(),
734        };
735
736        let completions = handler.complete(&reference, &argument, None).await.unwrap();
737
738        // Should match analyze_data and analyze_text
739        assert_eq!(completions.len(), 2);
740        assert!(completions.contains(&"analyze_data".to_string()));
741        assert!(completions.contains(&"analyze_text".to_string()));
742    }
743
744    #[tokio::test]
745    async fn test_schema_completion() {
746        let schema = serde_json::json!({
747            "type": "object",
748            "properties": {
749                "priority": {
750                    "type": "string",
751                    "enum": ["low", "medium", "high"]
752                },
753                "email": {
754                    "type": "string",
755                    "format": "email"
756                }
757            }
758        });
759
760        let handler = SchemaCompletionHandler::new(schema);
761
762        let reference = CompletionReference::Tool {
763            name: "create_task".to_string(),
764        };
765
766        // Test enum completion
767        let argument = CompletionArgument {
768            name: "priority".to_string(),
769            value: "m".to_string(),
770        };
771
772        let completions = handler.complete(&reference, &argument, None).await.unwrap();
773        assert_eq!(completions, vec!["medium".to_string()]);
774
775        // Test format completion
776        let argument = CompletionArgument {
777            name: "email".to_string(),
778            value: "user".to_string(),
779        };
780
781        let completions = handler.complete(&reference, &argument, None).await.unwrap();
782        assert_eq!(completions, vec!["user@example.com".to_string()]);
783    }
784
785    #[tokio::test]
786    async fn test_composite_completion() {
787        let fuzzy = FuzzyCompletionHandler::new(vec!["prompt1", "prompt2"]);
788        let schema = SchemaCompletionHandler::new(serde_json::json!({
789            "type": "object",
790            "properties": {
791                "status": {
792                    "type": "string",
793                    "enum": ["active", "inactive"]
794                }
795            }
796        }));
797
798        let composite = CompositeCompletionHandler::new()
799            .add_handler("prompts", fuzzy)
800            .add_handler("tool_create_task_status", schema);
801
802        // Test prompt completion
803        let reference = CompletionReference::Prompt {
804            name: "test".to_string(),
805        };
806        let argument = CompletionArgument {
807            name: "name".to_string(),
808            value: "prom".to_string(),
809        };
810
811        let completions = composite
812            .complete(&reference, &argument, None)
813            .await
814            .unwrap();
815        assert!(completions.contains(&"prompt1".to_string()));
816
817        // Test tool parameter completion
818        let reference = CompletionReference::Tool {
819            name: "create_task".to_string(),
820        };
821        let argument = CompletionArgument {
822            name: "status".to_string(),
823            value: "a".to_string(),
824        };
825
826        let completions = composite
827            .complete(&reference, &argument, None)
828            .await
829            .unwrap();
830        assert_eq!(completions, vec!["active".to_string()]);
831    }
832}