Skip to main content

terraphim_session_analyzer/patterns/
matcher.rs

1//! Pattern matcher implementation using Aho-Corasick algorithm
2//!
3//! This module provides efficient multi-pattern string matching to identify
4//! tool usage in Bash commands.
5
6use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind};
7use anyhow::Result;
8use std::collections::HashMap;
9
10// Terraphim imports for knowledge graph automata
11#[cfg(feature = "terraphim")]
12use terraphim_automata::find_matches as terraphim_find_matches;
13#[cfg(feature = "terraphim")]
14use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
15
16use super::loader::ToolPattern;
17
18/// Trait for pattern matching implementations
19pub trait PatternMatcher: Send + Sync {
20    /// Initialize the matcher with tool patterns
21    ///
22    /// # Errors
23    ///
24    /// Returns an error if the automaton cannot be built from the patterns
25    fn initialize(&mut self, patterns: &[ToolPattern]) -> Result<()>;
26
27    /// Find all tool matches in the given text
28    ///
29    /// Returns matches ordered by position (leftmost-longest)
30    fn find_matches<'a>(&self, text: &'a str) -> Vec<ToolMatch<'a>>;
31
32    /// Get the matcher type identifier
33    #[allow(dead_code)] // May be used for debugging
34    fn matcher_type(&self) -> &'static str;
35}
36
37/// Represents a tool match found in text
38#[derive(Debug, Clone, PartialEq)]
39pub struct ToolMatch<'a> {
40    /// The name of the matched tool
41    pub tool_name: String,
42
43    /// Start position in the text
44    pub start: usize,
45
46    /// End position in the text
47    pub end: usize,
48
49    /// The matched text
50    pub text: &'a str,
51
52    /// Category of the tool
53    pub category: String,
54
55    /// Confidence score (0.0 - 1.0)
56    pub confidence: f32,
57}
58
59/// Aho-Corasick based pattern matcher
60///
61/// Uses efficient automaton-based matching for high performance
62/// even with many patterns.
63pub struct AhoCorasickMatcher {
64    /// The Aho-Corasick automaton
65    automaton: Option<AhoCorasick>,
66
67    /// Mapping from pattern index to tool metadata
68    pattern_to_tool: HashMap<usize, ToolInfo>,
69}
70
71#[derive(Debug, Clone)]
72struct ToolInfo {
73    name: String,
74    category: String,
75    confidence: f32,
76}
77
78impl Default for AhoCorasickMatcher {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl AhoCorasickMatcher {
85    /// Create a new uninitialized matcher
86    #[must_use]
87    pub fn new() -> Self {
88        Self {
89            automaton: None,
90            pattern_to_tool: HashMap::new(),
91        }
92    }
93
94    /// Build the Aho-Corasick automaton from patterns
95    fn build_automaton(&mut self, patterns: &[ToolPattern]) -> Result<()> {
96        let mut all_patterns = Vec::new();
97        self.pattern_to_tool.clear();
98
99        for tool in patterns.iter() {
100            for pattern in &tool.patterns {
101                let pattern_idx = all_patterns.len();
102                all_patterns.push(pattern.clone());
103
104                self.pattern_to_tool.insert(
105                    pattern_idx,
106                    ToolInfo {
107                        name: tool.name.clone(),
108                        category: tool.metadata.category.clone(),
109                        confidence: tool.metadata.confidence,
110                    },
111                );
112            }
113        }
114
115        let automaton = AhoCorasickBuilder::new()
116            .ascii_case_insensitive(true)
117            .match_kind(MatchKind::LeftmostLongest)
118            .build(&all_patterns)
119            .map_err(|e| anyhow::anyhow!("Failed to build Aho-Corasick automaton: {e}"))?;
120
121        self.automaton = Some(automaton);
122        Ok(())
123    }
124}
125
126impl PatternMatcher for AhoCorasickMatcher {
127    fn initialize(&mut self, patterns: &[ToolPattern]) -> Result<()> {
128        self.build_automaton(patterns)
129    }
130
131    fn find_matches<'a>(&self, text: &'a str) -> Vec<ToolMatch<'a>> {
132        let Some(ref automaton) = self.automaton else {
133            return Vec::new();
134        };
135
136        let mut matches = Vec::new();
137
138        for mat in automaton.find_iter(text) {
139            if let Some(tool_info) = self.pattern_to_tool.get(&mat.pattern().as_usize()) {
140                matches.push(ToolMatch {
141                    tool_name: tool_info.name.clone(),
142                    start: mat.start(),
143                    end: mat.end(),
144                    text: &text[mat.start()..mat.end()],
145                    category: tool_info.category.clone(),
146                    confidence: tool_info.confidence,
147                });
148            }
149        }
150
151        matches
152    }
153
154    fn matcher_type(&self) -> &'static str {
155        "aho-corasick"
156    }
157}
158
159/// Terraphim-based pattern matcher using knowledge graph automata
160///
161/// This implementation uses the actual terraphim_automata library for pattern matching,
162/// which provides knowledge graph-based semantic search capabilities.
163#[cfg(feature = "terraphim")]
164pub struct TerraphimMatcher {
165    /// Thesaurus containing the pattern mappings
166    thesaurus: Option<Thesaurus>,
167
168    /// Mapping from tool name to metadata
169    tool_metadata: HashMap<String, (String, f32)>, // (category, confidence)
170
171    /// Fallback Aho-Corasick matcher for error cases
172    fallback: AhoCorasickMatcher,
173}
174
175#[cfg(feature = "terraphim")]
176impl Default for TerraphimMatcher {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182#[cfg(feature = "terraphim")]
183impl TerraphimMatcher {
184    /// Create a new uninitialized Terraphim matcher
185    #[must_use]
186    pub fn new() -> Self {
187        Self {
188            thesaurus: None,
189            tool_metadata: HashMap::new(),
190            fallback: AhoCorasickMatcher::new(),
191        }
192    }
193
194    /// Build a Thesaurus from tool patterns
195    fn build_thesaurus(&mut self, patterns: &[ToolPattern]) -> Result<()> {
196        let mut thesaurus = Thesaurus::new("Tool Patterns".to_string());
197        let mut pattern_id = 0u64;
198
199        // Clear and rebuild metadata map
200        self.tool_metadata.clear();
201
202        for tool in patterns {
203            // Store tool metadata
204            self.tool_metadata.insert(
205                tool.name.clone(),
206                (tool.metadata.category.clone(), tool.metadata.confidence),
207            );
208
209            for pattern in &tool.patterns {
210                pattern_id += 1;
211
212                // Create a normalized term for this pattern
213                let normalized_term =
214                    NormalizedTerm::new(pattern_id, NormalizedTermValue::from(tool.name.as_str()));
215                let normalized_term =
216                    if let Some(url) = tool.metadata.description.as_ref().map(|d| d.to_string()) {
217                        normalized_term.with_url(url)
218                    } else {
219                        normalized_term
220                    };
221
222                // Insert the pattern -> normalized term mapping
223                thesaurus.insert(NormalizedTermValue::from(pattern.as_str()), normalized_term);
224            }
225        }
226
227        self.thesaurus = Some(thesaurus);
228        Ok(())
229    }
230}
231
232#[cfg(feature = "terraphim")]
233impl PatternMatcher for TerraphimMatcher {
234    fn initialize(&mut self, patterns: &[ToolPattern]) -> Result<()> {
235        // Build the terraphim thesaurus
236        self.build_thesaurus(patterns)?;
237
238        // Also initialize fallback in case terraphim fails
239        self.fallback.initialize(patterns)?;
240
241        Ok(())
242    }
243
244    fn find_matches<'a>(&self, text: &'a str) -> Vec<ToolMatch<'a>> {
245        // Use the actual terraphim_automata library
246        let Some(ref thesaurus) = self.thesaurus else {
247            // If thesaurus not initialized, use fallback
248            return self.fallback.find_matches(text);
249        };
250
251        // Call the actual terraphim_automata find_matches function
252        match terraphim_find_matches(text, thesaurus.clone(), true) {
253            Ok(matches) => {
254                // Convert terraphim matches to our ToolMatch format
255                matches
256                    .into_iter()
257                    .filter_map(|m| {
258                        let tool_name = m.normalized_term.value.to_string();
259
260                        // Look up category and confidence from metadata
261                        let (category, confidence) = self
262                            .tool_metadata
263                            .get(&tool_name)
264                            .map(|(cat, conf)| (cat.clone(), *conf))
265                            .unwrap_or_else(|| ("unknown".to_string(), 0.5));
266
267                        // Extract position from the pos field
268                        m.pos.map(|(start, end)| ToolMatch {
269                            tool_name,
270                            start,
271                            end,
272                            text: &text[start..end],
273                            category,
274                            confidence,
275                        })
276                    })
277                    .collect()
278            }
279            Err(_) => {
280                // If terraphim fails, fall back to aho-corasick
281                self.fallback.find_matches(text)
282            }
283        }
284    }
285
286    fn matcher_type(&self) -> &'static str {
287        if self.thesaurus.is_some() {
288            "terraphim-automata"
289        } else {
290            "terraphim-automata (uninitialized)"
291        }
292    }
293}
294
295/// Factory function to create a new pattern matcher
296///
297/// Returns Terraphim matcher if the feature is enabled,
298/// otherwise returns the default Aho-Corasick implementation
299#[must_use]
300#[allow(dead_code)] // Used in doc examples
301pub fn create_matcher() -> Box<dyn PatternMatcher> {
302    #[cfg(feature = "terraphim")]
303    {
304        Box::new(TerraphimMatcher::new())
305    }
306
307    #[cfg(not(feature = "terraphim"))]
308    {
309        Box::new(AhoCorasickMatcher::new())
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::patterns::loader::ToolMetadata;
317
318    fn create_test_patterns() -> Vec<ToolPattern> {
319        vec![
320            ToolPattern {
321                name: "wrangler".to_string(),
322                patterns: vec!["npx wrangler".to_string(), "bunx wrangler".to_string()],
323                metadata: ToolMetadata {
324                    category: "cloudflare".to_string(),
325                    description: Some("Cloudflare Workers CLI".to_string()),
326                    confidence: 0.95,
327                },
328            },
329            ToolPattern {
330                name: "npm".to_string(),
331                patterns: vec!["npm ".to_string()],
332                metadata: ToolMetadata {
333                    category: "package-manager".to_string(),
334                    description: Some("Node package manager".to_string()),
335                    confidence: 0.9,
336                },
337            },
338        ]
339    }
340
341    #[test]
342    fn test_matcher_initialization() {
343        let patterns = create_test_patterns();
344        let mut matcher = AhoCorasickMatcher::new();
345
346        let result = matcher.initialize(&patterns);
347        assert!(result.is_ok());
348        assert!(matcher.automaton.is_some());
349    }
350
351    #[test]
352    fn test_find_matches_basic() {
353        let patterns = create_test_patterns();
354        let mut matcher = AhoCorasickMatcher::new();
355        matcher.initialize(&patterns).unwrap();
356
357        let text = "npx wrangler deploy --env production";
358        let matches = matcher.find_matches(text);
359
360        assert_eq!(matches.len(), 1);
361        assert_eq!(matches[0].tool_name, "wrangler");
362        assert_eq!(matches[0].text, "npx wrangler");
363        assert_eq!(matches[0].category, "cloudflare");
364    }
365
366    #[test]
367    fn test_find_matches_case_insensitive() {
368        let patterns = create_test_patterns();
369        let mut matcher = AhoCorasickMatcher::new();
370        matcher.initialize(&patterns).unwrap();
371
372        let text = "NPX WRANGLER deploy";
373        let matches = matcher.find_matches(text);
374
375        assert_eq!(matches.len(), 1);
376        assert_eq!(matches[0].tool_name, "wrangler");
377    }
378
379    #[test]
380    fn test_find_matches_multiple_tools() {
381        let patterns = create_test_patterns();
382        let mut matcher = AhoCorasickMatcher::new();
383        matcher.initialize(&patterns).unwrap();
384
385        let text = "npm install && npx wrangler deploy";
386        let matches = matcher.find_matches(text);
387
388        assert_eq!(matches.len(), 2);
389        assert_eq!(matches[0].tool_name, "npm");
390        assert_eq!(matches[1].tool_name, "wrangler");
391    }
392
393    #[test]
394    fn test_find_matches_alternative_pattern() {
395        let patterns = create_test_patterns();
396        let mut matcher = AhoCorasickMatcher::new();
397        matcher.initialize(&patterns).unwrap();
398
399        let text = "bunx wrangler dev";
400        let matches = matcher.find_matches(text);
401
402        assert_eq!(matches.len(), 1);
403        assert_eq!(matches[0].tool_name, "wrangler");
404        assert_eq!(matches[0].text, "bunx wrangler");
405    }
406
407    #[test]
408    fn test_find_matches_no_matches() {
409        let patterns = create_test_patterns();
410        let mut matcher = AhoCorasickMatcher::new();
411        matcher.initialize(&patterns).unwrap();
412
413        let text = "echo hello world";
414        let matches = matcher.find_matches(text);
415
416        assert_eq!(matches.len(), 0);
417    }
418
419    #[test]
420    fn test_matcher_type() {
421        let matcher = AhoCorasickMatcher::new();
422        assert_eq!(matcher.matcher_type(), "aho-corasick");
423    }
424
425    #[test]
426    fn test_create_matcher_factory() {
427        let matcher = create_matcher();
428
429        // Factory returns different matchers based on features
430        #[cfg(feature = "terraphim")]
431        assert_eq!(matcher.matcher_type(), "terraphim-automata (uninitialized)");
432
433        #[cfg(not(feature = "terraphim"))]
434        assert_eq!(matcher.matcher_type(), "aho-corasick");
435    }
436
437    #[test]
438    fn test_uninitialized_matcher() {
439        let matcher = AhoCorasickMatcher::new();
440        let matches = matcher.find_matches("npx wrangler deploy");
441        assert_eq!(matches.len(), 0);
442    }
443}
444
445#[cfg(test)]
446mod wrangler_tests {
447    use super::*;
448    use crate::patterns::loader::ToolMetadata;
449
450    /// Create comprehensive wrangler patterns with all package manager variants
451    fn create_wrangler_patterns() -> Vec<ToolPattern> {
452        vec![ToolPattern {
453            name: "wrangler".to_string(),
454            patterns: vec![
455                "npx wrangler".to_string(),
456                "bunx wrangler".to_string(),
457                "pnpm wrangler".to_string(),
458                "yarn wrangler".to_string(),
459            ],
460            metadata: ToolMetadata {
461                category: "cloudflare".to_string(),
462                description: Some("Cloudflare Workers CLI".to_string()),
463                confidence: 0.95,
464            },
465        }]
466    }
467
468    #[test]
469    fn test_wrangler_login_npx() {
470        let patterns = create_wrangler_patterns();
471        let mut matcher = AhoCorasickMatcher::new();
472        matcher.initialize(&patterns).unwrap();
473
474        let text = "npx wrangler login";
475        let matches = matcher.find_matches(text);
476
477        assert_eq!(matches.len(), 1);
478        assert_eq!(matches[0].tool_name, "wrangler");
479        assert_eq!(matches[0].text, "npx wrangler");
480        assert_eq!(matches[0].category, "cloudflare");
481        assert_eq!(matches[0].confidence, 0.95);
482    }
483
484    #[test]
485    fn test_wrangler_login_bunx() {
486        let patterns = create_wrangler_patterns();
487        let mut matcher = AhoCorasickMatcher::new();
488        matcher.initialize(&patterns).unwrap();
489
490        let text = "bunx wrangler login";
491        let matches = matcher.find_matches(text);
492
493        assert_eq!(matches.len(), 1);
494        assert_eq!(matches[0].tool_name, "wrangler");
495        assert_eq!(matches[0].text, "bunx wrangler");
496        assert_eq!(matches[0].category, "cloudflare");
497    }
498
499    #[test]
500    fn test_wrangler_deploy_basic() {
501        let patterns = create_wrangler_patterns();
502        let mut matcher = AhoCorasickMatcher::new();
503        matcher.initialize(&patterns).unwrap();
504
505        let text = "npx wrangler deploy";
506        let matches = matcher.find_matches(text);
507
508        assert_eq!(matches.len(), 1);
509        assert_eq!(matches[0].tool_name, "wrangler");
510        assert_eq!(matches[0].text, "npx wrangler");
511    }
512
513    #[test]
514    fn test_wrangler_deploy_with_env() {
515        let patterns = create_wrangler_patterns();
516        let mut matcher = AhoCorasickMatcher::new();
517        matcher.initialize(&patterns).unwrap();
518
519        // Test npx variant
520        let text = "npx wrangler deploy --env production";
521        let matches = matcher.find_matches(text);
522
523        assert_eq!(matches.len(), 1);
524        assert_eq!(matches[0].tool_name, "wrangler");
525        assert_eq!(matches[0].text, "npx wrangler");
526
527        // Test bunx variant
528        let text = "bunx wrangler deploy --env staging";
529        let matches = matcher.find_matches(text);
530
531        assert_eq!(matches.len(), 1);
532        assert_eq!(matches[0].tool_name, "wrangler");
533        assert_eq!(matches[0].text, "bunx wrangler");
534    }
535
536    #[test]
537    fn test_wrangler_deploy_with_minify() {
538        let patterns = create_wrangler_patterns();
539        let mut matcher = AhoCorasickMatcher::new();
540        matcher.initialize(&patterns).unwrap();
541
542        let text = "npx wrangler deploy --minify";
543        let matches = matcher.find_matches(text);
544
545        assert_eq!(matches.len(), 1);
546        assert_eq!(matches[0].tool_name, "wrangler");
547        assert_eq!(matches[0].text, "npx wrangler");
548    }
549
550    #[test]
551    fn test_wrangler_deploy_complex_flags() {
552        let patterns = create_wrangler_patterns();
553        let mut matcher = AhoCorasickMatcher::new();
554        matcher.initialize(&patterns).unwrap();
555
556        let text = "npx wrangler deploy --env prod --minify --compatibility-date 2024-01-01";
557        let matches = matcher.find_matches(text);
558
559        assert_eq!(matches.len(), 1);
560        assert_eq!(matches[0].tool_name, "wrangler");
561        assert_eq!(matches[0].text, "npx wrangler");
562        assert_eq!(matches[0].start, 0);
563        assert_eq!(matches[0].end, 12); // "npx wrangler" is 12 characters
564    }
565
566    #[test]
567    fn test_wrangler_all_package_managers() {
568        let patterns = create_wrangler_patterns();
569        let mut matcher = AhoCorasickMatcher::new();
570        matcher.initialize(&patterns).unwrap();
571
572        // Test all package manager variants
573        let test_cases = vec![
574            ("npx wrangler deploy", "npx wrangler"),
575            ("bunx wrangler deploy", "bunx wrangler"),
576            ("pnpm wrangler deploy", "pnpm wrangler"),
577            ("yarn wrangler deploy", "yarn wrangler"),
578        ];
579
580        for (command, expected_text) in test_cases {
581            let matches = matcher.find_matches(command);
582            assert_eq!(matches.len(), 1, "Failed for command: {command}");
583            assert_eq!(matches[0].tool_name, "wrangler");
584            assert_eq!(matches[0].text, expected_text);
585            assert_eq!(matches[0].category, "cloudflare");
586        }
587    }
588
589    #[test]
590    fn test_wrangler_publish() {
591        let patterns = create_wrangler_patterns();
592        let mut matcher = AhoCorasickMatcher::new();
593        matcher.initialize(&patterns).unwrap();
594
595        let text = "npx wrangler publish";
596        let matches = matcher.find_matches(text);
597
598        assert_eq!(matches.len(), 1);
599        assert_eq!(matches[0].tool_name, "wrangler");
600        assert_eq!(matches[0].text, "npx wrangler");
601    }
602
603    #[test]
604    fn test_wrangler_dev() {
605        let patterns = create_wrangler_patterns();
606        let mut matcher = AhoCorasickMatcher::new();
607        matcher.initialize(&patterns).unwrap();
608
609        let text = "bunx wrangler dev";
610        let matches = matcher.find_matches(text);
611
612        assert_eq!(matches.len(), 1);
613        assert_eq!(matches[0].tool_name, "wrangler");
614        assert_eq!(matches[0].text, "bunx wrangler");
615    }
616
617    #[test]
618    fn test_wrangler_tail() {
619        let patterns = create_wrangler_patterns();
620        let mut matcher = AhoCorasickMatcher::new();
621        matcher.initialize(&patterns).unwrap();
622
623        let text = "npx wrangler tail";
624        let matches = matcher.find_matches(text);
625
626        assert_eq!(matches.len(), 1);
627        assert_eq!(matches[0].tool_name, "wrangler");
628        assert_eq!(matches[0].text, "npx wrangler");
629    }
630
631    #[test]
632    fn test_wrangler_case_insensitive() {
633        let patterns = create_wrangler_patterns();
634        let mut matcher = AhoCorasickMatcher::new();
635        matcher.initialize(&patterns).unwrap();
636
637        let text = "NPX WRANGLER DEPLOY";
638        let matches = matcher.find_matches(text);
639
640        assert_eq!(matches.len(), 1);
641        assert_eq!(matches[0].tool_name, "wrangler");
642    }
643
644    #[test]
645    fn test_wrangler_in_pipeline() {
646        let patterns = create_wrangler_patterns();
647        let mut matcher = AhoCorasickMatcher::new();
648        matcher.initialize(&patterns).unwrap();
649
650        let text = "npm install && npx wrangler deploy && npm test";
651        let matches = matcher.find_matches(text);
652
653        // Should find wrangler
654        let wrangler_matches: Vec<_> = matches
655            .iter()
656            .filter(|m| m.tool_name == "wrangler")
657            .collect();
658        assert_eq!(wrangler_matches.len(), 1);
659        assert_eq!(wrangler_matches[0].text, "npx wrangler");
660    }
661
662    #[test]
663    fn test_wrangler_multiple_commands() {
664        let patterns = create_wrangler_patterns();
665        let mut matcher = AhoCorasickMatcher::new();
666        matcher.initialize(&patterns).unwrap();
667
668        let text = "npx wrangler login && bunx wrangler deploy";
669        let matches = matcher.find_matches(text);
670
671        // Should find both wrangler invocations
672        assert_eq!(matches.len(), 2);
673        assert_eq!(matches[0].tool_name, "wrangler");
674        assert_eq!(matches[0].text, "npx wrangler");
675        assert_eq!(matches[1].tool_name, "wrangler");
676        assert_eq!(matches[1].text, "bunx wrangler");
677    }
678
679    #[test]
680    fn test_wrangler_with_output_redirection() {
681        let patterns = create_wrangler_patterns();
682        let mut matcher = AhoCorasickMatcher::new();
683        matcher.initialize(&patterns).unwrap();
684
685        let text = "npx wrangler deploy > deploy.log 2>&1";
686        let matches = matcher.find_matches(text);
687
688        assert_eq!(matches.len(), 1);
689        assert_eq!(matches[0].tool_name, "wrangler");
690        assert_eq!(matches[0].text, "npx wrangler");
691    }
692
693    #[test]
694    fn test_wrangler_subcommands() {
695        let patterns = create_wrangler_patterns();
696        let mut matcher = AhoCorasickMatcher::new();
697        matcher.initialize(&patterns).unwrap();
698
699        let subcommands = vec![
700            "login",
701            "deploy",
702            "publish",
703            "dev",
704            "tail",
705            "whoami",
706            "init",
707            "secret",
708            "kv:namespace",
709            "pages",
710        ];
711
712        for subcommand in subcommands {
713            let text = format!("npx wrangler {subcommand}");
714            let matches = matcher.find_matches(&text);
715
716            assert_eq!(matches.len(), 1, "Failed for subcommand: {subcommand}");
717            assert_eq!(matches[0].tool_name, "wrangler");
718            assert_eq!(matches[0].text, "npx wrangler");
719        }
720    }
721}
722
723#[cfg(all(test, feature = "terraphim"))]
724mod terraphim_tests {
725    use super::*;
726    use crate::patterns::loader::ToolMetadata;
727
728    fn create_test_patterns() -> Vec<ToolPattern> {
729        vec![
730            ToolPattern {
731                name: "wrangler".to_string(),
732                patterns: vec!["npx wrangler".to_string(), "bunx wrangler".to_string()],
733                metadata: ToolMetadata {
734                    category: "cloudflare".to_string(),
735                    description: Some("Cloudflare Workers CLI".to_string()),
736                    confidence: 0.95,
737                },
738            },
739            ToolPattern {
740                name: "npm".to_string(),
741                patterns: vec!["npm ".to_string()],
742                metadata: ToolMetadata {
743                    category: "package-manager".to_string(),
744                    description: Some("Node package manager".to_string()),
745                    confidence: 0.9,
746                },
747            },
748        ]
749    }
750
751    #[test]
752    fn test_terraphim_matcher_initialization() {
753        let patterns = create_test_patterns();
754        let mut matcher = TerraphimMatcher::new();
755
756        let result = matcher.initialize(&patterns);
757        assert!(result.is_ok());
758    }
759
760    #[test]
761    fn test_terraphim_find_matches_basic() {
762        let patterns = create_test_patterns();
763        let mut matcher = TerraphimMatcher::new();
764        matcher.initialize(&patterns).unwrap();
765
766        let text = "npx wrangler deploy --env production";
767        let matches = matcher.find_matches(text);
768
769        assert_eq!(matches.len(), 1);
770        assert_eq!(matches[0].tool_name, "wrangler");
771        assert_eq!(matches[0].category, "cloudflare");
772    }
773
774    #[test]
775    fn test_terraphim_find_matches_case_insensitive() {
776        let patterns = create_test_patterns();
777        let mut matcher = TerraphimMatcher::new();
778        matcher.initialize(&patterns).unwrap();
779
780        let text = "NPX WRANGLER deploy";
781        let matches = matcher.find_matches(text);
782
783        assert_eq!(matches.len(), 1);
784        assert_eq!(matches[0].tool_name, "wrangler");
785    }
786
787    #[test]
788    fn test_terraphim_find_matches_multiple_tools() {
789        let patterns = create_test_patterns();
790        let mut matcher = TerraphimMatcher::new();
791        matcher.initialize(&patterns).unwrap();
792
793        let text = "npm install && npx wrangler deploy";
794        let matches = matcher.find_matches(text);
795
796        assert_eq!(matches.len(), 2);
797        assert_eq!(matches[0].tool_name, "npm");
798        assert_eq!(matches[1].tool_name, "wrangler");
799    }
800
801    #[test]
802    fn test_terraphim_find_matches_alternative_pattern() {
803        let patterns = create_test_patterns();
804        let mut matcher = TerraphimMatcher::new();
805        matcher.initialize(&patterns).unwrap();
806
807        let text = "bunx wrangler dev";
808        let matches = matcher.find_matches(text);
809
810        assert_eq!(matches.len(), 1);
811        assert_eq!(matches[0].tool_name, "wrangler");
812    }
813
814    #[test]
815    fn test_terraphim_find_matches_no_matches() {
816        let patterns = create_test_patterns();
817        let mut matcher = TerraphimMatcher::new();
818        matcher.initialize(&patterns).unwrap();
819
820        let text = "echo hello world";
821        let matches = matcher.find_matches(text);
822
823        assert_eq!(matches.len(), 0);
824    }
825
826    #[test]
827    fn test_terraphim_matcher_type() {
828        let matcher = TerraphimMatcher::new();
829        assert_eq!(matcher.matcher_type(), "terraphim-automata (uninitialized)");
830
831        // After initialization, should be terraphim-automata
832        let patterns = create_test_patterns();
833        let mut matcher = TerraphimMatcher::new();
834        matcher.initialize(&patterns).unwrap();
835        assert_eq!(matcher.matcher_type(), "terraphim-automata");
836    }
837
838    #[test]
839    fn test_terraphim_create_matcher_factory() {
840        let matcher = create_matcher();
841        // Uninitialized matcher
842        assert_eq!(matcher.matcher_type(), "terraphim-automata (uninitialized)");
843    }
844
845    #[test]
846    fn test_terraphim_uninitialized_matcher() {
847        let matcher = TerraphimMatcher::new();
848        let matches = matcher.find_matches("npx wrangler deploy");
849        assert_eq!(matches.len(), 0);
850    }
851}