Skip to main content

max / makenotwork

54.2 KB · 1610 lines History Blame Raw
1 //! PoM's projection onto the shared operator status payload.
2 //!
3 //! Spec + rationale: maintainer wiki.
4 //! <!-- wiki: release-status-payload -->
5 //!
6 //! `GET /status.json` serves this. Sando and Bento report on the thing they
7 //! *are*; PoM reports on the things it *watches*, so every target it monitors
8 //! becomes one node and the viewer needs no knowledge of health checks, TLS, or
9 //! incidents to draw PoM.
10 //!
11 //! [`payload`] is a pure function of `(&[TargetView], now)`. Every clock is an
12 //! argument, so a fixture renders identically forever and the mapping is
13 //! testable without a database.
14 //!
15 //! # What maps to what
16 //!
17 //! | PoM | payload |
18 //! |---|---|
19 //! | monitored target | node, `kind = "target"` |
20 //! | latest health check | node status + `health` condition |
21 //! | open incident | `incident` condition carrying the why |
22 //! | TLS certificate | `tls` condition (expiry) |
23 //! | domain registration | `whois` condition (expiry) |
24 //! | uptime / latency / version | fields |
25 //!
26 //! # Every signal colors the target
27 //!
28 //! The load-bearing difference from Sando. A Sando gate guards *promotion* and
29 //! must not color a tier, burn-in blocks for 48 hours as routine. PoM has no
30 //! such notion: a cert 3 days from expiry, an open incident, a domain about to
31 //! lapse are each a real problem with the target regardless of what the health
32 //! endpoint says. So a target's status is the worst of every condition on it,
33 //! and the why is never lost because it is in the conditions either way.
34 //!
35 //! PoM declares no actions. It observes; it does not act, and there is no route
36 //! to trigger a recheck. An empty actions map is the honest shape.
37
38 use chrono::{DateTime, Utc};
39 use ops_status::{Condition, Field, Node, Payload, Status, Value};
40
41 use crate::types::HealthStatus;
42
43 /// The `source` name PoM answers to in a viewer's config.
44 pub const SOURCE: &str = "pom";
45
46 /// Days before a certificate expires at which the target goes `degraded`.
47 const TLS_EXPIRY_WARN_DAYS: i64 = 14;
48
49 /// Days before a domain registration lapses at which the target goes `degraded`.
50 const WHOIS_EXPIRY_WARN_DAYS: i64 = 30;
51
52 /// Everything the payload needs about one monitored target, already read out of
53 /// the database.
54 ///
55 /// A dedicated view rather than the API's `TargetStatus`: that type is shaped
56 /// for the existing `/api/status` JSON and carries pre-formatted strings, which
57 /// is exactly what the shared contract forbids a producer from owning. Building
58 /// this from typed rows keeps [`payload`] pure and the DB out of the mapping.
59 pub(crate) struct TargetView {
60 /// Config key, e.g. "mnw".
61 pub name: String,
62 /// Human-readable label, e.g. "MakeNotWork".
63 pub label: String,
64 /// The most recent health snapshot. `None` before the first check.
65 pub health: Option<HealthView>,
66 /// Whether the target configures an HTTP health check at all.
67 ///
68 /// Distinct from `health` being `None`, which only says no snapshot has been
69 /// stored. Without this the two collapse and a target that will never have a
70 /// health endpoint reports `health: pending` forever: the desktop apps and the
71 /// SDK are test-only targets, so af, bb, go and sk sat permanently pending and
72 /// held the whole source there with them. A row that cannot ever go green is
73 /// worse than no row, because a surface where most rows are unsatisfiable is
74 /// one nobody reads.
75 pub health_configured: bool,
76 /// Uptime percentage over the last 24 hours, if any checks fell in the window.
77 pub uptime_24h: Option<f64>,
78 /// Mean response time over the last 24 hours of operational checks, in ms.
79 pub latency_avg_ms: Option<f64>,
80 /// Latest TLS check. `None` if TLS is not monitored for this target.
81 pub tls: Option<TlsView>,
82 /// Currently open incident, if the target is in one.
83 pub incident: Option<IncidentView>,
84 /// Latest WHOIS check. `None` if domain expiry is not monitored.
85 pub whois: Option<WhoisView>,
86 /// Latest backup freshness, one per monitored database. Empty if the target
87 /// has no backup monitoring configured.
88 pub backups: Vec<BackupView>,
89 /// Latest scan-pipeline check. `None` if not monitored for this target.
90 pub scan_pipeline: Option<ScanView>,
91 /// Latest local systemd daemon-health check. `None` if not monitored.
92 pub systemd: Option<SystemdView>,
93 /// Latest SyncKit field-version readout. `None` if not monitored.
94 pub synckit_fleet: Option<SyncKitFleetView>,
95 /// Latest test run and PoM's staleness verdict. `None` if the target has no
96 /// test config.
97 pub tests: Option<TestsView>,
98 /// Latest DNS record checks. `None` if DNS is not monitored for this target.
99 pub dns: Option<DnsView>,
100 /// Latest CORS preflight checks. `None` if CORS is not monitored.
101 pub cors: Option<CorsView>,
102 }
103
104 pub(crate) struct HealthView {
105 pub status: HealthStatus,
106 pub checked_at: String,
107 pub version: Option<String>,
108 /// The failure reason when the check did not pass.
109 pub error: Option<String>,
110 }
111
112 pub(crate) struct TlsView {
113 pub valid: bool,
114 pub days_remaining: i64,
115 pub checked_at: String,
116 pub error: Option<String>,
117 }
118
119 pub(crate) struct IncidentView {
120 pub from_status: String,
121 pub to_status: String,
122 pub started_at: String,
123 }
124
125 pub(crate) struct WhoisView {
126 pub days_remaining: Option<i64>,
127 pub checked_at: String,
128 pub error: Option<String>,
129 }
130
131 pub(crate) struct BackupView {
132 pub database: String,
133 /// One of "ok", "stale", "missing", "error".
134 pub status: String,
135 pub age_hours: Option<i64>,
136 pub checked_at: String,
137 pub error: Option<String>,
138 }
139
140 pub(crate) struct ScanView {
141 /// One of "operational", "degraded", "unreachable".
142 pub status: String,
143 /// Fired-threshold issue lines, the why behind a non-operational status.
144 pub issues: Vec<String>,
145 pub checked_at: String,
146 pub error: Option<String>,
147 }
148
149 pub(crate) struct SystemdView {
150 /// One of "operational", "degraded", "down".
151 pub status: String,
152 /// Unhealthy-unit / failed-sweep lines, the why behind a non-operational
153 /// status.
154 pub issues: Vec<String>,
155 pub checked_at: String,
156 pub error: Option<String>,
157 }
158
159 /// A SyncKit field-version readout. No status field: the reading cannot be
160 /// unhealthy, only unavailable. See [`crate::checks::synckit_fleet`].
161 pub(crate) struct SyncKitFleetView {
162 /// Devices that synced within the server's activity window.
163 pub devices: i64,
164 /// The window that count covers, in days.
165 pub window_days: i64,
166 /// Version and device count, most devices first. `None` version means
167 /// "syncing, version unknown".
168 pub versions: Vec<(Option<String>, i64)>,
169 pub checked_at: String,
170 /// Why the readout could not be taken. `None` on a successful reading.
171 pub error: Option<String>,
172 }
173
174 pub(crate) struct TestsView {
175 /// Whether any run has ever been recorded. Separates "no evidence"
176 /// (pending) from a run that failed.
177 pub ran: bool,
178 /// Whether the most recent run passed.
179 pub passed: bool,
180 pub total_passed: Option<i64>,
181 pub total_failed: Option<i64>,
182 /// When the most recent run started, RFC 3339.
183 pub started_at: Option<String>,
184 /// PoM's staleness verdict for the run: true when it is older than the
185 /// configured threshold or a version was deployed after it ran.
186 pub stale: bool,
187 pub stale_reason: Option<String>,
188 }
189
190 pub(crate) struct DnsView {
191 /// One entry per monitored record; non-empty by construction.
192 pub records: Vec<DnsRecordView>,
193 /// Latest checked_at across the records, for the condition's `since`.
194 pub checked_at: Option<String>,
195 }
196
197 pub(crate) struct DnsRecordView {
198 pub name: String,
199 pub record_type: String,
200 pub matches: bool,
201 pub error: Option<String>,
202 }
203
204 pub(crate) struct CorsView {
205 /// One entry per monitored URL; non-empty by construction.
206 pub checks: Vec<CorsCheckView>,
207 pub checked_at: Option<String>,
208 }
209
210 pub(crate) struct CorsCheckView {
211 pub url: String,
212 pub origin: String,
213 pub passes: bool,
214 pub error: Option<String>,
215 }
216
217 /// Restate every monitored target as the shared payload.
218 ///
219 /// `now` is an argument rather than read from the clock so the mapping stays
220 /// pure and snapshot-testable.
221 pub(crate) fn payload(targets: &[TargetView], now: DateTime<Utc>) -> Payload {
222 let mut payload = Payload::new(SOURCE, now);
223 for target in targets {
224 payload.nodes.push(target_node(target));
225 }
226 payload
227 }
228
229 fn target_node(target: &TargetView) -> Node {
230 // Only speak about health where health is watched. A stored snapshot still
231 // reports even if the config was since removed, so history stays visible.
232 let mut conditions = Vec::new();
233 if target.health_configured || target.health.is_some() {
234 conditions.push(health_condition(target.health.as_ref()));
235 }
236 if let Some(incident) = &target.incident {
237 conditions.push(incident_condition(incident));
238 }
239 if let Some(tls) = &target.tls {
240 conditions.push(tls_condition(tls));
241 }
242 if let Some(whois) = &target.whois
243 && let Some(condition) = whois_condition(whois)
244 {
245 conditions.push(condition);
246 }
247 for backup in &target.backups {
248 conditions.push(backup_condition(backup));
249 }
250 if let Some(scan) = &target.scan_pipeline {
251 conditions.push(scan_condition(scan));
252 }
253 if let Some(sd) = &target.systemd {
254 conditions.push(systemd_condition(sd));
255 }
256 if let Some(fleet) = &target.synckit_fleet {
257 conditions.push(synckit_fleet_condition(fleet));
258 }
259 if let Some(tests) = &target.tests {
260 conditions.push(tests_condition(tests));
261 }
262 if let Some(dns) = &target.dns {
263 conditions.push(dns_condition(dns));
264 }
265 if let Some(cors) = &target.cors {
266 conditions.push(cors_condition(cors));
267 }
268
269 // The target's status is the worst thing said about it. There is always at
270 // least the health condition, so the max is well-defined.
271 let status = conditions
272 .iter()
273 .map(|c| c.status)
274 .max()
275 .unwrap_or(Status::Unknown);
276
277 Node {
278 id: format!("target:{}", target.name),
279 kind: "target".into(),
280 label: target.label.clone(),
281 status,
282 fields: target_fields(target),
283 conditions,
284 children: Vec::new(),
285 actions: Vec::new(),
286 }
287 }
288
289 /// A health snapshot's status, or `pending` when the target has never been
290 /// checked, PoM has evidence of nothing rather than evidence of health.
291 fn health_condition(health: Option<&HealthView>) -> Condition {
292 let Some(health) = health else {
293 return Condition {
294 condition_type: "health".into(),
295 status: Status::Pending,
296 since: None,
297 detail: Some("no health check recorded yet".into()),
298 };
299 };
300 Condition {
301 condition_type: "health".into(),
302 status: health_status(health.status),
303 since: parse_instant(&health.checked_at),
304 // The error is the why on a failing check; a passing one needs no words.
305 detail: health.error.clone(),
306 }
307 }
308
309 /// PoM's four-value health vocabulary onto the shared five.
310 ///
311 /// `unreachable` and `error` both map to `failed`: from an operator's chair a
312 /// target that 5xxs and one that will not answer are the same event, it is
313 /// down. The distinction is preserved in the health snapshot's own error text.
314 fn health_status(status: HealthStatus) -> Status {
315 match status {
316 HealthStatus::Operational => Status::Ok,
317 HealthStatus::Degraded => Status::Degraded,
318 HealthStatus::Error | HealthStatus::Unreachable => Status::Failed,
319 }
320 }
321
322 /// An open incident is reported at the severity it escalated *to*, carrying when
323 /// it started so the viewer can age it.
324 fn incident_condition(incident: &IncidentView) -> Condition {
325 Condition {
326 condition_type: "incident".into(),
327 status: health_status_from_str(&incident.to_status),
328 since: parse_instant(&incident.started_at),
329 detail: Some(format!(
330 "{} to {}",
331 incident.from_status, incident.to_status
332 )),
333 }
334 }
335
336 /// Incident rows store the health status as a string. An unrecognized value is
337 /// `unknown` rather than a parse failure, keeping the node legible against a
338 /// future status PoM learns before the viewer does.
339 fn health_status_from_str(raw: &str) -> Status {
340 raw.parse::<HealthStatus>()
341 .map_or(Status::Unknown, health_status)
342 }
343
344 /// TLS state as a condition. A failed probe or an invalid chain is `failed`; a
345 /// certificate inside the warning window is `degraded`; anything further out is
346 /// `ok` and still carries its days-remaining, since the number is the point.
347 fn tls_condition(tls: &TlsView) -> Condition {
348 let (status, detail) = if let Some(error) = &tls.error {
349 (Status::Failed, format!("tls check failed: {error}"))
350 } else if !tls.valid {
351 (Status::Failed, "certificate chain is not valid".into())
352 } else if tls.days_remaining < 0 {
353 (
354 Status::Failed,
355 format!("certificate expired {} days ago", -tls.days_remaining),
356 )
357 } else if tls.days_remaining <= TLS_EXPIRY_WARN_DAYS {
358 (
359 Status::Degraded,
360 format!("certificate expires in {} days", tls.days_remaining),
361 )
362 } else {
363 (
364 Status::Ok,
365 format!("certificate valid, {} days remaining", tls.days_remaining),
366 )
367 };
368 Condition {
369 condition_type: "tls".into(),
370 status,
371 since: parse_instant(&tls.checked_at),
372 detail: Some(detail),
373 }
374 }
375
376 /// Domain-expiry state as a condition, or `None` when there is nothing to say.
377 ///
378 /// A WHOIS lookup that failed is `degraded`, not `failed`: registrar WHOIS is
379 /// flaky and a lookup error is not evidence the domain lapsed. Absent both an
380 /// error and a days count there is genuinely no signal, so no condition is
381 /// emitted rather than a permanently `unknown` one that trains the eye to skip
382 /// the target.
383 fn whois_condition(whois: &WhoisView) -> Option<Condition> {
384 let (status, detail) = if let Some(error) = &whois.error {
385 (Status::Degraded, format!("whois lookup failed: {error}"))
386 } else {
387 match whois.days_remaining {
388 Some(days) if days < 0 => (
389 Status::Failed,
390 format!("domain registration expired {} days ago", -days),
391 ),
392 Some(days) if days <= WHOIS_EXPIRY_WARN_DAYS => (
393 Status::Degraded,
394 format!("domain registration expires in {days} days"),
395 ),
396 Some(days) => (
397 Status::Ok,
398 format!("domain registration valid, {days} days remaining"),
399 ),
400 None => return None,
401 }
402 };
403 Some(Condition {
404 condition_type: "whois".into(),
405 status,
406 since: parse_instant(&whois.checked_at),
407 detail: Some(detail),
408 })
409 }
410
411 /// Backup freshness as a condition, one per database. A stale backup or a check
412 /// error is `degraded`; a missing backup is `failed`. The 40-day-stale backup
413 /// that stayed green by every check that existed is exactly the signal this
414 /// surfaces, the `type` names the database so several read as distinct rows.
415 fn backup_condition(backup: &BackupView) -> Condition {
416 let db = &backup.database;
417 let (status, detail) = match backup.status.as_str() {
418 "ok" => (
419 Status::Ok,
420 match backup.age_hours {
421 Some(hours) => format!("{db} backup is {hours}h old"),
422 None => format!("{db} backup present"),
423 },
424 ),
425 "stale" => (
426 Status::Degraded,
427 match backup.age_hours {
428 Some(hours) => format!("{db} backup is stale, {hours}h old"),
429 None => format!("{db} backup is stale"),
430 },
431 ),
432 "missing" => (Status::Failed, format!("no backup found for {db}")),
433 "error" => (
434 Status::Degraded,
435 match &backup.error {
436 Some(error) => format!("{db} backup check failed: {error}"),
437 None => format!("{db} backup check failed"),
438 },
439 ),
440 other => (Status::Unknown, format!("{db} backup status {other:?}")),
441 };
442 Condition {
443 condition_type: format!("backup:{db}"),
444 status,
445 since: parse_instant(&backup.checked_at),
446 detail: Some(detail),
447 }
448 }
449
450 /// Scan-pipeline state as a condition. `operational` is ok, `degraded` is
451 /// degraded, `unreachable` is failed; the fired-threshold issues, or the probe
452 /// error, are the why.
453 fn scan_condition(scan: &ScanView) -> Condition {
454 let status = match scan.status.as_str() {
455 "operational" => Status::Ok,
456 "degraded" => Status::Degraded,
457 "unreachable" => Status::Failed,
458 _ => Status::Unknown,
459 };
460 let detail = if let Some(error) = &scan.error {
461 format!("unreachable: {error}")
462 } else if !scan.issues.is_empty() {
463 scan.issues.join("; ")
464 } else {
465 "pipeline operational".into()
466 };
467 Condition {
468 condition_type: "scan_pipeline".into(),
469 status,
470 since: parse_instant(&scan.checked_at),
471 detail: Some(detail),
472 }
473 }
474
475 /// Local systemd daemon health as a condition. A down watched unit is `failed`
476 /// (red, a daemon that watches the platform is itself dead); a crash-loop or a
477 /// host-wide failed unit is `degraded` (yellow, look at this). A probe failure
478 /// (no systemd, no `--user` bus) is `failed` and names the error.
479 fn systemd_condition(sd: &SystemdView) -> Condition {
480 let status = match sd.status.as_str() {
481 "operational" => Status::Ok,
482 "degraded" => Status::Degraded,
483 "down" => Status::Failed,
484 _ => Status::Unknown,
485 };
486 let detail = if let Some(error) = &sd.error {
487 format!("probe error: {error}")
488 } else if !sd.issues.is_empty() {
489 sd.issues.join("; ")
490 } else {
491 "all watched daemons healthy".into()
492 };
493 Condition {
494 condition_type: "systemd".into(),
495 status,
496 since: parse_instant(&sd.checked_at),
497 detail: Some(detail),
498 }
499 }
500
501 /// The SyncKit field-version readout as a condition. Only two outcomes, and
502 /// neither of them depends on which versions came back: `ok` when the reading was
503 /// taken (the detail is the distribution), `degraded` when it could not be. No
504 /// version mix can fail this condition, because a user still on an old SDK is a
505 /// fact about the world rather than an incident PoM can page anyone about. A
506 /// broken readout *is* worth yellow: PoM has stopped being able to answer "which
507 /// SyncKit is in the field" and would otherwise go on reporting green while
508 /// knowing nothing. It stays out of red because whether the platform is up is the
509 /// `health` condition's job.
510 fn synckit_fleet_condition(fleet: &SyncKitFleetView) -> Condition {
511 let since = parse_instant(&fleet.checked_at);
512 let (status, detail) = match &fleet.error {
513 Some(error) => (
514 Status::Degraded,
515 format!("fleet readout unavailable: {error}"),
516 ),
517 None if fleet.devices == 0 => (
518 Status::Ok,
519 format!("no devices synced in {}d", fleet.window_days),
520 ),
521 None => (
522 Status::Ok,
523 format!(
524 "{} device{} in {}d: {}",
525 fleet.devices,
526 plural(fleet.devices as usize),
527 fleet.window_days,
528 fleet_summary(&fleet.versions)
529 ),
530 ),
531 };
532 Condition {
533 condition_type: "synckit_fleet".into(),
534 status,
535 since,
536 detail: Some(detail),
537 }
538 }
539
540 /// The distribution as one line, e.g. `0.6.0 x12, unknown x3`. An absent version
541 /// is spelled out rather than dropped: a fleet that is half unknown is a real
542 /// reading and hiding the unknowns would overstate what PoM knows.
543 fn fleet_summary(versions: &[(Option<String>, i64)]) -> String {
544 if versions.is_empty() {
545 return "no versions reported".into();
546 }
547 versions
548 .iter()
549 .map(|(version, devices)| format!("{} x{devices}", version.as_deref().unwrap_or("unknown")))
550 .collect::<Vec<_>>()
551 .join(", ")
552 }
553
554 /// Test state as a condition. A never-run target is `pending`: evidence of
555 /// nothing, like one never health-checked. A failing or stale run is `degraded`,
556 /// not `failed`: a red suite is a real regression signal, but the running
557 /// service is covered by `health`, so a test result colors the target yellow
558 /// (look at this) rather than red (it is down). Staleness reuses PoM's own
559 /// verdict, which fires on age past the configured threshold or a version
560 /// deployed since the last run.
561 fn tests_condition(tests: &TestsView) -> Condition {
562 if !tests.ran {
563 return Condition {
564 condition_type: "tests".into(),
565 status: Status::Pending,
566 since: None,
567 detail: Some("no tests have been run yet".into()),
568 };
569 }
570 let since = tests.started_at.as_deref().and_then(parse_instant);
571 let (status, detail) = if !tests.passed {
572 let detail = match (tests.total_passed, tests.total_failed) {
573 (Some(p), Some(f)) => format!("last run failed: {f} failed, {p} passed"),
574 _ => "last test run failed".into(),
575 };
576 (Status::Degraded, detail)
577 } else if tests.stale {
578 (
579 Status::Degraded,
580 tests
581 .stale_reason
582 .clone()
583 .unwrap_or_else(|| "test results are stale".into()),
584 )
585 } else {
586 let detail = match tests.total_passed {
587 Some(p) => format!("tests passing, {p} passed"),
588 None => "tests passing".into(),
589 };
590 (Status::Ok, detail)
591 };
592 Condition {
593 condition_type: "tests".into(),
594 status,
595 since,
596 detail: Some(detail),
597 }
598 }
599
600 /// DNS state as one aggregate condition across a target's monitored records. A
601 /// record whose lookup errored or whose value does not match is `degraded`: a
602 /// wrong or unverifiable record is a real misconfiguration, but a record that
603 /// points the site somewhere dead already shows as a failed `health` check, so
604 /// DNS colors the target yellow and names what is off. All records clean is
605 /// `ok`. One aggregate condition rather than one per record keeps a target that
606 /// watches several records to a single legible row.
607 fn dns_condition(dns: &DnsView) -> Condition {
608 let bad: Vec<String> = dns
609 .records
610 .iter()
611 .filter_map(|r| {
612 if let Some(error) = &r.error {
613 Some(format!(
614 "{} {} lookup failed: {error}",
615 r.name, r.record_type
616 ))
617 } else if !r.matches {
618 Some(format!("{} {} does not match", r.name, r.record_type))
619 } else {
620 None
621 }
622 })
623 .collect();
624 let since = dns.checked_at.as_deref().and_then(parse_instant);
625 let (status, detail) = if bad.is_empty() {
626 (
627 Status::Ok,
628 format!(
629 "{} record{} match",
630 dns.records.len(),
631 plural(dns.records.len())
632 ),
633 )
634 } else {
635 (Status::Degraded, bad.join("; "))
636 };
637 Condition {
638 condition_type: "dns".into(),
639 status,
640 since,
641 detail: Some(detail),
642 }
643 }
644
645 /// CORS state as one aggregate condition across a target's monitored URLs. A
646 /// preflight that failed to run, or that the server answered without allowing
647 /// the origin, is `degraded`: broken CORS breaks a browser client, but it is a
648 /// configuration fault rather than the service being down, so it colors the
649 /// target yellow. All preflights allowed is `ok`.
650 fn cors_condition(cors: &CorsView) -> Condition {
651 let bad: Vec<String> = cors
652 .checks
653 .iter()
654 .filter_map(|c| {
655 if let Some(error) = &c.error {
656 Some(format!("{} preflight failed: {error}", c.url))
657 } else if !c.passes {
658 Some(format!("{} does not allow {}", c.url, c.origin))
659 } else {
660 None
661 }
662 })
663 .collect();
664 let since = cors.checked_at.as_deref().and_then(parse_instant);
665 let (status, detail) = if bad.is_empty() {
666 (
667 Status::Ok,
668 format!(
669 "{} preflight check{} pass",
670 cors.checks.len(),
671 plural(cors.checks.len())
672 ),
673 )
674 } else {
675 (Status::Degraded, bad.join("; "))
676 };
677 Condition {
678 condition_type: "cors".into(),
679 status,
680 since,
681 detail: Some(detail),
682 }
683 }
684
685 /// The plural suffix for a count: "" for one, "s" otherwise.
686 fn plural(n: usize) -> &'static str {
687 if n == 1 { "" } else { "s" }
688 }
689
690 /// Uptime as a bar, latency as a magnitude, version as a comparable, each a
691 /// number that *means* something rather than a pre-rendered string.
692 fn target_fields(target: &TargetView) -> Vec<Field> {
693 let mut fields = Vec::new();
694
695 if let Some(version) = target.health.as_ref().and_then(|h| h.version.clone()) {
696 fields.push(Field::new("version", Value::Version { value: version }));
697 }
698 if let Some(uptime) = target.uptime_24h {
699 fields.push(Field::new(
700 "uptime 24h",
701 Value::Progress {
702 value: uptime,
703 max: 100.0,
704 unit: Some("%".into()),
705 },
706 ));
707 }
708 if let Some(latency) = target.latency_avg_ms {
709 fields.push(Field::new(
710 "latency 24h",
711 Value::Quantity {
712 value: latency,
713 unit: Some("ms".into()),
714 },
715 ));
716 }
717 // The fleet distribution is a field, not just a condition detail: it is the
718 // answer to a question an operator asks on purpose ("which SyncKit is out
719 // there"), not a by-product of something being wrong. A successful readout of
720 // zero devices still reports, so the number is visibly zero rather than
721 // missing.
722 if let Some(fleet) = &target.synckit_fleet
723 && fleet.error.is_none()
724 {
725 fields.push(Field::new(
726 "synckit fleet",
727 Value::Text {
728 value: fleet_summary(&fleet.versions),
729 },
730 ));
731 fields.push(Field::new(
732 "synckit devices",
733 Value::Quantity {
734 value: fleet.devices as f64,
735 unit: None,
736 },
737 ));
738 }
739 if let Some(checked) = target
740 .health
741 .as_ref()
742 .and_then(|h| parse_instant(&h.checked_at))
743 {
744 fields.push(Field::new("checked", Value::Instant { value: checked }));
745 }
746
747 fields
748 }
749
750 /// A timestamp that fails to parse costs only itself. A viewer that blanks on
751 /// one malformed row is worse than one missing a tooltip.
752 fn parse_instant(raw: &str) -> Option<DateTime<Utc>> {
753 DateTime::parse_from_rfc3339(raw)
754 .ok()
755 .map(|d| d.with_timezone(&Utc))
756 }
757
758 #[cfg(test)]
759 mod tests {
760 use super::*;
761
762 fn now() -> DateTime<Utc> {
763 "2026-07-21T18:24:39Z".parse().unwrap()
764 }
765
766 fn checked_at() -> String {
767 "2026-07-21T18:24:00Z".into()
768 }
769
770 fn healthy(name: &str) -> TargetView {
771 TargetView {
772 name: name.into(),
773 label: name.to_uppercase(),
774 health_configured: true,
775 health: Some(HealthView {
776 status: HealthStatus::Operational,
777 checked_at: checked_at(),
778 version: Some("1.4.0".into()),
779 error: None,
780 }),
781 uptime_24h: Some(100.0),
782 latency_avg_ms: Some(42.0),
783 tls: None,
784 incident: None,
785 whois: None,
786 backups: Vec::new(),
787 scan_pipeline: None,
788 systemd: None,
789 synckit_fleet: None,
790 tests: None,
791 dns: None,
792 cors: None,
793 }
794 }
795
796 fn node<'a>(p: &'a Payload, id: &str) -> &'a Node {
797 p.node(id).unwrap_or_else(|| panic!("no node {id}"))
798 }
799
800 #[test]
801 fn a_healthy_target_is_ok_and_structurally_sound() {
802 let p = payload(&[healthy("mnw")], now());
803
804 assert_eq!(p.source, SOURCE);
805 assert_eq!(p.schema, ops_status::SCHEMA_VERSION);
806 assert_eq!(p.validate(), Ok(()));
807 assert_eq!(node(&p, "target:mnw").status, Status::Ok);
808 assert_eq!(node(&p, "target:mnw").label, "MNW");
809 assert_eq!(p.worst_status(), Status::Ok);
810 }
811
812 #[test]
813 fn uptime_and_latency_are_typed_values_not_strings() {
814 let p = payload(&[healthy("mnw")], now());
815 let n = node(&p, "target:mnw");
816
817 let uptime = n.fields.iter().find(|f| f.label == "uptime 24h").unwrap();
818 assert_eq!(
819 uptime.value,
820 Value::Progress {
821 value: 100.0,
822 max: 100.0,
823 unit: Some("%".into())
824 }
825 );
826 let latency = n.fields.iter().find(|f| f.label == "latency 24h").unwrap();
827 assert_eq!(
828 latency.value,
829 Value::Quantity {
830 value: 42.0,
831 unit: Some("ms".into())
832 }
833 );
834 }
835
836 #[test]
837 fn an_unreachable_target_is_failed_and_says_why() {
838 let mut t = healthy("mnw");
839 t.health = Some(HealthView {
840 status: HealthStatus::Unreachable,
841 checked_at: checked_at(),
842 version: None,
843 error: Some("connection timed out".into()),
844 });
845 let p = payload(&[t], now());
846
847 let n = node(&p, "target:mnw");
848 assert_eq!(n.status, Status::Failed);
849 assert_eq!(n.conditions[0].condition_type, "health");
850 assert_eq!(
851 n.conditions[0].detail.as_deref(),
852 Some("connection timed out")
853 );
854 assert_eq!(p.worst_status(), Status::Failed);
855 }
856
857 #[test]
858 fn a_target_never_checked_is_pending_not_healthy() {
859 let mut t = healthy("new");
860 t.health = None;
861 let p = payload(&[t], now());
862
863 let n = node(&p, "target:new");
864 assert_eq!(n.status, Status::Pending);
865 assert_eq!(
866 n.conditions[0].detail.as_deref(),
867 Some("no health check recorded yet")
868 );
869 // No health snapshot means no version and no checked-at field.
870 assert!(n.fields.iter().all(|f| f.label != "version"));
871 assert!(n.fields.iter().all(|f| f.label != "checked"));
872 assert_eq!(p.validate(), Ok(()));
873 }
874
875 #[test]
876 fn an_expiring_certificate_degrades_an_otherwise_healthy_target() {
877 // The Sando contrast: unlike a promotion gate, an expiring cert is a real
878 // problem with the target and must color it even when health is green.
879 let mut t = healthy("mnw");
880 t.tls = Some(TlsView {
881 valid: true,
882 days_remaining: 9,
883 checked_at: checked_at(),
884 error: None,
885 });
886 let p = payload(&[t], now());
887
888 let n = node(&p, "target:mnw");
889 assert_eq!(n.status, Status::Degraded);
890 let tls = n
891 .conditions
892 .iter()
893 .find(|c| c.condition_type == "tls")
894 .unwrap();
895 assert_eq!(tls.status, Status::Degraded);
896 assert!(tls.detail.as_deref().unwrap().contains("9 days"));
897 }
898
899 #[test]
900 fn an_expired_certificate_fails_the_target() {
901 let mut t = healthy("mnw");
902 t.tls = Some(TlsView {
903 valid: true,
904 days_remaining: -3,
905 checked_at: checked_at(),
906 error: None,
907 });
908 let p = payload(&[t], now());
909
910 let n = node(&p, "target:mnw");
911 assert_eq!(n.status, Status::Failed);
912 let tls = n
913 .conditions
914 .iter()
915 .find(|c| c.condition_type == "tls")
916 .unwrap();
917 assert!(
918 tls.detail
919 .as_deref()
920 .unwrap()
921 .contains("expired 3 days ago")
922 );
923 }
924
925 #[test]
926 fn a_healthy_certificate_stays_ok_but_still_reports_its_runway() {
927 let mut t = healthy("mnw");
928 t.tls = Some(TlsView {
929 valid: true,
930 days_remaining: 60,
931 checked_at: checked_at(),
932 error: None,
933 });
934 let p = payload(&[t], now());
935
936 let n = node(&p, "target:mnw");
937 assert_eq!(n.status, Status::Ok);
938 let tls = n
939 .conditions
940 .iter()
941 .find(|c| c.condition_type == "tls")
942 .unwrap();
943 assert_eq!(tls.status, Status::Ok);
944 assert!(tls.detail.as_deref().unwrap().contains("60 days remaining"));
945 }
946
947 #[test]
948 fn an_open_incident_surfaces_with_its_transition_and_start() {
949 let mut t = healthy("mnw");
950 t.incident = Some(IncidentView {
951 from_status: "operational".into(),
952 to_status: "unreachable".into(),
953 started_at: "2026-07-21T17:00:00Z".into(),
954 });
955 // Health has recovered on paper but the incident is still open: the node
956 // must not read green while an incident stands.
957 let p = payload(&[t], now());
958
959 let n = node(&p, "target:mnw");
960 assert_eq!(n.status, Status::Failed);
961 let incident = n
962 .conditions
963 .iter()
964 .find(|c| c.condition_type == "incident")
965 .unwrap();
966 assert_eq!(incident.status, Status::Failed);
967 assert_eq!(
968 incident.detail.as_deref(),
969 Some("operational to unreachable")
970 );
971 assert_eq!(
972 incident.since,
973 Some("2026-07-21T17:00:00Z".parse::<DateTime<Utc>>().unwrap())
974 );
975 }
976
977 #[test]
978 fn a_failed_whois_lookup_is_degraded_not_failed() {
979 // Registrar WHOIS is flaky; a lookup error is not proof the domain lapsed.
980 let mut t = healthy("mnw");
981 t.whois = Some(WhoisView {
982 days_remaining: None,
983 checked_at: checked_at(),
984 error: Some("connection reset".into()),
985 });
986 let p = payload(&[t], now());
987
988 let n = node(&p, "target:mnw");
989 assert_eq!(n.status, Status::Degraded);
990 }
991
992 #[test]
993 fn an_expiring_domain_degrades_the_target() {
994 let mut t = healthy("mnw");
995 t.whois = Some(WhoisView {
996 days_remaining: Some(12),
997 checked_at: checked_at(),
998 error: None,
999 });
1000 let p = payload(&[t], now());
1001
1002 let n = node(&p, "target:mnw");
1003 assert_eq!(n.status, Status::Degraded);
1004 let whois = n
1005 .conditions
1006 .iter()
1007 .find(|c| c.condition_type == "whois")
1008 .unwrap();
1009 assert!(whois.detail.as_deref().unwrap().contains("12 days"));
1010 }
1011
1012 #[test]
1013 fn a_whois_check_with_no_signal_emits_no_condition() {
1014 let mut t = healthy("mnw");
1015 t.whois = Some(WhoisView {
1016 days_remaining: None,
1017 checked_at: checked_at(),
1018 error: None,
1019 });
1020 let p = payload(&[t], now());
1021
1022 let n = node(&p, "target:mnw");
1023 assert!(n.conditions.iter().all(|c| c.condition_type != "whois"));
1024 assert_eq!(n.status, Status::Ok);
1025 }
1026
1027 #[test]
1028 fn the_loudest_of_several_problems_wins_the_target() {
1029 let mut t = healthy("mnw");
1030 t.health = Some(HealthView {
1031 status: HealthStatus::Degraded,
1032 checked_at: checked_at(),
1033 version: Some("1.4.0".into()),
1034 error: Some("unexpected status 503".into()),
1035 });
1036 t.tls = Some(TlsView {
1037 valid: true,
1038 days_remaining: -1,
1039 checked_at: checked_at(),
1040 error: None,
1041 });
1042 let p = payload(&[t], now());
1043
1044 // health is degraded, tls is failed: the target is failed.
1045 assert_eq!(node(&p, "target:mnw").status, Status::Failed);
1046 }
1047
1048 #[test]
1049 fn one_targets_failure_does_not_touch_another() {
1050 let mut down = healthy("mt");
1051 down.health = Some(HealthView {
1052 status: HealthStatus::Error,
1053 checked_at: checked_at(),
1054 version: None,
1055 error: Some("500 Internal Server Error".into()),
1056 });
1057 let p = payload(&[healthy("mnw"), down], now());
1058
1059 assert_eq!(node(&p, "target:mnw").status, Status::Ok);
1060 assert_eq!(node(&p, "target:mt").status, Status::Failed);
1061 assert_eq!(p.worst_status(), Status::Failed);
1062 assert_eq!(p.validate(), Ok(()));
1063 }
1064
1065 #[test]
1066 fn an_unknown_incident_status_stays_legible() {
1067 let mut t = healthy("mnw");
1068 t.incident = Some(IncidentView {
1069 from_status: "operational".into(),
1070 to_status: "sideways".into(),
1071 started_at: checked_at(),
1072 });
1073 let p = payload(&[t], now());
1074
1075 let incident = node(&p, "target:mnw")
1076 .conditions
1077 .iter()
1078 .find(|c| c.condition_type == "incident")
1079 .unwrap();
1080 assert_eq!(incident.status, Status::Unknown);
1081 }
1082
1083 #[test]
1084 fn a_malformed_timestamp_costs_only_that_timestamp() {
1085 let mut t = healthy("mnw");
1086 t.health = Some(HealthView {
1087 status: HealthStatus::Operational,
1088 checked_at: "not a timestamp".into(),
1089 version: Some("1.4.0".into()),
1090 error: None,
1091 });
1092 let p = payload(&[t], now());
1093
1094 let n = node(&p, "target:mnw");
1095 assert_eq!(n.status, Status::Ok);
1096 assert_eq!(n.conditions[0].since, None);
1097 assert!(n.fields.iter().all(|f| f.label != "checked"));
1098 }
1099
1100 #[test]
1101 fn a_stale_backup_degrades_the_target_and_names_the_database() {
1102 // The 40-day-stale backup that stayed green by every check that existed.
1103 let mut t = healthy("mnw");
1104 t.backups = vec![BackupView {
1105 database: "makenotwork".into(),
1106 status: "stale".into(),
1107 age_hours: Some(960),
1108 checked_at: checked_at(),
1109 error: None,
1110 }];
1111 let p = payload(&[t], now());
1112
1113 let n = node(&p, "target:mnw");
1114 assert_eq!(n.status, Status::Degraded);
1115 let backup = n
1116 .conditions
1117 .iter()
1118 .find(|c| c.condition_type == "backup:makenotwork")
1119 .unwrap();
1120 assert_eq!(backup.status, Status::Degraded);
1121 assert!(backup.detail.as_deref().unwrap().contains("960h"));
1122 }
1123
1124 #[test]
1125 fn a_missing_backup_fails_the_target() {
1126 let mut t = healthy("mnw");
1127 t.backups = vec![BackupView {
1128 database: "makenotwork".into(),
1129 status: "missing".into(),
1130 age_hours: None,
1131 checked_at: checked_at(),
1132 error: None,
1133 }];
1134 let p = payload(&[t], now());
1135 assert_eq!(node(&p, "target:mnw").status, Status::Failed);
1136 }
1137
1138 #[test]
1139 fn several_databases_read_as_distinct_conditions() {
1140 let mut t = healthy("mnw");
1141 t.backups = vec![
1142 BackupView {
1143 database: "makenotwork".into(),
1144 status: "ok".into(),
1145 age_hours: Some(6),
1146 checked_at: checked_at(),
1147 error: None,
1148 },
1149 BackupView {
1150 database: "multithreaded".into(),
1151 status: "ok".into(),
1152 age_hours: Some(7),
1153 checked_at: checked_at(),
1154 error: None,
1155 },
1156 ];
1157 let p = payload(&[t], now());
1158
1159 let n = node(&p, "target:mnw");
1160 assert_eq!(n.status, Status::Ok);
1161 assert!(
1162 n.conditions
1163 .iter()
1164 .any(|c| c.condition_type == "backup:makenotwork")
1165 );
1166 assert!(
1167 n.conditions
1168 .iter()
1169 .any(|c| c.condition_type == "backup:multithreaded")
1170 );
1171 }
1172
1173 #[test]
1174 fn a_degraded_scan_pipeline_carries_its_issues() {
1175 let mut t = healthy("mnw");
1176 t.scan_pipeline = Some(ScanView {
1177 status: "degraded".into(),
1178 issues: vec!["thumbnail error rate 22%".into(), "queue stuck: 4".into()],
1179 checked_at: checked_at(),
1180 error: None,
1181 });
1182 let p = payload(&[t], now());
1183
1184 let n = node(&p, "target:mnw");
1185 assert_eq!(n.status, Status::Degraded);
1186 let scan = n
1187 .conditions
1188 .iter()
1189 .find(|c| c.condition_type == "scan_pipeline")
1190 .unwrap();
1191 assert!(
1192 scan.detail
1193 .as_deref()
1194 .unwrap()
1195 .contains("thumbnail error rate 22%")
1196 );
1197 assert!(scan.detail.as_deref().unwrap().contains("queue stuck: 4"));
1198 }
1199
1200 #[test]
1201 fn an_unreachable_scan_pipeline_fails_the_target() {
1202 let mut t = healthy("mnw");
1203 t.scan_pipeline = Some(ScanView {
1204 status: "unreachable".into(),
1205 issues: Vec::new(),
1206 checked_at: checked_at(),
1207 error: Some("502 Bad Gateway".into()),
1208 });
1209 let p = payload(&[t], now());
1210
1211 let n = node(&p, "target:mnw");
1212 assert_eq!(n.status, Status::Failed);
1213 let scan = n
1214 .conditions
1215 .iter()
1216 .find(|c| c.condition_type == "scan_pipeline")
1217 .unwrap();
1218 assert!(scan.detail.as_deref().unwrap().contains("502 Bad Gateway"));
1219 }
1220
1221 fn fleet_view(versions: Vec<(Option<&str>, i64)>) -> SyncKitFleetView {
1222 SyncKitFleetView {
1223 devices: versions.iter().map(|(_, d)| d).sum(),
1224 window_days: 30,
1225 versions: versions
1226 .into_iter()
1227 .map(|(v, d)| (v.map(str::to_string), d))
1228 .collect(),
1229 checked_at: checked_at(),
1230 error: None,
1231 }
1232 }
1233
1234 #[test]
1235 fn a_fleet_readout_reports_the_distribution_without_degrading() {
1236 let mut t = healthy("mnw");
1237 t.synckit_fleet = Some(fleet_view(vec![(Some("0.6.0"), 12), (None, 3)]));
1238 let p = payload(&[t], now());
1239
1240 let n = node(&p, "target:mnw");
1241 assert_eq!(n.status, Status::Ok);
1242 let fleet = n
1243 .conditions
1244 .iter()
1245 .find(|c| c.condition_type == "synckit_fleet")
1246 .unwrap();
1247 assert_eq!(fleet.status, Status::Ok);
1248 let detail = fleet.detail.as_deref().unwrap();
1249 assert!(detail.contains("15 devices in 30d"), "got {detail}");
1250 assert!(detail.contains("0.6.0 x12"), "got {detail}");
1251 assert!(detail.contains("unknown x3"), "got {detail}");
1252 }
1253
1254 #[test]
1255 fn an_ancient_version_in_the_field_does_not_degrade_the_target() {
1256 // The whole design call: version age is a fact, not an incident. A fleet
1257 // entirely on a year-old SDK must still read green.
1258 let mut t = healthy("mnw");
1259 t.synckit_fleet = Some(fleet_view(vec![(Some("0.1.0"), 200)]));
1260 let p = payload(&[t], now());
1261
1262 let n = node(&p, "target:mnw");
1263 assert_eq!(n.status, Status::Ok);
1264 }
1265
1266 #[test]
1267 fn an_empty_fleet_is_ok_and_says_so() {
1268 let mut t = healthy("mnw");
1269 t.synckit_fleet = Some(fleet_view(vec![]));
1270 let p = payload(&[t], now());
1271
1272 let n = node(&p, "target:mnw");
1273 assert_eq!(n.status, Status::Ok);
1274 let fleet = n
1275 .conditions
1276 .iter()
1277 .find(|c| c.condition_type == "synckit_fleet")
1278 .unwrap();
1279 assert!(
1280 fleet
1281 .detail
1282 .as_deref()
1283 .unwrap()
1284 .contains("no devices synced in 30d")
1285 );
1286 }
1287
1288 #[test]
1289 fn an_unavailable_fleet_readout_degrades_but_never_fails() {
1290 let mut t = healthy("mnw");
1291 let mut fleet = fleet_view(vec![]);
1292 fleet.error = Some("HTTP 401 (alerts ingest token rejected)".into());
1293 t.synckit_fleet = Some(fleet);
1294 let p = payload(&[t], now());
1295
1296 let n = node(&p, "target:mnw");
1297 assert_eq!(
1298 n.status,
1299 Status::Degraded,
1300 "a readout PoM cannot take is yellow, not red: health owns whether MNW is up"
1301 );
1302 let fleet = n
1303 .conditions
1304 .iter()
1305 .find(|c| c.condition_type == "synckit_fleet")
1306 .unwrap();
1307 assert!(fleet.detail.as_deref().unwrap().contains("401"));
1308 }
1309
1310 #[test]
1311 fn a_successful_readout_becomes_target_fields() {
1312 let mut t = healthy("mnw");
1313 t.synckit_fleet = Some(fleet_view(vec![(Some("0.6.0"), 12)]));
1314 let p = payload(&[t], now());
1315
1316 let n = node(&p, "target:mnw");
1317 assert!(n.fields.iter().any(|f| f.label == "synckit fleet"));
1318 assert!(n.fields.iter().any(|f| f.label == "synckit devices"));
1319 }
1320
1321 #[test]
1322 fn an_unavailable_readout_contributes_no_fields() {
1323 // A blank column beats a field reading "0 devices" that is really "PoM
1324 // could not ask".
1325 let mut t = healthy("mnw");
1326 let mut fleet = fleet_view(vec![]);
1327 fleet.error = Some("request: timed out".into());
1328 t.synckit_fleet = Some(fleet);
1329 let p = payload(&[t], now());
1330
1331 let n = node(&p, "target:mnw");
1332 assert!(!n.fields.iter().any(|f| f.label.starts_with("synckit")));
1333 }
1334
1335 fn tests_view(passed: bool, stale: bool) -> TestsView {
1336 TestsView {
1337 ran: true,
1338 passed,
1339 total_passed: Some(226),
1340 total_failed: if passed { Some(0) } else { Some(3) },
1341 started_at: Some(checked_at()),
1342 stale,
1343 stale_reason: if stale {
1344 Some("tests are 12 days old (threshold: 7d)".into())
1345 } else {
1346 None
1347 },
1348 }
1349 }
1350
1351 fn condition<'a>(n: &'a Node, ty: &str) -> &'a Condition {
1352 n.conditions
1353 .iter()
1354 .find(|c| c.condition_type == ty)
1355 .unwrap_or_else(|| panic!("no {ty} condition"))
1356 }
1357
1358 #[test]
1359 fn a_never_run_test_target_is_pending_not_healthy() {
1360 let mut t = healthy("mnw");
1361 t.tests = Some(TestsView {
1362 ran: false,
1363 passed: false,
1364 total_passed: None,
1365 total_failed: None,
1366 started_at: None,
1367 stale: true,
1368 stale_reason: Some("no tests have been run".into()),
1369 });
1370 let p = payload(&[t], now());
1371 let n = node(&p, "target:mnw");
1372 // Pending is quieter than degraded: no evidence is not a failure.
1373 assert_eq!(n.status, Status::Pending);
1374 assert_eq!(
1375 condition(n, "tests").detail.as_deref(),
1376 Some("no tests have been run yet")
1377 );
1378 }
1379
1380 #[test]
1381 fn a_failing_test_run_degrades_but_does_not_fail_the_target() {
1382 // A red suite is a regression signal; the running service is health's job.
1383 let mut t = healthy("mnw");
1384 t.tests = Some(tests_view(false, false));
1385 let p = payload(&[t], now());
1386 let n = node(&p, "target:mnw");
1387 assert_eq!(n.status, Status::Degraded);
1388 let c = condition(n, "tests");
1389 assert_eq!(c.status, Status::Degraded);
1390 assert!(c.detail.as_deref().unwrap().contains("3 failed"));
1391 }
1392
1393 #[test]
1394 fn a_stale_but_passing_test_run_degrades_the_target() {
1395 let mut t = healthy("mnw");
1396 t.tests = Some(tests_view(true, true));
1397 let p = payload(&[t], now());
1398 let n = node(&p, "target:mnw");
1399 assert_eq!(n.status, Status::Degraded);
1400 assert!(
1401 condition(n, "tests")
1402 .detail
1403 .as_deref()
1404 .unwrap()
1405 .contains("12 days old")
1406 );
1407 }
1408
1409 #[test]
1410 fn a_fresh_passing_test_run_leaves_the_target_ok() {
1411 let mut t = healthy("mnw");
1412 t.tests = Some(tests_view(true, false));
1413 let p = payload(&[t], now());
1414 let n = node(&p, "target:mnw");
1415 assert_eq!(n.status, Status::Ok);
1416 assert_eq!(condition(n, "tests").status, Status::Ok);
1417 }
1418
1419 #[test]
1420 fn a_dns_mismatch_degrades_the_target_and_names_the_record() {
1421 let mut t = healthy("mnw");
1422 t.dns = Some(DnsView {
1423 records: vec![
1424 DnsRecordView {
1425 name: "makenot.work".into(),
1426 record_type: "A".into(),
1427 matches: true,
1428 error: None,
1429 },
1430 DnsRecordView {
1431 name: "makenot.work".into(),
1432 record_type: "MX".into(),
1433 matches: false,
1434 error: None,
1435 },
1436 ],
1437 checked_at: Some(checked_at()),
1438 });
1439 let p = payload(&[t], now());
1440 let n = node(&p, "target:mnw");
1441 assert_eq!(n.status, Status::Degraded);
1442 let c = condition(n, "dns");
1443 assert_eq!(c.status, Status::Degraded);
1444 assert!(c.detail.as_deref().unwrap().contains("MX does not match"));
1445 }
1446
1447 #[test]
1448 fn a_dns_lookup_error_reads_as_a_flaky_degrade_not_a_mismatch() {
1449 let mut t = healthy("mnw");
1450 t.dns = Some(DnsView {
1451 records: vec![DnsRecordView {
1452 name: "makenot.work".into(),
1453 record_type: "TXT".into(),
1454 matches: false,
1455 error: Some("SERVFAIL".into()),
1456 }],
1457 checked_at: Some(checked_at()),
1458 });
1459 let p = payload(&[t], now());
1460 let c = condition(node(&p, "target:mnw"), "dns");
1461 assert_eq!(c.status, Status::Degraded);
1462 assert!(
1463 c.detail
1464 .as_deref()
1465 .unwrap()
1466 .contains("lookup failed: SERVFAIL")
1467 );
1468 }
1469
1470 #[test]
1471 fn all_dns_records_matching_stays_ok_and_counts_them() {
1472 let mut t = healthy("mnw");
1473 t.dns = Some(DnsView {
1474 records: vec![DnsRecordView {
1475 name: "makenot.work".into(),
1476 record_type: "A".into(),
1477 matches: true,
1478 error: None,
1479 }],
1480 checked_at: Some(checked_at()),
1481 });
1482 let p = payload(&[t], now());
1483 let n = node(&p, "target:mnw");
1484 assert_eq!(n.status, Status::Ok);
1485 // Singular, not "1 records match".
1486 assert_eq!(
1487 condition(n, "dns").detail.as_deref(),
1488 Some("1 record match")
1489 );
1490 }
1491
1492 #[test]
1493 fn a_cors_misconfiguration_degrades_the_target() {
1494 let mut t = healthy("mnw");
1495 t.cors = Some(CorsView {
1496 checks: vec![CorsCheckView {
1497 url: "https://makenot.work/api".into(),
1498 origin: "https://app.makenot.work".into(),
1499 passes: false,
1500 error: None,
1501 }],
1502 checked_at: Some(checked_at()),
1503 });
1504 let p = payload(&[t], now());
1505 let n = node(&p, "target:mnw");
1506 assert_eq!(n.status, Status::Degraded);
1507 assert!(
1508 condition(n, "cors")
1509 .detail
1510 .as_deref()
1511 .unwrap()
1512 .contains("does not allow https://app.makenot.work")
1513 );
1514 }
1515
1516 #[test]
1517 fn passing_cors_preflights_stay_ok() {
1518 let mut t = healthy("mnw");
1519 t.cors = Some(CorsView {
1520 checks: vec![CorsCheckView {
1521 url: "https://makenot.work/api".into(),
1522 origin: "https://app.makenot.work".into(),
1523 passes: true,
1524 error: None,
1525 }],
1526 checked_at: Some(checked_at()),
1527 });
1528 let p = payload(&[t], now());
1529 assert_eq!(node(&p, "target:mnw").status, Status::Ok);
1530 }
1531
1532 #[test]
1533 fn the_new_conditions_do_not_disturb_a_target_that_has_none_of_them() {
1534 // A target without test/dns/cors config emits none of the three, exactly
1535 // as before, the additive property the shared contract exists to give.
1536 let p = payload(&[healthy("mnw")], now());
1537 let n = node(&p, "target:mnw");
1538 assert!(n.conditions.iter().all(|c| c.condition_type != "tests"));
1539 assert!(n.conditions.iter().all(|c| c.condition_type != "dns"));
1540 assert!(n.conditions.iter().all(|c| c.condition_type != "cors"));
1541 assert_eq!(p.validate(), Ok(()));
1542 }
1543
1544 #[test]
1545 fn a_target_that_does_not_watch_health_says_nothing_about_health() {
1546 // The test-only targets (the desktop apps, the SDK) have no HTTP endpoint
1547 // and never will. Emitting `health: pending` for them is a row that can
1548 // never go green, which held af/bb/go/sk pending forever and the whole
1549 // source with them.
1550 let mut t = healthy("af");
1551 t.health_configured = false;
1552 t.health = None;
1553 let p = payload(&[t], now());
1554 let n = node(&p, "target:af");
1555 assert!(
1556 n.conditions.iter().all(|c| c.condition_type != "health"),
1557 "unconfigured health must emit no condition, got {:?}",
1558 n.conditions
1559 );
1560 assert_eq!(p.validate(), Ok(()));
1561 }
1562
1563 #[test]
1564 fn a_stored_snapshot_still_reports_after_the_config_is_removed() {
1565 // Config is truth for whether to watch, but evidence already collected
1566 // should not vanish: dropping the block should not silently erase the
1567 // last thing PoM knew about that target's health.
1568 let mut t = healthy("mnw");
1569 t.health_configured = false;
1570 let p = payload(&[t], now());
1571 let n = node(&p, "target:mnw");
1572 assert!(n.conditions.iter().any(|c| c.condition_type == "health"));
1573 assert_eq!(p.validate(), Ok(()));
1574 }
1575
1576 #[test]
1577 fn a_watched_target_with_no_snapshot_yet_is_still_pending() {
1578 // The case the flag must not break: health IS configured, no check has
1579 // run, so "evidence of nothing" is the honest answer.
1580 let mut t = healthy("mnw");
1581 t.health = None;
1582 let p = payload(&[t], now());
1583 let n = node(&p, "target:mnw");
1584 let h = n
1585 .conditions
1586 .iter()
1587 .find(|c| c.condition_type == "health")
1588 .expect("configured health must still emit a condition");
1589 assert_eq!(h.status, Status::Pending);
1590 assert_eq!(p.validate(), Ok(()));
1591 }
1592
1593 #[test]
1594 fn render_is_a_pure_function_of_state_and_clock() {
1595 let a = payload(&[healthy("mnw")], now());
1596 let b = payload(&[healthy("mnw")], now());
1597 assert_eq!(
1598 serde_json::to_value(&a).unwrap(),
1599 serde_json::to_value(&b).unwrap()
1600 );
1601 }
1602
1603 #[test]
1604 fn pom_declares_no_actions() {
1605 let p = payload(&[healthy("mnw")], now());
1606 assert!(p.actions.is_empty());
1607 assert!(node(&p, "target:mnw").actions.is_empty());
1608 }
1609 }
1610