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 = NormalizedTerm {
214                    id: pattern_id,
215                    value: NormalizedTermValue::from(tool.name.as_str()),
216                    display_value: None,
217                    url: tool.metadata.description.as_ref().map(|d| d.to_string()),
218                };
219
220                // Insert the pattern -> normalized term mapping
221                thesaurus.insert(NormalizedTermValue::from(pattern.as_str()), normalized_term);
222            }
223        }
224
225        self.thesaurus = Some(thesaurus);
226        Ok(())
227    }
228}
229
230#[cfg(feature = "terraphim")]
231impl PatternMatcher for TerraphimMatcher {
232    fn initialize(&mut self, patterns: &[ToolPattern]) -> Result<()> {
233        // Build the terraphim thesaurus
234        self.build_thesaurus(patterns)?;
235
236        // Also initialize fallback in case terraphim fails
237        self.fallback.initialize(patterns)?;
238
239        Ok(())
240    }
241
242    fn find_matches<'a>(&self, text: &'a str) -> Vec<ToolMatch<'a>> {
243        // Use the actual terraphim_automata library
244        let Some(ref thesaurus) = self.thesaurus else {
245            // If thesaurus not initialized, use fallback
246            return self.fallback.find_matches(text);
247        };
248
249        // Call the actual terraphim_automata find_matches function
250        match terraphim_find_matches(text, thesaurus.clone(), true) {
251            Ok(matches) => {
252                // Convert terraphim matches to our ToolMatch format
253                matches
254                    .into_iter()
255                    .filter_map(|m| {
256                        let tool_name = m.normalized_term.value.to_string();
257
258                        // Look up category and confidence from metadata
259                        let (category, confidence) = self
260                            .tool_metadata
261                            .get(&tool_name)
262                            .map(|(cat, conf)| (cat.clone(), *conf))
263                            .unwrap_or_else(|| ("unknown".to_string(), 0.5));
264
265                        // Extract position from the pos field
266                        m.pos.map(|(start, end)| ToolMatch {
267                            tool_name,
268                            start,
269                            end,
270                            text: &text[start..end],
271                            category,
272                            confidence,
273                        })
274                    })
275                    .collect()
276            }
277            Err(_) => {
278                // If terraphim fails, fall back to aho-corasick
279                self.fallback.find_matches(text)
280            }
281        }
282    }
283
284    fn matcher_type(&self) -> &'static str {
285        if self.thesaurus.is_some() {
286            "terraphim-automata"
287        } else {
288            "terraphim-automata (uninitialized)"
289        }
290    }
291}
292
293/// Factory function to create a new pattern matcher
294///
295/// Returns Terraphim matcher if the feature is enabled,
296/// otherwise returns the default Aho-Corasick implementation
297#[must_use]
298#[allow(dead_code)] // Used in doc examples
299pub fn create_matcher() -> Box<dyn PatternMatcher> {
300    #[cfg(feature = "terraphim")]
301    {
302        Box::new(TerraphimMatcher::new())
303    }
304
305    #[cfg(not(feature = "terraphim"))]
306    {
307        Box::new(AhoCorasickMatcher::new())
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::patterns::loader::ToolMetadata;
315
316    fn create_test_patterns() -> Vec<ToolPattern> {
317        vec![
318            ToolPattern {
319                name: "wrangler".to_string(),
320                patterns: vec!["npx wrangler".to_string(), "bunx wrangler".to_string()],
321                metadata: ToolMetadata {
322                    category: "cloudflare".to_string(),
323                    description: Some("Cloudflare Workers CLI".to_string()),
324                    confidence: 0.95,
325                },
326            },
327            ToolPattern {
328                name: "npm".to_string(),
329                patterns: vec!["npm ".to_string()],
330                metadata: ToolMetadata {
331                    category: "package-manager".to_string(),
332                    description: Some("Node package manager".to_string()),
333                    confidence: 0.9,
334                },
335            },
336        ]
337    }
338
339    #[test]
340    fn test_matcher_initialization() {
341        let patterns = create_test_patterns();
342        let mut matcher = AhoCorasickMatcher::new();
343
344        let result = matcher.initialize(&patterns);
345        assert!(result.is_ok());
346        assert!(matcher.automaton.is_some());
347    }
348
349    #[test]
350    fn test_find_matches_basic() {
351        let patterns = create_test_patterns();
352        let mut matcher = AhoCorasickMatcher::new();
353        matcher.initialize(&patterns).unwrap();
354
355        let text = "npx wrangler deploy --env production";
356        let matches = matcher.find_matches(text);
357
358        assert_eq!(matches.len(), 1);
359        assert_eq!(matches[0].tool_name, "wrangler");
360        assert_eq!(matches[0].text, "npx wrangler");
361        assert_eq!(matches[0].category, "cloudflare");
362    }
363
364    #[test]
365    fn test_find_matches_case_insensitive() {
366        let patterns = create_test_patterns();
367        let mut matcher = AhoCorasickMatcher::new();
368        matcher.initialize(&patterns).unwrap();
369
370        let text = "NPX WRANGLER deploy";
371        let matches = matcher.find_matches(text);
372
373        assert_eq!(matches.len(), 1);
374        assert_eq!(matches[0].tool_name, "wrangler");
375    }
376
377    #[test]
378    fn test_find_matches_multiple_tools() {
379        let patterns = create_test_patterns();
380        let mut matcher = AhoCorasickMatcher::new();
381        matcher.initialize(&patterns).unwrap();
382
383        let text = "npm install && npx wrangler deploy";
384        let matches = matcher.find_matches(text);
385
386        assert_eq!(matches.len(), 2);
387        assert_eq!(matches[0].tool_name, "npm");
388        assert_eq!(matches[1].tool_name, "wrangler");
389    }
390
391    #[test]
392    fn test_find_matches_alternative_pattern() {
393        let patterns = create_test_patterns();
394        let mut matcher = AhoCorasickMatcher::new();
395        matcher.initialize(&patterns).unwrap();
396
397        let text = "bunx wrangler dev";
398        let matches = matcher.find_matches(text);
399
400        assert_eq!(matches.len(), 1);
401        assert_eq!(matches[0].tool_name, "wrangler");
402        assert_eq!(matches[0].text, "bunx wrangler");
403    }
404
405    #[test]
406    fn test_find_matches_no_matches() {
407        let patterns = create_test_patterns();
408        let mut matcher = AhoCorasickMatcher::new();
409        matcher.initialize(&patterns).unwrap();
410
411        let text = "echo hello world";
412        let matches = matcher.find_matches(text);
413
414        assert_eq!(matches.len(), 0);
415    }
416
417    #[test]
418    fn test_matcher_type() {
419        let matcher = AhoCorasickMatcher::new();
420        assert_eq!(matcher.matcher_type(), "aho-corasick");
421    }
422
423    #[test]
424    fn test_create_matcher_factory() {
425        let matcher = create_matcher();
426
427        // Factory returns different matchers based on features
428        #[cfg(feature = "terraphim")]
429        assert_eq!(matcher.matcher_type(), "terraphim-automata (uninitialized)");
430
431        #[cfg(not(feature = "terraphim"))]
432        assert_eq!(matcher.matcher_type(), "aho-corasick");
433    }
434
435    #[test]
436    fn test_uninitialized_matcher() {
437        let matcher = AhoCorasickMatcher::new();
438        let matches = matcher.find_matches("npx wrangler deploy");
439        assert_eq!(matches.len(), 0);
440    }
441}
442
443#[cfg(test)]
444mod wrangler_tests {
445    use super::*;
446    use crate::patterns::loader::ToolMetadata;
447
448    /// Create comprehensive wrangler patterns with all package manager variants
449    fn create_wrangler_patterns() -> Vec<ToolPattern> {
450        vec![ToolPattern {
451            name: "wrangler".to_string(),
452            patterns: vec![
453                "npx wrangler".to_string(),
454                "bunx wrangler".to_string(),
455                "pnpm wrangler".to_string(),
456                "yarn wrangler".to_string(),
457            ],
458            metadata: ToolMetadata {
459                category: "cloudflare".to_string(),
460                description: Some("Cloudflare Workers CLI".to_string()),
461                confidence: 0.95,
462            },
463        }]
464    }
465
466    #[test]
467    fn test_wrangler_login_npx() {
468        let patterns = create_wrangler_patterns();
469        let mut matcher = AhoCorasickMatcher::new();
470        matcher.initialize(&patterns).unwrap();
471
472        let text = "npx wrangler login";
473        let matches = matcher.find_matches(text);
474
475        assert_eq!(matches.len(), 1);
476        assert_eq!(matches[0].tool_name, "wrangler");
477        assert_eq!(matches[0].text, "npx wrangler");
478        assert_eq!(matches[0].category, "cloudflare");
479        assert_eq!(matches[0].confidence, 0.95);
480    }
481
482    #[test]
483    fn test_wrangler_login_bunx() {
484        let patterns = create_wrangler_patterns();
485        let mut matcher = AhoCorasickMatcher::new();
486        matcher.initialize(&patterns).unwrap();
487
488        let text = "bunx wrangler login";
489        let matches = matcher.find_matches(text);
490
491        assert_eq!(matches.len(), 1);
492        assert_eq!(matches[0].tool_name, "wrangler");
493        assert_eq!(matches[0].text, "bunx wrangler");
494        assert_eq!(matches[0].category, "cloudflare");
495    }
496
497    #[test]
498    fn test_wrangler_deploy_basic() {
499        let patterns = create_wrangler_patterns();
500        let mut matcher = AhoCorasickMatcher::new();
501        matcher.initialize(&patterns).unwrap();
502
503        let text = "npx wrangler deploy";
504        let matches = matcher.find_matches(text);
505
506        assert_eq!(matches.len(), 1);
507        assert_eq!(matches[0].tool_name, "wrangler");
508        assert_eq!(matches[0].text, "npx wrangler");
509    }
510
511    #[test]
512    fn test_wrangler_deploy_with_env() {
513        let patterns = create_wrangler_patterns();
514        let mut matcher = AhoCorasickMatcher::new();
515        matcher.initialize(&patterns).unwrap();
516
517        // Test npx variant
518        let text = "npx wrangler deploy --env production";
519        let matches = matcher.find_matches(text);
520
521        assert_eq!(matches.len(), 1);
522        assert_eq!(matches[0].tool_name, "wrangler");
523        assert_eq!(matches[0].text, "npx wrangler");
524
525        // Test bunx variant
526        let text = "bunx wrangler deploy --env staging";
527        let matches = matcher.find_matches(text);
528
529        assert_eq!(matches.len(), 1);
530        assert_eq!(matches[0].tool_name, "wrangler");
531        assert_eq!(matches[0].text, "bunx wrangler");
532    }
533
534    #[test]
535    fn test_wrangler_deploy_with_minify() {
536        let patterns = create_wrangler_patterns();
537        let mut matcher = AhoCorasickMatcher::new();
538        matcher.initialize(&patterns).unwrap();
539
540        let text = "npx wrangler deploy --minify";
541        let matches = matcher.find_matches(text);
542
543        assert_eq!(matches.len(), 1);
544        assert_eq!(matches[0].tool_name, "wrangler");
545        assert_eq!(matches[0].text, "npx wrangler");
546    }
547
548    #[test]
549    fn test_wrangler_deploy_complex_flags() {
550        let patterns = create_wrangler_patterns();
551        let mut matcher = AhoCorasickMatcher::new();
552        matcher.initialize(&patterns).unwrap();
553
554        let text = "npx wrangler deploy --env prod --minify --compatibility-date 2024-01-01";
555        let matches = matcher.find_matches(text);
556
557        assert_eq!(matches.len(), 1);
558        assert_eq!(matches[0].tool_name, "wrangler");
559        assert_eq!(matches[0].text, "npx wrangler");
560        assert_eq!(matches[0].start, 0);
561        assert_eq!(matches[0].end, 12); // "npx wrangler" is 12 characters
562    }
563
564    #[test]
565    fn test_wrangler_all_package_managers() {
566        let patterns = create_wrangler_patterns();
567        let mut matcher = AhoCorasickMatcher::new();
568        matcher.initialize(&patterns).unwrap();
569
570        // Test all package manager variants
571        let test_cases = vec![
572            ("npx wrangler deploy", "npx wrangler"),
573            ("bunx wrangler deploy", "bunx wrangler"),
574            ("pnpm wrangler deploy", "pnpm wrangler"),
575            ("yarn wrangler deploy", "yarn wrangler"),
576        ];
577
578        for (command, expected_text) in test_cases {
579            let matches = matcher.find_matches(command);
580            assert_eq!(matches.len(), 1, "Failed for command: {command}");
581            assert_eq!(matches[0].tool_name, "wrangler");
582            assert_eq!(matches[0].text, expected_text);
583            assert_eq!(matches[0].category, "cloudflare");
584        }
585    }
586
587    #[test]
588    fn test_wrangler_publish() {
589        let patterns = create_wrangler_patterns();
590        let mut matcher = AhoCorasickMatcher::new();
591        matcher.initialize(&patterns).unwrap();
592
593        let text = "npx wrangler publish";
594        let matches = matcher.find_matches(text);
595
596        assert_eq!(matches.len(), 1);
597        assert_eq!(matches[0].tool_name, "wrangler");
598        assert_eq!(matches[0].text, "npx wrangler");
599    }
600
601    #[test]
602    fn test_wrangler_dev() {
603        let patterns = create_wrangler_patterns();
604        let mut matcher = AhoCorasickMatcher::new();
605        matcher.initialize(&patterns).unwrap();
606
607        let text = "bunx wrangler dev";
608        let matches = matcher.find_matches(text);
609
610        assert_eq!(matches.len(), 1);
611        assert_eq!(matches[0].tool_name, "wrangler");
612        assert_eq!(matches[0].text, "bunx wrangler");
613    }
614
615    #[test]
616    fn test_wrangler_tail() {
617        let patterns = create_wrangler_patterns();
618        let mut matcher = AhoCorasickMatcher::new();
619        matcher.initialize(&patterns).unwrap();
620
621        let text = "npx wrangler tail";
622        let matches = matcher.find_matches(text);
623
624        assert_eq!(matches.len(), 1);
625        assert_eq!(matches[0].tool_name, "wrangler");
626        assert_eq!(matches[0].text, "npx wrangler");
627    }
628
629    #[test]
630    fn test_wrangler_case_insensitive() {
631        let patterns = create_wrangler_patterns();
632        let mut matcher = AhoCorasickMatcher::new();
633        matcher.initialize(&patterns).unwrap();
634
635        let text = "NPX WRANGLER DEPLOY";
636        let matches = matcher.find_matches(text);
637
638        assert_eq!(matches.len(), 1);
639        assert_eq!(matches[0].tool_name, "wrangler");
640    }
641
642    #[test]
643    fn test_wrangler_in_pipeline() {
644        let patterns = create_wrangler_patterns();
645        let mut matcher = AhoCorasickMatcher::new();
646        matcher.initialize(&patterns).unwrap();
647
648        let text = "npm install && npx wrangler deploy && npm test";
649        let matches = matcher.find_matches(text);
650
651        // Should find wrangler
652        let wrangler_matches: Vec<_> = matches
653            .iter()
654            .filter(|m| m.tool_name == "wrangler")
655            .collect();
656        assert_eq!(wrangler_matches.len(), 1);
657        assert_eq!(wrangler_matches[0].text, "npx wrangler");
658    }
659
660    #[test]
661    fn test_wrangler_multiple_commands() {
662        let patterns = create_wrangler_patterns();
663        let mut matcher = AhoCorasickMatcher::new();
664        matcher.initialize(&patterns).unwrap();
665
666        let text = "npx wrangler login && bunx wrangler deploy";
667        let matches = matcher.find_matches(text);
668
669        // Should find both wrangler invocations
670        assert_eq!(matches.len(), 2);
671        assert_eq!(matches[0].tool_name, "wrangler");
672        assert_eq!(matches[0].text, "npx wrangler");
673        assert_eq!(matches[1].tool_name, "wrangler");
674        assert_eq!(matches[1].text, "bunx wrangler");
675    }
676
677    #[test]
678    fn test_wrangler_with_output_redirection() {
679        let patterns = create_wrangler_patterns();
680        let mut matcher = AhoCorasickMatcher::new();
681        matcher.initialize(&patterns).unwrap();
682
683        let text = "npx wrangler deploy > deploy.log 2>&1";
684        let matches = matcher.find_matches(text);
685
686        assert_eq!(matches.len(), 1);
687        assert_eq!(matches[0].tool_name, "wrangler");
688        assert_eq!(matches[0].text, "npx wrangler");
689    }
690
691    #[test]
692    fn test_wrangler_subcommands() {
693        let patterns = create_wrangler_patterns();
694        let mut matcher = AhoCorasickMatcher::new();
695        matcher.initialize(&patterns).unwrap();
696
697        let subcommands = vec![
698            "login",
699            "deploy",
700            "publish",
701            "dev",
702            "tail",
703            "whoami",
704            "init",
705            "secret",
706            "kv:namespace",
707            "pages",
708        ];
709
710        for subcommand in subcommands {
711            let text = format!("npx wrangler {subcommand}");
712            let matches = matcher.find_matches(&text);
713
714            assert_eq!(matches.len(), 1, "Failed for subcommand: {subcommand}");
715            assert_eq!(matches[0].tool_name, "wrangler");
716            assert_eq!(matches[0].text, "npx wrangler");
717        }
718    }
719}
720
721#[cfg(all(test, feature = "terraphim"))]
722mod terraphim_tests {
723    use super::*;
724    use crate::patterns::loader::ToolMetadata;
725
726    fn create_test_patterns() -> Vec<ToolPattern> {
727        vec![
728            ToolPattern {
729                name: "wrangler".to_string(),
730                patterns: vec!["npx wrangler".to_string(), "bunx wrangler".to_string()],
731                metadata: ToolMetadata {
732                    category: "cloudflare".to_string(),
733                    description: Some("Cloudflare Workers CLI".to_string()),
734                    confidence: 0.95,
735                },
736            },
737            ToolPattern {
738                name: "npm".to_string(),
739                patterns: vec!["npm ".to_string()],
740                metadata: ToolMetadata {
741                    category: "package-manager".to_string(),
742                    description: Some("Node package manager".to_string()),
743                    confidence: 0.9,
744                },
745            },
746        ]
747    }
748
749    #[test]
750    fn test_terraphim_matcher_initialization() {
751        let patterns = create_test_patterns();
752        let mut matcher = TerraphimMatcher::new();
753
754        let result = matcher.initialize(&patterns);
755        assert!(result.is_ok());
756    }
757
758    #[test]
759    fn test_terraphim_find_matches_basic() {
760        let patterns = create_test_patterns();
761        let mut matcher = TerraphimMatcher::new();
762        matcher.initialize(&patterns).unwrap();
763
764        let text = "npx wrangler deploy --env production";
765        let matches = matcher.find_matches(text);
766
767        assert_eq!(matches.len(), 1);
768        assert_eq!(matches[0].tool_name, "wrangler");
769        assert_eq!(matches[0].category, "cloudflare");
770    }
771
772    #[test]
773    fn test_terraphim_find_matches_case_insensitive() {
774        let patterns = create_test_patterns();
775        let mut matcher = TerraphimMatcher::new();
776        matcher.initialize(&patterns).unwrap();
777
778        let text = "NPX WRANGLER deploy";
779        let matches = matcher.find_matches(text);
780
781        assert_eq!(matches.len(), 1);
782        assert_eq!(matches[0].tool_name, "wrangler");
783    }
784
785    #[test]
786    fn test_terraphim_find_matches_multiple_tools() {
787        let patterns = create_test_patterns();
788        let mut matcher = TerraphimMatcher::new();
789        matcher.initialize(&patterns).unwrap();
790
791        let text = "npm install && npx wrangler deploy";
792        let matches = matcher.find_matches(text);
793
794        assert_eq!(matches.len(), 2);
795        assert_eq!(matches[0].tool_name, "npm");
796        assert_eq!(matches[1].tool_name, "wrangler");
797    }
798
799    #[test]
800    fn test_terraphim_find_matches_alternative_pattern() {
801        let patterns = create_test_patterns();
802        let mut matcher = TerraphimMatcher::new();
803        matcher.initialize(&patterns).unwrap();
804
805        let text = "bunx wrangler dev";
806        let matches = matcher.find_matches(text);
807
808        assert_eq!(matches.len(), 1);
809        assert_eq!(matches[0].tool_name, "wrangler");
810    }
811
812    #[test]
813    fn test_terraphim_find_matches_no_matches() {
814        let patterns = create_test_patterns();
815        let mut matcher = TerraphimMatcher::new();
816        matcher.initialize(&patterns).unwrap();
817
818        let text = "echo hello world";
819        let matches = matcher.find_matches(text);
820
821        assert_eq!(matches.len(), 0);
822    }
823
824    #[test]
825    fn test_terraphim_matcher_type() {
826        let matcher = TerraphimMatcher::new();
827        assert_eq!(matcher.matcher_type(), "terraphim-automata (uninitialized)");
828
829        // After initialization, should be terraphim-automata
830        let patterns = create_test_patterns();
831        let mut matcher = TerraphimMatcher::new();
832        matcher.initialize(&patterns).unwrap();
833        assert_eq!(matcher.matcher_type(), "terraphim-automata");
834    }
835
836    #[test]
837    fn test_terraphim_create_matcher_factory() {
838        let matcher = create_matcher();
839        // Uninitialized matcher
840        assert_eq!(matcher.matcher_type(), "terraphim-automata (uninitialized)");
841    }
842
843    #[test]
844    fn test_terraphim_uninitialized_matcher() {
845        let matcher = TerraphimMatcher::new();
846        let matches = matcher.find_matches("npx wrangler deploy");
847        assert_eq!(matches.len(), 0);
848    }
849}