Skip to main content

libverify_core/controls/
harness_gate.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, EvidenceState};
3
4/// Verifies that all deterministic gates (tests, lint, typecheck) passed.
5pub struct HarnessGateControl;
6
7impl Control for HarnessGateControl {
8    fn id(&self) -> ControlId {
9        builtin::id(builtin::HARNESS_GATE)
10    }
11
12    fn description(&self) -> &'static str {
13        "All deterministic gates (tests, lint, typecheck) must pass"
14    }
15
16    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
17        let id = self.id();
18
19        let results = match &evidence.harness_results {
20            EvidenceState::NotApplicable => {
21                return vec![ControlFinding::not_applicable(
22                    id,
23                    "Harness results evidence is not applicable",
24                )];
25            }
26            EvidenceState::Missing { gaps } => {
27                return vec![ControlFinding::indeterminate(
28                    id,
29                    "Harness results evidence is unavailable",
30                    vec![],
31                    gaps.clone(),
32                )];
33            }
34            EvidenceState::Complete { value } | EvidenceState::Partial { value, .. } => value,
35        };
36
37        if results.is_empty() {
38            return vec![ControlFinding::indeterminate(
39                id,
40                "No harness results found",
41                vec![],
42                vec![],
43            )];
44        }
45
46        let failed: Vec<&str> = results
47            .iter()
48            .filter(|r| !r.passed)
49            .map(|r| r.name.as_str())
50            .collect();
51
52        if failed.is_empty() {
53            vec![ControlFinding::satisfied(
54                id,
55                format!("{} harness(es) passed", results.len()),
56                results.iter().map(|r| r.name.clone()).collect(),
57            )]
58        } else {
59            vec![ControlFinding::violated(
60                id,
61                format!("{} harness(es) failed: {}", failed.len(), failed.join(", ")),
62                failed.iter().map(|s| s.to_string()).collect(),
63            )]
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::control::ControlStatus;
72    use crate::evidence::{EvidenceGap, HarnessResult};
73
74    fn harness(name: &str, passed: bool) -> HarnessResult {
75        HarnessResult {
76            name: name.to_string(),
77            passed,
78            total: 10,
79            passed_count: if passed { 10 } else { 8 },
80            failed_count: if passed { 0 } else { 2 },
81            skipped_count: 0,
82            duration_secs: None,
83            source_format: None,
84        }
85    }
86
87    fn make_bundle(results: Vec<HarnessResult>) -> EvidenceBundle {
88        EvidenceBundle {
89            harness_results: EvidenceState::complete(results),
90            ..Default::default()
91        }
92    }
93
94    #[test]
95    fn all_pass_is_satisfied() {
96        let findings = HarnessGateControl.evaluate(&make_bundle(vec![
97            harness("unit-tests", true),
98            harness("lint", true),
99        ]));
100        assert_eq!(findings.len(), 1);
101        assert_eq!(findings[0].status, ControlStatus::Satisfied);
102        assert!(findings[0].rationale.contains("2 harness(es) passed"));
103    }
104
105    #[test]
106    fn one_fails_is_violated() {
107        let findings = HarnessGateControl.evaluate(&make_bundle(vec![
108            harness("unit-tests", true),
109            harness("lint", false),
110        ]));
111        assert_eq!(findings.len(), 1);
112        assert_eq!(findings[0].status, ControlStatus::Violated);
113        assert!(findings[0].rationale.contains("lint"));
114        assert!(findings[0].subjects.contains(&"lint".to_string()));
115    }
116
117    #[test]
118    fn empty_results_is_indeterminate() {
119        let findings = HarnessGateControl.evaluate(&make_bundle(vec![]));
120        assert_eq!(findings.len(), 1);
121        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
122    }
123
124    #[test]
125    fn missing_evidence_is_indeterminate() {
126        let bundle = EvidenceBundle {
127            harness_results: EvidenceState::missing(vec![EvidenceGap::CollectionFailed {
128                source: "ci".to_string(),
129                subject: "harness".to_string(),
130                detail: "timeout".to_string(),
131            }]),
132            ..Default::default()
133        };
134        let findings = HarnessGateControl.evaluate(&bundle);
135        assert_eq!(findings.len(), 1);
136        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
137        assert_eq!(findings[0].evidence_gaps.len(), 1);
138    }
139
140    #[test]
141    fn not_applicable_when_evidence_not_applicable() {
142        let bundle = EvidenceBundle {
143            harness_results: EvidenceState::not_applicable(),
144            ..Default::default()
145        };
146        let findings = HarnessGateControl.evaluate(&bundle);
147        assert_eq!(findings.len(), 1);
148        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
149    }
150
151    #[test]
152    fn control_id_is_harness_gate() {
153        assert_eq!(HarnessGateControl.id(), builtin::id(builtin::HARNESS_GATE));
154    }
155
156    #[test]
157    fn partial_evidence_still_evaluates() {
158        let bundle = EvidenceBundle {
159            harness_results: EvidenceState::partial(
160                vec![harness("unit-tests", true)],
161                vec![EvidenceGap::Truncated {
162                    source: "ci".to_string(),
163                    subject: "harness_results".to_string(),
164                }],
165            ),
166            ..Default::default()
167        };
168        let findings = HarnessGateControl.evaluate(&bundle);
169        assert_eq!(findings.len(), 1);
170        assert_eq!(findings[0].status, ControlStatus::Satisfied);
171    }
172}