Skip to main content

max / makenotwork

53.1 KB · 1710 lines History Blame Raw
1 //! Pure formatting functions for CLI display output.
2 //!
3 //! Each function takes data types and writes formatted output to a `String`,
4 //! keeping display logic separate from async I/O for testability.
5
6 use std::fmt::Write;
7
8 use crate::db::{DnsCheckRow, IncidentRow, PruneResult, RouteCheckRow, TlsCheckRow, WhoisCheckRow};
9 use crate::types::{
10 DnsCheckResult, HealthSnapshot, LatencyStats, TestRun, TestStaleness, VersionRow, WhoisResult,
11 };
12
13 /// Strip terminal control characters from an untrusted remote string before it
14 /// is written to the operator's terminal.
15 ///
16 /// A monitored host's health `version`/`uptime`/`error`, a whois/DNS error, a
17 /// mesh peer's fields, or *any* TCP service on an ssh_banner target can embed raw
18 /// ANSI escape sequences (ESC/CSI) that clear or rewrite the screen, e.g. a fake
19 /// all-clear, the moment they're printed on `pom status`/`mesh` (fuzz-2026-07-06
20 /// ANSI/terminal injection). Every such value passes through here at the display
21 /// sink. Keeps printable UTF-8; drops C0/C1 controls (incl. ESC, CR, LF) and DEL.
22 fn scrub(s: &str) -> String {
23 s.chars().filter(|c| !c.is_control()).collect()
24 }
25
26 /// Format a single health snapshot as a human-readable line.
27 pub fn format_health_snapshot(s: &HealthSnapshot) -> String {
28 let mut out = String::new();
29 write!(out, "[{}] {}: {}", s.status.icon(), s.target, s.status).unwrap();
30 write!(out, " ({}ms)", s.response_time_ms).unwrap();
31 if let Some(details) = &s.details {
32 if let Some(v) = &details.version {
33 write!(out, " v{}", scrub(v)).unwrap();
34 }
35 if let Some(u) = &details.uptime {
36 write!(out, " up {}", scrub(u)).unwrap();
37 }
38 }
39 writeln!(out).unwrap();
40 if let Some(err) = &s.error {
41 writeln!(out, " {}", scrub(err)).unwrap();
42 }
43 out
44 }
45
46 /// Format a list of health snapshots for CLI display.
47 pub fn format_health_snapshots(snapshots: &[HealthSnapshot]) -> String {
48 let mut out = String::new();
49 for s in snapshots {
50 out.push_str(&format_health_snapshot(s));
51 }
52 out
53 }
54
55 /// Format a test run result for CLI display.
56 pub fn format_test_result(target_name: &str, run: &TestRun) -> String {
57 let mut out = String::new();
58 let result = if run.passed { "PASSED" } else { "FAILED" };
59 writeln!(out, "{target_name}: {result}").unwrap();
60 if let Some(d) = run.duration_secs {
61 writeln!(out, "Duration: {d}s").unwrap();
62 }
63 if let (Some(p), Some(f)) = (run.summary.total_passed, run.summary.total_failed) {
64 writeln!(out, "Tests: {p} passed, {f} failed").unwrap();
65 }
66 for step in &run.summary.steps {
67 let mark = if step.passed { "PASS" } else { "FAIL" };
68 writeln!(out, " {mark} {}", step.name).unwrap();
69 }
70 if !run.passed {
71 writeln!(out, "\nRaw output:\n{}", run.raw_output).unwrap();
72 }
73 out
74 }
75
76 /// Format a single target's status block for CLI display.
77 #[allow(clippy::too_many_arguments)]
78 pub fn format_status_target(
79 name: &str,
80 label: &str,
81 health: Option<&HealthSnapshot>,
82 latency: Option<&LatencyStats>,
83 tls: Option<&TlsCheckRow>,
84 route_checks: Option<&[RouteCheckRow]>,
85 dns_checks: Option<&[DnsCheckRow]>,
86 whois: Option<&WhoisCheckRow>,
87 test: Option<&TestRun>,
88 staleness: Option<&TestStaleness>,
89 incident: Option<&IncidentRow>,
90 ) -> String {
91 let mut out = String::new();
92 writeln!(out, "=== {name} ({label}) ===").unwrap();
93
94 if let Some(h) = health {
95 write!(out, " Health: [{}] {}", h.status.icon(), h.status).unwrap();
96 write!(out, " ({}ms)", h.response_time_ms).unwrap();
97 if let Some(d) = &h.details
98 && let Some(v) = &d.version
99 {
100 write!(out, " v{}", scrub(v)).unwrap();
101 }
102 writeln!(out).unwrap();
103 } else {
104 writeln!(out, " Health: no data").unwrap();
105 }
106
107 if let Some(l) = latency {
108 writeln!(
109 out,
110 " Latency (24h): avg {:.0}ms, p95 {}ms, range {}-{}ms ({} samples)",
111 l.avg_ms, l.p95_ms, l.min_ms, l.max_ms, l.sample_count
112 )
113 .unwrap();
114 }
115
116 if let Some(t) = tls {
117 if let Some(ref err) = t.error {
118 writeln!(out, " TLS: [ERR] {}: {}", t.host, scrub(err)).unwrap();
119 } else if t.days_remaining <= 0 {
120 writeln!(
121 out,
122 " TLS: [ERR] {}: EXPIRED (expired {})",
123 t.host, t.not_after
124 )
125 .unwrap();
126 } else if t.days_remaining <= 14 {
127 writeln!(
128 out,
129 " TLS: [WARN] {}: {}d remaining (expires {})",
130 t.host, t.days_remaining, t.not_after
131 )
132 .unwrap();
133 } else {
134 writeln!(
135 out,
136 " TLS: [OK] {}: {}d remaining (expires {})",
137 t.host, t.days_remaining, t.not_after
138 )
139 .unwrap();
140 }
141 }
142
143 if let Some(checks) = route_checks
144 && !checks.is_empty()
145 {
146 let total = checks.len();
147 let ok_count = checks.iter().filter(|c| c.ok).count();
148 if ok_count == total {
149 writeln!(out, " Routes: {ok_count}/{total} OK").unwrap();
150 } else {
151 let failed: Vec<&str> = checks
152 .iter()
153 .filter(|c| !c.ok)
154 .map(|c| c.path.as_str())
155 .collect();
156 writeln!(
157 out,
158 " Routes: {ok_count}/{total} (FAIL: {})",
159 failed.join(", ")
160 )
161 .unwrap();
162 }
163 }
164
165 if let Some(checks) = dns_checks
166 && !checks.is_empty()
167 {
168 let total = checks.len();
169 let ok_count = checks.iter().filter(|c| c.matches).count();
170 if ok_count == total {
171 writeln!(out, " DNS: {ok_count}/{total} match").unwrap();
172 } else {
173 let failed: Vec<String> = checks
174 .iter()
175 .filter(|c| !c.matches)
176 .map(|c| format!("{} {}", c.name, c.record_type))
177 .collect();
178 writeln!(
179 out,
180 " DNS: {ok_count}/{total} (MISMATCH: {})",
181 failed.join(", ")
182 )
183 .unwrap();
184 }
185 }
186
187 if let Some(w) = whois {
188 if let Some(ref err) = w.error {
189 writeln!(out, " WHOIS: [ERR] {}: {}", w.domain, scrub(err)).unwrap();
190 } else if let Some(days) = w.days_remaining {
191 if days <= 0 {
192 writeln!(out, " WHOIS: [ERR] {}: EXPIRED", w.domain).unwrap();
193 } else if days <= 30 {
194 writeln!(out, " WHOIS: [WARN] {}: {}d remaining", w.domain, days).unwrap();
195 } else {
196 writeln!(out, " WHOIS: [OK] {}: {}d remaining", w.domain, days).unwrap();
197 }
198 }
199 }
200
201 if let Some(t) = test {
202 let result = if t.passed { "PASSED" } else { "FAILED" };
203 write!(out, " Tests: {result}").unwrap();
204 if let Some(d) = t.duration_secs {
205 write!(out, " ({d}s)").unwrap();
206 }
207 writeln!(out).unwrap();
208 if let (Some(p), Some(f)) = (t.summary.total_passed, t.summary.total_failed) {
209 writeln!(out, " {p} passed, {f} failed").unwrap();
210 }
211 } else {
212 writeln!(out, " Tests: no data").unwrap();
213 }
214
215 if let Some(s) = staleness
216 && s.stale
217 && let Some(reason) = &s.reason
218 {
219 writeln!(out, " Tests: STALE ({})", scrub(reason)).unwrap();
220 }
221
222 if let Some(inc) = incident {
223 writeln!(
224 out,
225 " Incident: [ACTIVE] {} since {}",
226 inc.to_status, inc.started_at
227 )
228 .unwrap();
229 }
230
231 writeln!(out).unwrap();
232 out
233 }
234
235 /// Format health check history for CLI display.
236 pub fn format_health_history(history: &[HealthSnapshot]) -> String {
237 if history.is_empty() {
238 return "No health check history.\n".to_string();
239 }
240 let mut out = String::new();
241 for h in history {
242 writeln!(
243 out,
244 "[{}] {}: {} ({}ms) {}",
245 h.status.icon(),
246 h.target,
247 h.status,
248 h.response_time_ms,
249 h.checked_at
250 )
251 .unwrap();
252 }
253 out
254 }
255
256 /// Format test run history for CLI display.
257 pub fn format_test_history(history: &[TestRun]) -> String {
258 if history.is_empty() {
259 return "No test run history.\n".to_string();
260 }
261 let mut out = String::new();
262 for r in history {
263 let result = if r.passed { "PASS" } else { "FAIL" };
264 write!(out, "[{result}] {}", r.target).unwrap();
265 if let Some(d) = r.duration_secs {
266 write!(out, " ({d}s)").unwrap();
267 }
268 write!(out, " {}", r.started_at).unwrap();
269 if let (Some(p), Some(f)) = (r.summary.total_passed, r.summary.total_failed) {
270 write!(out, " ({p} passed, {f} failed)").unwrap();
271 }
272 writeln!(out).unwrap();
273 }
274 out
275 }
276
277 /// Format regression warnings for CLI display.
278 pub fn format_regressions(regressions: &[String]) -> String {
279 let mut out = String::new();
280 writeln!(out, "\nREGRESSIONS (passed last run, failed now):").unwrap();
281 for name in regressions {
282 writeln!(out, " {name}").unwrap();
283 }
284 out
285 }
286
287 /// Format test duration trend for CLI display.
288 pub fn format_test_duration_trend(durations: &[(String, i64)], drift: Option<&str>) -> String {
289 let mut out = String::new();
290 if !durations.is_empty() {
291 write!(out, " Duration trend (last {}): ", durations.len()).unwrap();
292 let strs: Vec<String> = durations.iter().map(|(_, d)| format!("{d}s")).collect();
293 writeln!(out, "{}", strs.join(", ")).unwrap();
294 }
295 if let Some(msg) = drift {
296 writeln!(out, " DRIFT: {msg}").unwrap();
297 }
298 out
299 }
300
301 /// Format DNS check results and WHOIS results for CLI display.
302 pub fn format_dns_results(dns_results: &[DnsCheckResult], whois_results: &[WhoisResult]) -> String {
303 let mut out = String::new();
304
305 if !dns_results.is_empty() {
306 writeln!(out, "DNS Records:").unwrap();
307 for r in dns_results {
308 if let Some(ref err) = r.error {
309 writeln!(out, " [ERR] {} {}: {}", r.name, r.record_type, scrub(err)).unwrap();
310 } else if r.matches {
311 writeln!(out, " [OK] {} {}: {:?}", r.name, r.record_type, r.actual).unwrap();
312 } else {
313 writeln!(
314 out,
315 " [FAIL] {} {}: expected {:?}, got {:?}",
316 r.name, r.record_type, r.expected, r.actual
317 )
318 .unwrap();
319 }
320 }
321 }
322
323 if !whois_results.is_empty() {
324 if !dns_results.is_empty() {
325 writeln!(out).unwrap();
326 }
327 writeln!(out, "WHOIS:").unwrap();
328 for w in whois_results {
329 if let Some(ref err) = w.error {
330 writeln!(out, " [ERR] {}: {}", w.domain, scrub(err)).unwrap();
331 } else {
332 let days_str = w.days_remaining.map_or_else(
333 || "expiry unknown".to_string(),
334 |d| format!("{d}d remaining"),
335 );
336 let registrar_str = w.registrar.as_deref().unwrap_or("unknown registrar");
337 writeln!(out, " [OK] {}: {days_str} ({registrar_str})", w.domain).unwrap();
338 }
339 }
340 }
341
342 out
343 }
344
345 /// Format prune results for CLI display.
346 pub fn format_prune(result: &PruneResult, days: i64) -> String {
347 format!(
348 "Pruned {} health checks, {} test runs, {} test details, {} peer heartbeats, {} alerts, {} TLS checks, {} incidents, {} route checks, {} DNS checks, {} WHOIS checks, {} backup checks, {} systemd checks, {} CA bundle checks, {} synckit fleet checks older than {} days.\n",
349 result.health,
350 result.tests,
351 result.test_details,
352 result.heartbeats,
353 result.alerts,
354 result.tls,
355 result.incidents,
356 result.routes,
357 result.dns,
358 result.whois,
359 result.backups,
360 result.systemd,
361 result.ca_bundle,
362 result.synckit_fleet,
363 days
364 )
365 }
366
367 /// Format the `pom versions` roll-up as one aligned table.
368 ///
369 /// Every cell is a value a monitored target chose, so all of them are scrubbed.
370 /// A missing value prints as `-`: the table answers "what is live everywhere",
371 /// and a row that has to be left out of the answer is itself the finding.
372 pub fn format_versions(rows: &[VersionRow]) -> String {
373 if rows.is_empty() {
374 return "No targets configured.\n".to_string();
375 }
376
377 let cells: Vec<[String; 6]> = rows
378 .iter()
379 .map(|r| {
380 [
381 scrub(&r.target),
382 r.version.as_deref().map_or_else(dash, scrub),
383 r.git_sha.as_deref().map_or_else(dash, short_sha),
384 r.version_since.as_deref().map_or_else(dash, minute_stamp),
385 r.checked_at.as_deref().map_or_else(dash, minute_stamp),
386 r.commits_behind.map_or_else(dash, |n| n.to_string()),
387 ]
388 })
389 .collect();
390
391 // SINCE is when this version was first seen, not when it was deployed:
392 // nothing tells PoM about a deploy, and the column header should not
393 // promise more than a poller can know.
394 const HEADERS: [&str; 6] = ["TARGET", "VERSION", "SHA", "SINCE", "CHECKED", "BEHIND"];
395 let widths: Vec<usize> = (0..HEADERS.len())
396 .map(|i| {
397 cells
398 .iter()
399 .map(|row| row[i].chars().count())
400 .chain(std::iter::once(HEADERS[i].len()))
401 .max()
402 .unwrap_or(0)
403 })
404 .collect();
405
406 let mut out = String::new();
407 write_row(&mut out, &HEADERS.map(String::from), &widths);
408 for (row, source) in cells.iter().zip(rows) {
409 write_row(&mut out, row, &widths);
410 // The blank in the BEHIND column has a reason whenever a count was
411 // attempted and did not come back. Printing it under the row keeps the
412 // table one line per target while still saying what happened.
413 if let Some(err) = &source.behind_error {
414 writeln!(out, " behind: {}", scrub(err)).unwrap();
415 }
416 }
417 out
418 }
419
420 /// Write one table row, every column but the last padded to its width.
421 fn write_row(out: &mut String, cells: &[String; 6], widths: &[usize]) {
422 for (i, cell) in cells.iter().enumerate() {
423 if i + 1 == cells.len() {
424 writeln!(out, "{cell}").unwrap();
425 } else {
426 let pad = widths[i].saturating_sub(cell.chars().count());
427 write!(out, "{cell}{:pad$} ", "", pad = pad).unwrap();
428 }
429 }
430 }
431
432 fn dash() -> String {
433 "-".to_string()
434 }
435
436 /// First 8 characters of a commit id, enough to identify it by eye.
437 fn short_sha(sha: &str) -> String {
438 scrub(sha).chars().take(8).collect()
439 }
440
441 /// RFC 3339 down to the minute: seconds and offset are noise in a table whose
442 /// rows are minutes to hours apart. Anything that does not look like a
443 /// timestamp is passed through rather than truncated into nonsense.
444 fn minute_stamp(ts: &str) -> String {
445 let scrubbed = scrub(ts);
446 match (scrubbed.len() >= 16, scrubbed.get(..16)) {
447 (true, Some(head)) if scrubbed.as_bytes()[10] == b'T' => head.replacen('T', " ", 1),
448 _ => scrubbed,
449 }
450 }
451
452 /// Format mesh data (from JSON) for human-readable CLI display.
453 pub fn format_mesh(data: &serde_json::Value) -> String {
454 let Some(instances) = data.get("instances").and_then(|v| v.as_object()) else {
455 return "No mesh data available.\n".to_string();
456 };
457
458 let mut out = String::new();
459 for (name, instance_data) in instances {
460 let instance = instance_data.get("instance");
461 let id = instance
462 .and_then(|i| i.get("id"))
463 .and_then(|v| v.as_str())
464 .unwrap_or("?");
465 let version = instance
466 .and_then(|i| i.get("version"))
467 .and_then(|v| v.as_str())
468 .unwrap_or("?");
469
470 writeln!(out, "=== {} ===", scrub(name)).unwrap();
471 writeln!(out, " ID: {}", scrub(id)).unwrap();
472 writeln!(out, " Version: {}", scrub(version)).unwrap();
473
474 // Targets
475 if let Some(targets) = instance_data.get("targets").and_then(|v| v.as_object()) {
476 for (target_name, target_data) in targets {
477 let status = target_data
478 .get("status")
479 .and_then(|v| v.as_str())
480 .unwrap_or("?");
481 let ms = target_data
482 .get("response_time_ms")
483 .and_then(serde_json::Value::as_i64);
484 let ms_str = ms.map(|m| format!(" ({m}ms)")).unwrap_or_default();
485 writeln!(
486 out,
487 " Target {}: {}{ms_str}",
488 scrub(target_name),
489 scrub(status)
490 )
491 .unwrap();
492 }
493 }
494
495 // Peers
496 if let Some(peers) = instance_data.get("peers").and_then(|v| v.as_object()) {
497 for (peer_name, peer_data) in peers {
498 let status = peer_data
499 .get("status")
500 .and_then(|v| v.as_str())
501 .unwrap_or("?");
502 let latency = peer_data
503 .get("latency_ms")
504 .and_then(serde_json::Value::as_u64)
505 .map(|ms| format!(" ({ms}ms)"))
506 .unwrap_or_default();
507 writeln!(
508 out,
509 " Peer {}: {}{latency}",
510 scrub(peer_name),
511 scrub(status)
512 )
513 .unwrap();
514 }
515 }
516
517 // Error fallback
518 if let Some(err) = instance_data.get("error").and_then(|v| v.as_str()) {
519 writeln!(out, " ({})", scrub(err)).unwrap();
520 }
521
522 writeln!(out).unwrap();
523 }
524 out
525 }
526
527 #[cfg(test)]
528 mod tests {
529 use super::*;
530 use crate::types::*;
531
532 #[test]
533 fn scrub_strips_ansi_and_control_chars() {
534 // A monitored host trying to clear/rewrite the operator's terminal.
535 let hostile = "1.0\u{1b}[2J\u{1b}[1;1H FAKE ALL-CLEAR\r\n";
536 let cleaned = scrub(hostile);
537 assert!(!cleaned.contains('\u{1b}'), "ESC must be stripped");
538 assert!(!cleaned.contains('\r') && !cleaned.contains('\n'));
539 assert_eq!(cleaned, "1.0[2J[1;1H FAKE ALL-CLEAR");
540 // Printable non-ASCII Unicode is preserved.
541 assert_eq!(scrub("v1.2 \u{2014} ok"), "v1.2 \u{2014} ok");
542 }
543
544 #[test]
545 fn health_snapshot_scrubs_hostile_error() {
546 let s = HealthSnapshot {
547 id: None,
548 target: "mnw".to_string(),
549 status: HealthStatus::Error,
550 checked_at: "2026-07-07T00:00:00+00:00".to_string(),
551 response_time_ms: 5,
552 details: None,
553 error: Some("boom\u{1b}[2Jcleared".to_string()),
554 };
555 let out = format_health_snapshot(&s);
556 assert!(
557 !out.contains('\u{1b}'),
558 "ESC from a remote error must not reach the terminal"
559 );
560 }
561
562 // format_health_snapshot
563
564 #[test]
565 fn health_snapshot_operational_with_details() {
566 let s = HealthSnapshot {
567 id: None,
568 target: "mnw".to_string(),
569 status: HealthStatus::Operational,
570 checked_at: "2026-03-10T00:00:00Z".to_string(),
571 response_time_ms: 95,
572 details: Some(HealthDetails {
573 version: Some("1.2.0".to_string()),
574 git_sha: None,
575 uptime: Some("5d 3h".to_string()),
576 checks: None,
577 monitoring: None,
578 }),
579 error: None,
580 };
581 let out = format_health_snapshot(&s);
582 assert!(out.contains("[OK]"));
583 assert!(out.contains("mnw"));
584 assert!(out.contains("operational"));
585 assert!(out.contains("(95ms)"));
586 assert!(out.contains("v1.2.0"));
587 assert!(out.contains("up 5d 3h"));
588 }
589
590 #[test]
591 fn health_snapshot_unreachable_with_error() {
592 let s = HealthSnapshot {
593 id: None,
594 target: "api".to_string(),
595 status: HealthStatus::Unreachable,
596 checked_at: "2026-03-10T00:00:00Z".to_string(),
597 response_time_ms: 0,
598 details: None,
599 error: Some("connection refused".to_string()),
600 };
601 let out = format_health_snapshot(&s);
602 assert!(out.contains("[DOWN]"));
603 assert!(out.contains("unreachable"));
604 assert!(out.contains("connection refused"));
605 }
606
607 #[test]
608 fn health_snapshot_degraded_no_details() {
609 let s = HealthSnapshot {
610 id: None,
611 target: "svc".to_string(),
612 status: HealthStatus::Degraded,
613 checked_at: "2026-03-10T00:00:00Z".to_string(),
614 response_time_ms: 2500,
615 details: None,
616 error: None,
617 };
618 let out = format_health_snapshot(&s);
619 assert!(out.contains("[WARN]"));
620 assert!(out.contains("degraded"));
621 assert!(out.contains("(2500ms)"));
622 assert!(!out.contains("up "));
623 assert!(!out.contains(" v"));
624 }
625
626 #[test]
627 fn health_snapshot_error_status() {
628 let s = HealthSnapshot {
629 id: None,
630 target: "db".to_string(),
631 status: HealthStatus::Error,
632 checked_at: "2026-03-10T00:00:00Z".to_string(),
633 response_time_ms: 500,
634 details: None,
635 error: Some("500 internal server error".to_string()),
636 };
637 let out = format_health_snapshot(&s);
638 assert!(out.contains("[ERR]"));
639 assert!(out.contains("error"));
640 assert!(out.contains("500 internal server error"));
641 }
642
643 #[test]
644 fn health_snapshots_multiple() {
645 let snapshots = vec![
646 HealthSnapshot {
647 id: None,
648 target: "a".to_string(),
649 status: HealthStatus::Operational,
650 checked_at: "2026-03-10T00:00:00Z".to_string(),
651 response_time_ms: 50,
652 details: None,
653 error: None,
654 },
655 HealthSnapshot {
656 id: None,
657 target: "b".to_string(),
658 status: HealthStatus::Degraded,
659 checked_at: "2026-03-10T00:00:00Z".to_string(),
660 response_time_ms: 3000,
661 details: None,
662 error: None,
663 },
664 ];
665 let out = format_health_snapshots(&snapshots);
666 assert!(out.contains("[OK]"));
667 assert!(out.contains("[WARN]"));
668 assert!(out.contains('a'));
669 assert!(out.contains('b'));
670 }
671
672 // format_test_result
673
674 #[test]
675 fn test_result_passed() {
676 let run = TestRun {
677 id: None,
678 target: "mnw".to_string(),
679 started_at: "2026-03-10T00:00:00Z".to_string(),
680 finished_at: Some("2026-03-10T00:02:00Z".to_string()),
681 duration_secs: Some(120),
682 exit_code: Some(0),
683 passed: true,
684 summary: TestSummary {
685 steps: vec![
686 StepResult {
687 name: "cargo check".to_string(),
688 passed: true,
689 },
690 StepResult {
691 name: "cargo test".to_string(),
692 passed: true,
693 },
694 ],
695 total_passed: Some(759),
696 total_failed: Some(0),
697 details: vec![],
698 },
699 raw_output: String::new(),
700 filter: None,
701 };
702 let out = format_test_result("mnw", &run);
703 assert!(out.contains("mnw: PASSED"));
704 assert!(out.contains("Duration: 120s"));
705 assert!(out.contains("Tests: 759 passed, 0 failed"));
706 assert!(out.contains("PASS cargo check"));
707 assert!(out.contains("PASS cargo test"));
708 assert!(!out.contains("Raw output"));
709 }
710
711 #[test]
712 fn test_result_failed_shows_raw_output() {
713 let run = TestRun {
714 id: None,
715 target: "mnw".to_string(),
716 started_at: "2026-03-10T00:00:00Z".to_string(),
717 finished_at: Some("2026-03-10T00:01:00Z".to_string()),
718 duration_secs: Some(60),
719 exit_code: Some(1),
720 passed: false,
721 summary: TestSummary {
722 steps: vec![
723 StepResult {
724 name: "cargo check".to_string(),
725 passed: true,
726 },
727 StepResult {
728 name: "cargo test".to_string(),
729 passed: false,
730 },
731 ],
732 total_passed: Some(750),
733 total_failed: Some(9),
734 details: vec![],
735 },
736 raw_output: "thread 'test_foo' panicked at 'assertion failed'".to_string(),
737 filter: None,
738 };
739 let out = format_test_result("mnw", &run);
740 assert!(out.contains("mnw: FAILED"));
741 assert!(out.contains("PASS cargo check"));
742 assert!(out.contains("FAIL cargo test"));
743 assert!(out.contains("750 passed, 9 failed"));
744 assert!(out.contains("Raw output:"));
745 assert!(out.contains("assertion failed"));
746 }
747
748 #[test]
749 fn test_result_no_duration_or_counts() {
750 let run = TestRun {
751 id: None,
752 target: "svc".to_string(),
753 started_at: "2026-03-10T00:00:00Z".to_string(),
754 finished_at: None,
755 duration_secs: None,
756 exit_code: None,
757 passed: true,
758 summary: TestSummary {
759 steps: vec![],
760 total_passed: None,
761 total_failed: None,
762 details: vec![],
763 },
764 raw_output: String::new(),
765 filter: None,
766 };
767 let out = format_test_result("svc", &run);
768 assert!(out.contains("svc: PASSED"));
769 assert!(!out.contains("Duration:"));
770 assert!(!out.contains("Tests:"));
771 }
772
773 // format_status_target
774
775 #[test]
776 fn status_target_with_health_and_tests() {
777 let health = HealthSnapshot {
778 id: None,
779 target: "mnw".to_string(),
780 status: HealthStatus::Operational,
781 checked_at: "2026-03-10T00:00:00Z".to_string(),
782 response_time_ms: 95,
783 details: Some(HealthDetails {
784 version: Some("2.1.0".to_string()),
785 git_sha: None,
786 uptime: None,
787 checks: None,
788 monitoring: None,
789 }),
790 error: None,
791 };
792 let test = TestRun {
793 id: None,
794 target: "mnw".to_string(),
795 started_at: "2026-03-10T00:00:00Z".to_string(),
796 finished_at: Some("2026-03-10T00:01:00Z".to_string()),
797 duration_secs: Some(60),
798 exit_code: Some(0),
799 passed: true,
800 summary: TestSummary {
801 steps: vec![],
802 total_passed: Some(100),
803 total_failed: Some(0),
804 details: vec![],
805 },
806 raw_output: String::new(),
807 filter: None,
808 };
809 let out = format_status_target(
810 "mnw",
811 "MakeNotWork",
812 Some(&health),
813 None,
814 None,
815 None,
816 None,
817 None,
818 Some(&test),
819 None,
820 None,
821 );
822 assert!(out.contains("=== mnw (MakeNotWork) ==="));
823 assert!(out.contains("Health: [OK] operational (95ms) v2.1.0"));
824 assert!(out.contains("Tests: PASSED (60s)"));
825 assert!(out.contains("100 passed, 0 failed"));
826 }
827
828 #[test]
829 fn status_target_no_data() {
830 let out = format_status_target(
831 "mnw",
832 "MakeNotWork",
833 None,
834 None,
835 None,
836 None,
837 None,
838 None,
839 None,
840 None,
841 None,
842 );
843 assert!(out.contains("=== mnw (MakeNotWork) ==="));
844 assert!(out.contains("Health: no data"));
845 assert!(out.contains("Tests: no data"));
846 }
847
848 #[test]
849 fn status_target_health_only() {
850 let health = HealthSnapshot {
851 id: None,
852 target: "mnw".to_string(),
853 status: HealthStatus::Degraded,
854 checked_at: "2026-03-10T00:00:00Z".to_string(),
855 response_time_ms: 2000,
856 details: None,
857 error: None,
858 };
859 let out = format_status_target(
860 "mnw",
861 "MakeNotWork",
862 Some(&health),
863 None,
864 None,
865 None,
866 None,
867 None,
868 None,
869 None,
870 None,
871 );
872 assert!(out.contains("Health: [WARN] degraded (2000ms)"));
873 assert!(out.contains("Tests: no data"));
874 }
875
876 #[test]
877 fn status_target_failed_tests() {
878 let test = TestRun {
879 id: None,
880 target: "mnw".to_string(),
881 started_at: "2026-03-10T00:00:00Z".to_string(),
882 finished_at: None,
883 duration_secs: None,
884 exit_code: Some(1),
885 passed: false,
886 summary: TestSummary {
887 steps: vec![],
888 total_passed: Some(80),
889 total_failed: Some(5),
890 details: vec![],
891 },
892 raw_output: String::new(),
893 filter: None,
894 };
895 let out = format_status_target(
896 "mnw",
897 "MakeNotWork",
898 None,
899 None,
900 None,
901 None,
902 None,
903 None,
904 Some(&test),
905 None,
906 None,
907 );
908 assert!(out.contains("Tests: FAILED"));
909 assert!(out.contains("80 passed, 5 failed"));
910 }
911
912 // format_status_target with TLS
913
914 #[test]
915 fn status_target_tls_ok() {
916 let tls = TlsCheckRow {
917 id: 1,
918 target: "mnw".to_string(),
919 host: "makenot.work".to_string(),
920 valid: true,
921 days_remaining: 47,
922 not_before: "2026-01-10T00:00:00Z".to_string(),
923 not_after: "2026-04-27T00:00:00Z".to_string(),
924 subject: "CN=makenot.work".to_string(),
925 issuer: "CN=Let's Encrypt".to_string(),
926 checked_at: "2026-03-11T00:00:00Z".to_string(),
927 error: None,
928 webpki_trusted: Some(true),
929 platform_trusted: Some(true),
930 webpki_error: None,
931 platform_error: None,
932 };
933 let out = format_status_target(
934 "mnw",
935 "MakeNotWork",
936 None,
937 None,
938 Some(&tls),
939 None,
940 None,
941 None,
942 None,
943 None,
944 None,
945 );
946 assert!(out.contains("TLS: [OK] makenot.work"));
947 assert!(out.contains("47d remaining"));
948 assert!(out.contains("expires 2026-04-27"));
949 }
950
951 #[test]
952 fn status_target_tls_warning() {
953 let tls = TlsCheckRow {
954 id: 1,
955 target: "mnw".to_string(),
956 host: "makenot.work".to_string(),
957 valid: true,
958 days_remaining: 12,
959 not_before: "2026-01-10T00:00:00Z".to_string(),
960 not_after: "2026-03-23T00:00:00Z".to_string(),
961 subject: "CN=makenot.work".to_string(),
962 issuer: "CN=Let's Encrypt".to_string(),
963 checked_at: "2026-03-11T00:00:00Z".to_string(),
964 error: None,
965 webpki_trusted: Some(true),
966 platform_trusted: Some(true),
967 webpki_error: None,
968 platform_error: None,
969 };
970 let out = format_status_target(
971 "mnw",
972 "MakeNotWork",
973 None,
974 None,
975 Some(&tls),
976 None,
977 None,
978 None,
979 None,
980 None,
981 None,
982 );
983 assert!(out.contains("TLS: [WARN] makenot.work"));
984 assert!(out.contains("12d remaining"));
985 }
986
987 #[test]
988 fn status_target_tls_error() {
989 let tls = TlsCheckRow {
990 id: 1,
991 target: "mnw".to_string(),
992 host: "makenot.work".to_string(),
993 valid: false,
994 days_remaining: 0,
995 not_before: String::new(),
996 not_after: String::new(),
997 subject: String::new(),
998 issuer: String::new(),
999 checked_at: "2026-03-11T00:00:00Z".to_string(),
1000 error: Some("connection refused".to_string()),
1001 webpki_trusted: Some(false),
1002 platform_trusted: Some(false),
1003 webpki_error: Some("TCP connect failed".to_string()),
1004 platform_error: Some("TCP connect failed".to_string()),
1005 };
1006 let out = format_status_target(
1007 "mnw",
1008 "MakeNotWork",
1009 None,
1010 None,
1011 Some(&tls),
1012 None,
1013 None,
1014 None,
1015 None,
1016 None,
1017 None,
1018 );
1019 assert!(out.contains("TLS: [ERR] makenot.work"));
1020 assert!(out.contains("connection refused"));
1021 }
1022
1023 // format_status_target with incident
1024
1025 #[test]
1026 fn status_target_with_active_incident() {
1027 let incident = IncidentRow {
1028 id: 1,
1029 target: "mnw".to_string(),
1030 started_at: "2026-03-11T14:30:00Z".to_string(),
1031 ended_at: None,
1032 duration_secs: None,
1033 from_status: "operational".to_string(),
1034 to_status: "degraded".to_string(),
1035 };
1036 let out = format_status_target(
1037 "mnw",
1038 "MakeNotWork",
1039 None,
1040 None,
1041 None,
1042 None,
1043 None,
1044 None,
1045 None,
1046 None,
1047 Some(&incident),
1048 );
1049 assert!(out.contains("Incident: [ACTIVE] degraded since 2026-03-11T14:30:00Z"));
1050 }
1051
1052 #[test]
1053 fn status_target_no_incident() {
1054 let out = format_status_target(
1055 "mnw",
1056 "MakeNotWork",
1057 None,
1058 None,
1059 None,
1060 None,
1061 None,
1062 None,
1063 None,
1064 None,
1065 None,
1066 );
1067 assert!(!out.contains("Incident"));
1068 }
1069
1070 // format_status_target with latency
1071
1072 #[test]
1073 fn status_target_with_latency() {
1074 let latency = LatencyStats {
1075 min_ms: 95,
1076 max_ms: 210,
1077 avg_ms: 120.0,
1078 p95_ms: 180,
1079 sample_count: 288,
1080 };
1081 let out = format_status_target(
1082 "mnw",
1083 "MakeNotWork",
1084 None,
1085 Some(&latency),
1086 None,
1087 None,
1088 None,
1089 None,
1090 None,
1091 None,
1092 None,
1093 );
1094 assert!(out.contains("Latency (24h): avg 120ms, p95 180ms, range 95-210ms (288 samples)"));
1095 }
1096
1097 #[test]
1098 fn status_target_without_latency() {
1099 let out = format_status_target(
1100 "mnw",
1101 "MakeNotWork",
1102 None,
1103 None,
1104 None,
1105 None,
1106 None,
1107 None,
1108 None,
1109 None,
1110 None,
1111 );
1112 assert!(!out.contains("Latency"));
1113 }
1114
1115 // format_health_history
1116
1117 #[test]
1118 fn health_history_empty() {
1119 let out = format_health_history(&[]);
1120 assert_eq!(out, "No health check history.\n");
1121 }
1122
1123 #[test]
1124 fn health_history_with_entries() {
1125 let history = vec![
1126 HealthSnapshot {
1127 id: Some(2),
1128 target: "mnw".to_string(),
1129 status: HealthStatus::Operational,
1130 checked_at: "2026-03-10T01:00:00Z".to_string(),
1131 response_time_ms: 120,
1132 details: None,
1133 error: None,
1134 },
1135 HealthSnapshot {
1136 id: Some(1),
1137 target: "mnw".to_string(),
1138 status: HealthStatus::Degraded,
1139 checked_at: "2026-03-10T00:00:00Z".to_string(),
1140 response_time_ms: 2500,
1141 details: None,
1142 error: None,
1143 },
1144 ];
1145 let out = format_health_history(&history);
1146 assert!(out.contains("[OK] mnw"));
1147 assert!(out.contains("(120ms)"));
1148 assert!(out.contains("2026-03-10T01:00:00Z"));
1149 assert!(out.contains("[WARN] mnw"));
1150 assert!(out.contains("(2500ms)"));
1151 }
1152
1153 // format_test_history
1154
1155 #[test]
1156 fn test_history_empty() {
1157 let out = format_test_history(&[]);
1158 assert_eq!(out, "No test run history.\n");
1159 }
1160
1161 #[test]
1162 fn test_history_with_entries() {
1163 let history = vec![
1164 TestRun {
1165 id: Some(2),
1166 target: "mnw".to_string(),
1167 started_at: "2026-03-10T01:00:00Z".to_string(),
1168 finished_at: None,
1169 duration_secs: Some(120),
1170 exit_code: Some(0),
1171 passed: true,
1172 summary: TestSummary {
1173 steps: vec![],
1174 total_passed: Some(810),
1175 total_failed: Some(0),
1176 details: vec![],
1177 },
1178 raw_output: String::new(),
1179 filter: None,
1180 },
1181 TestRun {
1182 id: Some(1),
1183 target: "mnw".to_string(),
1184 started_at: "2026-03-10T00:00:00Z".to_string(),
1185 finished_at: None,
1186 duration_secs: None,
1187 exit_code: Some(1),
1188 passed: false,
1189 summary: TestSummary {
1190 steps: vec![],
1191 total_passed: None,
1192 total_failed: None,
1193 details: vec![],
1194 },
1195 raw_output: String::new(),
1196 filter: None,
1197 },
1198 ];
1199 let out = format_test_history(&history);
1200 assert!(out.contains("[PASS] mnw (120s) 2026-03-10T01:00:00Z"));
1201 assert!(out.contains("810 passed, 0 failed"));
1202 assert!(out.contains("[FAIL] mnw 2026-03-10T00:00:00Z"));
1203 }
1204
1205 #[test]
1206 fn test_history_no_duration_no_counts() {
1207 let history = vec![TestRun {
1208 id: Some(1),
1209 target: "svc".to_string(),
1210 started_at: "2026-03-10T00:00:00Z".to_string(),
1211 finished_at: None,
1212 duration_secs: None,
1213 exit_code: None,
1214 passed: true,
1215 summary: TestSummary {
1216 steps: vec![],
1217 total_passed: None,
1218 total_failed: None,
1219 details: vec![],
1220 },
1221 raw_output: String::new(),
1222 filter: None,
1223 }];
1224 let out = format_test_history(&history);
1225 // Should not have duration or counts
1226 assert!(!out.contains('('));
1227 assert!(out.contains("[PASS] svc 2026-03-10T00:00:00Z"));
1228 }
1229
1230 // format_prune
1231
1232 #[test]
1233 fn prune_formatting() {
1234 let result = PruneResult {
1235 health: 5,
1236 tests: 3,
1237 test_details: 15,
1238 heartbeats: 10,
1239 alerts: 2,
1240 tls: 1,
1241 incidents: 4,
1242 routes: 0,
1243 dns: 8,
1244 whois: 2,
1245 backups: 1,
1246 systemd: 6,
1247 ca_bundle: 2,
1248 synckit_fleet: 7,
1249 };
1250 let out = format_prune(&result, 30);
1251 assert_eq!(
1252 out,
1253 "Pruned 5 health checks, 3 test runs, 15 test details, 10 peer heartbeats, 2 alerts, 1 TLS checks, 4 incidents, 0 route checks, 8 DNS checks, 2 WHOIS checks, 1 backup checks, 6 systemd checks, 2 CA bundle checks, 7 synckit fleet checks older than 30 days.\n"
1254 );
1255 }
1256
1257 #[test]
1258 fn prune_zero_records() {
1259 let result = PruneResult {
1260 health: 0,
1261 tests: 0,
1262 test_details: 0,
1263 heartbeats: 0,
1264 alerts: 0,
1265 tls: 0,
1266 incidents: 0,
1267 routes: 0,
1268 dns: 0,
1269 whois: 0,
1270 backups: 0,
1271 systemd: 0,
1272 ca_bundle: 0,
1273 synckit_fleet: 0,
1274 };
1275 let out = format_prune(&result, 7);
1276 assert!(out.contains("Pruned 0 health checks, 0 test runs, 0 test details, 0 peer heartbeats, 0 alerts, 0 TLS checks, 0 incidents, 0 route checks, 0 DNS checks, 0 WHOIS checks, 0 backup checks, 0 systemd checks, 0 CA bundle checks, 0 synckit fleet checks older than 7 days."));
1277 }
1278
1279 // format_mesh
1280
1281 #[test]
1282 fn mesh_no_instances() {
1283 let data = serde_json::json!({});
1284 let out = format_mesh(&data);
1285 assert_eq!(out, "No mesh data available.\n");
1286 }
1287
1288 #[test]
1289 fn mesh_empty_instances() {
1290 let data = serde_json::json!({ "instances": {} });
1291 let out = format_mesh(&data);
1292 // Empty map, no output lines beyond the empty string
1293 assert!(out.is_empty());
1294 }
1295
1296 #[test]
1297 fn mesh_single_instance_with_targets_and_peers() {
1298 let data = serde_json::json!({
1299 "instances": {
1300 "hetzner": {
1301 "instance": {
1302 "id": "uuid-123",
1303 "version": "0.2.0"
1304 },
1305 "targets": {
1306 "mnw": {
1307 "status": "operational",
1308 "response_time_ms": 95
1309 }
1310 },
1311 "peers": {
1312 "astra": {
1313 "status": "online",
1314 "latency_ms": 42
1315 }
1316 }
1317 }
1318 }
1319 });
1320 let out = format_mesh(&data);
1321 assert!(out.contains("=== hetzner ==="));
1322 assert!(out.contains("ID: uuid-123"));
1323 assert!(out.contains("Version: 0.2.0"));
1324 assert!(out.contains("Target mnw: operational (95ms)"));
1325 assert!(out.contains("Peer astra: online (42ms)"));
1326 }
1327
1328 #[test]
1329 fn mesh_missing_instance_details() {
1330 let data = serde_json::json!({
1331 "instances": {
1332 "node-1": {}
1333 }
1334 });
1335 let out = format_mesh(&data);
1336 assert!(out.contains("=== node-1 ==="));
1337 assert!(out.contains("ID: ?"));
1338 assert!(out.contains("Version: ?"));
1339 }
1340
1341 #[test]
1342 fn mesh_instance_with_error() {
1343 let data = serde_json::json!({
1344 "instances": {
1345 "node-2": {
1346 "error": "connection refused"
1347 }
1348 }
1349 });
1350 let out = format_mesh(&data);
1351 assert!(out.contains("=== node-2 ==="));
1352 assert!(out.contains("(connection refused)"));
1353 }
1354
1355 #[test]
1356 fn mesh_target_without_response_time() {
1357 let data = serde_json::json!({
1358 "instances": {
1359 "node": {
1360 "instance": { "id": "x", "version": "1.0" },
1361 "targets": {
1362 "svc": { "status": "unreachable" }
1363 }
1364 }
1365 }
1366 });
1367 let out = format_mesh(&data);
1368 assert!(out.contains("Target svc: unreachable"));
1369 // No (Xms) suffix
1370 assert!(!out.contains("Target svc: unreachable ("));
1371 }
1372
1373 #[test]
1374 fn mesh_peer_without_latency() {
1375 let data = serde_json::json!({
1376 "instances": {
1377 "node": {
1378 "instance": { "id": "x", "version": "1.0" },
1379 "peers": {
1380 "other": { "status": "missing" }
1381 }
1382 }
1383 }
1384 });
1385 let out = format_mesh(&data);
1386 assert!(out.contains("Peer other: missing"));
1387 // No (Xms) suffix
1388 assert!(!out.contains("Peer other: missing ("));
1389 }
1390
1391 #[test]
1392 fn mesh_multiple_instances() {
1393 let data = serde_json::json!({
1394 "instances": {
1395 "alpha": {
1396 "instance": { "id": "a1", "version": "0.1.0" }
1397 },
1398 "beta": {
1399 "instance": { "id": "b2", "version": "0.2.0" }
1400 }
1401 }
1402 });
1403 let out = format_mesh(&data);
1404 assert!(out.contains("=== alpha ==="));
1405 assert!(out.contains("=== beta ==="));
1406 assert!(out.contains("ID: a1"));
1407 assert!(out.contains("ID: b2"));
1408 }
1409
1410 // format_status_target with staleness
1411
1412 #[test]
1413 fn status_target_stale_by_version() {
1414 let staleness = TestStaleness {
1415 stale: true,
1416 reason: Some("version changed: 0.1.8 -> 0.1.9".to_string()),
1417 current_version: Some("0.1.9".to_string()),
1418 tested_version: Some("0.1.8".to_string()),
1419 last_test_at: Some("2026-03-10T00:00:00Z".to_string()),
1420 days_since_test: Some(1),
1421 };
1422 let out = format_status_target(
1423 "mnw",
1424 "MakeNotWork",
1425 None,
1426 None,
1427 None,
1428 None,
1429 None,
1430 None,
1431 None,
1432 Some(&staleness),
1433 None,
1434 );
1435 assert!(out.contains("Tests: STALE"));
1436 assert!(out.contains("version changed: 0.1.8 -> 0.1.9"));
1437 }
1438
1439 #[test]
1440 fn status_target_stale_by_age() {
1441 let staleness = TestStaleness {
1442 stale: true,
1443 reason: Some("tests are 10 days old (threshold: 7d)".to_string()),
1444 current_version: Some("0.1.9".to_string()),
1445 tested_version: Some("0.1.9".to_string()),
1446 last_test_at: Some("2026-03-01T00:00:00Z".to_string()),
1447 days_since_test: Some(10),
1448 };
1449 let out = format_status_target(
1450 "mnw",
1451 "MakeNotWork",
1452 None,
1453 None,
1454 None,
1455 None,
1456 None,
1457 None,
1458 None,
1459 Some(&staleness),
1460 None,
1461 );
1462 assert!(out.contains("Tests: STALE"));
1463 assert!(out.contains("tests are 10 days old"));
1464 }
1465
1466 #[test]
1467 fn status_target_not_stale() {
1468 let staleness = TestStaleness {
1469 stale: false,
1470 reason: None,
1471 current_version: Some("0.1.9".to_string()),
1472 tested_version: Some("0.1.9".to_string()),
1473 last_test_at: Some("2026-03-10T00:00:00Z".to_string()),
1474 days_since_test: Some(1),
1475 };
1476 let out = format_status_target(
1477 "mnw",
1478 "MakeNotWork",
1479 None,
1480 None,
1481 None,
1482 None,
1483 None,
1484 None,
1485 None,
1486 Some(&staleness),
1487 None,
1488 );
1489 assert!(!out.contains("STALE"));
1490 }
1491
1492 #[test]
1493 fn status_target_no_staleness_data() {
1494 let out = format_status_target(
1495 "mnw",
1496 "MakeNotWork",
1497 None,
1498 None,
1499 None,
1500 None,
1501 None,
1502 None,
1503 None,
1504 None,
1505 None,
1506 );
1507 assert!(!out.contains("STALE"));
1508 }
1509
1510 // format_status_target with routes
1511
1512 #[test]
1513 fn status_target_all_routes_ok() {
1514 let checks = vec![
1515 RouteCheckRow {
1516 id: 1,
1517 target: "mnw".to_string(),
1518 path: "/".to_string(),
1519 status_code: 200,
1520 ok: true,
1521 response_time_ms: 50,
1522 checked_at: "2026-03-13T00:00:00Z".to_string(),
1523 error: None,
1524 },
1525 RouteCheckRow {
1526 id: 2,
1527 target: "mnw".to_string(),
1528 path: "/docs".to_string(),
1529 status_code: 200,
1530 ok: true,
1531 response_time_ms: 60,
1532 checked_at: "2026-03-13T00:00:00Z".to_string(),
1533 error: None,
1534 },
1535 ];
1536 let out = format_status_target(
1537 "mnw",
1538 "MakeNotWork",
1539 None,
1540 None,
1541 None,
1542 Some(&checks),
1543 None,
1544 None,
1545 None,
1546 None,
1547 None,
1548 );
1549 assert!(out.contains("Routes: 2/2 OK"));
1550 }
1551
1552 #[test]
1553 fn status_target_some_routes_failing() {
1554 let checks = vec![
1555 RouteCheckRow {
1556 id: 1,
1557 target: "mnw".to_string(),
1558 path: "/".to_string(),
1559 status_code: 200,
1560 ok: true,
1561 response_time_ms: 50,
1562 checked_at: "2026-03-13T00:00:00Z".to_string(),
1563 error: None,
1564 },
1565 RouteCheckRow {
1566 id: 2,
1567 target: "mnw".to_string(),
1568 path: "/docs/faq".to_string(),
1569 status_code: 404,
1570 ok: false,
1571 response_time_ms: 30,
1572 checked_at: "2026-03-13T00:00:00Z".to_string(),
1573 error: Some("HTTP 404".to_string()),
1574 },
1575 RouteCheckRow {
1576 id: 3,
1577 target: "mnw".to_string(),
1578 path: "/pricing".to_string(),
1579 status_code: 500,
1580 ok: false,
1581 response_time_ms: 20,
1582 checked_at: "2026-03-13T00:00:00Z".to_string(),
1583 error: Some("HTTP 500".to_string()),
1584 },
1585 ];
1586 let out = format_status_target(
1587 "mnw",
1588 "MakeNotWork",
1589 None,
1590 None,
1591 None,
1592 Some(&checks),
1593 None,
1594 None,
1595 None,
1596 None,
1597 None,
1598 );
1599 assert!(out.contains("Routes: 1/3 (FAIL: /docs/faq, /pricing)"));
1600 }
1601
1602 // format_versions
1603
1604 fn version_row(target: &str, version: Option<&str>, sha: Option<&str>) -> VersionRow {
1605 VersionRow {
1606 target: target.to_string(),
1607 label: format!("{target} label"),
1608 version: version.map(String::from),
1609 git_sha: sha.map(String::from),
1610 checked_at: Some("2026-07-29T18:04:37.123456+00:00".to_string()),
1611 version_since: Some("2026-07-28T09:00:00+00:00".to_string()),
1612 commits_behind: Some(8),
1613 behind_error: None,
1614 }
1615 }
1616
1617 #[test]
1618 fn versions_table_aligns_columns_and_shortens_the_sha() {
1619 let rows = vec![
1620 version_row("mnw", Some("0.11.0"), Some("6402bf4e9c1d2e3f4a5b")),
1621 version_row("multithreaded", Some("0.4.2"), Some("aaaabbbbcccc")),
1622 ];
1623 let out = format_versions(&rows);
1624 let lines: Vec<&str> = out.lines().collect();
1625
1626 assert!(lines[0].starts_with("TARGET"));
1627 // The header pads to the widest target, so every column starts at the
1628 // same offset on every line.
1629 let version_col = lines[0].find("VERSION").unwrap();
1630 assert_eq!(lines[1].find("0.11.0"), Some(version_col));
1631 assert_eq!(lines[2].find("0.4.2"), Some(version_col));
1632
1633 assert!(out.contains("6402bf4e "), "sha shortened to 8: {out}");
1634 assert!(!out.contains("6402bf4e9c1d"));
1635 assert!(
1636 out.contains("2026-07-29 18:04"),
1637 "timestamp to the minute: {out}"
1638 );
1639 }
1640
1641 #[test]
1642 fn versions_table_prints_a_dash_for_every_missing_value() {
1643 let rows = vec![VersionRow {
1644 target: "mt".to_string(),
1645 label: "Multithreaded".to_string(),
1646 version: None,
1647 git_sha: None,
1648 checked_at: None,
1649 version_since: None,
1650 commits_behind: None,
1651 behind_error: None,
1652 }];
1653 let out = format_versions(&rows);
1654 let row = out.lines().nth(1).unwrap();
1655 assert_eq!(
1656 row.split_whitespace().collect::<Vec<_>>(),
1657 ["mt", "-", "-", "-", "-", "-"]
1658 );
1659 }
1660
1661 #[test]
1662 fn versions_table_says_why_a_count_is_blank() {
1663 let mut row = version_row("mnw", Some("0.11.0"), Some("6402bf4e"));
1664 row.commits_behind = None;
1665 row.behind_error = Some("git rev-list exited 128: bad revision".to_string());
1666 let out = format_versions(&[row]);
1667 assert!(out.contains("behind: git rev-list exited 128"), "{out}");
1668 }
1669
1670 #[test]
1671 fn versions_table_scrubs_a_hostile_version_string() {
1672 // Same terminal-injection surface as every other display sink: the
1673 // version and sha are whatever the monitored target chose to send.
1674 let mut row = version_row("mnw", Some("1.0\u{1b}[2J FAKE"), Some("6402bf4e"));
1675 row.behind_error = Some("boom\u{1b}[2J".to_string());
1676 let out = format_versions(&[row]);
1677 assert!(!out.contains('\u{1b}'), "ESC must not reach the terminal");
1678 }
1679
1680 #[test]
1681 fn versions_empty_config_says_so() {
1682 assert_eq!(format_versions(&[]), "No targets configured.\n");
1683 }
1684
1685 #[test]
1686 fn minute_stamp_passes_through_anything_that_is_not_a_timestamp() {
1687 assert_eq!(minute_stamp("2026-07-29T18:04:37Z"), "2026-07-29 18:04");
1688 assert_eq!(minute_stamp("whenever"), "whenever");
1689 assert_eq!(minute_stamp("2026-07-29 18:04:37"), "2026-07-29 18:04:37");
1690 }
1691
1692 #[test]
1693 fn status_target_no_route_checks() {
1694 let out = format_status_target(
1695 "mnw",
1696 "MakeNotWork",
1697 None,
1698 None,
1699 None,
1700 None,
1701 None,
1702 None,
1703 None,
1704 None,
1705 None,
1706 );
1707 assert!(!out.contains("Routes"));
1708 }
1709 }
1710