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
}