ricecoder_learning/
pattern_extraction_property.rs

1/// Property-based tests for pattern extraction consistency
2/// **Feature: ricecoder-learning, Property 5: Pattern Extraction Consistency**
3/// **Validates: Requirements 3.1, 3.2**
4
5#[cfg(test)]
6mod tests {
7    use proptest::prelude::*;
8    use crate::{Decision, DecisionContext, PatternCapturer};
9    use std::path::PathBuf;
10
11    /// Strategy for generating decision contexts
12    fn decision_context_strategy() -> impl Strategy<Value = DecisionContext> {
13        (
14            "/project",
15            "/project/src/main.rs",
16            0u32..1000,
17            "test_agent",
18        )
19            .prop_map(|(project, file, line, agent)| DecisionContext {
20                project_path: PathBuf::from(project),
21                file_path: PathBuf::from(file),
22                line_number: line,
23                agent_type: agent.to_string(),
24            })
25    }
26
27    /// Strategy for generating JSON values
28    fn json_value_strategy() -> impl Strategy<Value = serde_json::Value> {
29        prop_oneof![
30            Just(serde_json::json!({})),
31            Just(serde_json::json!({"key": "value"})),
32            Just(serde_json::json!({"number": 42})),
33            Just(serde_json::json!({"array": [1, 2, 3]})),
34            Just(serde_json::json!({"nested": {"inner": "value"}})),
35        ]
36    }
37
38    /// Strategy for generating decisions
39    fn decision_strategy() -> impl Strategy<Value = Decision> {
40        (
41            decision_context_strategy(),
42            "code_generation|refactoring|analysis",
43            json_value_strategy(),
44            json_value_strategy(),
45        )
46            .prop_map(|(context, decision_type, input, output)| {
47                Decision::new(context, decision_type.to_string(), input, output)
48            })
49    }
50
51    /// Property 5: Pattern Extraction Consistency
52    /// For any set of repeated decisions, the Pattern Capturer SHALL extract identical
53    /// patterns when processing the same decision history.
54    #[test]
55    fn prop_pattern_extraction_consistency(
56    ) {
57        proptest!(|(decisions in prop::collection::vec(decision_strategy(), 2..20))| {
58            let capturer = PatternCapturer::new();
59
60            // Extract patterns twice from the same decision history
61            let patterns1 = capturer.extract_patterns(&decisions).expect("First extraction failed");
62            let patterns2 = capturer.extract_patterns(&decisions).expect("Second extraction failed");
63
64            // Both extractions should produce the same number of patterns
65            prop_assert_eq!(
66                patterns1.len(),
67                patterns2.len(),
68                "Pattern count should be consistent"
69            );
70
71            // Sort patterns by type and occurrences for comparison
72            let mut sorted1 = patterns1.clone();
73            let mut sorted2 = patterns2.clone();
74            sorted1.sort_by(|a, b| a.pattern_type.cmp(&b.pattern_type).then(b.occurrences.cmp(&a.occurrences)));
75            sorted2.sort_by(|a, b| a.pattern_type.cmp(&b.pattern_type).then(b.occurrences.cmp(&a.occurrences)));
76
77            // Patterns should have identical properties
78            for (p1, p2) in sorted1.iter().zip(sorted2.iter()) {
79                prop_assert_eq!(&p1.pattern_type, &p2.pattern_type, "Pattern types should match");
80                prop_assert_eq!(&p1.description, &p2.description, "Pattern descriptions should match");
81                prop_assert_eq!(p1.occurrences, p2.occurrences, "Pattern occurrences should match");
82                prop_assert_eq!(
83                    p1.examples.len(),
84                    p2.examples.len(),
85                    "Pattern examples count should match"
86                );
87
88                // Confidence should be bounded
89                prop_assert!(p1.confidence >= 0.0 && p1.confidence <= 1.0, "Confidence should be bounded");
90                prop_assert!(p2.confidence >= 0.0 && p2.confidence <= 1.0, "Confidence should be bounded");
91            }
92        });
93    }
94
95    /// Property: Pattern extraction should be deterministic
96    /// For any decision history, extracting patterns multiple times should always
97    /// produce identical results
98    #[test]
99    fn prop_pattern_extraction_deterministic() {
100        proptest!(|(decisions in prop::collection::vec(decision_strategy(), 2..20))| {
101            let capturer = PatternCapturer::new();
102
103            // Extract patterns multiple times
104            let patterns1 = capturer.extract_patterns(&decisions).expect("Extraction 1 failed");
105            let patterns2 = capturer.extract_patterns(&decisions).expect("Extraction 2 failed");
106            let patterns3 = capturer.extract_patterns(&decisions).expect("Extraction 3 failed");
107
108            // All extractions should produce the same patterns
109            prop_assert_eq!(patterns1.len(), patterns2.len(), "Extraction 1 and 2 should match");
110            prop_assert_eq!(patterns2.len(), patterns3.len(), "Extraction 2 and 3 should match");
111
112            // Sort patterns for comparison
113            let mut sorted1 = patterns1.clone();
114            let mut sorted2 = patterns2.clone();
115            let mut sorted3 = patterns3.clone();
116            sorted1.sort_by(|a, b| a.pattern_type.cmp(&b.pattern_type).then(b.occurrences.cmp(&a.occurrences)));
117            sorted2.sort_by(|a, b| a.pattern_type.cmp(&b.pattern_type).then(b.occurrences.cmp(&a.occurrences)));
118            sorted3.sort_by(|a, b| a.pattern_type.cmp(&b.pattern_type).then(b.occurrences.cmp(&a.occurrences)));
119
120            // Verify pattern properties are identical across all extractions
121            for (p1, p2) in sorted1.iter().zip(sorted2.iter()) {
122                prop_assert_eq!(&p1.pattern_type, &p2.pattern_type, "Type should match");
123                prop_assert_eq!(p1.occurrences, p2.occurrences, "Occurrences should match");
124            }
125
126            for (p2, p3) in sorted2.iter().zip(sorted3.iter()) {
127                prop_assert_eq!(&p2.pattern_type, &p3.pattern_type, "Type should match");
128                prop_assert_eq!(p2.occurrences, p3.occurrences, "Occurrences should match");
129            }
130        });
131    }
132
133    /// Property: Pattern extraction should preserve decision information
134    /// For any extracted pattern, all examples should be present in the original decisions
135    #[test]
136    fn prop_pattern_examples_from_decisions() {
137        proptest!(|(decisions in prop::collection::vec(decision_strategy(), 2..20))| {
138            let capturer = PatternCapturer::new();
139            let patterns = capturer.extract_patterns(&decisions).expect("Extraction failed");
140
141            for pattern in patterns {
142                // Each example in the pattern should correspond to a decision
143                for example in &pattern.examples {
144                    let found = decisions.iter().any(|d| {
145                        d.decision_type == pattern.pattern_type
146                            && d.input == example.input
147                            && d.output == example.output
148                    });
149
150                    prop_assert!(
151                        found,
152                        "Pattern example should come from original decisions"
153                    );
154                }
155            }
156        });
157    }
158
159    /// Property: Pattern extraction should handle empty input
160    /// For an empty decision list, pattern extraction should return an empty list
161    #[test]
162    fn prop_pattern_extraction_empty_input() {
163        let capturer = PatternCapturer::new();
164        let patterns = capturer.extract_patterns(&[]).expect("Extraction failed");
165        assert!(patterns.is_empty(), "Empty input should produce empty patterns");
166    }
167
168    /// Property: Pattern extraction should respect minimum occurrences
169    /// For decisions with fewer occurrences than the minimum, no patterns should be extracted
170    #[test]
171    fn prop_pattern_extraction_respects_minimum() {
172        proptest!(|(decision in decision_strategy())| {
173            let capturer = PatternCapturer::with_settings(5, 0.5);
174
175            // Single decision should not produce patterns
176            let patterns = capturer.extract_patterns(&[decision]).expect("Extraction failed");
177            prop_assert!(patterns.is_empty(), "Single decision should not produce patterns");
178        });
179    }
180
181    /// Property: Pattern confidence should be bounded
182    /// For any pattern, confidence should always be between 0 and 1
183    #[test]
184    fn prop_pattern_confidence_consistency() {
185        proptest!(|(decisions in prop::collection::vec(decision_strategy(), 2..20))| {
186            let capturer = PatternCapturer::new();
187
188            let patterns1 = capturer.extract_patterns(&decisions).expect("Extraction 1 failed");
189            let patterns2 = capturer.extract_patterns(&decisions).expect("Extraction 2 failed");
190
191            // All patterns should have bounded confidence
192            for pattern in patterns1.iter().chain(patterns2.iter()) {
193                prop_assert!(
194                    pattern.confidence >= 0.0 && pattern.confidence <= 1.0,
195                    "Confidence should be bounded: {}",
196                    pattern.confidence
197                );
198            }
199        });
200    }
201
202    /// Property: Pattern extraction should be order-independent for identical decisions
203    /// For a set of identical decisions in different orders, patterns should be the same
204    #[test]
205    fn prop_pattern_extraction_order_independent() {
206        proptest!(|(decision in decision_strategy())| {
207            let capturer = PatternCapturer::new();
208
209            // Create multiple copies of the same decision
210            let decisions = vec![decision.clone(), decision.clone(), decision.clone()];
211
212            // Extract patterns
213            let patterns1 = capturer.extract_patterns(&decisions).expect("Extraction 1 failed");
214
215            // Reverse the order
216            let mut reversed = decisions.clone();
217            reversed.reverse();
218            let patterns2 = capturer.extract_patterns(&reversed).expect("Extraction 2 failed");
219
220            // Should produce the same patterns
221            prop_assert_eq!(patterns1.len(), patterns2.len(), "Pattern count should match");
222
223            for (p1, p2) in patterns1.iter().zip(patterns2.iter()) {
224                prop_assert_eq!(&p1.pattern_type, &p2.pattern_type, "Pattern type should match");
225                prop_assert_eq!(p1.occurrences, p2.occurrences, "Occurrences should match");
226            }
227        });
228    }
229}