1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum PatternType {
17 Reference,
19 Code,
21}
22
23impl PatternType {
24 pub fn display_name(&self) -> &'static str {
26 match self {
27 PatternType::Reference => "引用模式",
28 PatternType::Code => "代码模式",
29 }
30 }
31
32 pub fn icon(&self) -> &'static str {
34 match self {
35 PatternType::Reference => "🔗",
36 PatternType::Code => "💻",
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(tag = "type", rename_all = "snake_case")]
48pub enum PatternSource {
49 UserConversation {
51 example: String,
53 },
54 ProjectCodeStyle {
56 language: String,
58 },
59 SystemPreset,
61 Manual,
63}
64
65impl PatternSource {
66 pub fn user_conversation(example: impl Into<String>) -> Self {
68 PatternSource::UserConversation {
69 example: example.into(),
70 }
71 }
72
73 pub fn project_code_style(language: impl Into<String>) -> Self {
75 PatternSource::ProjectCodeStyle {
76 language: language.into(),
77 }
78 }
79
80 pub fn is_preset(&self) -> bool {
82 matches!(self, PatternSource::SystemPreset)
83 }
84
85 pub fn is_manual(&self) -> bool {
87 matches!(self, PatternSource::Manual)
88 }
89
90 pub fn display_name(&self) -> &'static str {
92 match self {
93 PatternSource::UserConversation { .. } => "用户对话",
94 PatternSource::ProjectCodeStyle { .. } => "项目风格",
95 PatternSource::SystemPreset => "系统预设",
96 PatternSource::Manual => "手动添加",
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ConversationPattern {
113 pub id: String,
115 pub pattern_type: PatternType,
117 pub pattern: String,
119 pub source: PatternSource,
121 pub frequency: u32,
123 pub last_used: DateTime<Utc>,
125 pub confidence: f32,
127 pub is_active: bool,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub description: Option<String>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 pub tags: Vec<String>,
135}
136
137impl ConversationPattern {
138 pub fn new(
140 pattern_type: PatternType,
141 pattern: impl Into<String>,
142 source: PatternSource,
143 ) -> Self {
144 let id = uuid::Uuid::new_v4().to_string();
145 Self {
146 id,
147 pattern_type,
148 pattern: pattern.into(),
149 source,
150 frequency: 1,
151 last_used: Utc::now(),
152 confidence: 0.5,
153 is_active: true,
154 description: None,
155 tags: Vec::new(),
156 }
157 }
158
159 pub fn preset(pattern_type: PatternType, pattern: impl Into<String>) -> Self {
161 let mut p = Self::new(pattern_type, pattern, PatternSource::SystemPreset);
162 p.confidence = 1.0;
163 p.frequency = 100; p
165 }
166
167 pub fn manual(pattern_type: PatternType, pattern: impl Into<String>) -> Self {
169 let mut p = Self::new(pattern_type, pattern, PatternSource::Manual);
170 p.confidence = 0.9;
171 p.is_active = true;
172 p
173 }
174
175 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
177 self.description = Some(desc.into());
178 self
179 }
180
181 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
183 self.tags.push(tag.into());
184 self
185 }
186
187 pub fn mark_used(&mut self) {
189 self.frequency = self.frequency.saturating_add(1);
190 self.last_used = Utc::now();
191 self.confidence = (self.confidence + 0.01).min(1.0);
193 }
194
195 pub fn deactivate(&mut self) {
197 self.is_active = false;
198 }
199
200 pub fn activate(&mut self) {
202 self.is_active = true;
203 }
204
205 pub fn format_line(&self) -> String {
207 let active_marker = if self.is_active { "" } else { "[inactive] " };
208 let freq_marker = if self.frequency > 10 { "★" } else { "" };
209 format!(
210 "{}{} {} {} (freq: {}, conf: {:.2}) {}",
211 active_marker,
212 self.pattern_type.icon(),
213 self.pattern_type.display_name(),
214 &self.pattern,
215 self.frequency,
216 self.confidence,
217 freq_marker
218 )
219 }
220
221 pub fn format_for_prompt(&self) -> String {
223 match &self.description {
224 Some(desc) => format!("{}: {}", &self.pattern, desc),
225 None => self.pattern.clone(),
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
239 fn test_pattern_type_display_name() {
240 assert_eq!(PatternType::Reference.display_name(), "引用模式");
241 assert_eq!(PatternType::Code.display_name(), "代码模式");
242 }
243
244 #[test]
245 fn test_pattern_type_icon() {
246 assert_eq!(PatternType::Reference.icon(), "🔗");
247 assert_eq!(PatternType::Code.icon(), "💻");
248 }
249
250 #[test]
251 fn test_pattern_type_equality() {
252 assert_eq!(PatternType::Reference, PatternType::Reference);
253 assert_eq!(PatternType::Code, PatternType::Code);
254 assert_ne!(PatternType::Reference, PatternType::Code);
255 }
256
257 #[test]
258 fn test_pattern_type_hash() {
259 use std::collections::HashSet;
260 let mut set = HashSet::new();
261 set.insert(PatternType::Reference);
262 set.insert(PatternType::Code);
263 set.insert(PatternType::Reference); assert_eq!(set.len(), 2);
266 }
267
268 #[test]
269 fn test_pattern_type_serialization() {
270 let pt = PatternType::Reference;
271 let json = serde_json::to_string(&pt).unwrap();
272 assert_eq!(json, "\"reference\"");
273
274 let decoded: PatternType = serde_json::from_str(&json).unwrap();
275 assert_eq!(decoded, PatternType::Reference);
276
277 let pt2 = PatternType::Code;
278 let json2 = serde_json::to_string(&pt2).unwrap();
279 assert_eq!(json2, "\"code\"");
280 }
281
282 #[test]
287 fn test_pattern_source_user_conversation() {
288 let source = PatternSource::user_conversation("User mentioned PR #123");
289 match source {
290 PatternSource::UserConversation { example } => {
291 assert_eq!(example, "User mentioned PR #123");
292 }
293 _ => panic!("Expected UserConversation variant"),
294 }
295 }
296
297 #[test]
298 fn test_pattern_source_project_code_style() {
299 let source = PatternSource::project_code_style("rust");
300 match source {
301 PatternSource::ProjectCodeStyle { language } => {
302 assert_eq!(language, "rust");
303 }
304 _ => panic!("Expected ProjectCodeStyle variant"),
305 }
306 }
307
308 #[test]
309 fn test_pattern_source_is_preset() {
310 assert!(PatternSource::SystemPreset.is_preset());
311 assert!(!PatternSource::user_conversation("test").is_preset());
312 assert!(!PatternSource::project_code_style("rust").is_preset());
313 assert!(!PatternSource::Manual.is_preset());
314 }
315
316 #[test]
317 fn test_pattern_source_is_manual() {
318 assert!(PatternSource::Manual.is_manual());
319 assert!(!PatternSource::SystemPreset.is_manual());
320 assert!(!PatternSource::user_conversation("test").is_manual());
321 assert!(!PatternSource::project_code_style("rust").is_manual());
322 }
323
324 #[test]
325 fn test_pattern_source_display_name() {
326 assert_eq!(PatternSource::user_conversation("test").display_name(), "用户对话");
327 assert_eq!(PatternSource::project_code_style("rust").display_name(), "项目风格");
328 assert_eq!(PatternSource::SystemPreset.display_name(), "系统预设");
329 assert_eq!(PatternSource::Manual.display_name(), "手动添加");
330 }
331
332 #[test]
333 fn test_pattern_source_serialization() {
334 let source = PatternSource::user_conversation("example context");
336 let json = serde_json::to_string(&source).unwrap();
337 let decoded: PatternSource = serde_json::from_str(&json).unwrap();
338 assert_eq!(decoded, source);
339
340 let source2 = PatternSource::project_code_style("typescript");
342 let json2 = serde_json::to_string(&source2).unwrap();
343 let decoded2: PatternSource = serde_json::from_str(&json2).unwrap();
344 assert_eq!(decoded2, source2);
345
346 let source3 = PatternSource::SystemPreset;
348 let json3 = serde_json::to_string(&source3).unwrap();
349 assert!(json3.contains("system_preset"));
350
351 let source4 = PatternSource::Manual;
353 let json4 = serde_json::to_string(&source4).unwrap();
354 assert!(json4.contains("manual"));
355 }
356
357 #[test]
362 fn test_pattern_creation() {
363 let pattern = ConversationPattern::new(
364 PatternType::Reference,
365 r"PR #\d+",
366 PatternSource::user_conversation("User mentioned PR #123"),
367 );
368 assert!(pattern.is_active);
369 assert_eq!(pattern.frequency, 1);
370 assert_eq!(pattern.confidence, 0.5);
371 assert!(pattern.description.is_none());
372 assert!(pattern.tags.is_empty());
373 assert!(!pattern.id.is_empty()); }
375
376 #[test]
377 fn test_pattern_creation_with_all_types() {
378 let ref_pattern = ConversationPattern::new(
380 PatternType::Reference,
381 r"issue #\d+",
382 PatternSource::Manual,
383 );
384 assert_eq!(ref_pattern.pattern_type, PatternType::Reference);
385
386 let code_pattern = ConversationPattern::new(
388 PatternType::Code,
389 r"fn \w+\(",
390 PatternSource::SystemPreset,
391 );
392 assert_eq!(code_pattern.pattern_type, PatternType::Code);
393 }
394
395 #[test]
396 fn test_pattern_preset() {
397 let pattern = ConversationPattern::preset(PatternType::Reference, r"PR #\d+");
398
399 assert!(pattern.source.is_preset());
400 assert!(pattern.is_active);
401 assert_eq!(pattern.confidence, 1.0);
402 assert_eq!(pattern.frequency, 100); }
404
405 #[test]
406 fn test_pattern_manual() {
407 let pattern = ConversationPattern::manual(PatternType::Code, "custom-pattern");
408
409 assert!(pattern.source.is_manual());
410 assert!(pattern.is_active);
411 assert_eq!(pattern.confidence, 0.9);
412 }
413
414 #[test]
415 fn test_pattern_with_description() {
416 let pattern = ConversationPattern::new(
417 PatternType::Reference,
418 "test-pattern",
419 PatternSource::Manual,
420 )
421 .with_description("This is a test pattern");
422
423 assert_eq!(pattern.description, Some("This is a test pattern".to_string()));
424 }
425
426 #[test]
427 fn test_pattern_with_tag() {
428 let pattern = ConversationPattern::new(
429 PatternType::Code,
430 "test-pattern",
431 PatternSource::Manual,
432 )
433 .with_tag("rust")
434 .with_tag("async");
435
436 assert_eq!(pattern.tags, vec!["rust", "async"]);
437 }
438
439 #[test]
440 fn test_pattern_builder_chain() {
441 let pattern = ConversationPattern::preset(PatternType::Reference, r"\bPR\s*#\d+\b")
442 .with_description("Pull Request reference")
443 .with_tag("git")
444 .with_tag("github");
445
446 assert_eq!(pattern.pattern, r"\bPR\s*#\d+\b");
447 assert_eq!(pattern.description, Some("Pull Request reference".to_string()));
448 assert_eq!(pattern.tags, vec!["git", "github"]);
449 assert!(pattern.source.is_preset());
450 }
451
452 #[test]
457 fn test_pattern_mark_used() {
458 let mut pattern = ConversationPattern::new(
459 PatternType::Code,
460 "fn test()",
461 PatternSource::Manual,
462 );
463 let initial_confidence = pattern.confidence;
464 let initial_last_used = pattern.last_used;
465
466 pattern.mark_used();
467
468 assert_eq!(pattern.frequency, 2);
469 assert!(pattern.confidence > initial_confidence);
470 assert!(pattern.last_used >= initial_last_used);
471 }
472
473 #[test]
474 fn test_pattern_mark_used_confidence_cap() {
475 let mut pattern = ConversationPattern::new(
476 PatternType::Code,
477 "test",
478 PatternSource::Manual,
479 );
480
481 pattern.confidence = 0.999;
483
484 pattern.mark_used();
485
486 assert!(pattern.confidence <= 1.0);
488 }
489
490 #[test]
491 fn test_pattern_mark_used_frequency_overflow() {
492 let mut pattern = ConversationPattern::new(
493 PatternType::Code,
494 "test",
495 PatternSource::Manual,
496 );
497
498 pattern.frequency = u32::MAX - 1;
500
501 pattern.mark_used();
502
503 assert_eq!(pattern.frequency, u32::MAX);
505 }
506
507 #[test]
508 fn test_pattern_deactivate() {
509 let mut pattern = ConversationPattern::new(
510 PatternType::Reference,
511 "test",
512 PatternSource::Manual,
513 );
514
515 assert!(pattern.is_active);
516 pattern.deactivate();
517 assert!(!pattern.is_active);
518 }
519
520 #[test]
521 fn test_pattern_activate() {
522 let mut pattern = ConversationPattern::new(
523 PatternType::Reference,
524 "test",
525 PatternSource::Manual,
526 );
527
528 pattern.deactivate();
529 assert!(!pattern.is_active);
530
531 pattern.activate();
532 assert!(pattern.is_active);
533 }
534
535 #[test]
536 fn test_pattern_activate_deactivate_cycle() {
537 let mut pattern = ConversationPattern::preset(PatternType::Code, "test");
538
539 for _ in 0..3 {
541 pattern.deactivate();
542 assert!(!pattern.is_active);
543 pattern.activate();
544 assert!(pattern.is_active);
545 }
546 }
547
548 #[test]
553 fn test_format_line_active_high_frequency() {
554 let mut pattern = ConversationPattern::preset(PatternType::Reference, "test-pattern");
555 pattern.frequency = 15; let line = pattern.format_line();
558
559 assert!(line.contains("🔗"));
560 assert!(line.contains("引用模式"));
561 assert!(line.contains("test-pattern"));
562 assert!(line.contains("freq: 15"));
563 assert!(line.contains("★")); assert!(!line.contains("[inactive]"));
565 }
566
567 #[test]
568 fn test_format_line_active_low_frequency() {
569 let pattern = ConversationPattern::new(
570 PatternType::Code,
571 "test-pattern",
572 PatternSource::Manual,
573 );
574
575 let line = pattern.format_line();
576
577 assert!(line.contains("💻"));
578 assert!(line.contains("代码模式"));
579 assert!(!line.contains("★")); assert!(!line.contains("[inactive]"));
581 }
582
583 #[test]
584 fn test_format_line_inactive() {
585 let mut pattern = ConversationPattern::preset(PatternType::Reference, "test-pattern");
586 pattern.deactivate();
587
588 let line = pattern.format_line();
589
590 assert!(line.contains("[inactive]"));
591 }
592
593 #[test]
594 fn test_format_for_prompt_with_description() {
595 let pattern = ConversationPattern::preset(PatternType::Reference, r"\bPR\s*#\d+\b")
596 .with_description("Pull Request reference format");
597
598 let prompt = pattern.format_for_prompt();
599
600 assert_eq!(prompt, r"\bPR\s*#\d+\b: Pull Request reference format");
601 }
602
603 #[test]
604 fn test_format_for_prompt_without_description() {
605 let pattern = ConversationPattern::preset(PatternType::Reference, "simple-pattern");
606
607 let prompt = pattern.format_for_prompt();
608
609 assert_eq!(prompt, "simple-pattern");
610 }
611
612 #[test]
617 fn test_serialization() {
618 let pattern = ConversationPattern::preset(PatternType::Reference, r"PR #\d+")
619 .with_description("Test pattern");
620
621 let json = serde_json::to_string(&pattern).unwrap();
622 let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
623
624 assert_eq!(decoded.pattern, pattern.pattern);
625 assert_eq!(decoded.pattern_type, PatternType::Reference);
626 assert_eq!(decoded.description, Some("Test pattern".to_string()));
627 }
628
629 #[test]
630 fn test_serialization_with_tags() {
631 let pattern = ConversationPattern::preset(PatternType::Code, r"fn \w+")
632 .with_tag("rust")
633 .with_tag("function");
634
635 let json = serde_json::to_string(&pattern).unwrap();
636 let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
637
638 assert_eq!(decoded.tags, vec!["rust", "function"]);
639 }
640
641 #[test]
642 fn test_serialization_roundtrip() {
643 let original = ConversationPattern::new(
644 PatternType::Reference,
645 r"issue #\d+",
646 PatternSource::user_conversation("User said issue #42"),
647 )
648 .with_description("Issue reference")
649 .with_tag("git");
650
651 let json = serde_json::to_string(&original).unwrap();
652 let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
653
654 assert_eq!(decoded.id, original.id);
655 assert_eq!(decoded.pattern_type, original.pattern_type);
656 assert_eq!(decoded.pattern, original.pattern);
657 assert_eq!(decoded.source, original.source);
658 assert_eq!(decoded.frequency, original.frequency);
659 assert_eq!(decoded.confidence, original.confidence);
660 assert_eq!(decoded.is_active, original.is_active);
661 assert_eq!(decoded.description, original.description);
662 assert_eq!(decoded.tags, original.tags);
663 }
664
665 #[test]
670 fn test_empty_pattern_string() {
671 let pattern = ConversationPattern::new(
672 PatternType::Reference,
673 "",
674 PatternSource::Manual,
675 );
676
677 assert_eq!(pattern.pattern, "");
678 }
679
680 #[test]
681 fn test_special_regex_chars_in_pattern() {
682 let pattern = ConversationPattern::new(
683 PatternType::Code,
684 r"fn\s+\w+\s*\([^)]*\)\s*\{",
685 PatternSource::Manual,
686 );
687
688 assert_eq!(pattern.pattern, r"fn\s+\w+\s*\([^)]*\)\s*\{");
689 }
690
691 #[test]
692 fn test_unicode_pattern() {
693 let pattern = ConversationPattern::new(
694 PatternType::Reference,
695 "中文模式",
696 PatternSource::user_conversation("测试"),
697 );
698
699 assert_eq!(pattern.pattern, "中文模式");
700 }
701
702 #[test]
703 fn test_very_long_pattern() {
704 let long_pattern = "x".repeat(10000);
705 let pattern = ConversationPattern::new(
706 PatternType::Code,
707 long_pattern.clone(),
708 PatternSource::Manual,
709 );
710
711 assert_eq!(pattern.pattern.len(), 10000);
712 }
713
714 #[test]
715 fn test_confidence_boundary() {
716 let mut pattern = ConversationPattern::new(
717 PatternType::Code,
718 "test",
719 PatternSource::Manual,
720 );
721
722 pattern.confidence = 0.0;
724 assert_eq!(pattern.confidence, 0.0);
725
726 pattern.confidence = 1.0;
728 assert_eq!(pattern.confidence, 1.0);
729 }
730
731 #[test]
732 fn test_unique_ids() {
733 let p1 = ConversationPattern::new(PatternType::Code, "test", PatternSource::Manual);
734 let p2 = ConversationPattern::new(PatternType::Code, "test", PatternSource::Manual);
735
736 assert_ne!(p1.id, p2.id);
738 }
739}