## Coverage Report

#### Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/ruchy/src/quality/gates.rs

    Line
    Count
    Source
    1
    //! Quality gate enforcement system (RUCHY-0815)
    2
    3
    use std::collections::HashMap;
    4
    use std::path::{Path, PathBuf};
    5
    use serde::{Deserialize, Serialize};
    6
    use crate::quality::scoring::{QualityScore, Grade};
    7
    8
    /// Quality gate configuration
    9
    #[derive(Debug, Clone, Serialize, Deserialize)]
    10
    pub struct QualityGateConfig {
    11
        /// Minimum overall score required (0.0-1.0)
    12
        pub min_score: f64,
    13
    14
        /// Minimum grade required
    15
        pub min_grade: Grade,
    16
    17
        /// Component-specific thresholds
    18
        pub component_thresholds: ComponentThresholds,
    19
    20
        /// Anti-gaming rules
    21
        pub anti_gaming: AntiGamingRules,
    22
    23
        /// CI/CD integration settings
    24
        pub ci_integration: CiIntegration,
    25
    26
        /// Project-specific overrides
    27
        pub project_overrides: HashMap<String, f64>,
    28
    }
    29
    30
    /// Component-specific quality thresholds
    31
    #[derive(Debug, Clone, Serialize, Deserialize)]
    32
    pub struct ComponentThresholds {
    33
        /// Minimum correctness score (0.0-1.0)
    34
        pub correctness: f64,
    35
    36
        /// Minimum performance score (0.0-1.0)
    37
        pub performance: f64,
    38
    39
        /// Minimum maintainability score (0.0-1.0)
    40
        pub maintainability: f64,
    41
    42
        /// Minimum safety score (0.0-1.0)
    43
        pub safety: f64,
    44
    45
        /// Minimum idiomaticity score (0.0-1.0)
    46
        pub idiomaticity: f64,
    47
    }
    48
    49
    /// Anti-gaming rules to prevent score manipulation
    50
    #[derive(Debug, Clone, Serialize, Deserialize)]
    51
    pub struct AntiGamingRules {
    52
        /// Minimum confidence level required (0.0-1.0)
    53
        pub min_confidence: f64,
    54
    55
        /// Maximum cache hit rate allowed (0.0-1.0) - prevents stale analysis
    56
        pub max_cache_hit_rate: f64,
    57
    58
        /// Require deep analysis for critical files
    59
        pub require_deep_analysis: Vec<String>,
    60
    61
        /// Penalty for files that are too small (gaming by splitting)
    62
        pub min_file_size_bytes: usize,
    63
    64
        /// Penalty for excessive test file ratios (gaming with trivial tests)
    65
        pub max_test_ratio: f64,
    66
    }
    67
    68
    /// CI/CD integration configuration
    69
    #[derive(Debug, Clone, Serialize, Deserialize)]
    70
    pub struct CiIntegration {
    71
        /// Fail CI/CD pipeline on gate failure
    72
        pub fail_on_violation: bool,
    73
    74
        /// Export results in `JUnit` XML format
    75
        pub junit_xml: bool,
    76
    77
        /// Export results in JSON format for tooling
    78
        pub json_output: bool,
    79
    80
        /// Send notifications on quality degradation
    81
        pub notifications: NotificationConfig,
    82
    83
        /// Block merge requests below threshold
    84
        pub block_merge: bool,
    85
    }
    86
    87
    /// Notification configuration
    88
    #[derive(Debug, Clone, Serialize, Deserialize)]
    89
    pub struct NotificationConfig {
    90
        /// Enable Slack notifications
    91
        pub slack: bool,
    92
    93
        /// Enable email notifications
    94
        pub email: bool,
    95
    96
        /// Webhook URL for custom notifications
    97
        pub webhook: Option<String>,
    98
    }
    99
    100
    /// Quality gate enforcement result
    101
    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
    102
    pub struct GateResult {
    103
        /// Whether the quality gate passed
    104
        pub passed: bool,
    105
    106
        /// Overall score achieved
    107
        pub score: f64,
    108
    109
        /// Grade achieved
    110
        pub grade: Grade,
    111
    112
        /// Specific violations found
    113
        pub violations: Vec<Violation>,
    114
    115
        /// Confidence in the result
    116
        pub confidence: f64,
    117
    118
        /// Anti-gaming warnings
    119
        pub gaming_warnings: Vec<String>,
    120
    }
    121
    122
    /// Specific quality gate violation
    123
    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
    124
    pub struct Violation {
    125
        /// Type of violation
    126
        pub violation_type: ViolationType,
    127
    128
        /// Actual value that caused violation
    129
        pub actual: f64,
    130
    131
        /// Required threshold
    132
        pub required: f64,
    133
    134
        /// Severity of the violation
    135
        pub severity: Severity,
    136
    137
        /// Human-readable message
    138
        pub message: String,
    139
    }
    140
    141
    /// Types of quality gate violations
    142
    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
    143
    pub enum ViolationType {
    144
        OverallScore,
    145
        Grade,
    146
        Correctness,
    147
        Performance,
    148
        Maintainability,
    149
        Safety,
    150
        Idiomaticity,
    151
        Confidence,
    152
        Gaming,
    153
    }
    154
    155
    /// Violation severity levels
    156
    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
    157
    pub enum Severity {
    158
        Critical, // Must fix to pass
    159
        High,     // Should fix soon
    160
        Medium,   // Should improve
    161
        Low,      // Nice to improve
    162
    }
    163
    164
    /// Quality gate enforcer
    165
    pub struct QualityGateEnforcer {
    166
        config: QualityGateConfig,
    167
    }
    168
    169
    impl Default for QualityGateConfig {
    170
    0
        fn default() -> Self {
    171
    0
            Self {
    172
    0
                min_score: 0.7, // B- grade minimum
    173
    0
                min_grade: Grade::BMinus,
    174
    0
                component_thresholds: ComponentThresholds {
    175
    0
                    correctness: 0.8,   // High correctness required
    176
    0
                    performance: 0.6,   // Moderate performance required
    177
    0
                    maintainability: 0.7, // Good maintainability required
    178
    0
                    safety: 0.8,        // High safety required
    179
    0
                    idiomaticity: 0.5,  // Basic idiomaticity required
    180
    0
                },
    181
    0
                anti_gaming: AntiGamingRules {
    182
    0
                    min_confidence: 0.6,
    183
    0
                    max_cache_hit_rate: 0.8,
    184
    0
                    require_deep_analysis: vec![
    185
    0
                        "src/main.rs".to_string(),
    186
    0
                        "src/lib.rs".to_string(),
    187
    0
                    ],
    188
    0
                    min_file_size_bytes: 100,
    189
    0
                    max_test_ratio: 2.0,
    190
    0
                },
    191
    0
                ci_integration: CiIntegration {
    192
    0
                    fail_on_violation: true,
    193
    0
                    junit_xml: true,
    194
    0
                    json_output: true,
    195
    0
                    notifications: NotificationConfig {
    196
    0
                        slack: false,
    197
    0
                        email: false,
    198
    0
                        webhook: None,
    199
    0
                    },
    200
    0
                    block_merge: true,
    201
    0
                },
    202
    0
                project_overrides: HashMap::new(),
    203
    0
            }
    204
    0
        }
    205
    }
    206
    207
    impl QualityGateEnforcer {
    208
    0
        pub fn new(config: QualityGateConfig) -> Self {
    209
    0
            Self { config }
    210
    0
        }
    211
    212
        /// Load configuration from .ruchy/score.toml
    213
    0
        pub fn load_config(project_root: &Path) -> anyhow::Result<QualityGateConfig> {
    214
    0
            let config_path = project_root.join(".ruchy").join("score.toml");
    215
    216
    0
            if config_path.exists() {
    217
    0
                let content = std::fs::read_to_string(&config_path)?;
    218
    0
                let config: QualityGateConfig = toml::from_str(&content)?;
    219
    0
                Ok(config)
    220
            } else {
    221
                // Create default configuration file
    222
    0
                let default_config = QualityGateConfig::default();
    223
    0
                std::fs::create_dir_all(project_root.join(".ruchy"))?;
    224
    0
                let toml_content = toml::to_string_pretty(&default_config)?;
    225
    0
                std::fs::write(&config_path, toml_content)?;
    226
    0
                Ok(default_config)
    227
            }
    228
    0
        }
    229
    230
        /// Enforce quality gates on a score
    231
    0
        pub fn enforce_gates(&self, score: &QualityScore, file_path: Option<&PathBuf>) -> GateResult {
    232
    0
            let mut violations = Vec::new();
    233
    0
            let mut gaming_warnings = Vec::new();
    234
    235
            // Check overall score threshold
    236
    0
            if score.value < self.config.min_score {
    237
    0
                violations.push(Violation {
    238
    0
                    violation_type: ViolationType::OverallScore,
    239
    0
                    actual: score.value,
    240
    0
                    required: self.config.min_score,
    241
    0
                    severity: Severity::Critical,
    242
    0
                    message: format!(
    243
    0
                        "Overall score {:.1}% below minimum {:.1}%",
    244
    0
                        score.value * 100.0,
    245
    0
                        self.config.min_score * 100.0
    246
    0
                    ),
    247
    0
                });
    248
    0
            }
    249
    250
            // Check grade requirement
    251
    0
            if score.grade < self.config.min_grade {
    252
    0
                violations.push(Violation {
    253
    0
                    violation_type: ViolationType::Grade,
    254
    0
                    actual: score.value,
    255
    0
                    required: self.config.min_score,
    256
    0
                    severity: Severity::Critical,
    257
    0
                    message: format!(
    258
    0
                        "Grade {} below minimum {}",
    259
    0
                        score.grade,
    260
    0
                        self.config.min_grade
    261
    0
                    ),
    262
    0
                });
    263
    0
            }
    264
    265
            // Check component thresholds
    266
    0
            self.check_component_thresholds(score, &mut violations);
    267
    268
            // Check anti-gaming rules
    269
    0
            self.check_anti_gaming_rules(score, file_path, &mut gaming_warnings, &mut violations);
    270
    271
            // Check confidence threshold
    272
    0
            if score.confidence < self.config.anti_gaming.min_confidence {
    273
    0
                violations.push(Violation {
    274
    0
                    violation_type: ViolationType::Confidence,
    275
    0
                    actual: score.confidence,
    276
    0
                    required: self.config.anti_gaming.min_confidence,
    277
    0
                    severity: Severity::High,
    278
    0
                    message: format!(
    279
    0
                        "Confidence {:.1}% below minimum {:.1}%",
    280
    0
                        score.confidence * 100.0,
    281
    0
                        self.config.anti_gaming.min_confidence * 100.0
    282
    0
                    ),
    283
    0
                });
    284
    0
            }
    285
    286
    0
            let passed = violations.iter().all(|v| v.severity != Severity::Critical);
    287
    288
    0
            GateResult {
    289
    0
                passed,
    290
    0
                score: score.value,
    291
    0
                grade: score.grade,
    292
    0
                violations,
    293
    0
                confidence: score.confidence,
    294
    0
                gaming_warnings,
    295
    0
            }
    296
    0
        }
    297
    298
    0
        fn check_component_thresholds(&self, score: &QualityScore, violations: &mut Vec<Violation>) {
    299
    0
            let thresholds = &self.config.component_thresholds;
    300
    301
    0
            if score.components.correctness < thresholds.correctness {
    302
    0
                violations.push(Violation {
    303
    0
                    violation_type: ViolationType::Correctness,
    304
    0
                    actual: score.components.correctness,
    305
    0
                    required: thresholds.correctness,
    306
    0
                    severity: Severity::Critical,
    307
    0
                    message: format!(
    308
    0
                        "Correctness {:.1}% below minimum {:.1}%",
    309
    0
                        score.components.correctness * 100.0,
    310
    0
                        thresholds.correctness * 100.0
    311
    0
                    ),
    312
    0
                });
    313
    0
            }
    314
    315
    0
            if score.components.performance < thresholds.performance {
    316
    0
                violations.push(Violation {
    317
    0
                    violation_type: ViolationType::Performance,
    318
    0
                    actual: score.components.performance,
    319
    0
                    required: thresholds.performance,
    320
    0
                    severity: Severity::High,
    321
    0
                    message: format!(
    322
    0
                        "Performance {:.1}% below minimum {:.1}%",
    323
    0
                        score.components.performance * 100.0,
    324
    0
                        thresholds.performance * 100.0
    325
    0
                    ),
    326
    0
                });
    327
    0
            }
    328
    329
    0
            if score.components.maintainability < thresholds.maintainability {
    330
    0
                violations.push(Violation {
    331
    0
                    violation_type: ViolationType::Maintainability,
    332
    0
                    actual: score.components.maintainability,
    333
    0
                    required: thresholds.maintainability,
    334
    0
                    severity: Severity::High,
    335
    0
                    message: format!(
    336
    0
                        "Maintainability {:.1}% below minimum {:.1}%",
    337
    0
                        score.components.maintainability * 100.0,
    338
    0
                        thresholds.maintainability * 100.0
    339
    0
                    ),
    340
    0
                });
    341
    0
            }
    342
    343
    0
            if score.components.safety < thresholds.safety {
    344
    0
                violations.push(Violation {
    345
    0
                    violation_type: ViolationType::Safety,
    346
    0
                    actual: score.components.safety,
    347
    0
                    required: thresholds.safety,
    348
    0
                    severity: Severity::Critical,
    349
    0
                    message: format!(
    350
    0
                        "Safety {:.1}% below minimum {:.1}%",
    351
    0
                        score.components.safety * 100.0,
    352
    0
                        thresholds.safety * 100.0
    353
    0
                    ),
    354
    0
                });
    355
    0
            }
    356
    357
    0
            if score.components.idiomaticity < thresholds.idiomaticity {
    358
    0
                violations.push(Violation {
    359
    0
                    violation_type: ViolationType::Idiomaticity,
    360
    0
                    actual: score.components.idiomaticity,
    361
    0
                    required: thresholds.idiomaticity,
    362
    0
                    severity: Severity::Medium,
    363
    0
                    message: format!(
    364
    0
                        "Idiomaticity {:.1}% below minimum {:.1}%",
    365
    0
                        score.components.idiomaticity * 100.0,
    366
    0
                        thresholds.idiomaticity * 100.0
    367
    0
                    ),
    368
    0
                });
    369
    0
            }
    370
    0
        }
    371
    372
    0
        fn check_anti_gaming_rules(
    373
    0
            &self,
    374
    0
            score: &QualityScore,
    375
    0
            file_path: Option<&PathBuf>,
    376
    0
            gaming_warnings: &mut Vec<String>,
    377
    0
            violations: &mut Vec<Violation>,
    378
    0
        ) {
    379
            // Check cache hit rate (prevent stale analysis gaming)
    380
    0
            if score.cache_hit_rate > self.config.anti_gaming.max_cache_hit_rate {
    381
    0
                gaming_warnings.push(format!(
    382
    0
                    "High cache hit rate {:.1}% may indicate stale analysis",
    383
    0
                    score.cache_hit_rate * 100.0
    384
    0
                ));
    385
    0
            }
    386
    387
            // Check file size requirements
    388
    0
            if let Some(path) = file_path {
    389
    0
                if let Ok(metadata) = std::fs::metadata(path) {
    390
    0
                    if metadata.len() < self.config.anti_gaming.min_file_size_bytes as u64 {
    391
    0
                        gaming_warnings.push(format!(
    392
    0
                            "File {} is very small ({} bytes) - may indicate gaming by splitting",
    393
    0
                            path.display(),
    394
    0
                            metadata.len()
    395
    0
                        ));
    396
    0
                    }
    397
    0
                }
    398
    399
                // Check if critical files require deep analysis
    400
    0
                let path_str = path.to_string_lossy();
    401
    0
                if self.config.anti_gaming.require_deep_analysis.iter().any(|p| path_str.contains(p))
    402
    0
                    && score.confidence < 0.9 {
    403
    0
                        violations.push(Violation {
    404
    0
                            violation_type: ViolationType::Gaming,
    405
    0
                            actual: score.confidence,
    406
    0
                            required: 0.9,
    407
    0
                            severity: Severity::Critical,
    408
    0
                            message: format!(
    409
    0
                                "Critical file {} requires deep analysis (confidence < 90%)",
    410
    0
                                path.display()
    411
    0
                            ),
    412
    0
                        });
    413
    0
                    }
    414
    0
            }
    415
    0
        }
    416
    417
        /// Export results for CI/CD integration
    418
    0
        pub fn export_ci_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> {
    419
    0
            if self.config.ci_integration.json_output {
    420
    0
                self.export_json_results(results, output_dir)?;
    421
    0
            }
    422
    423
    0
            if self.config.ci_integration.junit_xml {
    424
    0
                self.export_junit_results(results, output_dir)?;
    425
    0
            }
    426
    427
    0
            Ok(())
    428
    0
        }
    429
    430
    0
        fn export_json_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> {
    431
    0
            let output_path = output_dir.join("quality-gates.json");
    432
    0
            let json_content = serde_json::to_string_pretty(results)?;
    433
    0
            std::fs::write(output_path, json_content)?;
    434
    0
            Ok(())
    435
    0
        }
    436
    437
    0
        fn export_junit_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> {
    438
    0
            let output_path = output_dir.join("quality-gates.xml");
    439
    440
    0
            let total = results.len();
    441
    0
            let failures = results.iter().filter(|r| !r.passed).count();
    442
    443
    0
            let mut xml = format!(r#"<?xml version="1.0" encoding="UTF-8"?>
    444
    0
    <testsuite name="Quality Gates" tests="{total}" failures="{failures}" time="0.0">
    445
    0
    "#);
    446
    447
    0
            for (i, result) in results.iter().enumerate() {
    448
    0
                let test_name = format!("quality-gate-{i}");
    449
    0
                if result.passed {
    450
    0
                    xml.push_str(&format!(
    451
    0
                        r#"  <testcase name="{test_name}" classname="QualityGate" time="0.0"/>
    452
    0
    "#
    453
    0
                    ));
    454
    0
                } else {
    455
    0
                    xml.push_str(&format!(
    456
    0
                        r#"  <testcase name="{}" classname="QualityGate" time="0.0">
    457
    0
        <failure message="Quality gate violation">Score: {:.1}%, Grade: {}</failure>
    458
    0
      </testcase>
    459
    0
    "#,
    460
    0
                        test_name,
    461
    0
                        result.score * 100.0,
    462
    0
                        result.grade
    463
    0
                    ));
    464
    0
                }
    465
            }
    466
    467
    0
            xml.push_str("</testsuite>\n");
    468
    0
            std::fs::write(output_path, xml)?;
    469
    0
            Ok(())
    470
    0
        }
    471
    }
    472
    473
    impl PartialOrd for Grade {
    474
    0
        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
    475
    0
            Some(self.cmp(other))
    476
    0
        }
    477
    }
    478
    479
    impl Ord for Grade {
    480
    0
        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
    481
            // use std::cmp::Ordering;
    482
            use Grade::{F, D, CMinus, C, CPlus, BMinus, B, BPlus, AMinus, A, APlus};
    483
    484
    0
            let self_rank = match self {
    485
    0
                F => 0,
    486
    0
                D => 1,
    487
    0
                CMinus => 2,
    488
    0
                C => 3,
    489
    0
                CPlus => 4,
    490
    0
                BMinus => 5,
    491
    0
                B => 6,
    492
    0
                BPlus => 7,
    493
    0
                AMinus => 8,
    494
    0
                A => 9,
    495
    0
                APlus => 10,
    496
            };
    497
    498
    0
            let other_rank = match other {
    499
    0
                F => 0,
    500
    0
                D => 1,
    501
    0
                CMinus => 2,
    502
    0
                C => 3,
    503
    0
                CPlus => 4,
    504
    0
                BMinus => 5,
    505
    0
                B => 6,
    506
    0
                BPlus => 7,
    507
    0
                AMinus => 8,
    508
    0
                A => 9,
    509
    0
                APlus => 10,
    510
            };
    511
    512
    0
            self_rank.cmp(&other_rank)
    513
    0
        }
    514
    }
    515
    516
    // PartialEq and Eq are now derived in scoring.rs
    517
    518
    #[cfg(test)]
    519
    mod tests {
    520
        use super::*;
    521
        use crate::quality::scoring::{QualityScore, Grade};
    522
        use tempfile::TempDir;
    523
    524
        fn create_minimal_score() -> QualityScore {
    525
            use crate::quality::scoring::ScoreComponents;
    526
            QualityScore {
    527
                value: 0.5,
    528
                components: ScoreComponents {
    529
                    correctness: 0.5,
    530
                    performance: 0.5,
    531
                    maintainability: 0.5,
    532
                    safety: 0.5,
    533
                    idiomaticity: 0.5,
    534
                },
    535
                grade: Grade::D,
    536
                confidence: 0.4,
    537
                cache_hit_rate: 0.3,
    538
            }
    539
        }
    540
    541
        fn create_passing_score() -> QualityScore {
    542
            use crate::quality::scoring::ScoreComponents;
    543
            QualityScore {
    544
                value: 0.85,
    545
                components: ScoreComponents {
    546
                    correctness: 0.9,
    547
                    performance: 0.8,
    548
                    maintainability: 0.8,
    549
                    safety: 0.9,
    550
                    idiomaticity: 0.7,
    551
                },
    552
                grade: Grade::APlus,
    553
                confidence: 0.9,
    554
                cache_hit_rate: 0.2,
    555
            }
    556
        }
    557
    558
        // Test 1: Default Configuration Creation
    559
        #[test]
    560
        fn test_default_quality_gate_config() {
    561
            let config = QualityGateConfig::default();
    562
    563
            assert_eq!(config.min_score, 0.7);
    564
            assert_eq!(config.min_grade, Grade::BMinus);
    565
            assert_eq!(config.component_thresholds.correctness, 0.8);
    566
            assert_eq!(config.component_thresholds.safety, 0.8);
    567
            assert_eq!(config.anti_gaming.min_confidence, 0.6);
    568
            assert!(config.ci_integration.fail_on_violation);
    569
            assert!(config.project_overrides.is_empty());
    570
        }
    571
    572
        // Test 2: Quality Gate Enforcer Creation
    573
        #[test]
    574
        fn test_quality_gate_enforcer_creation() {
    575
            let config = QualityGateConfig::default();
    576
            let enforcer = QualityGateEnforcer::new(config);
    577
    578
            // Verify enforcer uses the provided config
    579
            let score = create_minimal_score();
    580
            let result = enforcer.enforce_gates(&score, None);
    581
    582
            // Should fail with default thresholds
    583
            assert!(!result.passed);
    584
            assert!(!result.violations.is_empty());
    585
        }
    586
    587
        // Test 3: Passing Quality Gate - All Criteria Met
    588
        #[test]
    589
        fn test_quality_gate_passes_with_high_score() {
    590
            let config = QualityGateConfig::default();
    591
            let enforcer = QualityGateEnforcer::new(config);
    592
            let score = create_passing_score();
    593
    594
            let result = enforcer.enforce_gates(&score, None);
    595
    596
            assert!(result.passed, "High quality score should pass all gates");
    597
            assert_eq!(result.score, 0.85);
    598
            assert_eq!(result.grade, Grade::APlus);
    599
            assert!(result.violations.is_empty());
    600
            assert_eq!(result.confidence, 0.9);
    601
            assert!(result.gaming_warnings.is_empty());
    602
        }
    603
    604
        // Test 4: Failing Overall Score Threshold
    605
        #[test]
    606
        fn test_quality_gate_fails_overall_score() {
    607
            let config = QualityGateConfig::default(); // min_score: 0.7
    608
            let enforcer = QualityGateEnforcer::new(config);
    609
    610
            let mut score = create_minimal_score();
    611
            score.value = 0.6; // Below 0.7 threshold
    612
    613
            let result = enforcer.enforce_gates(&score, None);
    614
    615
            assert!(!result.passed, "Score below threshold should fail");
    616
    617
            // Should have overall score violation
    618
            let overall_violations: Vec<_> = result.violations.iter()
    619
                .filter(|v| v.violation_type == ViolationType::OverallScore)
    620
                .collect();
    621
            assert_eq!(overall_violations.len(), 1);
    622
    623
            let violation = &overall_violations[0];
    624
            assert_eq!(violation.actual, 0.6);
    625
            assert_eq!(violation.required, 0.7);
    626
            assert_eq!(violation.severity, Severity::Critical);
    627
            assert!(violation.message.contains("60.0%"));
    628
            assert!(violation.message.contains("70.0%"));
    629
        }
    630
    631
        // Test 5: Confidence Threshold Violation
    632
        #[test]
    633
        fn test_confidence_threshold_violation() {
    634
            let config = QualityGateConfig::default(); // min_confidence: 0.6
    635
            let enforcer = QualityGateEnforcer::new(config);
    636
    637
            let mut score = create_passing_score();
    638
            score.confidence = 0.4; // Below 0.6 threshold
    639
    640
            let result = enforcer.enforce_gates(&score, None);
    641
    642
            let confidence_violations: Vec<_> = result.violations.iter()
    643
                .filter(|v| v.violation_type == ViolationType::Confidence)
    644
                .collect();
    645
            assert_eq!(confidence_violations.len(), 1);
    646
    647
            let violation = &confidence_violations[0];
    648
            assert_eq!(violation.severity, Severity::High);
    649
            assert_eq!(violation.actual, 0.4);
    650
            assert_eq!(violation.required, 0.6);
    651
        }
    652
    653
        // Test 6: Configuration File Loading (Success)
    654
        #[test]
    655
        fn test_load_config_creates_default() {
    656
            let temp_dir = TempDir::new().unwrap();
    657
            let project_root = temp_dir.path();
    658
    659
            let config = QualityGateEnforcer::load_config(project_root).unwrap();
    660
    661
            // Should create default config
    662
            assert_eq!(config.min_score, 0.7);
    663
            assert_eq!(config.min_grade, Grade::BMinus);
    664
    665
            // Should create .ruchy/score.toml file
    666
            let config_path = project_root.join(".ruchy").join("score.toml");
    667
            assert!(config_path.exists(), "Config file should be created");
    668
    669
            // File should contain valid TOML
    670
            let content = std::fs::read_to_string(config_path).unwrap();
    671
            assert!(content.contains("min_score"));
    672
            assert!(content.contains("0.7"));
    673
        }
    674
    675
        // Test 7: Serialization/Deserialization
    676
        #[test]
    677
        fn test_config_serialization() {
    678
            let original_config = QualityGateConfig::default();
    679
    680
            // Serialize to TOML
    681
            let toml_content = toml::to_string(&original_config).unwrap();
    682
            assert!(toml_content.contains("min_score"));
    683
    684
            // Deserialize back
    685
            let deserialized_config: QualityGateConfig = toml::from_str(&toml_content).unwrap();
    686
            assert_eq!(deserialized_config.min_score, original_config.min_score);
    687
            assert_eq!(deserialized_config.min_grade, original_config.min_grade);
    688
        }
    689
    }