1use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use anyhow::Result;
12use chrono::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_by_pattern(&self, pattern: &str) -> Option<&ConversationPattern> {
231 self.patterns.iter().find(|p| p.pattern == pattern)
232 }
233
234 pub fn get_pattern_mut(&mut self, id: &str) -> Option<&mut ConversationPattern> {
236 self.patterns.iter_mut().find(|p| p.id == id)
237 }
238
239 pub fn deactivate_pattern(&mut self, id: &str) -> bool {
241 if let Some(pattern) = self.get_pattern_mut(id) {
242 pattern.deactivate();
243 true
244 } else {
245 false
246 }
247 }
248
249 pub fn activate_pattern(&mut self, id: &str) -> bool {
251 if let Some(pattern) = self.get_pattern_mut(id) {
252 pattern.activate();
253 true
254 } else {
255 false
256 }
257 }
258
259 pub fn remove_pattern(&mut self, id: &str) -> bool {
261 let len_before = self.patterns.len();
262 self.patterns.retain(|p| p.id != id);
263 if self.patterns.len() < len_before {
264 self.rebuild_index();
265 true
266 } else {
267 false
268 }
269 }
270
271 pub fn len(&self) -> usize {
273 self.patterns.len()
274 }
275
276 pub fn is_empty(&self) -> bool {
278 self.patterns.is_empty()
279 }
280
281 pub fn count_by_type(&self, pattern_type: PatternType) -> usize {
283 self.patterns
284 .iter()
285 .filter(|p| p.pattern_type == pattern_type)
286 .count()
287 }
288
289 pub fn active_count_by_type(&self, pattern_type: PatternType) -> usize {
291 self.get_active_patterns_by_type(pattern_type).len()
292 }
293
294 pub fn all_patterns(&self) -> &[ConversationPattern] {
296 &self.patterns
297 }
298
299 pub fn prune(&mut self) {
301 let now = Utc::now();
302 let threshold_days = self.config.inactive_after_days;
303
304 self.patterns.retain(|p| {
305 if p.is_active {
307 return true;
308 }
309 if p.source.is_preset() {
311 return true;
312 }
313 if p.source.is_manual() {
315 return true;
316 }
317 if p.frequency >= self.config.min_frequency
319 && p.confidence >= self.config.min_confidence_threshold
320 {
321 return true;
322 }
323 let age = (now - p.last_used).num_days();
325 age < threshold_days
326 });
327
328 self.rebuild_index();
329 }
330
331 pub fn stats(&self) -> PatternRegistryStats {
333 let total = self.patterns.len();
334 let active = self.patterns.iter().filter(|p| p.is_active).count();
335 let reference_count = self.count_by_type(PatternType::Reference);
336 let code_count = self.count_by_type(PatternType::Code);
337 let presets = self.patterns.iter().filter(|p| p.source.is_preset()).count();
338 let manual = self.patterns.iter().filter(|p| p.source.is_manual()).count();
339 let learned = total - presets - manual;
340
341 PatternRegistryStats {
342 total,
343 active,
344 inactive: total - active,
345 reference_count,
346 code_count,
347 presets,
348 manual,
349 learned,
350 }
351 }
352
353 pub fn get_patterns_file_path() -> Result<PathBuf> {
359 let home = std::env::var_os("HOME")
360 .or_else(|| std::env::var_os("USERPROFILE"))
361 .ok_or_else(|| anyhow::anyhow!("HOME or USERPROFILE not set"))?;
362 Ok(PathBuf::from(home).join(MATRIX_DIR).join("patterns.json"))
363 }
364
365 pub fn from_file(path: &Path) -> Result<Self> {
370 if !path.exists() {
371 return Ok(Self::new());
373 }
374
375 let data = fs::read_to_string(path)?;
376
377 if data.trim().is_empty() {
379 return Ok(Self::new());
380 }
381
382 match serde_json::from_str::<PatternRegistry>(&data) {
384 Ok(mut registry) => {
385 registry.rebuild_index();
386 Ok(registry)
387 }
388 Err(e) => {
389 tracing::warn!(
391 "Failed to parse patterns file {:?}: {}. Using empty registry.",
392 path,
393 e
394 );
395 Ok(Self::new())
396 }
397 }
398 }
399
400 pub fn from_default_file() -> Result<Self> {
402 let path = Self::get_patterns_file_path()?;
403 Self::from_file(&path)
404 }
405
406 pub fn save_to_file(&self, path: &Path) -> Result<()> {
411 let parent = path.parent();
413 if let Some(dir) = parent {
414 if !dir.exists() {
415 fs::create_dir_all(dir)?;
416 }
417 }
418
419 let json = serde_json::to_string_pretty(self)?;
421
422 let tmp_path = path.with_extension("json.tmp");
424 fs::write(&tmp_path, json)?;
425
426 fs::rename(&tmp_path, path)?;
428
429 Ok(())
430 }
431
432 pub fn save_to_default_file(&self) -> Result<()> {
434 let path = Self::get_patterns_file_path()?;
435 self.save_to_file(&path)
436 }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct PatternRegistryStats {
446 pub total: usize,
448 pub active: usize,
450 pub inactive: usize,
452 pub reference_count: usize,
454 pub code_count: usize,
456 pub presets: usize,
458 pub manual: usize,
460 pub learned: usize,
462}
463
464impl std::fmt::Display for PatternRegistryStats {
465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466 writeln!(f, "Pattern Registry Stats:")?;
467 writeln!(f, " Total: {} (active: {}, inactive: {})", self.total, self.active, self.inactive)?;
468 writeln!(f, " Reference: {}, Code: {}", self.reference_count, self.code_count)?;
469 writeln!(f, " Presets: {}, Manual: {}, Learned: {}", self.presets, self.manual, self.learned)
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use crate::memory::conversation_pattern::PatternSource;
477
478 #[test]
483 fn test_config_default() {
484 let config = PatternRegistryConfig::default();
485
486 assert_eq!(config.max_patterns_per_type, 100);
487 assert_eq!(config.min_confidence_threshold, 0.3);
488 assert_eq!(config.min_frequency, 2);
489 assert!(config.auto_learn);
490 assert_eq!(config.inactive_after_days, 90);
491 }
492
493 #[test]
494 fn test_config_with_max_patterns() {
495 let config = PatternRegistryConfig::with_max_patterns(50);
496
497 assert_eq!(config.max_patterns_per_type, 50);
498 assert_eq!(config.min_confidence_threshold, 0.3);
500 assert_eq!(config.min_frequency, 2);
501 }
502
503 #[test]
504 fn test_config_minimal() {
505 let config = PatternRegistryConfig::minimal();
506
507 assert_eq!(config.max_patterns_per_type, 50);
508 assert_eq!(config.min_confidence_threshold, 0.5);
509 assert_eq!(config.min_frequency, 3);
510 assert!(config.auto_learn);
511 assert_eq!(config.inactive_after_days, 60);
512 }
513
514 #[test]
515 fn test_config_serialization() {
516 let config = PatternRegistryConfig::with_max_patterns(75);
517 let json = serde_json::to_string(&config).unwrap();
518 let decoded: PatternRegistryConfig = serde_json::from_str(&json).unwrap();
519
520 assert_eq!(decoded.max_patterns_per_type, 75);
521 assert_eq!(decoded.min_confidence_threshold, config.min_confidence_threshold);
522 }
523
524 #[test]
529 fn test_registry_creation() {
530 let registry = PatternRegistry::new();
531 assert!(registry.is_empty());
533 assert_eq!(registry.count_by_type(PatternType::Reference), 0);
534 assert_eq!(registry.count_by_type(PatternType::Code), 0);
535 }
536
537 #[test]
538 fn test_registry_default() {
539 let registry = PatternRegistry::default();
540 assert!(registry.is_empty());
541 }
542
543 #[test]
544 fn test_registry_with_config() {
545 let config = PatternRegistryConfig::minimal();
546 let registry = PatternRegistry::with_config(config);
547
548 assert_eq!(registry.config.max_patterns_per_type, 50);
549 assert!(registry.is_empty()); }
551
552 #[test]
557 fn test_add_pattern() {
558 let mut registry = PatternRegistry::new();
559 assert!(registry.is_empty());
560
561 let pattern = ConversationPattern::new(
562 PatternType::Reference,
563 r"test-pattern-\d+",
564 PatternSource::Manual,
565 );
566 registry.add_pattern(pattern);
567
568 assert_eq!(registry.len(), 1);
569 }
570
571 #[test]
572 fn test_add_patterns_batch() {
573 let mut registry = PatternRegistry::new();
574 assert!(registry.is_empty());
575
576 let patterns = vec![
577 ConversationPattern::new(PatternType::Reference, "batch-1", PatternSource::Manual),
578 ConversationPattern::new(PatternType::Code, "batch-2", PatternSource::Manual),
579 ConversationPattern::new(PatternType::Reference, "batch-3", PatternSource::Manual),
580 ];
581
582 registry.add_patterns(patterns);
583
584 assert_eq!(registry.len(), 3);
585 }
586
587 #[test]
588 fn test_duplicate_prevention() {
589 let mut registry = PatternRegistry::new();
590 assert!(registry.is_empty());
591
592 let pattern1 = ConversationPattern::new(
593 PatternType::Reference,
594 "duplicate-test",
595 PatternSource::Manual,
596 );
597 let pattern2 = ConversationPattern::new(
598 PatternType::Reference,
599 "duplicate-test",
600 PatternSource::user_conversation("test"),
601 );
602
603 registry.add_pattern(pattern1);
604 registry.add_pattern(pattern2);
605
606 assert_eq!(registry.len(), 1);
607 }
608
609 #[test]
610 fn test_duplicate_prevention_in_batch() {
611 let mut registry = PatternRegistry::new();
612 assert!(registry.is_empty());
613
614 let patterns = vec![
615 ConversationPattern::new(PatternType::Reference, "same-pattern", PatternSource::Manual),
616 ConversationPattern::new(PatternType::Reference, "same-pattern", PatternSource::user_conversation("test")),
617 ];
618
619 registry.add_patterns(patterns);
620
621 assert_eq!(registry.len(), 1);
623 }
624
625 #[test]
626 fn test_capacity_limit_removes_lowest_frequency() {
627 let config = PatternRegistryConfig::with_max_patterns(2);
628 let mut registry = PatternRegistry::with_config(config);
629 assert!(registry.is_empty());
630
631 let mut p1 = ConversationPattern::new(PatternType::Reference, "pattern-1", PatternSource::Manual);
633 p1.frequency = 10;
634
635 let mut p2 = ConversationPattern::new(PatternType::Reference, "pattern-2", PatternSource::Manual);
636 p2.frequency = 5;
637
638 let p3 = ConversationPattern::new(PatternType::Reference, "pattern-3", PatternSource::Manual);
639
640 registry.add_pattern(p1);
641 registry.add_pattern(p2);
642 registry.add_pattern(p3); assert!(registry.get_pattern_by_pattern("pattern-1").is_some());
646 assert!(registry.get_pattern_by_pattern("pattern-3").is_some());
647 }
648
649 #[test]
654 fn test_get_active_patterns_empty() {
655 let registry = PatternRegistry::new();
656
657 let refs = registry.get_active_reference_patterns();
658 let codes = registry.get_active_code_patterns();
659
660 assert!(refs.is_empty());
661 assert!(codes.is_empty());
662 }
663
664 #[test]
665 fn test_get_active_patterns_by_type_empty() {
666 let registry = PatternRegistry::new();
667
668 let ref_patterns = registry.get_active_patterns_by_type(PatternType::Reference);
669 let code_patterns = registry.get_active_patterns_by_type(PatternType::Code);
670
671 assert!(ref_patterns.is_empty());
672 assert!(code_patterns.is_empty());
673 }
674
675 #[test]
676 fn test_get_active_patterns_with_added_patterns() {
677 let mut registry = PatternRegistry::new();
678
679 registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "ref-pattern"));
680 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "code-pattern"));
681
682 let ref_patterns = registry.get_active_patterns_by_type(PatternType::Reference);
683 let code_patterns = registry.get_active_patterns_by_type(PatternType::Code);
684
685 assert_eq!(ref_patterns.len(), 1);
686 assert_eq!(code_patterns.len(), 1);
687
688 for p in &ref_patterns {
690 assert!(p.is_active);
691 assert_eq!(p.pattern_type, PatternType::Reference);
692 }
693 for p in &code_patterns {
694 assert!(p.is_active);
695 assert_eq!(p.pattern_type, PatternType::Code);
696 }
697 }
698
699 #[test]
700 fn test_get_pattern_by_id() {
701 let mut registry = PatternRegistry::new();
702
703 let pattern = ConversationPattern::manual(PatternType::Code, "test-get-by-id");
704 let id = pattern.id.clone();
705 registry.add_pattern(pattern);
706
707 let found = registry.get_pattern(&id);
708 assert!(found.is_some());
709 assert_eq!(found.unwrap().pattern, "test-get-by-id");
710 }
711
712 #[test]
713 fn test_get_pattern_by_id_not_found() {
714 let registry = PatternRegistry::new();
715
716 let found = registry.get_pattern("nonexistent-id");
717 assert!(found.is_none());
718 }
719
720 #[test]
721 fn test_get_pattern_mut() {
722 let mut registry = PatternRegistry::new();
723
724 let pattern = ConversationPattern::manual(PatternType::Code, "test-get-mut");
725 let id = pattern.id.clone();
726 registry.add_pattern(pattern);
727
728 let found = registry.get_pattern_mut(&id);
729 assert!(found.is_some());
730 found.unwrap().frequency = 100;
731
732 let found_again = registry.get_pattern(&id).unwrap();
733 assert_eq!(found_again.frequency, 100);
734 }
735
736 #[test]
737 fn test_all_patterns_empty() {
738 let registry = PatternRegistry::new();
739 let patterns = registry.all_patterns();
740
741 assert!(patterns.is_empty());
742 assert_eq!(patterns.len(), registry.len());
743 }
744
745 #[test]
746 fn test_all_patterns_with_added() {
747 let mut registry = PatternRegistry::new();
748 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "test-pattern"));
749
750 let patterns = registry.all_patterns();
751 assert_eq!(patterns.len(), 1);
752 }
753
754 #[test]
759 fn test_deactivate_pattern() {
760 let mut registry = PatternRegistry::new();
761
762 let pattern = ConversationPattern::manual(PatternType::Code, "test-deactivate");
764 let id = pattern.id.clone();
765 registry.add_pattern(pattern);
766
767 assert!(registry.deactivate_pattern(&id));
769 let p = registry.get_pattern(&id).unwrap();
770 assert!(!p.is_active);
771 }
772
773 #[test]
774 fn test_deactivate_pattern_not_found() {
775 let mut registry = PatternRegistry::new();
776
777 assert!(!registry.deactivate_pattern("nonexistent-id"));
778 }
779
780 #[test]
781 fn test_activate_pattern() {
782 let mut registry = PatternRegistry::new();
783
784 let pattern = ConversationPattern::manual(PatternType::Code, "test-activate");
785 let id = pattern.id.clone();
786 registry.add_pattern(pattern);
787
788 registry.deactivate_pattern(&id);
790 assert!(!registry.get_pattern(&id).unwrap().is_active);
791
792 assert!(registry.activate_pattern(&id));
794 assert!(registry.get_pattern(&id).unwrap().is_active);
795 }
796
797 #[test]
798 fn test_activate_pattern_not_found() {
799 let mut registry = PatternRegistry::new();
800
801 assert!(!registry.activate_pattern("nonexistent-id"));
802 }
803
804 #[test]
805 fn test_active_count_by_type() {
806 let mut registry = PatternRegistry::new();
807 let initial_active = registry.active_count_by_type(PatternType::Code);
808
809 let pattern = ConversationPattern::manual(PatternType::Code, "test-count");
811 let id = pattern.id.clone();
812 registry.add_pattern(pattern);
813
814 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active + 1);
815
816 registry.deactivate_pattern(&id);
817 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active);
818 }
819
820 #[test]
825 fn test_remove_pattern() {
826 let mut registry = PatternRegistry::new();
827
828 let pattern = ConversationPattern::manual(PatternType::Code, "test-remove");
829 let id = pattern.id.clone();
830 registry.add_pattern(pattern);
831
832 let len_before = registry.len();
833 assert!(registry.remove_pattern(&id));
834 assert_eq!(registry.len(), len_before - 1);
835 assert!(registry.get_pattern(&id).is_none());
836 }
837
838 #[test]
839 fn test_remove_pattern_not_found() {
840 let mut registry = PatternRegistry::new();
841
842 assert!(!registry.remove_pattern("nonexistent-id"));
843 }
844
845 #[test]
850 fn test_learn_patterns() {
851 let mut registry = PatternRegistry::new();
852 let initial_ref_count = registry.count_by_type(PatternType::Reference);
853
854 let new_pattern =
856 ConversationPattern::new(PatternType::Reference, "LEARN-123", PatternSource::Manual);
857 registry.learn_patterns(&[new_pattern]);
858
859 assert_eq!(
860 registry.count_by_type(PatternType::Reference),
861 initial_ref_count + 1
862 );
863
864 let same_pattern =
866 ConversationPattern::new(PatternType::Reference, "LEARN-123", PatternSource::Manual);
867 registry.learn_patterns(&[same_pattern]);
868
869 let p = registry.patterns.iter().find(|p| p.pattern == "LEARN-123").unwrap();
870 assert_eq!(p.frequency, 2); }
872
873 #[test]
874 fn test_learn_patterns_multiple_new() {
875 let mut registry = PatternRegistry::new();
876 let initial_count = registry.len();
877
878 let patterns = vec![
879 ConversationPattern::new(PatternType::Reference, "learn-1", PatternSource::Manual),
880 ConversationPattern::new(PatternType::Code, "learn-2", PatternSource::Manual),
881 ];
882
883 registry.learn_patterns(&patterns);
884
885 assert_eq!(registry.len(), initial_count + 2);
886 }
887
888 #[test]
889 fn test_learn_patterns_empty() {
890 let mut registry = PatternRegistry::new();
891 let initial_count = registry.len();
892
893 registry.learn_patterns(&[]);
894
895 assert_eq!(registry.len(), initial_count);
896 }
897
898 #[test]
903 fn test_stats_empty() {
904 let registry = PatternRegistry::new();
905 let stats = registry.stats();
906
907 assert_eq!(stats.total, 0);
908 assert_eq!(stats.presets, 0);
909 assert_eq!(stats.active, 0);
910 assert_eq!(stats.inactive, 0);
911 assert_eq!(stats.reference_count, 0);
912 assert_eq!(stats.code_count, 0);
913 }
914
915 #[test]
916 fn test_stats_with_manual_patterns() {
917 let mut registry = PatternRegistry::new();
918
919 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "manual-1"));
920 registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "manual-2"));
921
922 let stats = registry.stats();
923 assert_eq!(stats.total, 2);
924 assert_eq!(stats.manual, 2);
925 assert_eq!(stats.presets, 0);
926 }
927
928 #[test]
929 fn test_stats_display() {
930 let registry = PatternRegistry::new();
931 let stats = registry.stats();
932 let display = format!("{}", stats);
933
934 assert!(display.contains("Pattern Registry Stats"));
935 assert!(display.contains("Total:"));
936 assert!(display.contains("active:"));
937 assert!(display.contains("Reference:"));
938 assert!(display.contains("Code:"));
939 }
940
941 #[test]
946 fn test_serialization() {
947 let registry = PatternRegistry::new();
948 let json = serde_json::to_string(®istry).unwrap();
949 let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
950
951 assert_eq!(decoded.len(), registry.len());
952 assert_eq!(
953 decoded.count_by_type(PatternType::Reference),
954 registry.count_by_type(PatternType::Reference)
955 );
956 }
957
958 #[test]
959 fn test_serialization_preserves_patterns() {
960 let mut registry = PatternRegistry::new();
961
962 let pattern = ConversationPattern::manual(PatternType::Code, "serialize-test")
963 .with_description("Test serialization")
964 .with_tag("test");
965 let pattern_id = pattern.id.clone();
966 registry.add_pattern(pattern);
967
968 let json = serde_json::to_string(®istry).unwrap();
969 let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
970
971 let found = decoded.get_pattern(&pattern_id).unwrap();
972 assert_eq!(found.pattern, "serialize-test");
973 assert_eq!(found.description, Some("Test serialization".to_string()));
974 assert_eq!(found.tags, vec!["test"]);
975 }
976
977 #[test]
978 fn test_serialization_config_preserved() {
979 let config = PatternRegistryConfig::minimal();
980 let registry = PatternRegistry::with_config(config);
981
982 let json = serde_json::to_string(®istry).unwrap();
983 let decoded: PatternRegistry = serde_json::from_str(&json).unwrap();
984
985 assert_eq!(decoded.config.max_patterns_per_type, 50);
986 assert_eq!(decoded.config.min_confidence_threshold, 0.5);
987 }
988
989 #[test]
994 fn test_prune_keeps_active_patterns() {
995 let mut registry = PatternRegistry::new();
996
997 let pattern = ConversationPattern::manual(PatternType::Code, "active-pattern");
998 registry.add_pattern(pattern);
999
1000 let len_before = registry.len();
1001 registry.prune();
1002 let len_after = registry.len();
1003
1004 assert_eq!(len_after, len_before);
1006 }
1007
1008 #[test]
1009 fn test_prune_keeps_manual_patterns() {
1010 let mut registry = PatternRegistry::new();
1011
1012 let pattern = ConversationPattern::manual(PatternType::Code, "manual-pattern");
1013 registry.add_pattern(pattern);
1014
1015 let manual_count_before = registry.patterns.iter().filter(|p| p.source.is_manual()).count();
1016 registry.prune();
1017 let manual_count_after = registry.patterns.iter().filter(|p| p.source.is_manual()).count();
1018
1019 assert_eq!(manual_count_after, manual_count_before);
1020 }
1021
1022 #[test]
1027 fn test_len_and_is_empty() {
1028 let registry = PatternRegistry::new();
1029 assert!(registry.is_empty());
1030 assert_eq!(registry.len(), 0);
1031 }
1032
1033 #[test]
1034 fn test_count_by_type_empty() {
1035 let registry = PatternRegistry::new();
1036
1037 let ref_count = registry.count_by_type(PatternType::Reference);
1038 let code_count = registry.count_by_type(PatternType::Code);
1039
1040 assert_eq!(ref_count, 0);
1041 assert_eq!(code_count, 0);
1042 assert_eq!(ref_count + code_count, registry.len());
1043 }
1044
1045 #[test]
1046 fn test_count_by_type_with_patterns() {
1047 let mut registry = PatternRegistry::new();
1048 registry.add_pattern(ConversationPattern::manual(PatternType::Reference, "ref-1"));
1049 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "code-1"));
1050
1051 assert_eq!(registry.count_by_type(PatternType::Reference), 1);
1052 assert_eq!(registry.count_by_type(PatternType::Code), 1);
1053 }
1054
1055 #[test]
1060 fn test_config_custom() {
1061 let config = PatternRegistryConfig::with_max_patterns(50);
1062 let registry = PatternRegistry::with_config(config);
1063
1064 assert_eq!(registry.config.max_patterns_per_type, 50);
1065 }
1066
1067 #[test]
1068 fn test_add_pattern_same_pattern_different_types() {
1069 let mut registry = PatternRegistry::new();
1070 assert!(registry.is_empty());
1071
1072 let p1 = ConversationPattern::new(PatternType::Reference, "same-text", PatternSource::Manual);
1076 let p2 = ConversationPattern::new(PatternType::Code, "same-text", PatternSource::Manual);
1077
1078 registry.add_pattern(p1);
1079 registry.add_pattern(p2);
1080
1081 assert_eq!(registry.len(), 1);
1083 assert!(registry.all_patterns().iter().any(|p| p.pattern == "same-text" && p.pattern_type == PatternType::Reference));
1085 }
1086
1087 #[test]
1088 fn test_get_active_patterns_includes_only_active() {
1089 let mut registry = PatternRegistry::new();
1090
1091 let active_pattern = ConversationPattern::manual(PatternType::Code, "active-test");
1092 let active_id = active_pattern.id.clone();
1093
1094 let mut inactive_pattern = ConversationPattern::manual(PatternType::Code, "inactive-test");
1095 inactive_pattern.deactivate();
1096 let inactive_id = inactive_pattern.id.clone();
1097
1098 registry.add_pattern(active_pattern);
1099 registry.add_pattern(inactive_pattern);
1100
1101 let active_patterns = registry.get_active_patterns_by_type(PatternType::Code);
1102
1103 assert!(active_patterns.iter().any(|p| p.id == active_id));
1104 assert!(!active_patterns.iter().any(|p| p.id == inactive_id));
1105 }
1106
1107 #[test]
1108 fn test_type_index_rebuild_on_remove() {
1109 let mut registry = PatternRegistry::new();
1110
1111 let pattern = ConversationPattern::manual(PatternType::Code, "test-index");
1112 let id = pattern.id.clone();
1113 registry.add_pattern(pattern);
1114
1115 assert!(registry.get_pattern(&id).is_some());
1117
1118 registry.remove_pattern(&id);
1120 assert!(registry.get_pattern(&id).is_none());
1121
1122 let active = registry.get_active_patterns_by_type(PatternType::Code);
1124 assert!(!active.iter().any(|p| p.id == id));
1125 }
1126
1127 #[test]
1128 fn test_deactivate_then_activate_affects_active_count() {
1129 let mut registry = PatternRegistry::new();
1130
1131 let pattern = ConversationPattern::manual(PatternType::Code, "toggle-test");
1132 let id = pattern.id.clone();
1133 registry.add_pattern(pattern);
1134
1135 let initial_active = registry.active_count_by_type(PatternType::Code);
1136
1137 registry.deactivate_pattern(&id);
1138 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active - 1);
1139
1140 registry.activate_pattern(&id);
1141 assert_eq!(registry.active_count_by_type(PatternType::Code), initial_active);
1142 }
1143
1144 #[test]
1145 fn test_pattern_order_preserved_after_multiple_operations() {
1146 let mut registry = PatternRegistry::new();
1147
1148 let p1 = ConversationPattern::manual(PatternType::Reference, "order-1");
1149 let p2 = ConversationPattern::manual(PatternType::Code, "order-2");
1150 let p3 = ConversationPattern::manual(PatternType::Reference, "order-3");
1151
1152 registry.add_patterns(vec![p1, p2, p3]);
1153
1154 assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-1"));
1156 assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-2"));
1157 assert!(registry.all_patterns().iter().any(|p| p.pattern == "order-3"));
1158 }
1159
1160 #[test]
1165 fn test_get_patterns_file_path() {
1166 let path = PatternRegistry::get_patterns_file_path();
1167 assert!(path.is_ok());
1168
1169 let path = path.unwrap();
1170 assert!(path.to_string_lossy().contains(".matrix"));
1172 assert!(path.to_string_lossy().contains("patterns.json"));
1173 }
1174
1175 #[test]
1176 fn test_from_file_nonexistent() {
1177 let temp_dir = tempfile::tempdir().unwrap();
1178 let nonexistent_path = temp_dir.path().join("nonexistent_patterns.json");
1179
1180 let registry = PatternRegistry::from_file(&nonexistent_path).unwrap();
1181
1182 assert!(registry.is_empty());
1184 assert_eq!(registry.count_by_type(PatternType::Reference), 0);
1185 assert_eq!(registry.count_by_type(PatternType::Code), 0);
1186 }
1187
1188 #[test]
1189 fn test_from_file_empty_file() {
1190 let temp_dir = tempfile::tempdir().unwrap();
1191 let empty_path = temp_dir.path().join("empty_patterns.json");
1192
1193 fs::write(&empty_path, "").unwrap();
1195
1196 let registry = PatternRegistry::from_file(&empty_path).unwrap();
1197
1198 assert!(registry.is_empty());
1200 }
1201
1202 #[test]
1203 fn test_from_file_valid_json() {
1204 let temp_dir = tempfile::tempdir().unwrap();
1205 let file_path = temp_dir.path().join("valid_patterns.json");
1206
1207 let mut original = PatternRegistry::new();
1209 original.add_pattern(
1210 ConversationPattern::manual(PatternType::Code, "custom-pattern")
1211 .with_description("Custom test pattern"),
1212 );
1213 original.save_to_file(&file_path).unwrap();
1214
1215 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1217
1218 assert!(loaded.patterns.iter().any(|p| p.pattern == "custom-pattern"));
1220 assert!(loaded.patterns.iter().any(|p| p.description == Some("Custom test pattern".to_string())));
1221 }
1222
1223 #[test]
1224 fn test_from_file_malformed_json() {
1225 let temp_dir = tempfile::tempdir().unwrap();
1226 let malformed_path = temp_dir.path().join("malformed_patterns.json");
1227
1228 fs::write(&malformed_path, "{ not valid json }").unwrap();
1230
1231 let registry = PatternRegistry::from_file(&malformed_path).unwrap();
1233
1234 assert!(registry.is_empty());
1235 }
1236
1237 #[test]
1238 fn test_save_to_file_creates_directory() {
1239 let temp_dir = tempfile::tempdir().unwrap();
1240 let nested_path = temp_dir.path().join("nested").join("dir").join("patterns.json");
1241
1242 assert!(!nested_path.parent().unwrap().exists());
1244
1245 let registry = PatternRegistry::new();
1246 registry.save_to_file(&nested_path).unwrap();
1247
1248 assert!(nested_path.parent().unwrap().exists());
1250 assert!(nested_path.exists());
1251 }
1252
1253 #[test]
1254 fn test_save_to_file_roundtrip() {
1255 let temp_dir = tempfile::tempdir().unwrap();
1256 let file_path = temp_dir.path().join("roundtrip_patterns.json");
1257
1258 let mut original = PatternRegistry::new();
1260 let p1 = ConversationPattern::manual(PatternType::Reference, "roundtrip-ref")
1261 .with_description("Reference pattern for roundtrip test")
1262 .with_tag("test");
1263 let p1_id = p1.id.clone();
1264 original.add_pattern(p1);
1265
1266 let p2 = ConversationPattern::manual(PatternType::Code, "roundtrip-code")
1267 .with_description("Code pattern for roundtrip test");
1268 let p2_id = p2.id.clone();
1269 original.add_pattern(p2);
1270
1271 original.save_to_file(&file_path).unwrap();
1273
1274 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1276
1277 let loaded_p1 = loaded.get_pattern(&p1_id).unwrap();
1279 assert_eq!(loaded_p1.pattern, "roundtrip-ref");
1280 assert_eq!(loaded_p1.pattern_type, PatternType::Reference);
1281 assert_eq!(loaded_p1.description, Some("Reference pattern for roundtrip test".to_string()));
1282 assert_eq!(loaded_p1.tags, vec!["test"]);
1283
1284 let loaded_p2 = loaded.get_pattern(&p2_id).unwrap();
1285 assert_eq!(loaded_p2.pattern, "roundtrip-code");
1286 assert_eq!(loaded_p2.pattern_type, PatternType::Code);
1287 }
1288
1289 #[test]
1290 fn test_save_to_file_preserves_config() {
1291 let temp_dir = tempfile::tempdir().unwrap();
1292 let file_path = temp_dir.path().join("config_patterns.json");
1293
1294 let config = PatternRegistryConfig::minimal();
1296 let original = PatternRegistry::with_config(config);
1297
1298 original.save_to_file(&file_path).unwrap();
1299
1300 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1301
1302 assert_eq!(loaded.config.max_patterns_per_type, 50);
1303 assert_eq!(loaded.config.min_confidence_threshold, 0.5);
1304 assert_eq!(loaded.config.min_frequency, 3);
1305 }
1306
1307 #[test]
1308 fn test_from_default_file() {
1309 let result = PatternRegistry::from_default_file();
1312 assert!(result.is_ok());
1313 }
1314
1315 #[test]
1316 fn test_file_storage_integration() {
1317 let temp_dir = tempfile::tempdir().unwrap();
1318 let file_path = temp_dir.path().join("integration_patterns.json");
1319
1320 let mut registry = PatternRegistry::from_file(&file_path).unwrap();
1322 assert!(registry.is_empty());
1323
1324 let custom = ConversationPattern::manual(PatternType::Code, "integration-test")
1326 .with_description("Integration test pattern")
1327 .with_tag("integration");
1328 registry.add_pattern(custom);
1329
1330 registry.save_to_file(&file_path).unwrap();
1332
1333 let reloaded = PatternRegistry::from_file(&file_path).unwrap();
1335
1336 assert!(reloaded.patterns.iter().any(|p| p.pattern == "integration-test"));
1338 assert!(reloaded.patterns.iter().any(|p| p.tags.contains(&"integration".to_string())));
1339 }
1340
1341 #[test]
1346 fn test_large_patterns_save_and_load() {
1347 let temp_dir = tempfile::tempdir().unwrap();
1348 let file_path = temp_dir.path().join("large_patterns.json");
1349
1350 let mut registry = PatternRegistry::new();
1352
1353 for i in 0..50 {
1355 let ref_pattern = ConversationPattern::manual(PatternType::Reference, &format!("large-ref-{}", i))
1356 .with_description(&format!("Reference pattern {}", i))
1357 .with_tag(&format!("tag{}", i % 5));
1358 registry.add_pattern(ref_pattern);
1359
1360 let code_pattern = ConversationPattern::manual(PatternType::Code, &format!("large-code-{}", i))
1361 .with_description(&format!("Code pattern {}", i))
1362 .with_tag(&format!("codetag{}", i % 3));
1363 registry.add_pattern(code_pattern);
1364 }
1365
1366 let expected_count = 100;
1367 assert_eq!(registry.len(), expected_count);
1368
1369 registry.save_to_file(&file_path).unwrap();
1371
1372 let metadata = fs::metadata(&file_path).unwrap();
1374 assert!(metadata.len() > 1000); let loaded = PatternRegistry::from_file(&file_path).unwrap();
1378
1379 assert_eq!(loaded.len(), expected_count);
1381
1382 for i in 0..50 {
1384 assert!(
1385 loaded.patterns.iter().any(|p| p.pattern == format!("large-ref-{}", i)),
1386 "Missing large-ref-{}",
1387 i
1388 );
1389 assert!(
1390 loaded.patterns.iter().any(|p| p.pattern == format!("large-code-{}", i)),
1391 "Missing large-code-{}",
1392 i
1393 );
1394 }
1395 }
1396
1397 #[test]
1398 fn test_special_characters_in_patterns() {
1399 let temp_dir = tempfile::tempdir().unwrap();
1400 let file_path = temp_dir.path().join("special_chars_patterns.json");
1401
1402 let special_patterns = vec![
1404 ("unicode-你好", "Chinese characters"),
1406 ("unicode-日本語", "Japanese characters"),
1407 ("unicode-한국어", "Korean characters"),
1408 ("unicode-مرحبا", "Arabic characters"),
1409 ("unicode-🎉🔥", "Emoji"),
1410 ("regex-\\d+\\.\\w*", "Regex pattern with escapes"),
1412 ("regex-[a-z]+", "Regex character class"),
1413 ("regex-(foo|bar)", "Regex alternation"),
1414 ("json-\"quotes\"", "Contains quotes"),
1416 ("json-\\n\\t", "Escape sequences"),
1417 ("json-日本語\"test\"", "Mixed special chars"),
1418 ("whitespace-tab\ttab", "Contains tab"),
1420 ("whitespace-newline\nline", "Contains newline"),
1421 ];
1422
1423 let mut registry = PatternRegistry::new();
1424 let mut added_ids = Vec::new();
1425
1426 for (pattern_str, desc) in &special_patterns {
1427 let pattern = ConversationPattern::manual(PatternType::Code, *pattern_str)
1428 .with_description(*desc);
1429 let id = pattern.id.clone();
1430 registry.add_pattern(pattern);
1431 added_ids.push((id, *pattern_str, *desc));
1432 }
1433
1434 registry.save_to_file(&file_path).unwrap();
1436
1437 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1439
1440 for (id, pattern_str, desc) in &added_ids {
1442 let found = loaded.get_pattern(id);
1443 assert!(found.is_some(), "Pattern {} not found after reload", pattern_str);
1444 let p = found.unwrap();
1445 assert_eq!(p.pattern, *pattern_str, "Pattern mismatch for {}", pattern_str);
1446 assert_eq!(p.description, Some(desc.to_string()), "Description mismatch for {}", pattern_str);
1447 }
1448 }
1449
1450 #[test]
1451 fn test_empty_registry_save_and_load() {
1452 let temp_dir = tempfile::tempdir().unwrap();
1453 let file_path = temp_dir.path().join("empty_registry.json");
1454
1455 let registry = PatternRegistry::new();
1457 assert!(registry.is_empty());
1458
1459 registry.save_to_file(&file_path).unwrap();
1461
1462 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1464
1465 assert!(loaded.is_empty());
1467 }
1468
1469 #[test]
1470 fn test_whitespace_only_file() {
1471 let temp_dir = tempfile::tempdir().unwrap();
1472 let file_path = temp_dir.path().join("whitespace_patterns.json");
1473
1474 fs::write(&file_path, " \n\t \n ").unwrap();
1476
1477 let registry = PatternRegistry::from_file(&file_path).unwrap();
1478
1479 assert!(registry.is_empty());
1481 }
1482
1483 #[test]
1484 fn test_save_to_default_file_path() {
1485 let temp_dir = tempfile::tempdir().unwrap();
1487 let custom_path = temp_dir.path().join(".matrix").join("patterns.json");
1488
1489 let mut registry = PatternRegistry::new();
1491 let pattern = ConversationPattern::manual(PatternType::Code, "default-file-test")
1492 .with_description("Test save_to_file with custom path");
1493 registry.add_pattern(pattern);
1494
1495 registry.save_to_file(&custom_path).unwrap();
1496
1497 assert!(custom_path.exists());
1499
1500 let loaded = PatternRegistry::from_file(&custom_path).unwrap();
1502 assert!(loaded.patterns.iter().any(|p| p.pattern == "default-file-test"));
1503 }
1504
1505 #[test]
1506 fn test_atomic_write_rollback_safety() {
1507 let temp_dir = tempfile::tempdir().unwrap();
1508 let file_path = temp_dir.path().join("atomic_test.json");
1509
1510 let mut registry = PatternRegistry::new();
1512 registry.add_pattern(ConversationPattern::manual(PatternType::Code, "initial-pattern"));
1513 registry.save_to_file(&file_path).unwrap();
1514
1515 let tmp_path = file_path.with_extension("json.tmp");
1517 assert!(!tmp_path.exists(), "Temp file should not exist after save");
1518
1519 assert!(file_path.exists());
1521
1522 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1524 assert!(loaded.patterns.iter().any(|p| p.pattern == "initial-pattern"));
1525 }
1526
1527 #[test]
1528 fn test_long_description_and_tags() {
1529 let temp_dir = tempfile::tempdir().unwrap();
1530 let file_path = temp_dir.path().join("long_content.json");
1531
1532 let long_desc = "x".repeat(10000);
1534 let many_tags: Vec<String> = (0..100).map(|i| format!("tag-{}", i)).collect();
1535
1536 let mut registry = PatternRegistry::new();
1537 let mut pattern = ConversationPattern::manual(PatternType::Code, "long-pattern");
1538 pattern.description = Some(long_desc.clone());
1539 pattern.tags = many_tags.clone();
1540
1541 registry.add_pattern(pattern);
1542
1543 registry.save_to_file(&file_path).unwrap();
1545
1546 let loaded = PatternRegistry::from_file(&file_path).unwrap();
1548
1549 let found = loaded.patterns.iter().find(|p| p.pattern == "long-pattern").unwrap();
1551 assert_eq!(found.description, Some(long_desc));
1552 assert_eq!(found.tags.len(), 100);
1553 for i in 0..100 {
1554 assert!(found.tags.contains(&format!("tag-{}", i)));
1555 }
1556 }
1557}