use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatternType {
Reference,
Code,
}
impl PatternType {
pub fn display_name(&self) -> &'static str {
match self {
PatternType::Reference => "引用模式",
PatternType::Code => "代码模式",
}
}
pub fn icon(&self) -> &'static str {
match self {
PatternType::Reference => "🔗",
PatternType::Code => "💻",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PatternSource {
UserConversation {
example: String,
},
ProjectCodeStyle {
language: String,
},
SystemPreset,
Manual,
}
impl PatternSource {
pub fn user_conversation(example: impl Into<String>) -> Self {
PatternSource::UserConversation {
example: example.into(),
}
}
pub fn project_code_style(language: impl Into<String>) -> Self {
PatternSource::ProjectCodeStyle {
language: language.into(),
}
}
pub fn is_preset(&self) -> bool {
matches!(self, PatternSource::SystemPreset)
}
pub fn is_manual(&self) -> bool {
matches!(self, PatternSource::Manual)
}
pub fn display_name(&self) -> &'static str {
match self {
PatternSource::UserConversation { .. } => "用户对话",
PatternSource::ProjectCodeStyle { .. } => "项目风格",
PatternSource::SystemPreset => "系统预设",
PatternSource::Manual => "手动添加",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationPattern {
pub id: String,
pub pattern_type: PatternType,
pub pattern: String,
pub source: PatternSource,
pub frequency: u32,
pub last_used: DateTime<Utc>,
pub confidence: f32,
pub is_active: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
impl ConversationPattern {
pub fn new(
pattern_type: PatternType,
pattern: impl Into<String>,
source: PatternSource,
) -> Self {
let id = uuid::Uuid::new_v4().to_string();
Self {
id,
pattern_type,
pattern: pattern.into(),
source,
frequency: 1,
last_used: Utc::now(),
confidence: 0.5,
is_active: true,
description: None,
tags: Vec::new(),
}
}
pub fn preset(pattern_type: PatternType, pattern: impl Into<String>) -> Self {
let mut p = Self::new(pattern_type, pattern, PatternSource::SystemPreset);
p.confidence = 1.0;
p.frequency = 100; p
}
pub fn manual(pattern_type: PatternType, pattern: impl Into<String>) -> Self {
let mut p = Self::new(pattern_type, pattern, PatternSource::Manual);
p.confidence = 0.9;
p.is_active = true;
p
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn mark_used(&mut self) {
self.frequency = self.frequency.saturating_add(1);
self.last_used = Utc::now();
self.confidence = (self.confidence + 0.01).min(1.0);
}
pub fn deactivate(&mut self) {
self.is_active = false;
}
pub fn activate(&mut self) {
self.is_active = true;
}
pub fn format_line(&self) -> String {
let active_marker = if self.is_active { "" } else { "[inactive] " };
let freq_marker = if self.frequency > 10 { "★" } else { "" };
format!(
"{}{} {} {} (freq: {}, conf: {:.2}) {}",
active_marker,
self.pattern_type.icon(),
self.pattern_type.display_name(),
&self.pattern,
self.frequency,
self.confidence,
freq_marker
)
}
pub fn format_for_prompt(&self) -> String {
match &self.description {
Some(desc) => format!("{}: {}", &self.pattern, desc),
None => self.pattern.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pattern_type_display_name() {
assert_eq!(PatternType::Reference.display_name(), "引用模式");
assert_eq!(PatternType::Code.display_name(), "代码模式");
}
#[test]
fn test_pattern_type_icon() {
assert_eq!(PatternType::Reference.icon(), "🔗");
assert_eq!(PatternType::Code.icon(), "💻");
}
#[test]
fn test_pattern_type_equality() {
assert_eq!(PatternType::Reference, PatternType::Reference);
assert_eq!(PatternType::Code, PatternType::Code);
assert_ne!(PatternType::Reference, PatternType::Code);
}
#[test]
fn test_pattern_type_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(PatternType::Reference);
set.insert(PatternType::Code);
set.insert(PatternType::Reference);
assert_eq!(set.len(), 2);
}
#[test]
fn test_pattern_type_serialization() {
let pt = PatternType::Reference;
let json = serde_json::to_string(&pt).unwrap();
assert_eq!(json, "\"reference\"");
let decoded: PatternType = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, PatternType::Reference);
let pt2 = PatternType::Code;
let json2 = serde_json::to_string(&pt2).unwrap();
assert_eq!(json2, "\"code\"");
}
#[test]
fn test_pattern_source_user_conversation() {
let source = PatternSource::user_conversation("User mentioned PR #123");
match source {
PatternSource::UserConversation { example } => {
assert_eq!(example, "User mentioned PR #123");
}
_ => panic!("Expected UserConversation variant"),
}
}
#[test]
fn test_pattern_source_project_code_style() {
let source = PatternSource::project_code_style("rust");
match source {
PatternSource::ProjectCodeStyle { language } => {
assert_eq!(language, "rust");
}
_ => panic!("Expected ProjectCodeStyle variant"),
}
}
#[test]
fn test_pattern_source_is_preset() {
assert!(PatternSource::SystemPreset.is_preset());
assert!(!PatternSource::user_conversation("test").is_preset());
assert!(!PatternSource::project_code_style("rust").is_preset());
assert!(!PatternSource::Manual.is_preset());
}
#[test]
fn test_pattern_source_is_manual() {
assert!(PatternSource::Manual.is_manual());
assert!(!PatternSource::SystemPreset.is_manual());
assert!(!PatternSource::user_conversation("test").is_manual());
assert!(!PatternSource::project_code_style("rust").is_manual());
}
#[test]
fn test_pattern_source_display_name() {
assert_eq!(PatternSource::user_conversation("test").display_name(), "用户对话");
assert_eq!(PatternSource::project_code_style("rust").display_name(), "项目风格");
assert_eq!(PatternSource::SystemPreset.display_name(), "系统预设");
assert_eq!(PatternSource::Manual.display_name(), "手动添加");
}
#[test]
fn test_pattern_source_serialization() {
let source = PatternSource::user_conversation("example context");
let json = serde_json::to_string(&source).unwrap();
let decoded: PatternSource = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, source);
let source2 = PatternSource::project_code_style("typescript");
let json2 = serde_json::to_string(&source2).unwrap();
let decoded2: PatternSource = serde_json::from_str(&json2).unwrap();
assert_eq!(decoded2, source2);
let source3 = PatternSource::SystemPreset;
let json3 = serde_json::to_string(&source3).unwrap();
assert!(json3.contains("system_preset"));
let source4 = PatternSource::Manual;
let json4 = serde_json::to_string(&source4).unwrap();
assert!(json4.contains("manual"));
}
#[test]
fn test_pattern_creation() {
let pattern = ConversationPattern::new(
PatternType::Reference,
r"PR #\d+",
PatternSource::user_conversation("User mentioned PR #123"),
);
assert!(pattern.is_active);
assert_eq!(pattern.frequency, 1);
assert_eq!(pattern.confidence, 0.5);
assert!(pattern.description.is_none());
assert!(pattern.tags.is_empty());
assert!(!pattern.id.is_empty()); }
#[test]
fn test_pattern_creation_with_all_types() {
let ref_pattern = ConversationPattern::new(
PatternType::Reference,
r"issue #\d+",
PatternSource::Manual,
);
assert_eq!(ref_pattern.pattern_type, PatternType::Reference);
let code_pattern = ConversationPattern::new(
PatternType::Code,
r"fn \w+\(",
PatternSource::SystemPreset,
);
assert_eq!(code_pattern.pattern_type, PatternType::Code);
}
#[test]
fn test_pattern_preset() {
let pattern = ConversationPattern::preset(PatternType::Reference, r"PR #\d+");
assert!(pattern.source.is_preset());
assert!(pattern.is_active);
assert_eq!(pattern.confidence, 1.0);
assert_eq!(pattern.frequency, 100); }
#[test]
fn test_pattern_manual() {
let pattern = ConversationPattern::manual(PatternType::Code, "custom-pattern");
assert!(pattern.source.is_manual());
assert!(pattern.is_active);
assert_eq!(pattern.confidence, 0.9);
}
#[test]
fn test_pattern_with_description() {
let pattern = ConversationPattern::new(
PatternType::Reference,
"test-pattern",
PatternSource::Manual,
)
.with_description("This is a test pattern");
assert_eq!(pattern.description, Some("This is a test pattern".to_string()));
}
#[test]
fn test_pattern_with_tag() {
let pattern = ConversationPattern::new(
PatternType::Code,
"test-pattern",
PatternSource::Manual,
)
.with_tag("rust")
.with_tag("async");
assert_eq!(pattern.tags, vec!["rust", "async"]);
}
#[test]
fn test_pattern_builder_chain() {
let pattern = ConversationPattern::preset(PatternType::Reference, r"\bPR\s*#\d+\b")
.with_description("Pull Request reference")
.with_tag("git")
.with_tag("github");
assert_eq!(pattern.pattern, r"\bPR\s*#\d+\b");
assert_eq!(pattern.description, Some("Pull Request reference".to_string()));
assert_eq!(pattern.tags, vec!["git", "github"]);
assert!(pattern.source.is_preset());
}
#[test]
fn test_pattern_mark_used() {
let mut pattern = ConversationPattern::new(
PatternType::Code,
"fn test()",
PatternSource::Manual,
);
let initial_confidence = pattern.confidence;
let initial_last_used = pattern.last_used;
pattern.mark_used();
assert_eq!(pattern.frequency, 2);
assert!(pattern.confidence > initial_confidence);
assert!(pattern.last_used >= initial_last_used);
}
#[test]
fn test_pattern_mark_used_confidence_cap() {
let mut pattern = ConversationPattern::new(
PatternType::Code,
"test",
PatternSource::Manual,
);
pattern.confidence = 0.999;
pattern.mark_used();
assert!(pattern.confidence <= 1.0);
}
#[test]
fn test_pattern_mark_used_frequency_overflow() {
let mut pattern = ConversationPattern::new(
PatternType::Code,
"test",
PatternSource::Manual,
);
pattern.frequency = u32::MAX - 1;
pattern.mark_used();
assert_eq!(pattern.frequency, u32::MAX);
}
#[test]
fn test_pattern_deactivate() {
let mut pattern = ConversationPattern::new(
PatternType::Reference,
"test",
PatternSource::Manual,
);
assert!(pattern.is_active);
pattern.deactivate();
assert!(!pattern.is_active);
}
#[test]
fn test_pattern_activate() {
let mut pattern = ConversationPattern::new(
PatternType::Reference,
"test",
PatternSource::Manual,
);
pattern.deactivate();
assert!(!pattern.is_active);
pattern.activate();
assert!(pattern.is_active);
}
#[test]
fn test_pattern_activate_deactivate_cycle() {
let mut pattern = ConversationPattern::preset(PatternType::Code, "test");
for _ in 0..3 {
pattern.deactivate();
assert!(!pattern.is_active);
pattern.activate();
assert!(pattern.is_active);
}
}
#[test]
fn test_format_line_active_high_frequency() {
let mut pattern = ConversationPattern::preset(PatternType::Reference, "test-pattern");
pattern.frequency = 15;
let line = pattern.format_line();
assert!(line.contains("🔗"));
assert!(line.contains("引用模式"));
assert!(line.contains("test-pattern"));
assert!(line.contains("freq: 15"));
assert!(line.contains("★")); assert!(!line.contains("[inactive]"));
}
#[test]
fn test_format_line_active_low_frequency() {
let pattern = ConversationPattern::new(
PatternType::Code,
"test-pattern",
PatternSource::Manual,
);
let line = pattern.format_line();
assert!(line.contains("💻"));
assert!(line.contains("代码模式"));
assert!(!line.contains("★")); assert!(!line.contains("[inactive]"));
}
#[test]
fn test_format_line_inactive() {
let mut pattern = ConversationPattern::preset(PatternType::Reference, "test-pattern");
pattern.deactivate();
let line = pattern.format_line();
assert!(line.contains("[inactive]"));
}
#[test]
fn test_format_for_prompt_with_description() {
let pattern = ConversationPattern::preset(PatternType::Reference, r"\bPR\s*#\d+\b")
.with_description("Pull Request reference format");
let prompt = pattern.format_for_prompt();
assert_eq!(prompt, r"\bPR\s*#\d+\b: Pull Request reference format");
}
#[test]
fn test_format_for_prompt_without_description() {
let pattern = ConversationPattern::preset(PatternType::Reference, "simple-pattern");
let prompt = pattern.format_for_prompt();
assert_eq!(prompt, "simple-pattern");
}
#[test]
fn test_serialization() {
let pattern = ConversationPattern::preset(PatternType::Reference, r"PR #\d+")
.with_description("Test pattern");
let json = serde_json::to_string(&pattern).unwrap();
let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.pattern, pattern.pattern);
assert_eq!(decoded.pattern_type, PatternType::Reference);
assert_eq!(decoded.description, Some("Test pattern".to_string()));
}
#[test]
fn test_serialization_with_tags() {
let pattern = ConversationPattern::preset(PatternType::Code, r"fn \w+")
.with_tag("rust")
.with_tag("function");
let json = serde_json::to_string(&pattern).unwrap();
let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.tags, vec!["rust", "function"]);
}
#[test]
fn test_serialization_roundtrip() {
let original = ConversationPattern::new(
PatternType::Reference,
r"issue #\d+",
PatternSource::user_conversation("User said issue #42"),
)
.with_description("Issue reference")
.with_tag("git");
let json = serde_json::to_string(&original).unwrap();
let decoded: ConversationPattern = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.id, original.id);
assert_eq!(decoded.pattern_type, original.pattern_type);
assert_eq!(decoded.pattern, original.pattern);
assert_eq!(decoded.source, original.source);
assert_eq!(decoded.frequency, original.frequency);
assert_eq!(decoded.confidence, original.confidence);
assert_eq!(decoded.is_active, original.is_active);
assert_eq!(decoded.description, original.description);
assert_eq!(decoded.tags, original.tags);
}
#[test]
fn test_empty_pattern_string() {
let pattern = ConversationPattern::new(
PatternType::Reference,
"",
PatternSource::Manual,
);
assert_eq!(pattern.pattern, "");
}
#[test]
fn test_special_regex_chars_in_pattern() {
let pattern = ConversationPattern::new(
PatternType::Code,
r"fn\s+\w+\s*\([^)]*\)\s*\{",
PatternSource::Manual,
);
assert_eq!(pattern.pattern, r"fn\s+\w+\s*\([^)]*\)\s*\{");
}
#[test]
fn test_unicode_pattern() {
let pattern = ConversationPattern::new(
PatternType::Reference,
"中文模式",
PatternSource::user_conversation("测试"),
);
assert_eq!(pattern.pattern, "中文模式");
}
#[test]
fn test_very_long_pattern() {
let long_pattern = "x".repeat(10000);
let pattern = ConversationPattern::new(
PatternType::Code,
long_pattern.clone(),
PatternSource::Manual,
);
assert_eq!(pattern.pattern.len(), 10000);
}
#[test]
fn test_confidence_boundary() {
let mut pattern = ConversationPattern::new(
PatternType::Code,
"test",
PatternSource::Manual,
);
pattern.confidence = 0.0;
assert_eq!(pattern.confidence, 0.0);
pattern.confidence = 1.0;
assert_eq!(pattern.confidence, 1.0);
}
#[test]
fn test_unique_ids() {
let p1 = ConversationPattern::new(PatternType::Code, "test", PatternSource::Manual);
let p2 = ConversationPattern::new(PatternType::Code, "test", PatternSource::Manual);
assert_ne!(p1.id, p2.id);
}
}