ricecoder_learning/
scope_precedence_property.rs

1/// Property-based tests for scope precedence enforcement
2/// **Feature: ricecoder-learning, Property 3: Scope Precedence Enforcement**
3/// **Validates: Requirements 2.1**
4
5#[cfg(test)]
6mod tests {
7    use proptest::prelude::*;
8    use crate::{ConflictResolver, Rule, RuleScope, RuleSource};
9
10    proptest! {
11        /// Property: Project rules override global rules when both exist
12        /// For any conflicting rules in different scopes, the Learning System SHALL apply
13        /// project rules over global rules when both exist.
14        #[test]
15        fn prop_project_rules_override_global(
16            project_pattern in "[a-z0-9]{1,20}",
17            project_action in "[a-z0-9]{1,20}",
18            global_action in "[a-z0-9]{1,20}",
19        ) {
20            // Create project and global rules with the same pattern
21            let mut project_rule = Rule::new(
22                RuleScope::Project,
23                project_pattern.clone(),
24                project_action,
25                RuleSource::Learned,
26            );
27            project_rule.id = "project_rule".to_string();
28
29            let mut global_rule = Rule::new(
30                RuleScope::Global,
31                project_pattern.clone(),
32                global_action,
33                RuleSource::Learned,
34            );
35            global_rule.id = "global_rule".to_string();
36
37            let rules = vec![global_rule.clone(), project_rule.clone()];
38
39            // Apply precedence
40            let selected = ConflictResolver::apply_precedence(&rules);
41
42            // Project rule should be selected
43            prop_assert!(selected.is_some());
44            prop_assert_eq!(selected.unwrap().id, "project_rule");
45        }
46
47        /// Property: Global rules override session rules when both exist
48        /// For any conflicting rules in different scopes, the Learning System SHALL apply
49        /// global rules over session rules when both exist.
50        #[test]
51        fn prop_global_rules_override_session(
52            pattern in "[a-z0-9]{1,20}",
53            global_action in "[a-z0-9]{1,20}",
54            session_action in "[a-z0-9]{1,20}",
55        ) {
56            let mut global_rule = Rule::new(
57                RuleScope::Global,
58                pattern.clone(),
59                global_action,
60                RuleSource::Learned,
61            );
62            global_rule.id = "global_rule".to_string();
63
64            let mut session_rule = Rule::new(
65                RuleScope::Session,
66                pattern,
67                session_action,
68                RuleSource::Learned,
69            );
70            session_rule.id = "session_rule".to_string();
71
72            let rules = vec![session_rule.clone(), global_rule.clone()];
73
74            // Apply precedence
75            let selected = ConflictResolver::apply_precedence(&rules);
76
77            // Global rule should be selected
78            prop_assert!(selected.is_some());
79            prop_assert_eq!(selected.unwrap().id, "global_rule");
80        }
81
82        /// Property: Project rules override session rules when both exist
83        /// For any conflicting rules in different scopes, the Learning System SHALL apply
84        /// project rules over session rules when both exist.
85        #[test]
86        fn prop_project_rules_override_session(
87            pattern in "[a-z0-9]{1,20}",
88            project_action in "[a-z0-9]{1,20}",
89            session_action in "[a-z0-9]{1,20}",
90        ) {
91            let mut project_rule = Rule::new(
92                RuleScope::Project,
93                pattern.clone(),
94                project_action,
95                RuleSource::Learned,
96            );
97            project_rule.id = "project_rule".to_string();
98
99            let mut session_rule = Rule::new(
100                RuleScope::Session,
101                pattern,
102                session_action,
103                RuleSource::Learned,
104            );
105            session_rule.id = "session_rule".to_string();
106
107            let rules = vec![session_rule.clone(), project_rule.clone()];
108
109            // Apply precedence
110            let selected = ConflictResolver::apply_precedence(&rules);
111
112            // Project rule should be selected
113            prop_assert!(selected.is_some());
114            prop_assert_eq!(selected.unwrap().id, "project_rule");
115        }
116
117        /// Property: Project > Global > Session precedence is always maintained
118        /// For any set of rules with all three scopes, the precedence order
119        /// Project > Global > Session must be maintained.
120        #[test]
121        fn prop_full_precedence_order(
122            pattern in "[a-z0-9]{1,20}",
123            project_action in "[a-z0-9]{1,20}",
124            global_action in "[a-z0-9]{1,20}",
125            session_action in "[a-z0-9]{1,20}",
126        ) {
127            let mut project_rule = Rule::new(
128                RuleScope::Project,
129                pattern.clone(),
130                project_action,
131                RuleSource::Learned,
132            );
133            project_rule.id = "project_rule".to_string();
134
135            let mut global_rule = Rule::new(
136                RuleScope::Global,
137                pattern.clone(),
138                global_action,
139                RuleSource::Learned,
140            );
141            global_rule.id = "global_rule".to_string();
142
143            let mut session_rule = Rule::new(
144                RuleScope::Session,
145                pattern,
146                session_action,
147                RuleSource::Learned,
148            );
149            session_rule.id = "session_rule".to_string();
150
151            // Test all permutations of rule order
152            let permutations = vec![
153                vec![project_rule.clone(), global_rule.clone(), session_rule.clone()],
154                vec![project_rule.clone(), session_rule.clone(), global_rule.clone()],
155                vec![global_rule.clone(), project_rule.clone(), session_rule.clone()],
156                vec![global_rule.clone(), session_rule.clone(), project_rule.clone()],
157                vec![session_rule.clone(), project_rule.clone(), global_rule.clone()],
158                vec![session_rule.clone(), global_rule.clone(), project_rule.clone()],
159            ];
160
161            for rules in permutations {
162                let selected = ConflictResolver::apply_precedence(&rules);
163                prop_assert!(selected.is_some());
164                // Project rule should always be selected regardless of order
165                prop_assert_eq!(selected.unwrap().id, "project_rule");
166            }
167        }
168
169        /// Property: Resolve conflicts maintains precedence for multiple patterns
170        /// When resolving conflicts across multiple patterns, each pattern group
171        /// should have the highest precedence rule selected.
172        #[test]
173        fn prop_resolve_conflicts_maintains_precedence(
174            pattern1 in "[a-z0-9]{1,20}",
175            pattern2 in "[a-z0-9]{1,20}",
176        ) {
177            prop_assume!(pattern1 != pattern2);
178
179            let mut project_rule1 = Rule::new(
180                RuleScope::Project,
181                pattern1.clone(),
182                "project_action1".to_string(),
183                RuleSource::Learned,
184            );
185            project_rule1.id = "project_rule1".to_string();
186
187            let mut global_rule1 = Rule::new(
188                RuleScope::Global,
189                pattern1.clone(),
190                "global_action1".to_string(),
191                RuleSource::Learned,
192            );
193            global_rule1.id = "global_rule1".to_string();
194
195            let mut global_rule2 = Rule::new(
196                RuleScope::Global,
197                pattern2.clone(),
198                "global_action2".to_string(),
199                RuleSource::Learned,
200            );
201            global_rule2.id = "global_rule2".to_string();
202
203            let mut session_rule2 = Rule::new(
204                RuleScope::Session,
205                pattern2.clone(),
206                "session_action2".to_string(),
207                RuleSource::Learned,
208            );
209            session_rule2.id = "session_rule2".to_string();
210
211            let rules = vec![
212                project_rule1.clone(),
213                global_rule1.clone(),
214                global_rule2.clone(),
215                session_rule2.clone(),
216            ];
217
218            let resolved = ConflictResolver::resolve_conflicts(&rules).unwrap();
219
220            // Should have 2 rules (one per pattern)
221            prop_assert_eq!(resolved.len(), 2);
222
223            // Pattern 1 should have project rule
224            let pattern1_rule = resolved.iter().find(|r| r.pattern == pattern1);
225            prop_assert!(pattern1_rule.is_some());
226            prop_assert_eq!(pattern1_rule.unwrap().id.as_str(), "project_rule1");
227
228            // Pattern 2 should have global rule (higher precedence than session)
229            let pattern2_rule = resolved.iter().find(|r| r.pattern == pattern2);
230            prop_assert!(pattern2_rule.is_some());
231            prop_assert_eq!(pattern2_rule.unwrap().id.as_str(), "global_rule2");
232        }
233
234        /// Property: Get highest priority rule returns project rule when available
235        /// For any pattern with rules in multiple scopes, get_highest_priority_rule
236        /// should return the project rule if available.
237        #[test]
238        fn prop_get_highest_priority_returns_project(
239            pattern in "[a-z0-9]{1,20}",
240        ) {
241            let mut project_rule = Rule::new(
242                RuleScope::Project,
243                pattern.clone(),
244                "project_action".to_string(),
245                RuleSource::Learned,
246            );
247            project_rule.id = "project_rule".to_string();
248
249            let mut global_rule = Rule::new(
250                RuleScope::Global,
251                pattern.clone(),
252                "global_action".to_string(),
253                RuleSource::Learned,
254            );
255            global_rule.id = "global_rule".to_string();
256
257            let mut session_rule = Rule::new(
258                RuleScope::Session,
259                pattern.clone(),
260                "session_action".to_string(),
261                RuleSource::Learned,
262            );
263            session_rule.id = "session_rule".to_string();
264
265            let rules = vec![global_rule, session_rule, project_rule.clone()];
266
267            let highest = ConflictResolver::get_highest_priority_rule(&rules, &pattern);
268
269            prop_assert!(highest.is_some());
270            prop_assert_eq!(highest.unwrap().id, "project_rule");
271        }
272
273        /// Property: Get highest priority rule returns global when project unavailable
274        /// For any pattern with rules in global and session scopes (no project),
275        /// get_highest_priority_rule should return the global rule.
276        #[test]
277        fn prop_get_highest_priority_returns_global_when_no_project(
278            pattern in "[a-z0-9]{1,20}",
279        ) {
280            let mut global_rule = Rule::new(
281                RuleScope::Global,
282                pattern.clone(),
283                "global_action".to_string(),
284                RuleSource::Learned,
285            );
286            global_rule.id = "global_rule".to_string();
287
288            let mut session_rule = Rule::new(
289                RuleScope::Session,
290                pattern.clone(),
291                "session_action".to_string(),
292                RuleSource::Learned,
293            );
294            session_rule.id = "session_rule".to_string();
295
296            let rules = vec![session_rule, global_rule.clone()];
297
298            let highest = ConflictResolver::get_highest_priority_rule(&rules, &pattern);
299
300            prop_assert!(highest.is_some());
301            prop_assert_eq!(highest.unwrap().id, "global_rule");
302        }
303
304        /// Property: Scope precedence is independent of rule order
305        /// The precedence order should be maintained regardless of the order
306        /// in which rules are provided.
307        #[test]
308        fn prop_precedence_independent_of_order(
309            pattern in "[a-z0-9]{1,20}",
310        ) {
311            let mut project_rule = Rule::new(
312                RuleScope::Project,
313                pattern.clone(),
314                "project_action".to_string(),
315                RuleSource::Learned,
316            );
317            project_rule.id = "project_rule".to_string();
318
319            let mut global_rule = Rule::new(
320                RuleScope::Global,
321                pattern.clone(),
322                "global_action".to_string(),
323                RuleSource::Learned,
324            );
325            global_rule.id = "global_rule".to_string();
326
327            let mut session_rule = Rule::new(
328                RuleScope::Session,
329                pattern.clone(),
330                "session_action".to_string(),
331                RuleSource::Learned,
332            );
333            session_rule.id = "session_rule".to_string();
334
335            // Test multiple orderings
336            let orderings = vec![
337                vec![project_rule.clone(), global_rule.clone(), session_rule.clone()],
338                vec![session_rule.clone(), project_rule.clone(), global_rule.clone()],
339                vec![global_rule.clone(), session_rule.clone(), project_rule.clone()],
340            ];
341
342            let mut results = Vec::new();
343            for rules in orderings {
344                let selected = ConflictResolver::apply_precedence(&rules);
345                results.push(selected.map(|r| r.id));
346            }
347
348            // All results should be the same (project_rule)
349            prop_assert!(results.iter().all(|r| r == &Some("project_rule".to_string())));
350        }
351
352        /// Property: Cross-scope conflict detection works correctly
353        /// When checking for conflicts between project and global rules,
354        /// conflicts should be detected when patterns match but actions differ.
355        #[test]
356        fn prop_cross_scope_conflict_detection(
357            pattern in "[a-z0-9]{1,20}",
358            project_action in "[a-z0-9]{1,20}",
359            global_action in "[a-z0-9]{1,20}",
360        ) {
361            prop_assume!(project_action != global_action);
362
363            let mut project_rule = Rule::new(
364                RuleScope::Project,
365                pattern.clone(),
366                project_action,
367                RuleSource::Learned,
368            );
369            project_rule.id = "project_rule".to_string();
370
371            let mut global_rule = Rule::new(
372                RuleScope::Global,
373                pattern,
374                global_action,
375                RuleSource::Learned,
376            );
377            global_rule.id = "global_rule".to_string();
378
379            let conflicts = ConflictResolver::check_cross_scope_conflicts(
380                &[project_rule.clone()],
381                &[global_rule.clone()],
382            );
383
384            // Should detect the conflict
385            prop_assert_eq!(conflicts.len(), 1);
386            prop_assert_eq!(conflicts[0].0.id.as_str(), "project_rule");
387            prop_assert_eq!(conflicts[0].1.id.as_str(), "global_rule");
388        }
389
390        /// Property: No conflicts when patterns differ
391        /// When rules have different patterns, no conflicts should be detected
392        /// even if they have different actions.
393        #[test]
394        fn prop_no_conflict_different_patterns(
395            pattern1 in "[a-z0-9]{1,20}",
396            pattern2 in "[a-z0-9]{1,20}",
397        ) {
398            prop_assume!(pattern1 != pattern2);
399
400            let mut project_rule = Rule::new(
401                RuleScope::Project,
402                pattern1,
403                "action1".to_string(),
404                RuleSource::Learned,
405            );
406            project_rule.id = "project_rule".to_string();
407
408            let mut global_rule = Rule::new(
409                RuleScope::Global,
410                pattern2,
411                "action2".to_string(),
412                RuleSource::Learned,
413            );
414            global_rule.id = "global_rule".to_string();
415
416            let conflicts = ConflictResolver::check_cross_scope_conflicts(
417                &[project_rule],
418                &[global_rule],
419            );
420
421            // Should not detect any conflicts
422            prop_assert_eq!(conflicts.len(), 0);
423        }
424    }
425}