libverify_core/controls/
harness_gate.rs1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, EvidenceState};
3
4pub 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}