ricecoder_learning/
decision_capture_property.rs

1/// Property-based tests for decision capture completeness
2/// **Feature: ricecoder-learning, Property 1: Decision Capture Completeness**
3/// **Validates: Requirements 1.1**
4///
5/// Property: For any user decision made during code generation, the Learning System
6/// SHALL capture it with complete metadata (timestamp, context, decision type, input, output).
7
8#[cfg(test)]
9mod tests {
10    use crate::{Decision, DecisionContext, DecisionLogger};
11    use std::path::PathBuf;
12
13
14
15    /// Property 1: Decision Capture Completeness
16    /// For any decision, when captured, all metadata fields should be preserved
17    #[tokio::test]
18    async fn prop_decision_capture_preserves_all_metadata() {
19        let logger = DecisionLogger::new();
20
21        let context = DecisionContext {
22            project_path: PathBuf::from("/project1"),
23            file_path: PathBuf::from("/project1/src/main.rs"),
24            line_number: 42,
25            agent_type: "code_generator".to_string(),
26        };
27
28        let decision = Decision::new(
29            context.clone(),
30            "code_generation".to_string(),
31            serde_json::json!({"input": "test"}),
32            serde_json::json!({"output": "result"}),
33        );
34
35        let decision_id = decision.id.clone();
36        let decision_type = decision.decision_type.clone();
37
38        // Capture the decision
39        let result = logger.log_decision(decision).await;
40        assert!(result.is_ok());
41        assert_eq!(result.unwrap(), decision_id);
42
43        // Retrieve the decision
44        let retrieved = logger.get_decision(&decision_id).await;
45        assert!(retrieved.is_ok());
46
47        let retrieved_decision = retrieved.unwrap();
48
49        // Verify all metadata is preserved
50        assert_eq!(retrieved_decision.id, decision_id);
51        assert_eq!(retrieved_decision.decision_type, decision_type);
52        assert_eq!(retrieved_decision.context.project_path, context.project_path);
53        assert_eq!(retrieved_decision.context.file_path, context.file_path);
54        assert_eq!(retrieved_decision.context.line_number, context.line_number);
55        assert_eq!(retrieved_decision.context.agent_type, context.agent_type);
56        assert!(retrieved_decision.timestamp.timestamp() > 0);
57    }
58
59    /// Property 1: Decision Capture Completeness (History)
60    /// For any sequence of decisions, all should be captured in history
61    #[tokio::test]
62    async fn prop_all_decisions_appear_in_history() {
63        let logger = DecisionLogger::new();
64
65        let mut decision_ids = Vec::new();
66        for i in 0..50 {
67            let context = DecisionContext {
68                project_path: PathBuf::from(format!("/project{}", i % 5)),
69                file_path: PathBuf::from(format!("/project{}/src/file{}.rs", i % 5, i)),
70                line_number: (i * 10) as u32,
71                agent_type: format!("agent_{}", i % 3),
72            };
73
74            let decision = Decision::new(
75                context,
76                format!("type_{}", i % 4),
77                serde_json::json!({"index": i}),
78                serde_json::json!({"result": i * 2}),
79            );
80
81            decision_ids.push(decision.id.clone());
82            let result = logger.log_decision(decision).await;
83            assert!(result.is_ok());
84        }
85
86        // Verify all decisions appear in history
87        let history = logger.get_history().await;
88        assert_eq!(history.len(), decision_ids.len());
89
90        for (i, decision_id) in decision_ids.iter().enumerate() {
91            assert_eq!(history[i].id, *decision_id);
92        }
93    }
94
95    /// Property 1: Decision Capture Completeness (By Type)
96    /// For any sequence of decisions with different types, filtering by type should return all matching decisions
97    #[tokio::test]
98    async fn prop_decisions_filterable_by_type() {
99        let logger = DecisionLogger::new();
100
101        let mut type_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
102
103        for i in 0..50 {
104            let context = DecisionContext {
105                project_path: PathBuf::from("/project"),
106                file_path: PathBuf::from("/project/src/main.rs"),
107                line_number: i as u32,
108                agent_type: "agent".to_string(),
109            };
110
111            let decision_type = format!("type_{}", i % 4);
112            *type_counts.entry(decision_type.clone()).or_insert(0) += 1;
113
114            let decision = Decision::new(
115                context,
116                decision_type,
117                serde_json::json!({}),
118                serde_json::json!({}),
119            );
120
121            let result = logger.log_decision(decision).await;
122            assert!(result.is_ok());
123        }
124
125        // Verify filtering works for each type
126        for (decision_type, expected_count) in type_counts {
127            let filtered = logger.get_history_by_type(&decision_type).await;
128            assert_eq!(filtered.len(), expected_count);
129
130            // Verify all filtered decisions have the correct type
131            for decision in filtered {
132                assert_eq!(decision.decision_type, decision_type);
133            }
134        }
135    }
136
137    /// Property 1: Decision Capture Completeness (By Context)
138    /// For any sequence of decisions with different contexts, filtering by context should return all matching decisions
139    #[tokio::test]
140    async fn prop_decisions_filterable_by_context() {
141        let logger = DecisionLogger::new();
142
143        let context1 = DecisionContext {
144            project_path: PathBuf::from("/project1"),
145            file_path: PathBuf::from("/project1/src/main.rs"),
146            line_number: 10,
147            agent_type: "agent1".to_string(),
148        };
149
150        let context2 = DecisionContext {
151            project_path: PathBuf::from("/project2"),
152            file_path: PathBuf::from("/project2/src/lib.rs"),
153            line_number: 20,
154            agent_type: "agent2".to_string(),
155        };
156
157        // Capture decisions for context1
158        for i in 0..25 {
159            let decision = Decision::new(
160                context1.clone(),
161                format!("type_{}", i % 3),
162                serde_json::json!({}),
163                serde_json::json!({}),
164            );
165            let result = logger.log_decision(decision).await;
166            assert!(result.is_ok());
167        }
168
169        // Capture decisions for context2
170        for i in 0..25 {
171            let decision = Decision::new(
172                context2.clone(),
173                format!("type_{}", i % 3),
174                serde_json::json!({}),
175                serde_json::json!({}),
176            );
177            let result = logger.log_decision(decision).await;
178            assert!(result.is_ok());
179        }
180
181        // Verify filtering works for each context
182        let context1_decisions = logger.get_history_by_context(&context1).await;
183        assert_eq!(context1_decisions.len(), 25);
184        for decision in context1_decisions {
185            assert_eq!(decision.context.project_path, context1.project_path);
186            assert_eq!(decision.context.file_path, context1.file_path);
187        }
188
189        let context2_decisions = logger.get_history_by_context(&context2).await;
190        assert_eq!(context2_decisions.len(), 25);
191        for decision in context2_decisions {
192            assert_eq!(decision.context.project_path, context2.project_path);
193            assert_eq!(decision.context.file_path, context2.file_path);
194        }
195    }
196
197    /// Property 1: Decision Capture Completeness (Replay)
198    /// For any sequence of decisions, replaying should return them in the same order
199    #[tokio::test]
200    async fn prop_replay_preserves_decision_order() {
201        let logger = DecisionLogger::new();
202
203        let mut decision_ids = Vec::new();
204        for i in 0..50 {
205            let context = DecisionContext {
206                project_path: PathBuf::from("/project"),
207                file_path: PathBuf::from("/project/src/main.rs"),
208                line_number: i as u32,
209                agent_type: "agent".to_string(),
210            };
211
212            let decision = Decision::new(
213                context,
214                format!("type_{}", i % 4),
215                serde_json::json!({}),
216                serde_json::json!({}),
217            );
218
219            decision_ids.push(decision.id.clone());
220            let result = logger.log_decision(decision).await;
221            assert!(result.is_ok());
222        }
223
224        // Verify replay returns decisions in the same order
225        let replayed = logger.replay_decisions().await;
226        assert_eq!(replayed.len(), decision_ids.len());
227
228        for (i, decision_id) in decision_ids.iter().enumerate() {
229            assert_eq!(replayed[i].id, *decision_id);
230        }
231    }
232
233    /// Property 1: Decision Capture Completeness (Count)
234    /// For any sequence of decisions, the count should match the number captured
235    #[tokio::test]
236    async fn prop_decision_count_matches_captured() {
237        let logger = DecisionLogger::new();
238
239        assert_eq!(logger.decision_count().await, 0);
240
241        for i in 0..50 {
242            let context = DecisionContext {
243                project_path: PathBuf::from("/project"),
244                file_path: PathBuf::from("/project/src/main.rs"),
245                line_number: i as u32,
246                agent_type: "agent".to_string(),
247            };
248
249            let decision = Decision::new(
250                context,
251                "type".to_string(),
252                serde_json::json!({}),
253                serde_json::json!({}),
254            );
255
256            let result = logger.log_decision(decision).await;
257            assert!(result.is_ok());
258            assert_eq!(logger.decision_count().await, i + 1);
259        }
260
261        assert_eq!(logger.decision_count().await, 50);
262    }
263
264    /// Property 1: Decision Capture Completeness (Statistics)
265    /// For any sequence of decisions, statistics should accurately reflect captured decisions
266    #[tokio::test]
267    async fn prop_statistics_accurately_reflect_decisions() {
268        let logger = DecisionLogger::new();
269
270        let mut expected_type_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
271        let mut expected_agent_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
272
273        for i in 0..50 {
274            let context = DecisionContext {
275                project_path: PathBuf::from("/project"),
276                file_path: PathBuf::from("/project/src/main.rs"),
277                line_number: i as u32,
278                agent_type: format!("agent_{}", i % 3),
279            };
280
281            let decision_type = format!("type_{}", i % 4);
282
283            *expected_type_counts.entry(decision_type.clone()).or_insert(0) += 1;
284            *expected_agent_counts
285                .entry(context.agent_type.clone())
286                .or_insert(0) += 1;
287
288            let decision = Decision::new(
289                context,
290                decision_type,
291                serde_json::json!({}),
292                serde_json::json!({}),
293            );
294
295            let result = logger.log_decision(decision).await;
296            assert!(result.is_ok());
297        }
298
299        // Get statistics
300        let stats = logger.get_statistics().await;
301
302        // Verify total count
303        assert_eq!(stats.total_decisions, 50);
304
305        // Verify decision type counts
306        for (decision_type, expected_count) in expected_type_counts {
307            assert_eq!(stats.decision_types.get(&decision_type), Some(&expected_count));
308        }
309
310        // Verify agent type counts
311        for (agent_type, expected_count) in expected_agent_counts {
312            assert_eq!(stats.agent_types.get(&agent_type), Some(&expected_count));
313        }
314    }
315}