ricecoder_learning/
rule_validation_property.rs

1/// Property-based tests for rule validation accuracy
2///
3/// **Feature: ricecoder-learning, Property 4: Rule Validation Accuracy**
4/// **Validates: Requirements 2.3**
5///
6/// Tests that the rule validator correctly accepts valid rules and rejects invalid rules
7/// across a wide range of randomly generated scenarios.
8
9#[cfg(test)]
10mod tests {
11    use proptest::prelude::*;
12    use crate::{Rule, RuleScope, RuleSource, RuleValidator};
13
14    /// Strategy for generating valid rule patterns
15    fn valid_pattern_strategy() -> impl Strategy<Value = String> {
16        r"[a-zA-Z0-9_\-\.]+"
17            .prop_map(|s| s.to_string())
18            .prop_filter("pattern must not be empty", |s| !s.is_empty())
19    }
20
21    /// Strategy for generating valid rule actions
22    fn valid_action_strategy() -> impl Strategy<Value = String> {
23        r"[a-zA-Z0-9_\-\.\s]+"
24            .prop_map(|s| s.to_string())
25            .prop_filter("action must not be empty", |s| !s.is_empty())
26    }
27
28    /// Strategy for generating valid confidence scores
29    fn valid_confidence_strategy() -> impl Strategy<Value = f32> {
30        0.0f32..=1.0f32
31    }
32
33    /// Strategy for generating valid success rates
34    fn valid_success_rate_strategy() -> impl Strategy<Value = f32> {
35        0.0f32..=1.0f32
36    }
37
38    /// Strategy for generating valid rules
39    fn valid_rule_strategy() -> impl Strategy<Value = Rule> {
40        (
41            valid_pattern_strategy(),
42            valid_action_strategy(),
43            valid_confidence_strategy(),
44            valid_success_rate_strategy(),
45        )
46            .prop_map(|(pattern, action, confidence, success_rate)| {
47                let mut rule = Rule::new(
48                    RuleScope::Global,
49                    pattern,
50                    action,
51                    RuleSource::Learned,
52                );
53                rule.confidence = confidence;
54                rule.success_rate = success_rate;
55                rule
56            })
57    }
58
59    /// Strategy for generating invalid confidence scores
60    fn invalid_confidence_strategy() -> impl Strategy<Value = f32> {
61        prop_oneof![
62            Just(1.5f32),
63            Just(-0.5f32),
64            Just(2.0f32),
65            Just(-1.0f32),
66            Just(f32::NAN),
67            Just(f32::INFINITY),
68            Just(f32::NEG_INFINITY),
69        ]
70    }
71
72    /// Strategy for generating invalid success rates
73    fn invalid_success_rate_strategy() -> impl Strategy<Value = f32> {
74        prop_oneof![
75            Just(1.5f32),
76            Just(-0.5f32),
77            Just(2.0f32),
78            Just(-1.0f32),
79            Just(f32::NAN),
80            Just(f32::INFINITY),
81            Just(f32::NEG_INFINITY),
82        ]
83    }
84
85    /// Property 4: Valid rules are accepted
86    ///
87    /// For any valid rule, the validator SHALL accept it without errors.
88    proptest! {
89        #[test]
90        fn prop_valid_rules_accepted(rule in valid_rule_strategy()) {
91            let validator = RuleValidator::new();
92            assert!(
93                validator.validate(&rule).is_ok(),
94                "Valid rule should be accepted: {:?}",
95                rule
96            );
97        }
98
99        /// Property 4: Invalid confidence scores are rejected
100        ///
101        /// For any rule with an invalid confidence score, the validator SHALL reject it.
102        #[test]
103        fn prop_invalid_confidence_rejected(confidence in invalid_confidence_strategy()) {
104            let validator = RuleValidator::new();
105            let mut rule = Rule::new(
106                RuleScope::Global,
107                "pattern".to_string(),
108                "action".to_string(),
109                RuleSource::Learned,
110            );
111            rule.confidence = confidence;
112
113            assert!(
114                validator.validate(&rule).is_err(),
115                "Rule with invalid confidence {} should be rejected",
116                confidence
117            );
118        }
119
120        /// Property 4: Invalid success rates are rejected
121        ///
122        /// For any rule with an invalid success rate, the validator SHALL reject it.
123        #[test]
124        fn prop_invalid_success_rate_rejected(success_rate in invalid_success_rate_strategy()) {
125            let validator = RuleValidator::new();
126            let mut rule = Rule::new(
127                RuleScope::Global,
128                "pattern".to_string(),
129                "action".to_string(),
130                RuleSource::Learned,
131            );
132            rule.success_rate = success_rate;
133
134            assert!(
135                validator.validate(&rule).is_err(),
136                "Rule with invalid success rate {} should be rejected",
137                success_rate
138            );
139        }
140    }
141
142    /// Property 4: Empty patterns are rejected
143    ///
144    /// For any rule with an empty pattern, the validator SHALL reject it.
145    #[test]
146    fn prop_empty_pattern_rejected() {
147        let validator = RuleValidator::new();
148        let mut rule = Rule::new(
149            RuleScope::Global,
150            "pattern".to_string(),
151            "action".to_string(),
152            RuleSource::Learned,
153        );
154        rule.pattern = String::new();
155
156        assert!(
157            validator.validate(&rule).is_err(),
158            "Rule with empty pattern should be rejected"
159        );
160    }
161
162    /// Property 4: Empty actions are rejected
163    ///
164    /// For any rule with an empty action, the validator SHALL reject it.
165    #[test]
166    fn prop_empty_action_rejected() {
167        let validator = RuleValidator::new();
168        let mut rule = Rule::new(
169            RuleScope::Global,
170            "pattern".to_string(),
171            "action".to_string(),
172            RuleSource::Learned,
173        );
174        rule.action = String::new();
175
176        assert!(
177            validator.validate(&rule).is_err(),
178            "Rule with empty action should be rejected"
179        );
180    }
181
182    /// Property 4: Empty IDs are rejected
183    ///
184    /// For any rule with an empty ID, the validator SHALL reject it.
185    #[test]
186    fn prop_empty_id_rejected() {
187        let validator = RuleValidator::new();
188        let mut rule = Rule::new(
189            RuleScope::Global,
190            "pattern".to_string(),
191            "action".to_string(),
192            RuleSource::Learned,
193        );
194        rule.id = String::new();
195
196        assert!(
197            validator.validate(&rule).is_err(),
198            "Rule with empty ID should be rejected"
199        );
200    }
201
202    /// Property 4: Zero version is rejected
203    ///
204    /// For any rule with version 0, the validator SHALL reject it.
205    #[test]
206    fn prop_zero_version_rejected() {
207        let validator = RuleValidator::new();
208        let mut rule = Rule::new(
209            RuleScope::Global,
210            "pattern".to_string(),
211            "action".to_string(),
212            RuleSource::Learned,
213        );
214        rule.version = 0;
215
216        assert!(
217            validator.validate(&rule).is_err(),
218            "Rule with version 0 should be rejected"
219        );
220    }
221
222    proptest! {
223        /// Property 4: Conflict detection works correctly
224        ///
225        /// For any two rules with the same pattern and scope, the validator SHALL detect a conflict.
226        #[test]
227        fn prop_conflict_detection(pattern in valid_pattern_strategy()) {
228            let validator = RuleValidator::new();
229
230            let rule1 = Rule::new(
231                RuleScope::Global,
232                pattern.clone(),
233                "action1".to_string(),
234                RuleSource::Learned,
235            );
236
237            let rule2 = Rule::new(
238                RuleScope::Global,
239                pattern,
240                "action2".to_string(),
241                RuleSource::Learned,
242            );
243
244            assert!(
245                validator.check_conflicts(&rule2, &[rule1]).is_err(),
246                "Conflicting rules should be detected"
247            );
248        }
249
250        /// Property 4: No conflict for different scopes
251        ///
252        /// For any two rules with the same pattern but different scopes, the validator SHALL NOT detect a conflict.
253        #[test]
254        fn prop_no_conflict_different_scope(pattern in valid_pattern_strategy()) {
255            let validator = RuleValidator::new();
256
257            let rule1 = Rule::new(
258                RuleScope::Global,
259                pattern.clone(),
260                "action1".to_string(),
261                RuleSource::Learned,
262            );
263
264            let rule2 = Rule::new(
265                RuleScope::Project,
266                pattern,
267                "action2".to_string(),
268                RuleSource::Learned,
269            );
270
271            assert!(
272                validator.check_conflicts(&rule2, &[rule1]).is_ok(),
273                "Rules with different scopes should not conflict"
274            );
275        }
276
277        /// Property 4: No conflict for different patterns
278        ///
279        /// For any two rules with different patterns but the same scope, the validator SHALL NOT detect a conflict.
280        #[test]
281        fn prop_no_conflict_different_pattern(
282            pattern1 in valid_pattern_strategy(),
283            pattern2 in valid_pattern_strategy(),
284        ) {
285            prop_assume!(pattern1 != pattern2);
286
287            let validator = RuleValidator::new();
288
289            let rule1 = Rule::new(
290                RuleScope::Global,
291                pattern1,
292                "action1".to_string(),
293                RuleSource::Learned,
294            );
295
296            let rule2 = Rule::new(
297                RuleScope::Global,
298                pattern2,
299                "action2".to_string(),
300                RuleSource::Learned,
301            );
302
303            assert!(
304                validator.check_conflicts(&rule2, &[rule1]).is_ok(),
305                "Rules with different patterns should not conflict"
306            );
307        }
308    }
309
310    /// Property 4: Validation report accuracy
311    ///
312    /// For any invalid rule, the validation report SHALL contain at least one error.
313    #[test]
314    fn prop_validation_report_accuracy() {
315        let validator = RuleValidator::new();
316        let mut rule = Rule::new(
317            RuleScope::Global,
318            "pattern".to_string(),
319            "action".to_string(),
320            RuleSource::Learned,
321        );
322        rule.confidence = 1.5; // Invalid confidence
323
324        let report = validator.validate_with_report(&rule);
325        assert!(
326            report.has_errors(),
327            "Validation report should contain errors for invalid rule"
328        );
329    }
330
331    proptest! {
332        /// Property 4: Valid rules have no errors in report
333        ///
334        /// For any valid rule, the validation report SHALL contain no errors.
335        #[test]
336        fn prop_valid_rules_no_errors(rule in valid_rule_strategy()) {
337            let validator = RuleValidator::new();
338            let report = validator.validate_with_report(&rule);
339
340            assert!(
341                !report.has_errors(),
342                "Valid rule should have no errors in report: {:?}",
343                rule
344            );
345        }
346    }
347
348    /// Property 4: JSON action validation
349    ///
350    /// For any rule with a valid JSON action, the validator SHALL accept it.
351    #[test]
352    fn prop_json_action_accepted() {
353        let validator = RuleValidator::new();
354        let rule = Rule::new(
355            RuleScope::Global,
356            "pattern".to_string(),
357            r#"{"key": "value", "nested": {"inner": 42}}"#.to_string(),
358            RuleSource::Learned,
359        );
360
361        assert!(
362            validator.validate(&rule).is_ok(),
363            "Valid JSON action should be accepted"
364        );
365    }
366
367    /// Property 4: Invalid JSON action rejected
368    ///
369    /// For any rule with an invalid JSON action, the validator SHALL reject it.
370    #[test]
371    fn prop_invalid_json_action_rejected() {
372        let validator = RuleValidator::new();
373        let rule = Rule::new(
374            RuleScope::Global,
375            "pattern".to_string(),
376            r#"{"key": invalid, "nested": {inner: 42}}"#.to_string(),
377            RuleSource::Learned,
378        );
379
380        assert!(
381            validator.validate(&rule).is_err(),
382            "Invalid JSON action should be rejected"
383        );
384    }
385
386    /// Property 4: Metadata must be object
387    ///
388    /// For any rule with non-object metadata, the validator SHALL reject it.
389    #[test]
390    fn prop_non_object_metadata_rejected() {
391        let validator = RuleValidator::new();
392        let mut rule = Rule::new(
393            RuleScope::Global,
394            "pattern".to_string(),
395            "action".to_string(),
396            RuleSource::Learned,
397        );
398        rule.metadata = serde_json::json!([1, 2, 3]); // Array instead of object
399
400        assert!(
401            validator.validate(&rule).is_err(),
402            "Non-object metadata should be rejected"
403        );
404    }
405
406    proptest! {
407        /// Property 4: Multiple rules validation
408        ///
409        /// For any set of valid rules, the validator SHALL accept all of them.
410        #[test]
411        fn prop_multiple_valid_rules_accepted(rules in prop::collection::vec(valid_rule_strategy(), 1..10)) {
412            let validator = RuleValidator::new();
413
414            for rule in rules {
415                assert!(
416                    validator.validate(&rule).is_ok(),
417                    "All valid rules should be accepted"
418                );
419            }
420        }
421
422        /// Property 4: Boundary confidence values
423        ///
424        /// For any rule with confidence exactly 0.0 or 1.0, the validator SHALL accept it.
425        #[test]
426        fn prop_boundary_confidence_accepted(confidence in prop_oneof![Just(0.0f32), Just(1.0f32)]) {
427            let validator = RuleValidator::new();
428            let mut rule = Rule::new(
429                RuleScope::Global,
430                "pattern".to_string(),
431                "action".to_string(),
432                RuleSource::Learned,
433            );
434            rule.confidence = confidence;
435
436            assert!(
437                validator.validate(&rule).is_ok(),
438                "Boundary confidence values should be accepted"
439            );
440        }
441
442        /// Property 4: Boundary success rate values
443        ///
444        /// For any rule with success rate exactly 0.0 or 1.0, the validator SHALL accept it.
445        #[test]
446        fn prop_boundary_success_rate_accepted(success_rate in prop_oneof![Just(0.0f32), Just(1.0f32)]) {
447            let validator = RuleValidator::new();
448            let mut rule = Rule::new(
449                RuleScope::Global,
450                "pattern".to_string(),
451                "action".to_string(),
452                RuleSource::Learned,
453            );
454            rule.success_rate = success_rate;
455
456            assert!(
457                validator.validate(&rule).is_ok(),
458                "Boundary success rate values should be accepted"
459            );
460        }
461    }
462}