1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, EvidenceState};
3
4pub struct DeploymentHealthControl;
6
7impl Control for DeploymentHealthControl {
8 fn id(&self) -> ControlId {
9 builtin::id(builtin::DEPLOYMENT_HEALTH)
10 }
11
12 fn description(&self) -> &'static str {
13 "Deployment must maintain healthy service metrics"
14 }
15
16 fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
17 let id = self.id();
18
19 let diff = match &evidence.behavioral_diff {
20 EvidenceState::NotApplicable => {
21 return vec![ControlFinding::not_applicable(
22 id,
23 "Behavioral diff evidence is not applicable",
24 )];
25 }
26 EvidenceState::Missing { gaps } => {
27 return vec![ControlFinding::indeterminate(
28 id,
29 "Behavioral diff evidence is unavailable",
30 vec![],
31 gaps.clone(),
32 )];
33 }
34 EvidenceState::Complete { value } | EvidenceState::Partial { value, .. } => value,
35 };
36
37 let mut violations: Vec<String> = Vec::new();
38
39 for metric in &diff.metrics {
40 let lower = metric.name.to_lowercase();
41
42 if (lower.contains("error_rate") || lower.contains("_5xx")) && metric.current > 5.0 {
44 violations.push(format!(
45 "{}: {:.2}% (threshold: 5%)",
46 metric.name, metric.current,
47 ));
48 }
49
50 if (lower.contains("availability") || lower.contains("uptime")) && metric.current < 99.0
52 {
53 violations.push(format!(
54 "{}: {:.2}% (threshold: 99%)",
55 metric.name, metric.current,
56 ));
57 }
58 }
59
60 if violations.is_empty() {
61 vec![ControlFinding::satisfied(
62 id,
63 format!(
64 "Deployment {} is healthy — no critical thresholds breached",
65 diff.deployment_id
66 ),
67 vec![diff.deployment_id.clone()],
68 )]
69 } else {
70 vec![ControlFinding::violated(
71 id,
72 format!("Service health degraded: {}", violations.join("; ")),
73 violations,
74 )]
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::control::ControlStatus;
83 use crate::evidence::{BehavioralDiff, EvidenceGap, MetricObservation};
84
85 fn metric(name: &str, baseline: f64, current: f64) -> MetricObservation {
86 MetricObservation {
87 name: name.to_string(),
88 current,
89 baseline,
90 unit: None,
91 window_secs: None,
92 }
93 }
94
95 fn make_bundle(metrics: Vec<MetricObservation>) -> EvidenceBundle {
96 EvidenceBundle {
97 behavioral_diff: EvidenceState::complete(BehavioralDiff {
98 deployment_id: "deploy-42".to_string(),
99 environment: Some("production".to_string()),
100 metrics,
101 observed_at: None,
102 }),
103 ..Default::default()
104 }
105 }
106
107 #[test]
108 fn healthy_metrics_is_satisfied() {
109 let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
110 metric("error_rate_5xx", 0.5, 1.0),
111 metric("service_availability", 99.99, 99.95),
112 ]));
113 assert_eq!(findings.len(), 1);
114 assert_eq!(findings[0].status, ControlStatus::Satisfied);
115 }
116
117 #[test]
118 fn high_error_rate_is_violated() {
119 let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
120 metric("error_rate_5xx", 0.5, 6.0), ]));
122 assert_eq!(findings.len(), 1);
123 assert_eq!(findings[0].status, ControlStatus::Violated);
124 assert!(findings[0].rationale.contains("error_rate_5xx"));
125 }
126
127 #[test]
128 fn low_availability_is_violated() {
129 let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
130 metric("service_availability", 99.99, 98.5), ]));
132 assert_eq!(findings.len(), 1);
133 assert_eq!(findings[0].status, ControlStatus::Violated);
134 assert!(findings[0].rationale.contains("availability"));
135 }
136
137 #[test]
138 fn unrelated_metrics_are_not_checked() {
139 let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
140 metric("cpu_usage", 50.0, 95.0), metric("requests_rps", 1000.0, 500.0), ]));
143 assert_eq!(findings.len(), 1);
144 assert_eq!(findings[0].status, ControlStatus::Satisfied);
145 }
146
147 #[test]
148 fn missing_evidence_is_indeterminate() {
149 let bundle = EvidenceBundle {
150 behavioral_diff: EvidenceState::missing(vec![EvidenceGap::CollectionFailed {
151 source: "datadog".to_string(),
152 subject: "metrics".to_string(),
153 detail: "auth failed".to_string(),
154 }]),
155 ..Default::default()
156 };
157 let findings = DeploymentHealthControl.evaluate(&bundle);
158 assert_eq!(findings.len(), 1);
159 assert_eq!(findings[0].status, ControlStatus::Indeterminate);
160 }
161
162 #[test]
163 fn not_applicable_when_evidence_not_applicable() {
164 let bundle = EvidenceBundle::default();
165 let findings = DeploymentHealthControl.evaluate(&bundle);
166 assert_eq!(findings.len(), 1);
167 assert_eq!(findings[0].status, ControlStatus::NotApplicable);
168 }
169
170 #[test]
171 fn multiple_violations_are_all_reported() {
172 let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
173 metric("error_rate_5xx", 0.5, 7.0),
174 metric("service_availability", 99.99, 95.0),
175 ]));
176 assert_eq!(findings.len(), 1);
177 assert_eq!(findings[0].status, ControlStatus::Violated);
178 assert_eq!(findings[0].subjects.len(), 2);
179 }
180
181 #[test]
182 fn error_rate_at_boundary_is_satisfied() {
183 let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![metric(
185 "error_rate_5xx",
186 0.5,
187 5.0,
188 )]));
189 assert_eq!(findings.len(), 1);
190 assert_eq!(findings[0].status, ControlStatus::Satisfied);
191 }
192
193 #[test]
194 fn uptime_metric_also_detected() {
195 let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![metric(
196 "system_uptime",
197 99.99,
198 97.0,
199 )]));
200 assert_eq!(findings.len(), 1);
201 assert_eq!(findings[0].status, ControlStatus::Violated);
202 }
203}