1use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use anyhow::Result;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15use super::conversation_pattern::{ConversationPattern, PatternType};
16use crate::constants::MATRIX_DIR;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct PatternRegistryConfig {
25 pub max_patterns_per_type: usize,
27 pub min_confidence_threshold: f32,
29 pub min_frequency: u32,
31 pub auto_learn: bool,
33 pub inactive_after_days: i64,
35}
36
37impl Default for PatternRegistryConfig {
38 fn default() -> Self {
39 Self {
40 max_patterns_per_type: 100,
41 min_confidence_threshold: 0.3,
42 min_frequency: 2,
43 auto_learn: true,
44 inactive_after_days: 90,
45 }
46 }
47}
48
49impl PatternRegistryConfig {
50 pub fn with_max_patterns(max: usize) -> Self {
52 Self {
53 max_patterns_per_type: max,
54 ..Self::default()
55 }
56 }
57
58 pub fn minimal() -> Self {
60 Self {
61 max_patterns_per_type: 50,
62 min_confidence_threshold: 0.5,
63 min_frequency: 3,
64 auto_learn: true,
65 inactive_after_days: 60,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct PatternRegistry {
81 patterns: Vec<ConversationPattern>,
83 #[serde(default)]
85 config: PatternRegistryConfig,
86 #[serde(skip)]
88 type_index: HashMap<PatternType, Vec<usize>>,
89}
90
91impl Default for PatternRegistry {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl PatternRegistry {
98 pub fn new() -> Self {
102 Self {
103 patterns: Vec::new(),
104 config: PatternRegistryConfig::default(),
105 type_index: HashMap::new(),
106 }
107 }
108
109 pub fn with_config(config: PatternRegistryConfig) -> Self {
113 Self {
114 patterns: Vec::new(),
115 config,
116 type_index: HashMap::new(),
117 }
118 }
119
120 fn rebuild_index(&mut self) {
122 self.type_index.clear();
123 for (idx, pattern) in self.patterns.iter().enumerate() {
124 self.type_index
125 .entry(pattern.pattern_type)
126 .or_default()
127 .push(idx);
128 }
129 }
130
131 fn add_pattern_internal(&mut self, pattern: ConversationPattern) {
133 if self.patterns.iter().any(|p| p.pattern == pattern.pattern) {
135 return;
136 }
137
138 let type_count = self
140 .patterns
141 .iter()
142 .filter(|p| p.pattern_type == pattern.pattern_type)
143 .count();
144
145 if type_count >= self.config.max_patterns_per_type {
146 if let Some(idx) = self
148 .patterns
149 .iter()
150 .enumerate()
151 .filter(|(_, p)| p.pattern_type == pattern.pattern_type)
152 .min_by_key(|(_, p)| (p.frequency, (p.confidence * 100.0) as u32))
153 .map(|(i, _)| i)
154 {
155 self.patterns.remove(idx);
156 }
157 }
158
159 self.patterns.push(pattern);
160 }
161
162 pub fn add_pattern(&mut self, pattern: ConversationPattern) {
164 self.add_pattern_internal(pattern);
165 self.rebuild_index();
166 }
167
168 pub fn add_patterns(&mut self, patterns: Vec<ConversationPattern>) {
170 for pattern in patterns {
171 self.add_pattern_internal(pattern);
172 }
173 self.rebuild_index();
174 }
175
176 pub fn learn_patterns(&mut self, patterns: &[ConversationPattern]) {
181 for new_pattern in patterns {
182 if let Some(existing) = self
183 .patterns
184 .iter_mut()
185 .find(|p| p.pattern == new_pattern.pattern)
186 {
187 existing.mark_used();
188 } else if self.config.auto_learn {
189 self.add_pattern_internal(new_pattern.clone());
190 }
191 }
192 self.rebuild_index();
193 }
194
195 pub fn get_active_patterns(&self) -> Vec<&ConversationPattern> {
197 self.patterns.iter().filter(|p| p.is_active).collect()
198 }
199
200 pub fn get_active_patterns_by_type(&self, pattern_type: PatternType) -> Vec<&ConversationPattern> {
202 self.patterns
203 .iter()
204 .filter(|p| p.is_active && p.pattern_type == pattern_type)
205 .collect()
206 }
207
208 pub fn get_active_reference_patterns(&self) -> Vec<String> {
210 self.get_active_patterns_by_type(PatternType::Reference)
211 .iter()
212 .map(|p| p.pattern.clone())
213 .collect()
214 }
215
216 pub fn get_active_code_patterns(&self) -> Vec<String> {
218 self.get_active_patterns_by_type(PatternType::Code)
219 .iter()
220 .map(|p| p.pattern.clone())
221 .collect()
222 }
223
224 pub fn get_pattern(&self, id: &str) -> Option<&ConversationPattern> {
226 self.patterns.iter().find(|p| p.id == id)
227 }
228
229 pub fn get_pattern_mut(&mut self, id: &str) -> Option<&mut ConversationPattern> {
231 self.patterns.iter_mut().find(|p| p.id == id)
232 }
233
234 pub fn deactivate_pattern(&mut self, id: &str) -> bool {
236 if let Some(pattern) = self.get_pattern_mut(id) {
237 pattern.deactivate();
238 true
239 } else {
240 false
241 }
242 }
243
244 pub fn activate_pattern(&mut self, id: &str) -> bool {
246 if let Some(pattern) = self.get_pattern_mut(id) {
247 pattern.activate();
248 true
249 } else {
250 false
251 }
252 }
253
254 pub fn remove_pattern(&mut self, id: &str) -> bool {
256 let len_before = self.patterns.len();
257 self.patterns.retain(|p| p.id != id);
258 if self.patterns.len() < len_before {
259 self.rebuild_index();
260 true
261 } else {
262 false
263 }
264 }
265
266 pub fn len(&self) -> usize {
268 self.patterns.len()
269 }
270
271 pub fn is_empty(&self) -> bool {
273 self.patterns.is_empty()
274 }
275
276 pub fn count_by_type(&self, pattern_type: PatternType) -> usize {
278 self.patterns
279 .iter()
280 .filter(|p| p.pattern_type == pattern_type)
281 .count()
282 }
283
284 pub fn active_count_by_type(&self, pattern_type: PatternType) -> usize {
286 self.get_active_patterns_by_type(pattern_type).len()
287 }
288
289 pub fn all_patterns(&self) -> &[ConversationPattern] {
291 &self.patterns
292 }
293
294 pub fn prune(&mut self) {
296 let now = Utc::now();
297 let threshold_days = self.config.inactive_after_days;
298
299 self.patterns.retain(|p| {
300 if p.is_active {
302 return true;
303 }
304 if p.source.is_preset() {
306 return true;
307 }
308 if p.source.is_manual() {
310 return true;
311 }
312 if p.frequency >= self.config.min_frequency
314 && p.confidence >= self.config.min_confidence_threshold
315 {
316 return true;
317 }
318 let age = (now - p.last_used).num_days();
320 age < threshold_days
321 });
322
323 self.rebuild_index();
324 }
325
326 pub fn stats(&self) -> PatternRegistryStats {
328 let total = self.patterns.len();
329 let active = self.patterns.iter().filter(|p| p.is_active).count();
330 let reference_count = self.count_by_type(PatternType::Reference);
331 let code_count = self.count_by_type(PatternType::Code);
332 let presets = self.patterns.iter().filter(|p| p.source.is_preset()).count();
333 let manual = self.patterns.iter().filter(|p| p.source.is_manual()).count();
334 let learned = total - presets - manual;
335
336 PatternRegistryStats {
337 total,
338 active,
339 inactive: total - active,
340 reference_count,
341 code_count,
342 presets,
343 manual,
344 learned,
345 }
346 }
347
348 pub fn get_patterns_file_path() -> Result<PathBuf> {
354 let home = std::env::var_os("HOME")
355 .or_else(|| std::env::var_os("USERPROFILE"))
356 .ok_or_else(|| anyhow::anyhow!("HOME or USERPROFILE not set"))?;
357 Ok(PathBuf::from(home).join(MATRIX_DIR).join("patterns.json"))
358 }
359
360 pub fn from_file(path: &Path) -> Result<Self> {
365 if !path.exists() {
366 return Ok(Self::new());
368 }
369
370 let data = fs::read_to_string(path)?;
371
372 if data.trim().is_empty() {
374 return Ok(Self::new());
375 }
376
377 match serde_json::from_str::<PatternRegistry>(&data) {
379 Ok(mut registry) => {
380 registry.rebuild_index();
381 Ok(registry)
382 }
383 Err(e) => {
384 tracing::warn!(
386 "Failed to parse patterns file {:?}: {}. Using empty registry.",
387 path,
388 e
389 );
390 Ok(Self::new())
391 }
392 }
393 }
394
395 pub fn from_default_file() -> Result<Self> {
397 let path = Self::get_patterns_file_path()?;
398 Self::from_file(&path)
399 }
400
401 pub fn save_to_file(&self, path: &Path) -> Result<()> {
406 let parent = path.parent();
408 if let Some(dir) = parent {
409 if !dir.exists() {
410 fs::create_dir_all(dir)?;
411 }
412 }
413
414 let json = serde_json::to_string_pretty(self)?;
416
417 let tmp_path = path.with_extension("json.tmp");
419 fs::write(&tmp_path, json)?;
420
421 fs::rename(&tmp_path, path)?;
423
424 Ok(())
425 }
426
427 pub fn save_to_default_file(&self) -> Result<()> {
429 let path = Self::get_patterns_file_path()?;
430 self.save_to_file(&path)
431 }
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct PatternRegistryStats {
441 pub total: usize,
443 pub active: usize,
445 pub inactive: usize,
447 pub reference_count: usize,
449 pub code_count: usize,
451 pub presets: usize,
453 pub manual: usize,
455 pub learned: usize,
457}
458
459impl std::fmt::Display for PatternRegistryStats {
460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 writeln!(f, "Pattern Registry Stats:")?;
462 writeln!(f, " Total: {} (active: {}, inactive: {})", self.total, self.active, self.inactive)?;
463 writeln!(f, " Reference: {}, Code: {}", self.reference_count, self.code_count)?;
464 writeln!(f, " Presets: {}, Manual: {}, Learned: {}", self.presets, self.manual, self.learned)
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use crate::memory::conversation_pattern::PatternSource;
472
473 #[test]
478 fn test_config_default() {
479 let config = PatternRegistryConfig::default();
480
481 assert_eq!(config.max_patterns_per_type, 100);
482 assert_eq!(config.min_confidence_threshold, 0.3);
483 assert_eq!(config.min_frequency, 2);
484 assert!(config.auto_learn);
485 assert_eq!(config.inactive_after_days, 90);
486 }
487
488 #[test]
489 fn test_config_with_max_patterns() {
490 let config = PatternRegistryConfig::with_max_patterns(50);
491
492 assert_eq!(config.max_patterns_per_type, 50);
493 assert_eq!(config.min_confidence_threshold, 0.3);
495 assert_eq!(config.min_frequency, 2);
496 }
497
498 #[test]
499 fn test_config_minimal() {
500 let config = PatternRegistryConfig::minimal();
501
502 assert_eq!(config.max_patterns_per_type, 50);
503 assert_eq!(config.min_confidence_threshold, 0.5);
504 assert_eq!(config.min_frequency, 3);
505 assert!(config.auto_learn);
506 assert_eq!(config.inactive_after_days, 60);
507 }
508
509 #[test]
510 fn test_config_serialization() {
511 let config = PatternRegistryConfig::with_max_patterns(75);
512 let json = serde_json::to_string(&config).unwrap();
513 let decoded: PatternRegistryConfig = serde_json::from_str(&json).unwrap();
514
515 assert_eq!(decoded.max_patterns_per_type, 75);
516 assert_eq!(decoded.min_confidence_threshold, config.min_confidence_threshold);
517 }
518
519 #[test]
524 fn test_registry_creation() {
525 let registry = PatternRegistry::new();
526 assert!(registry.is_empty());
528 assert_eq!(registry.count_by_type(PatternType::Reference), 0);
529 assert_eq!(registry.count_by_type(PatternType::Code), 0);
530 }
531
532 #[test]
533 fn test_registry_default() {
534 let registry = PatternRegistry::default();
535 assert!(registry.is_empty());
536 }
537
538 #[test]
539 fn test_registry_with_config() {
540 let config = PatternRegistryConfig::minimal();
541 let registry = PatternRegistry::with_config(config);
542
543 assert_eq!(registry.config.max_patterns_per_type, 50);
544 assert!(registry.is_empty()); }
546
547 #[test]
552 fn test_add_pattern() {
553 let mut registry = PatternRegistry::new();
554 assert!(registry.is_empty());
555
556 let pattern = ConversationPattern::new(
557 PatternType::Reference,
558 r"test-pattern-\d+",
559 PatternSource::Manual,
560 );
561 registry.add_pattern(pattern);
562
563 assert_eq!(registry.len(), 1);
564 }
565
566 #[test]
567 fn test_add_patterns_batch() {
568 let mut registry = PatternRegistry::new();
569 assert!(registry.is_empty());
570
571 let patterns = vec![
572 ConversationPattern::new(PatternType::Reference, "batch-1", PatternSource::Manual),
573 ConversationPattern::new(PatternType::Code, "batch-2", PatternSource::Manual),
574 ConversationPattern::new(PatternType::Reference, "batch-3", PatternSource::Manual),
575 ];
576
577 registry.add_patterns(patterns);
578
579 assert_eq!(registry.len(), 3);
580 }
581
582 #[test]
583 fn test_duplicate_prevention() {
584 let mut registry = PatternRegistry::new();
585 assert!(registry.is_empty());
586
587 let pattern1 = ConversationPattern::new(
588 PatternType::Reference,
589 "duplicate-test",
590 PatternSource::Manual,
591 );
592 let pattern2 = ConversationPattern::new(
593 PatternType::Reference,
594 "duplicate-test",
595 PatternSource::user_conversation("test"),
596 );
597
598 registry.add_pattern(pattern1);
599 registry.add_pattern(pattern2);
600
601 assert_eq!(registry.len(), 1);
602 }
603
604 #[test]
605 fn test_duplicate_prevention_in_batch() {
606 let mut registry = PatternRegistry::new();
607 assert!(registry.is_empty());
608
609 let patterns = vec![
610 ConversationPattern::new(PatternType::Reference, "same-pattern", PatternSource::Manual),
611 ConversationPattern::new(PatternType::Reference, "same-pattern", PatternSource::user_conversation("test")),
612 ];
613
614 registry.add_patterns(patterns);
615
616 assert_eq!(registry.len(), 1);
618 }
619
620 #[test]
621 fn test_capacity_limit_removes_lowest_frequency() {
622 let config = PatternRegistryConfig::with_max_patterns(2);
623 let mut registry = PatternRegistry::with_config(config);
624 assert!(registry.is_empty());
625
626 let mut p1 = ConversationPattern::new(PatternType::Reference, "pattern-1", PatternSource::Manual);
628 p1.frequency = 10;
629
630 let mut p2 = ConversationPattern::new(PatternType::Reference, "pattern-2", PatternSource::Manual);
631 p2.frequency = 5;
632
633 let p3 = ConversationPattern::new(PatternType::Reference, "pattern-3", PatternSource::Manual);
634
635 registry.add_pattern(p1);
636 registry.add_pattern(p2);
637 registry.add_pattern(p3); assert!(registry.get_pattern_by_pattern("pattern-1").is_some());
641 assert!(registry.get_pattern_by_pattern("pattern-3").is_some());
642 }
643
644 #[test]
649 fn test_get_active_patterns_empty() {
650 let registry = PatternRegistry::new();
651
652 let refs = registry.get_active_reference_patterns();
653 let codes = registry.get_active_code_patterns();
654
655 assert!(refs.is_empty());
656 assert!(codes.is_empty());
657 }
658
659 #[test]
660 fn test_get_active_patterns_by_type_empty() {
661 let registry = PatternRegistry::new();
662
663 let ref_patterns = registry.get_active_patterns_by_type(PatternType::Reference);
664 let code_patterns = registry.get_active_patterns_by_type(PatternType::Code);
665
666 assert!(ref_patterns.is_empty());
667 assert!(code_patterns.is_empty());
668 }
669
670 #[test]
671 fn test_get_active_patterns_with_added_patterns() {
672 let mut registry = PatternRegistry::new();
673
674 registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "ref-pattern"));
675 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "code-pattern"));
676
677 let ref_patterns = registry.get_active_patterns_by_type(PatternType::Reference);
678 let code_patterns = registry.get_active_patterns_by_type(PatternType::Code);
679
680 assert_eq!(ref_patterns.len(), 1);
681 assert_eq!(code_patterns.len(), 1);
682
683 for p in &ref_patterns {
685 assert!(p.is_active);
686 assert_eq!(p.pattern_type, PatternType::Reference);
687 }
688 for p in &code_patterns {
689 assert!(p.is_active);
690 assert_eq!(p.pattern_type, PatternType::Code);
691 }
692 }
693
694 #[test]
695 fn test_get_pattern_by_id() {
696 let mut registry = PatternRegistry::new();
697
698 let pattern = ConversationPattern::manual(PatternType::Code, "test-get-by-id");
699 let id = pattern.id.clone();
700 registry.add_pattern(pattern);
701
702 let found = registry.get_pattern(&id);
703 assert!(found.is_some());
704 assert_eq!(found.unwrap().pattern, "test-get-by-id");
705 }
706
707 #[test]
708 fn test_get_pattern_by_id_not_found() {
709 let registry = PatternRegistry::new();
710
711 let found = registry.get_pattern("nonexistent-id");
712 assert!(found.is_none());
713 }
714
715 #[test]
716 fn test_get_pattern_mut() {
717 let mut registry = PatternRegistry::new();
718
719 let pattern = ConversationPattern::manual(PatternType::Code, "test-get-mut");
720 let id = pattern.id.clone();
721 registry.add_pattern(pattern);
722
723 let found = registry.get_pattern_mut(&id);
724 assert!(found.is_some());
725 found.unwrap().frequency = 100;
726
727 let found_again = registry.get_pattern(&id).unwrap();
728 assert_eq!(found_again.frequency, 100);
729 }
730
731 #[test]
732 fn test_all_patterns_empty() {
733 let registry = PatternRegistry::new();
734 let patterns = registry.all_patterns();
735
736 assert!(patterns.is_empty());
737 assert_eq!(patterns.len(), registry.len());
738 }
739
740 #[test]
741 fn test_all_patterns_with_added() {
742 let mut registry = PatternRegistry::new();
743 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "test-pattern"));
744
745 let patterns = registry.all_patterns();
746 assert_eq!(patterns.len(), 1);
747 }
748
749 #[test]
754 fn test_deactivate_pattern() {
755 let mut registry = PatternRegistry::new();
756
757 let pattern = ConversationPattern::manual(PatternType::Code, "test-deactivate");
759 let id = pattern.id.clone();
760 registry.add_pattern(pattern);
761
762 assert!(registry.deactivate_pattern(&id));
764 let p = registry.get_pattern(&id).unwrap();
765 assert!(!p.is_active);
766 }
767
768 #[test]
769 fn test_deactivate_pattern_not_found() {
770 let mut registry = PatternRegistry::new();
771
772 assert!(!registry.deactivate_pattern("nonexistent-id"));
773 }
774
775 #[test]
776 fn test_activate_pattern() {
777 let mut registry = PatternRegistry::new();
778
779 let pattern = ConversationPattern::manual(PatternType::Code, "test-activate");
780 let id = pattern.id.clone();
781 registry.add_pattern(pattern);
782
783 registry.deactivate_pattern(&id);
785 assert!(!registry.get_pattern(&id).unwrap().is_active);
786
787 assert!(registry.activate_pattern(&id));
789 assert!(registry.get_pattern(&id).unwrap().is_active);
790 }
791
792 #[test]
793 fn test_activate_pattern_not_found() {
794 let mut registry = PatternRegistry::new();
795
796 assert!(!registry.activate_pattern("nonexistent-id"));
797 }
798
799 #[test]
800 fn test_active_count_by_type() {
801 let mut registry = PatternRegistry::new();
802 let initial_active = registry.active_count_by_type(PatternType::Code);
803
804 let pattern = ConversationPattern::manual(PatternType::Code, "test-count");
806 let id = pattern.id.clone();
807 registry.add_pattern(pattern);
808
809 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active + 1);
810
811 registry.deactivate_pattern(&id);
812 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active);
813 }
814
815 #[test]
820 fn test_remove_pattern() {
821 let mut registry = PatternRegistry::new();
822
823 let pattern = ConversationPattern::manual(PatternType::Code, "test-remove");
824 let id = pattern.id.clone();
825 registry.add_pattern(pattern);
826
827 let len_before = registry.len();
828 assert!(registry.remove_pattern(&id));
829 assert_eq!(registry.len(), len_before - 1);
830 assert!(registry.get_pattern(&id).is_none());
831 }
832
833 #[test]
834 fn test_remove_pattern_not_found() {
835 let mut registry = PatternRegistry::new();
836
837 assert!(!registry.remove_pattern("nonexistent-id"));
838 }
839
840 #[test]
845 fn test_learn_patterns() {
846 let mut registry = PatternRegistry::new();
847 let initial_ref_count = registry.count_by_type(PatternType::Reference);
848
849 let new_pattern =
851 ConversationPattern::new(PatternType::Reference, "LEARN-123", PatternSource::Manual);
852 registry.learn_patterns(&[new_pattern]);
853
854 assert_eq!(
855 registry.count_by_type(PatternType::Reference),
856 initial_ref_count + 1
857 );
858
859 let same_pattern =
861 ConversationPattern::new(PatternType::Reference, "LEARN-123", PatternSource::Manual);
862 registry.learn_patterns(&[same_pattern]);
863
864 let p = registry.patterns.iter().find(|p| p.pattern == "LEARN-123").unwrap();
865 assert_eq!(p.frequency, 2); }
867
868 #[test]
869 fn test_learn_patterns_multiple_new() {
870 let mut registry = PatternRegistry::new();
871 let initial_count = registry.len();
872
873 let patterns = vec![
874 ConversationPattern::new(PatternType::Reference, "learn-1", PatternSource::Manual),
875 ConversationPattern::new(PatternType::Code, "learn-2", PatternSource::Manual),
876 ];
877
878 registry.learn_patterns(&patterns);
879
880 assert_eq!(registry.len(), initial_count + 2);
881 }
882
883 #[test]
884 fn test_learn_patterns_empty() {
885 let mut registry = PatternRegistry::new();
886 let initial_count = registry.len();
887
888 registry.learn_patterns(&[]);
889
890 assert_eq!(registry.len(), initial_count);
891 }
892
893 #[test]
898 fn test_stats_empty() {
899 let registry = PatternRegistry::new();
900 let stats = registry.stats();
901
902 assert_eq!(stats.total, 0);
903 assert_eq!(stats.presets, 0);
904 assert_eq!(stats.active, 0);
905 assert_eq!(stats.inactive, 0);
906 assert_eq!(stats.reference_count, 0);
907 assert_eq!(stats.code_count, 0);
908 }
909
910 #[test]
911 fn test_stats_with_manual_patterns() {
912 let mut registry = PatternRegistry::new();
913
914 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "manual-1"));
915 registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "manual-2"));
916
917 let stats = registry.stats();
918 assert_eq!(stats.total, 2);
919 assert_eq!(stats.manual, 2);
920 assert_eq!(stats.presets, 0);
921 }
922
923 #[test]
924 fn test_stats_display() {
925 let registry = PatternRegistry::new();
926 let stats = registry.stats();
927 let display = format!("{}", stats);
928
929 assert!(display.contains("Pattern Registry Stats"));
930 assert!(display.contains("Total:"));
931 assert!(display.contains("active:"));
932 assert!(display.contains("Reference:"));
933 assert!(display.contains("Code:"));
934 }
935
936 #[test]
941 fn test_serialization() {
942 let registry = PatternRegistry::new();
943 let json = serde_json::to_string(®istry).unwrap();
944 let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
945
946 assert_eq!(decoded.len(), registry.len());
947 assert_eq!(
948 decoded.count_by_type(PatternType::Reference),
949 registry.count_by_type(PatternType::Reference)
950 );
951 }
952
953 #[test]
954 fn test_serialization_preserves_patterns() {
955 let mut registry = PatternRegistry::new();
956
957 let pattern = ConversationPattern::manual(PatternType::Code, "serialize-test")
958 .with_description("Test serialization")
959 .with_tag("test");
960 let pattern_id = pattern.id.clone();
961 registry.add_pattern(pattern);
962
963 let json = serde_json::to_string(®istry).unwrap();
964 let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
965
966 let found = decoded.get_pattern(&pattern_id).unwrap();
967 assert_eq!(found.pattern, "serialize-test");
968 assert_eq!(found.description, Some("Test serialization".to_string()));
969 assert_eq!(found.tags, vec!["test"]);
970 }
971
972 #[test]
973 fn test_serialization_config_preserved() {
974 let config = PatternRegistryConfig::minimal();
975 let registry = PatternRegistry::with_config(config);
976
977 let json = serde_json::to_string(®istry).unwrap();
978 let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
979
980 assert_eq!(decoded.config.max_patterns_per_type, 50);
981 assert_eq!(decoded.config.min_confidence_threshold, 0.5);
982 }
983
984 #[test]
989 fn test_prune_keeps_active_patterns() {
990 let mut registry = PatternRegistry::new();
991
992 let pattern = ConversationPattern::manual(PatternType::Code, "active-pattern");
993 registry.add_pattern(pattern);
994
995 let len_before = registry.len();
996 registry.prune();
997 let len_after = registry.len();
998
999 assert_eq!(len_after, len_before);
1001 }
1002
1003 #[test]
1004 fn test_prune_keeps_manual_patterns() {
1005 let mut registry = PatternRegistry::new();
1006
1007 let pattern = ConversationPattern::manual(PatternType::Code, "manual-pattern");
1008 registry.add_pattern(pattern);
1009
1010 let manual_count_before = registry.patterns.iter().filter(|p| p.source.is_manual()).count();
1011 registry.prune();
1012 let manual_count_after = registry.patterns.iter().filter(|p| p.source.is_manual()).count();
1013
1014 assert_eq!(manual_count_after, manual_count_before);
1015 }
1016
1017 #[test]
1022 fn test_len_and_is_empty() {
1023 let registry = PatternRegistry::new();
1024 assert!(registry.is_empty());
1025 assert_eq!(registry.len(), 0);
1026 }
1027
1028 #[test]
1029 fn test_count_by_type_empty() {
1030 let registry = PatternRegistry::new();
1031
1032 let ref_count = registry.count_by_type(PatternType::Reference);
1033 let code_count = registry.count_by_type(PatternType::Code);
1034
1035 assert_eq!(ref_count, 0);
1036 assert_eq!(code_count, 0);
1037 assert_eq!(ref_count + code_count, registry.len());
1038 }
1039
1040 #[test]
1041 fn test_count_by_type_with_patterns() {
1042 let mut registry = PatternRegistry::new();
1043 registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "ref-1"));
1044 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "code-1"));
1045
1046 assert_eq!(registry.count_by_type(PatternType::Reference), 1);
1047 assert_eq!(registry.count_by_type(PatternType::Code), 1);
1048 }
1049
1050 #[test]
1055 fn test_config_custom() {
1056 let config = PatternRegistryConfig::with_max_patterns(50);
1057 let registry = PatternRegistry::with_config(config);
1058
1059 assert_eq!(registry.config.max_patterns_per_type, 50);
1060 }
1061
1062 #[test]
1063 fn test_add_pattern_same_pattern_different_types() {
1064 let mut registry = PatternRegistry::new();
1065 assert!(registry.is_empty());
1066
1067 let p1 = ConversationPattern::new(PatternType::Reference, "same-text", PatternSource::Manual);
1071 let p2 = ConversationPattern::new(PatternType::Code, "same-text", PatternSource::Manual);
1072
1073 registry.add_pattern(p1);
1074 registry.add_pattern(p2);
1075
1076 assert_eq!(registry.len(), 1);
1078 assert!(registry.all_patterns().iter().any(|p| p.pattern == "same-text" && p.pattern_type == PatternType::Reference));
1080 }
1081
1082 #[test]
1083 fn test_get_active_patterns_includes_only_active() {
1084 let mut registry = PatternRegistry::new();
1085
1086 let active_pattern = ConversationPattern::manual(PatternType::Code, "active-test");
1087 let active_id = active_pattern.id.clone();
1088
1089 let mut inactive_pattern = ConversationPattern::manual(PatternType::Code, "inactive-test");
1090 inactive_pattern.deactivate();
1091 let inactive_id = inactive_pattern.id.clone();
1092
1093 registry.add_pattern(active_pattern);
1094 registry.add_pattern(inactive_pattern);
1095
1096 let active_patterns = registry.get_active_patterns_by_type(PatternType::Code);
1097
1098 assert!(active_patterns.iter().any(|p| p.id == active_id));
1099 assert!(!active_patterns.iter().any(|p| p.id == inactive_id));
1100 }
1101
1102 #[test]
1103 fn test_type_index_rebuild_on_remove() {
1104 let mut registry = PatternRegistry::new();
1105
1106 let pattern = ConversationPattern::manual(PatternType::Code, "test-index");
1107 let id = pattern.id.clone();
1108 registry.add_pattern(pattern);
1109
1110 assert!(registry.get_pattern(&id).is_some());
1112
1113 registry.remove_pattern(&id);
1115 assert!(registry.get_pattern(&id).is_none());
1116
1117 let active = registry.get_active_patterns_by_type(PatternType::Code);
1119 assert!(!active.iter().any(|p| p.id == id));
1120 }
1121
1122 #[test]
1123 fn test_deactivate_then_activate_affects_active_count() {
1124 let mut registry = PatternRegistry::new();
1125
1126 let pattern = ConversationPattern::manual(PatternType::Code, "toggle-test");
1127 let id = pattern.id.clone();
1128 registry.add_pattern(pattern);
1129
1130 let initial_active = registry.active_count_by_type(PatternType::Code);
1131
1132 registry.deactivate_pattern(&id);
1133 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active - 1);
1134
1135 registry.activate_pattern(&id);
1136 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active);
1137 }
1138
1139 #[test]
1140 fn test_pattern_order_preserved_after_multiple_operations() {
1141 let mut registry = PatternRegistry::new();
1142
1143 let p1 = ConversationPattern::manual(PatternType::Reference, "order-1");
1144 let p2 = ConversationPattern::manual(PatternType::Code, "order-2");
1145 let p3 = ConversationPattern::manual(PatternType::Reference, "order-3");
1146
1147 registry.add_patterns(vec![p1, p2, p3]);
1148
1149 assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-1"));
1151 assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-2"));
1152 assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-3"));
1153 }
1154
1155 #[test]
1160 fn test_get_patterns_file_path() {
1161 let path = PatternRegistry::get_patterns_file_path();
1162 assert!(path.is_ok());
1163
1164 let path = path.unwrap();
1165 assert!(path.to_string_lossy().contains(".matrix"));
1167 assert!(path.to_string_lossy().contains("patterns.json"));
1168 }
1169
1170 #[test]
1171 fn test_from_file_nonexistent() {
1172 let temp_dir = tempfile::tempdir().unwrap();
1173 let nonexistent_path = temp_dir.path().join("nonexistent_patterns.json");
1174
1175 let registry = PatternRegistry::from_file(&nonexistent_path).unwrap();
1176
1177 assert!(registry.is_empty());
1179 assert_eq!(registry.count_by_type(PatternType::Reference), 0);
1180 assert_eq!(registry.count_by_type(PatternType::Code), 0);
1181 }
1182
1183 #[test]
1184 fn test_from_file_empty_file() {
1185 let temp_dir = tempfile::tempdir().unwrap();
1186 let empty_path = temp_dir.path().join("empty_patterns.json");
1187
1188 fs::write(&empty_path, "").unwrap();
1190
1191 let registry = PatternRegistry::from_file(&empty_path).unwrap();
1192
1193 assert!(registry.is_empty());
1195 }
1196
1197 #[test]
1198 fn test_from_file_valid_json() {
1199 let temp_dir = tempfile::tempdir().unwrap();
1200 let file_path = temp_dir.path().join("valid_patterns.json");
1201
1202 let mut original = PatternRegistry::new();
1204 original.add_pattern(
1205 ConversationPattern::manual(PatternType::Code, "custom-pattern")
1206 .with_description("Custom test pattern"),
1207 );
1208 original.save_to_file(&file_path).unwrap();
1209
1210 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1212
1213 assert!(loaded.patterns.iter().any(|p| p.pattern == "custom-pattern"));
1215 assert!(loaded.patterns.iter().any(|p| p.description == Some("Custom test pattern".to_string())));
1216 }
1217
1218 #[test]
1219 fn test_from_file_malformed_json() {
1220 let temp_dir = tempfile::tempdir().unwrap();
1221 let malformed_path = temp_dir.path().join("malformed_patterns.json");
1222
1223 fs::write(&malformed_path, "{ not valid json }").unwrap();
1225
1226 let registry = PatternRegistry::from_file(&malformed_path).unwrap();
1228
1229 assert!(registry.is_empty());
1230 }
1231
1232 #[test]
1233 fn test_save_to_file_creates_directory() {
1234 let temp_dir = tempfile::tempdir().unwrap();
1235 let nested_path = temp_dir.path().join("nested").join("dir").join("patterns.json");
1236
1237 assert!(!nested_path.parent().unwrap().exists());
1239
1240 let registry = PatternRegistry::new();
1241 registry.save_to_file(&nested_path).unwrap();
1242
1243 assert!(nested_path.parent().unwrap().exists());
1245 assert!(nested_path.exists());
1246 }
1247
1248 #[test]
1249 fn test_save_to_file_roundtrip() {
1250 let temp_dir = tempfile::tempdir().unwrap();
1251 let file_path = temp_dir.path().join("roundtrip_patterns.json");
1252
1253 let mut original = PatternRegistry::new();
1255 let p1 = ConversationPattern::manual(PatternType::Reference, "roundtrip-ref")
1256 .with_description("Reference pattern for roundtrip test")
1257 .with_tag("test");
1258 let p1_id = p1.id.clone();
1259 original.add_pattern(p1);
1260
1261 let p2 = ConversationPattern::manual(PatternType::Code, "roundtrip-code")
1262 .with_description("Code pattern for roundtrip test");
1263 let p2_id = p2.id.clone();
1264 original.add_pattern(p2);
1265
1266 original.save_to_file(&file_path).unwrap();
1268
1269 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1271
1272 let loaded_p1 = loaded.get_pattern(&p1_id).unwrap();
1274 assert_eq!(loaded_p1.pattern, "roundtrip-ref");
1275 assert_eq!(loaded_p1.pattern_type, PatternType::Reference);
1276 assert_eq!(loaded_p1.description, Some("Reference pattern for roundtrip test".to_string()));
1277 assert_eq!(loaded_p1.tags, vec!["test"]);
1278
1279 let loaded_p2 = loaded.get_pattern(&p2_id).unwrap();
1280 assert_eq!(loaded_p2.pattern, "roundtrip-code");
1281 assert_eq!(loaded_p2.pattern_type, PatternType::Code);
1282 }
1283
1284 #[test]
1285 fn test_save_to_file_preserves_config() {
1286 let temp_dir = tempfile::tempdir().unwrap();
1287 let file_path = temp_dir.path().join("config_patterns.json");
1288
1289 let config = PatternRegistryConfig::minimal();
1291 let original = PatternRegistry::with_config(config);
1292
1293 original.save_to_file(&file_path).unwrap();
1294
1295 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1296
1297 assert_eq!(loaded.config.max_patterns_per_type, 50);
1298 assert_eq!(loaded.config.min_confidence_threshold, 0.5);
1299 assert_eq!(loaded.config.min_frequency, 3);
1300 }
1301
1302 #[test]
1303 fn test_from_default_file() {
1304 let result = PatternRegistry::from_default_file();
1307 assert!(result.is_ok());
1308 }
1309
1310 #[test]
1311 fn test_file_storage_integration() {
1312 let temp_dir = tempfile::tempdir().unwrap();
1313 let file_path = temp_dir.path().join("integration_patterns.json");
1314
1315 let mut registry = PatternRegistry::from_file(&file_path).unwrap();
1317 assert!(registry.is_empty());
1318
1319 let custom = ConversationPattern::manual(PatternType::Code, "integration-test")
1321 .with_description("Integration test pattern")
1322 .with_tag("integration");
1323 registry.add_pattern(custom);
1324
1325 registry.save_to_file(&file_path).unwrap();
1327
1328 let reloaded = PatternRegistry::from_file(&file_path).unwrap();
1330
1331 assert!(reloaded.patterns.iter().any(|p| p.pattern == "integration-test"));
1333 assert!(reloaded.patterns.iter().any(|p| p.tags.contains(&"integration".to_string())));
1334 }
1335
1336 #[test]
1341 fn test_large_patterns_save_and_load() {
1342 let temp_dir = tempfile::tempdir().unwrap();
1343 let file_path = temp_dir.path().join("large_patterns.json");
1344
1345 let mut registry = PatternRegistry::new();
1347
1348 for i in 0..50 {
1350 let ref_pattern = ConversationPattern::manual(PatternType::Reference, &format!("large-ref-{}", i))
1351 .with_description(&format!("Reference pattern {}", i))
1352 .with_tag(&format!("tag{}", i % 5));
1353 registry.add_pattern(ref_pattern);
1354
1355 let code_pattern = ConversationPattern::manual(PatternType::Code, &format!("large-code-{}", i))
1356 .with_description(&format!("Code pattern {}", i))
1357 .with_tag(&format!("codetag{}", i % 3));
1358 registry.add_pattern(code_pattern);
1359 }
1360
1361 let expected_count = 100;
1362 assert_eq!(registry.len(), expected_count);
1363
1364 registry.save_to_file(&file_path).unwrap();
1366
1367 let metadata = fs::metadata(&file_path).unwrap();
1369 assert!(metadata.len() > 1000); let loaded = PatternRegistry::from_file(&file_path).unwrap();
1373
1374 assert_eq!(loaded.len(), expected_count);
1376
1377 for i in 0..50 {
1379 assert!(
1380 loaded.patterns.iter().any(|p| p.pattern == format!("large-ref-{}", i)),
1381 "Missing large-ref-{}",
1382 i
1383 );
1384 assert!(
1385 loaded.patterns.iter().any(|p| p.pattern == format!("large-code-{}", i)),
1386 "Missing large-code-{}",
1387 i
1388 );
1389 }
1390 }
1391
1392 #[test]
1393 fn test_special_characters_in_patterns() {
1394 let temp_dir = tempfile::tempdir().unwrap();
1395 let file_path = temp_dir.path().join("special_chars_patterns.json");
1396
1397 let special_patterns = vec![
1399 ("unicode-你好", "Chinese characters"),
1401 ("unicode-日本語", "Japanese characters"),
1402 ("unicode-한국어", "Korean characters"),
1403 ("unicode-مرحبا", "Arabic characters"),
1404 ("unicode-🎉🔥", "Emoji"),
1405 ("regex-\\d+\\.\\w*", "Regex pattern with escapes"),
1407 ("regex-[a-z]+", "Regex character class"),
1408 ("regex-(foo|bar)", "Regex alternation"),
1409 ("json-\"quotes\"", "Contains quotes"),
1411 ("json-\\n\\t", "Escape sequences"),
1412 ("json-日本語\"test\"", "Mixed special chars"),
1413 ("whitespace-tab\ttab", "Contains tab"),
1415 ("whitespace-newline\nline", "Contains newline"),
1416 ];
1417
1418 let mut registry = PatternRegistry::new();
1419 let mut added_ids = Vec::new();
1420
1421 for (pattern_str, desc) in &special_patterns {
1422 let pattern = ConversationPattern::manual(PatternType::Code, *pattern_str)
1423 .with_description(*desc);
1424 let id = pattern.id.clone();
1425 registry.add_pattern(pattern);
1426 added_ids.push((id, *pattern_str, *desc));
1427 }
1428
1429 registry.save_to_file(&file_path).unwrap();
1431
1432 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1434
1435 for (id, pattern_str, desc) in &added_ids {
1437 let found = loaded.get_pattern(id);
1438 assert!(found.is_some(), "Pattern {} not found after reload", pattern_str);
1439 let p = found.unwrap();
1440 assert_eq!(p.pattern, *pattern_str, "Pattern mismatch for {}", pattern_str);
1441 assert_eq!(p.description, Some(desc.to_string()), "Description mismatch for {}", pattern_str);
1442 }
1443 }
1444
1445 #[test]
1446 fn test_empty_registry_save_and_load() {
1447 let temp_dir = tempfile::tempdir().unwrap();
1448 let file_path = temp_dir.path().join("empty_registry.json");
1449
1450 let registry = PatternRegistry::new();
1452 assert!(registry.is_empty());
1453
1454 registry.save_to_file(&file_path).unwrap();
1456
1457 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1459
1460 assert!(loaded.is_empty());
1462 }
1463
1464 #[test]
1465 fn test_whitespace_only_file() {
1466 let temp_dir = tempfile::tempdir().unwrap();
1467 let file_path = temp_dir.path().join("whitespace_patterns.json");
1468
1469 fs::write(&file_path, " \n\t \n ").unwrap();
1471
1472 let registry = PatternRegistry::from_file(&file_path).unwrap();
1473
1474 assert!(registry.is_empty());
1476 }
1477
1478 #[test]
1479 fn test_save_to_default_file_path() {
1480 let temp_dir = tempfile::tempdir().unwrap();
1482 let custom_path = temp_dir.path().join(".matrix").join("patterns.json");
1483
1484 let mut registry = PatternRegistry::new();
1486 let pattern = ConversationPattern::manual(PatternType::Code, "default-file-test")
1487 .with_description("Test save_to_file with custom path");
1488 registry.add_pattern(pattern);
1489
1490 registry.save_to_file(&custom_path).unwrap();
1491
1492 assert!(custom_path.exists());
1494
1495 let loaded = PatternRegistry::from_file(&custom_path).unwrap();
1497 assert!(loaded.patterns.iter().any(|p| p.pattern == "default-file-test"));
1498 }
1499
1500 #[test]
1501 fn test_atomic_write_rollback_safety() {
1502 let temp_dir = tempfile::tempdir().unwrap();
1503 let file_path = temp_dir.path().join("atomic_test.json");
1504
1505 let mut registry = PatternRegistry::new();
1507 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "initial-pattern"));
1508 registry.save_to_file(&file_path).unwrap();
1509
1510 let tmp_path = file_path.with_extension("json.tmp");
1512 assert!(!tmp_path.exists(), "Temp file should not exist after save");
1513
1514 assert!(file_path.exists());
1516
1517 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1519 assert!(loaded.patterns.iter().any(|p| p.pattern == "initial-pattern"));
1520 }
1521
1522 #[test]
1523 fn test_long_description_and_tags() {
1524 let temp_dir = tempfile::tempdir().unwrap();
1525 let file_path = temp_dir.path().join("long_content.json");
1526
1527 let long_desc = "x".repeat(10000);
1529 let many_tags: Vec<String> = (0..100).map(|i| format!("tag-{}", i)).collect();
1530
1531 let mut registry = PatternRegistry::new();
1532 let mut pattern = ConversationPattern::manual(PatternType::Code, "long-pattern");
1533 pattern.description = Some(long_desc.clone());
1534 pattern.tags = many_tags.clone();
1535
1536 registry.add_pattern(pattern);
1537
1538 registry.save_to_file(&file_path).unwrap();
1540
1541 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1543
1544 let found = loaded.patterns.iter().find(|p| p.pattern == "long-pattern").unwrap();
1546 assert_eq!(found.description, Some(long_desc));
1547 assert_eq!(found.tags.len(), 100);
1548 for i in 0..100 {
1549 assert!(found.tags.contains(&format!("tag-{}", i)));
1550 }
1551 }
1552}
1553
1554impl PatternRegistry {
1556 fn get_pattern_by_pattern(&self, pattern_str: &str) -> Option<&ConversationPattern> {
1557 self.patterns.iter().find(|p| p.pattern == pattern_str)
1558 }
1559}