Coverage Report

Created: 2025-09-05 15: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
17
    fn default() -> Self {
171
17
        Self {
172
17
            min_score: 0.7, // B- grade minimum
173
17
            min_grade: Grade::BMinus,
174
17
            component_thresholds: ComponentThresholds {
175
17
                correctness: 0.8,   // High correctness required
176
17
                performance: 0.6,   // Moderate performance required
177
17
                maintainability: 0.7, // Good maintainability required  
178
17
                safety: 0.8,        // High safety required
179
17
                idiomaticity: 0.5,  // Basic idiomaticity required
180
17
            },
181
17
            anti_gaming: AntiGamingRules {
182
17
                min_confidence: 0.6,
183
17
                max_cache_hit_rate: 0.8,
184
17
                require_deep_analysis: vec![
185
17
                    "src/main.rs".to_string(),
186
17
                    "src/lib.rs".to_string(),
187
17
                ],
188
17
                min_file_size_bytes: 100,
189
17
                max_test_ratio: 2.0,
190
17
            },
191
17
            ci_integration: CiIntegration {
192
17
                fail_on_violation: true,
193
17
                junit_xml: true,
194
17
                json_output: true,
195
17
                notifications: NotificationConfig {
196
17
                    slack: false,
197
17
                    email: false,
198
17
                    webhook: None,
199
17
                },
200
17
                block_merge: true,
201
17
            },
202
17
            project_overrides: HashMap::new(),
203
17
        }
204
17
    }
205
}
206
207
impl QualityGateEnforcer {
208
23
    pub fn new(config: QualityGateConfig) -> Self {
209
23
        Self { config }
210
23
    }
211
    
212
    /// Load configuration from .ruchy/score.toml
213
21
    pub fn load_config(project_root: &Path) -> anyhow::Result<QualityGateConfig> {
214
21
        let config_path = project_root.join(".ruchy").join("score.toml");
215
        
216
21
        if config_path.exists() {
217
12
            let content = std::fs::read_to_string(&config_path)
?0
;
218
12
            let 
config11
:
QualityGateConfig11
= toml::from_str(&content)
?1
;
219
11
            Ok(config)
220
        } else {
221
            // Create default configuration file
222
9
            let default_config = QualityGateConfig::default();
223
9
            std::fs::create_dir_all(project_root.join(".ruchy"))
?0
;
224
9
            let toml_content = toml::to_string_pretty(&default_config)
?0
;
225
9
            std::fs::write(&config_path, toml_content)
?0
;
226
9
            Ok(default_config)
227
        }
228
21
    }
229
    
230
    /// Enforce quality gates on a score
231
28
    pub fn enforce_gates(&self, score: &QualityScore, file_path: Option<&PathBuf>) -> GateResult {
232
28
        let mut violations = Vec::new();
233
28
        let mut gaming_warnings = Vec::new();
234
        
235
        // Check overall score threshold
236
28
        if score.value < self.config.min_score {
237
2
            violations.push(Violation {
238
2
                violation_type: ViolationType::OverallScore,
239
2
                actual: score.value,
240
2
                required: self.config.min_score,
241
2
                severity: Severity::Critical,
242
2
                message: format!(
243
2
                    "Overall score {:.1}% below minimum {:.1}%",
244
2
                    score.value * 100.0,
245
2
                    self.config.min_score * 100.0
246
2
                ),
247
2
            });
248
26
        }
249
        
250
        // Check grade requirement
251
28
        if score.grade < self.config.min_grade {
252
2
            violations.push(Violation {
253
2
                violation_type: ViolationType::Grade,
254
2
                actual: score.value,
255
2
                required: self.config.min_score,
256
2
                severity: Severity::Critical,
257
2
                message: format!(
258
2
                    "Grade {} below minimum {}",
259
2
                    score.grade,
260
2
                    self.config.min_grade
261
2
                ),
262
2
            });
263
26
        }
264
        
265
        // Check component thresholds
266
28
        self.check_component_thresholds(score, &mut violations);
267
        
268
        // Check anti-gaming rules
269
28
        self.check_anti_gaming_rules(score, file_path, &mut gaming_warnings, &mut violations);
270
        
271
        // Check confidence threshold
272
28
        if score.confidence < self.config.anti_gaming.min_confidence {
273
25
            violations.push(Violation {
274
25
                violation_type: ViolationType::Confidence,
275
25
                actual: score.confidence,
276
25
                required: self.config.anti_gaming.min_confidence,
277
25
                severity: Severity::High,
278
25
                message: format!(
279
25
                    "Confidence {:.1}% below minimum {:.1}%",
280
25
                    score.confidence * 100.0,
281
25
                    self.config.anti_gaming.min_confidence * 100.0
282
25
                ),
283
25
            });
284
25
        
}3
285
        
286
28
        let passed = violations.iter().all(|v| 
v.severity25
!=
Severity::Critical25
);
287
        
288
28
        GateResult {
289
28
            passed,
290
28
            score: score.value,
291
28
            grade: score.grade,
292
28
            violations,
293
28
            confidence: score.confidence,
294
28
            gaming_warnings,
295
28
        }
296
28
    }
297
    
298
28
    fn check_component_thresholds(&self, score: &QualityScore, violations: &mut Vec<Violation>) {
299
28
        let thresholds = &self.config.component_thresholds;
300
        
301
28
        if score.components.correctness < thresholds.correctness {
302
2
            violations.push(Violation {
303
2
                violation_type: ViolationType::Correctness,
304
2
                actual: score.components.correctness,
305
2
                required: thresholds.correctness,
306
2
                severity: Severity::Critical,
307
2
                message: format!(
308
2
                    "Correctness {:.1}% below minimum {:.1}%",
309
2
                    score.components.correctness * 100.0,
310
2
                    thresholds.correctness * 100.0
311
2
                ),
312
2
            });
313
26
        }
314
        
315
28
        if score.components.performance < thresholds.performance {
316
2
            violations.push(Violation {
317
2
                violation_type: ViolationType::Performance,
318
2
                actual: score.components.performance,
319
2
                required: thresholds.performance,
320
2
                severity: Severity::High,
321
2
                message: format!(
322
2
                    "Performance {:.1}% below minimum {:.1}%",
323
2
                    score.components.performance * 100.0,
324
2
                    thresholds.performance * 100.0
325
2
                ),
326
2
            });
327
26
        }
328
        
329
28
        if score.components.maintainability < thresholds.maintainability {
330
2
            violations.push(Violation {
331
2
                violation_type: ViolationType::Maintainability,
332
2
                actual: score.components.maintainability,
333
2
                required: thresholds.maintainability,
334
2
                severity: Severity::High,
335
2
                message: format!(
336
2
                    "Maintainability {:.1}% below minimum {:.1}%",
337
2
                    score.components.maintainability * 100.0,
338
2
                    thresholds.maintainability * 100.0
339
2
                ),
340
2
            });
341
26
        }
342
        
343
28
        if score.components.safety < thresholds.safety {
344
2
            violations.push(Violation {
345
2
                violation_type: ViolationType::Safety,
346
2
                actual: score.components.safety,
347
2
                required: thresholds.safety,
348
2
                severity: Severity::Critical,
349
2
                message: format!(
350
2
                    "Safety {:.1}% below minimum {:.1}%",
351
2
                    score.components.safety * 100.0,
352
2
                    thresholds.safety * 100.0
353
2
                ),
354
2
            });
355
26
        }
356
        
357
28
        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
28
        }
370
28
    }
371
    
372
28
    fn check_anti_gaming_rules(
373
28
        &self,
374
28
        score: &QualityScore,
375
28
        file_path: Option<&PathBuf>,
376
28
        gaming_warnings: &mut Vec<String>,
377
28
        violations: &mut Vec<Violation>,
378
28
    ) {
379
        // Check cache hit rate (prevent stale analysis gaming)
380
28
        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
28
        }
386
        
387
        // Check file size requirements
388
28
        if let Some(
path24
) = file_path {
389
24
            if let Ok(metadata) = std::fs::metadata(path) {
390
24
                if metadata.len() < self.config.anti_gaming.min_file_size_bytes as u64 {
391
24
                    gaming_warnings.push(format!(
392
24
                        "File {} is very small ({} bytes) - may indicate gaming by splitting",
393
24
                        path.display(),
394
24
                        metadata.len()
395
24
                    ));
396
24
                
}0
397
0
            }
398
            
399
            // Check if critical files require deep analysis
400
24
            let path_str = path.to_string_lossy();
401
48
            if 
self.config.anti_gaming.require_deep_analysis.iter()24
.
any24
(|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
24
                }
414
4
        }
415
28
    }
416
    
417
    /// Export results for CI/CD integration
418
1
    pub fn export_ci_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> {
419
1
        if self.config.ci_integration.json_output {
420
1
            self.export_json_results(results, output_dir)
?0
;
421
0
        }
422
        
423
1
        if self.config.ci_integration.junit_xml {
424
1
            self.export_junit_results(results, output_dir)
?0
;
425
0
        }
426
        
427
1
        Ok(())
428
1
    }
429
    
430
1
    fn export_json_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> {
431
1
        let output_path = output_dir.join("quality-gates.json");
432
1
        let json_content = serde_json::to_string_pretty(results)
?0
;
433
1
        std::fs::write(output_path, json_content)
?0
;
434
1
        Ok(())
435
1
    }
436
    
437
1
    fn export_junit_results(&self, results: &[GateResult], output_dir: &Path) -> anyhow::Result<()> {
438
1
        let output_path = output_dir.join("quality-gates.xml");
439
        
440
1
        let total = results.len();
441
1
        let failures = results.iter().filter(|r| !r.passed).count();
442
        
443
1
        let mut xml = format!(r#"<?xml version="1.0" encoding="UTF-8"?>
444
1
<testsuite name="Quality Gates" tests="{total}" failures="{failures}" time="0.0">
445
1
"#);
446
        
447
1
        for (i, result) in results.iter().enumerate() {
448
1
            let test_name = format!("quality-gate-{i}");
449
1
            if result.passed {
450
1
                xml.push_str(&format!(
451
1
                    r#"  <testcase name="{test_name}" classname="QualityGate" time="0.0"/>
452
1
"#
453
1
                ));
454
1
            } 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
1
        xml.push_str("</testsuite>\n");
468
1
        std::fs::write(output_path, xml)
?0
;
469
1
        Ok(())
470
1
    }
471
}
472
473
impl PartialOrd for Grade {
474
28
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
475
28
        Some(self.cmp(other))
476
28
    }
477
}
478
479
impl Ord for Grade {
480
28
    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
28
        let self_rank = match self {
485
0
            F => 0,
486
2
            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
1
            A => 9,
495
25
            APlus => 10,
496
        };
497
        
498
28
        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
28
            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
28
        self_rank.cmp(&other_rank)
513
28
    }
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
2
    fn create_minimal_score() -> QualityScore {
525
        use crate::quality::scoring::ScoreComponents;
526
2
        QualityScore {
527
2
            value: 0.5,
528
2
            components: ScoreComponents {
529
2
                correctness: 0.5,
530
2
                performance: 0.5,
531
2
                maintainability: 0.5,
532
2
                safety: 0.5,
533
2
                idiomaticity: 0.5,
534
2
            },
535
2
            grade: Grade::D,
536
2
            confidence: 0.4,
537
2
            cache_hit_rate: 0.3,
538
2
        }
539
2
    }
540
541
2
    fn create_passing_score() -> QualityScore {
542
        use crate::quality::scoring::ScoreComponents;
543
2
        QualityScore {
544
2
            value: 0.85,
545
2
            components: ScoreComponents {
546
2
                correctness: 0.9,
547
2
                performance: 0.8,
548
2
                maintainability: 0.8,
549
2
                safety: 0.9,
550
2
                idiomaticity: 0.7,
551
2
            },
552
2
            grade: Grade::APlus,
553
2
            confidence: 0.9,
554
2
            cache_hit_rate: 0.2,
555
2
        }
556
2
    }
557
558
    // Test 1: Default Configuration Creation
559
    #[test]
560
1
    fn test_default_quality_gate_config() {
561
1
        let config = QualityGateConfig::default();
562
        
563
1
        assert_eq!(config.min_score, 0.7);
564
1
        assert_eq!(config.min_grade, Grade::BMinus);
565
1
        assert_eq!(config.component_thresholds.correctness, 0.8);
566
1
        assert_eq!(config.component_thresholds.safety, 0.8);
567
1
        assert_eq!(config.anti_gaming.min_confidence, 0.6);
568
1
        assert!(config.ci_integration.fail_on_violation);
569
1
        assert!(config.project_overrides.is_empty());
570
1
    }
571
572
    // Test 2: Quality Gate Enforcer Creation
573
    #[test]
574
1
    fn test_quality_gate_enforcer_creation() {
575
1
        let config = QualityGateConfig::default();
576
1
        let enforcer = QualityGateEnforcer::new(config.clone());
577
        
578
        // Verify enforcer uses the provided config
579
1
        let score = create_minimal_score();
580
1
        let result = enforcer.enforce_gates(&score, None);
581
        
582
        // Should fail with default thresholds
583
1
        assert!(!result.passed);
584
1
        assert!(!result.violations.is_empty());
585
1
    }
586
587
    // Test 3: Passing Quality Gate - All Criteria Met
588
    #[test]
589
1
    fn test_quality_gate_passes_with_high_score() {
590
1
        let config = QualityGateConfig::default();
591
1
        let enforcer = QualityGateEnforcer::new(config);
592
1
        let score = create_passing_score();
593
        
594
1
        let result = enforcer.enforce_gates(&score, None);
595
        
596
1
        assert!(result.passed, 
"High quality score should pass all gates"0
);
597
1
        assert_eq!(result.score, 0.85);
598
1
        assert_eq!(result.grade, Grade::APlus);
599
1
        assert!(result.violations.is_empty());
600
1
        assert_eq!(result.confidence, 0.9);
601
1
        assert!(result.gaming_warnings.is_empty());
602
1
    }
603
604
    // Test 4: Failing Overall Score Threshold
605
    #[test]
606
1
    fn test_quality_gate_fails_overall_score() {
607
1
        let config = QualityGateConfig::default(); // min_score: 0.7
608
1
        let enforcer = QualityGateEnforcer::new(config);
609
        
610
1
        let mut score = create_minimal_score();
611
1
        score.value = 0.6; // Below 0.7 threshold
612
        
613
1
        let result = enforcer.enforce_gates(&score, None);
614
        
615
1
        assert!(!result.passed, 
"Score below threshold should fail"0
);
616
        
617
        // Should have overall score violation
618
1
        let overall_violations: Vec<_> = result.violations.iter()
619
7
            .
filter1
(|v| v.violation_type == ViolationType::OverallScore)
620
1
            .collect();
621
1
        assert_eq!(overall_violations.len(), 1);
622
        
623
1
        let violation = &overall_violations[0];
624
1
        assert_eq!(violation.actual, 0.6);
625
1
        assert_eq!(violation.required, 0.7);
626
1
        assert_eq!(violation.severity, Severity::Critical);
627
1
        assert!(violation.message.contains("60.0%"));
628
1
        assert!(violation.message.contains("70.0%"));
629
1
    }
630
631
    // Test 5: Confidence Threshold Violation
632
    #[test]
633
1
    fn test_confidence_threshold_violation() {
634
1
        let config = QualityGateConfig::default(); // min_confidence: 0.6
635
1
        let enforcer = QualityGateEnforcer::new(config);
636
        
637
1
        let mut score = create_passing_score();
638
1
        score.confidence = 0.4; // Below 0.6 threshold
639
        
640
1
        let result = enforcer.enforce_gates(&score, None);
641
        
642
1
        let confidence_violations: Vec<_> = result.violations.iter()
643
1
            .filter(|v| v.violation_type == ViolationType::Confidence)
644
1
            .collect();
645
1
        assert_eq!(confidence_violations.len(), 1);
646
        
647
1
        let violation = &confidence_violations[0];
648
1
        assert_eq!(violation.severity, Severity::High);
649
1
        assert_eq!(violation.actual, 0.4);
650
1
        assert_eq!(violation.required, 0.6);
651
1
    }
652
653
    // Test 6: Configuration File Loading (Success)
654
    #[test]
655
1
    fn test_load_config_creates_default() {
656
1
        let temp_dir = TempDir::new().unwrap();
657
1
        let project_root = temp_dir.path();
658
        
659
1
        let config = QualityGateEnforcer::load_config(project_root).unwrap();
660
        
661
        // Should create default config
662
1
        assert_eq!(config.min_score, 0.7);
663
1
        assert_eq!(config.min_grade, Grade::BMinus);
664
        
665
        // Should create .ruchy/score.toml file
666
1
        let config_path = project_root.join(".ruchy").join("score.toml");
667
1
        assert!(config_path.exists(), 
"Config file should be created"0
);
668
        
669
        // File should contain valid TOML
670
1
        let content = std::fs::read_to_string(config_path).unwrap();
671
1
        assert!(content.contains("min_score"));
672
1
        assert!(content.contains("0.7"));
673
1
    }
674
675
    // Test 7: Serialization/Deserialization
676
    #[test]
677
1
    fn test_config_serialization() {
678
1
        let original_config = QualityGateConfig::default();
679
        
680
        // Serialize to TOML
681
1
        let toml_content = toml::to_string(&original_config).unwrap();
682
1
        assert!(toml_content.contains("min_score"));
683
        
684
        // Deserialize back
685
1
        let deserialized_config: QualityGateConfig = toml::from_str(&toml_content).unwrap();
686
1
        assert_eq!(deserialized_config.min_score, original_config.min_score);
687
1
        assert_eq!(deserialized_config.min_grade, original_config.min_grade);
688
1
    }
689
}