ricecoder_learning/
scope_isolation_property.rs

1/// Property-based tests for scope configuration isolation
2/// **Feature: ricecoder-learning, Property 8: Scope Configuration Isolation**
3/// **Validates: Requirements 1.5, 1.6, 1.7**
4
5#[cfg(test)]
6mod tests {
7    use crate::models::{Rule, RuleScope, RuleSource};
8    use crate::scope_config::ScopeFilter;
9    use proptest::prelude::*;
10
11    /// Strategy for generating random rule scopes
12    fn arb_rule_scope() -> impl Strategy<Value = RuleScope> {
13        prop_oneof![
14            Just(RuleScope::Global),
15            Just(RuleScope::Project),
16            Just(RuleScope::Session),
17        ]
18    }
19
20    /// Strategy for generating random rules
21    fn arb_rule(scope: RuleScope) -> impl Strategy<Value = Rule> {
22        (
23            "[a-z]{1,10}",
24            "[a-z]{1,10}",
25        )
26            .prop_map(move |(pattern, action)| {
27                Rule::new(
28                    scope,
29                    pattern,
30                    action,
31                    RuleSource::Learned,
32                )
33            })
34    }
35
36    /// Strategy for generating random rule collections
37    fn arb_rules_for_scope(scope: RuleScope) -> impl Strategy<Value = Vec<Rule>> {
38        prop::collection::vec(arb_rule(scope), 0..20)
39    }
40
41    /// Property 8: Scope Configuration Isolation
42    /// Test that rules in one scope don't affect other scopes
43    /// For any set of rules in different scopes, filtering by scope should return
44    /// only rules from that scope, and rules from one scope should not interfere
45    /// with rules from another scope unless they have the same pattern.
46    proptest! {
47        #[test]
48        fn prop_scope_isolation_no_cross_scope_interference(
49            global_rules in arb_rules_for_scope(RuleScope::Global),
50            project_rules in arb_rules_for_scope(RuleScope::Project),
51            session_rules in arb_rules_for_scope(RuleScope::Session),
52        ) {
53            // Combine all rules
54            let mut all_rules = global_rules.clone();
55            all_rules.extend(project_rules.clone());
56            all_rules.extend(session_rules.clone());
57
58            // Filter by each scope
59            let filtered_global = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Global);
60            let filtered_project = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Project);
61            let filtered_session = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Session);
62
63            // Verify that filtered rules match the original rules for each scope
64            prop_assert_eq!(filtered_global.len(), global_rules.len());
65            prop_assert_eq!(filtered_project.len(), project_rules.len());
66            prop_assert_eq!(filtered_session.len(), session_rules.len());
67
68            // Verify that all filtered rules have the correct scope
69            for rule in &filtered_global {
70                prop_assert_eq!(rule.scope, RuleScope::Global);
71            }
72            for rule in &filtered_project {
73                prop_assert_eq!(rule.scope, RuleScope::Project);
74            }
75            for rule in &filtered_session {
76                prop_assert_eq!(rule.scope, RuleScope::Session);
77            }
78
79            // Verify that there's no overlap between scopes
80            for global_rule in &filtered_global {
81                for project_rule in &filtered_project {
82                    prop_assert_ne!(&global_rule.id, &project_rule.id);
83                }
84                for session_rule in &filtered_session {
85                    prop_assert_ne!(&global_rule.id, &session_rule.id);
86                }
87            }
88            for project_rule in &filtered_project {
89                for session_rule in &filtered_session {
90                    prop_assert_ne!(&project_rule.id, &session_rule.id);
91                }
92            }
93        }
94
95        #[test]
96        fn prop_scope_isolation_filtering_is_idempotent(
97            global_rules in arb_rules_for_scope(RuleScope::Global),
98            project_rules in arb_rules_for_scope(RuleScope::Project),
99        ) {
100            // Combine rules
101            let mut all_rules = global_rules.clone();
102            all_rules.extend(project_rules.clone());
103
104            // Filter twice
105            let filtered_once = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Global);
106            let filtered_twice = ScopeFilter::filter_by_scope(&filtered_once, RuleScope::Global);
107
108            // Filtering twice should produce the same result as filtering once
109            prop_assert_eq!(filtered_once.len(), filtered_twice.len());
110            for (rule1, rule2) in filtered_once.iter().zip(filtered_twice.iter()) {
111                prop_assert_eq!(&rule1.id, &rule2.id);
112            }
113        }
114
115        #[test]
116        fn prop_scope_isolation_union_covers_all_scopes(
117            global_rules in arb_rules_for_scope(RuleScope::Global),
118            project_rules in arb_rules_for_scope(RuleScope::Project),
119            session_rules in arb_rules_for_scope(RuleScope::Session),
120        ) {
121            // Combine all rules
122            let mut all_rules = global_rules.clone();
123            all_rules.extend(project_rules.clone());
124            all_rules.extend(session_rules.clone());
125
126            // Filter by each scope
127            let filtered_global = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Global);
128            let filtered_project = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Project);
129            let filtered_session = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Session);
130
131            // Union of filtered rules should equal all rules
132            let mut union = filtered_global.clone();
133            union.extend(filtered_project.clone());
134            union.extend(filtered_session.clone());
135
136            prop_assert_eq!(union.len(), all_rules.len());
137        }
138
139        #[test]
140        fn prop_scope_isolation_no_interference_different_patterns(
141            global_rules in arb_rules_for_scope(RuleScope::Global),
142            project_rules in arb_rules_for_scope(RuleScope::Project),
143        ) {
144            // Check interference between global and project rules
145            let interference = ScopeFilter::check_scope_interference(&global_rules, &project_rules);
146
147            // Interference should only occur if there are rules with the same pattern
148            // but different actions
149            let mut expected_interference = false;
150            for global_rule in &global_rules {
151                for project_rule in &project_rules {
152                    if global_rule.pattern == project_rule.pattern && global_rule.action != project_rule.action {
153                        expected_interference = true;
154                        break;
155                    }
156                }
157                if expected_interference {
158                    break;
159                }
160            }
161
162            prop_assert_eq!(interference, expected_interference);
163        }
164
165        #[test]
166        fn prop_scope_isolation_filter_by_multiple_scopes(
167            global_rules in arb_rules_for_scope(RuleScope::Global),
168            project_rules in arb_rules_for_scope(RuleScope::Project),
169            session_rules in arb_rules_for_scope(RuleScope::Session),
170        ) {
171            // Combine all rules
172            let mut all_rules = global_rules.clone();
173            all_rules.extend(project_rules.clone());
174            all_rules.extend(session_rules.clone());
175
176            // Filter by multiple scopes
177            let filtered = ScopeFilter::filter_by_scopes(
178                &all_rules,
179                &[RuleScope::Project, RuleScope::Session],
180            );
181
182            // Should include project and session rules but not global
183            let expected_count = project_rules.len() + session_rules.len();
184            prop_assert_eq!(filtered.len(), expected_count);
185
186            // All filtered rules should be from project or session scope
187            for rule in &filtered {
188                prop_assert!(
189                    rule.scope == RuleScope::Project || rule.scope == RuleScope::Session,
190                    "Rule scope should be Project or Session, got {:?}",
191                    rule.scope
192                );
193            }
194        }
195
196        #[test]
197        fn prop_scope_isolation_precedence_respects_scopes(
198            global_rules in arb_rules_for_scope(RuleScope::Global),
199            project_rules in arb_rules_for_scope(RuleScope::Project),
200            session_rules in arb_rules_for_scope(RuleScope::Session),
201        ) {
202            // Combine all rules
203            let mut all_rules = global_rules.clone();
204            all_rules.extend(project_rules.clone());
205            all_rules.extend(session_rules.clone());
206
207            // Get rules with precedence for project scope
208            let project_precedence = ScopeFilter::get_rules_with_precedence(&all_rules, RuleScope::Project);
209
210            // Should include project and session rules but not global
211            for rule in &project_precedence {
212                prop_assert!(
213                    rule.scope == RuleScope::Project || rule.scope == RuleScope::Session,
214                    "Project precedence should only include Project and Session rules"
215                );
216            }
217
218            // Get rules with precedence for global scope
219            let global_precedence = ScopeFilter::get_rules_with_precedence(&all_rules, RuleScope::Global);
220
221            // Should include only global rules
222            for rule in &global_precedence {
223                prop_assert_eq!(
224                    rule.scope,
225                    RuleScope::Global,
226                    "Global precedence should only include Global rules"
227                );
228            }
229        }
230
231        #[test]
232        fn prop_scope_isolation_empty_scopes(
233            global_rules in arb_rules_for_scope(RuleScope::Global),
234        ) {
235            // Test with empty project and session rules
236            let project_rules: Vec<Rule> = Vec::new();
237            let session_rules: Vec<Rule> = Vec::new();
238
239            let mut all_rules = global_rules.clone();
240            all_rules.extend(project_rules.clone());
241            all_rules.extend(session_rules.clone());
242
243            // Filter by each scope
244            let filtered_global = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Global);
245            let filtered_project = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Project);
246            let filtered_session = ScopeFilter::filter_by_scope(&all_rules, RuleScope::Session);
247
248            // Verify counts
249            prop_assert_eq!(filtered_global.len(), global_rules.len());
250            prop_assert_eq!(filtered_project.len(), 0);
251            prop_assert_eq!(filtered_session.len(), 0);
252        }
253    }
254}