Skip to main content

max / makenotwork

31.0 KB · 1183 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4 use crate::types::*;
5
6 #[test]
7 fn scrub_strips_ansi_and_control_chars() {
8 // A monitored host trying to clear/rewrite the operator's terminal.
9 let hostile = "1.0\u{1b}[2J\u{1b}[1;1H FAKE ALL-CLEAR\r\n";
10 let cleaned = scrub(hostile);
11 assert!(!cleaned.contains('\u{1b}'), "ESC must be stripped");
12 assert!(!cleaned.contains('\r') && !cleaned.contains('\n'));
13 assert_eq!(cleaned, "1.0[2J[1;1H FAKE ALL-CLEAR");
14 // Printable non-ASCII Unicode is preserved.
15 assert_eq!(scrub("v1.2 \u{2014} ok"), "v1.2 \u{2014} ok");
16 }
17
18 #[test]
19 fn health_snapshot_scrubs_hostile_error() {
20 let s = HealthSnapshot {
21 id: None,
22 target: "mnw".to_string(),
23 status: HealthStatus::Error,
24 checked_at: "2026-07-07T00:00:00+00:00".to_string(),
25 response_time_ms: 5,
26 details: None,
27 error: Some("boom\u{1b}[2Jcleared".to_string()),
28 };
29 let out = format_health_snapshot(&s);
30 assert!(
31 !out.contains('\u{1b}'),
32 "ESC from a remote error must not reach the terminal"
33 );
34 }
35
36 // format_health_snapshot
37
38 #[test]
39 fn health_snapshot_operational_with_details() {
40 let s = HealthSnapshot {
41 id: None,
42 target: "mnw".to_string(),
43 status: HealthStatus::Operational,
44 checked_at: "2026-03-10T00:00:00Z".to_string(),
45 response_time_ms: 95,
46 details: Some(HealthDetails {
47 version: Some("1.2.0".to_string()),
48 git_sha: None,
49 uptime: Some("5d 3h".to_string()),
50 checks: None,
51 monitoring: None,
52 }),
53 error: None,
54 };
55 let out = format_health_snapshot(&s);
56 assert!(out.contains("[OK]"));
57 assert!(out.contains("mnw"));
58 assert!(out.contains("operational"));
59 assert!(out.contains("(95ms)"));
60 assert!(out.contains("v1.2.0"));
61 assert!(out.contains("up 5d 3h"));
62 }
63
64 #[test]
65 fn health_snapshot_unreachable_with_error() {
66 let s = HealthSnapshot {
67 id: None,
68 target: "api".to_string(),
69 status: HealthStatus::Unreachable,
70 checked_at: "2026-03-10T00:00:00Z".to_string(),
71 response_time_ms: 0,
72 details: None,
73 error: Some("connection refused".to_string()),
74 };
75 let out = format_health_snapshot(&s);
76 assert!(out.contains("[DOWN]"));
77 assert!(out.contains("unreachable"));
78 assert!(out.contains("connection refused"));
79 }
80
81 #[test]
82 fn health_snapshot_degraded_no_details() {
83 let s = HealthSnapshot {
84 id: None,
85 target: "svc".to_string(),
86 status: HealthStatus::Degraded,
87 checked_at: "2026-03-10T00:00:00Z".to_string(),
88 response_time_ms: 2500,
89 details: None,
90 error: None,
91 };
92 let out = format_health_snapshot(&s);
93 assert!(out.contains("[WARN]"));
94 assert!(out.contains("degraded"));
95 assert!(out.contains("(2500ms)"));
96 assert!(!out.contains("up "));
97 assert!(!out.contains(" v"));
98 }
99
100 #[test]
101 fn health_snapshot_error_status() {
102 let s = HealthSnapshot {
103 id: None,
104 target: "db".to_string(),
105 status: HealthStatus::Error,
106 checked_at: "2026-03-10T00:00:00Z".to_string(),
107 response_time_ms: 500,
108 details: None,
109 error: Some("500 internal server error".to_string()),
110 };
111 let out = format_health_snapshot(&s);
112 assert!(out.contains("[ERR]"));
113 assert!(out.contains("error"));
114 assert!(out.contains("500 internal server error"));
115 }
116
117 #[test]
118 fn health_snapshots_multiple() {
119 let snapshots = vec![
120 HealthSnapshot {
121 id: None,
122 target: "a".to_string(),
123 status: HealthStatus::Operational,
124 checked_at: "2026-03-10T00:00:00Z".to_string(),
125 response_time_ms: 50,
126 details: None,
127 error: None,
128 },
129 HealthSnapshot {
130 id: None,
131 target: "b".to_string(),
132 status: HealthStatus::Degraded,
133 checked_at: "2026-03-10T00:00:00Z".to_string(),
134 response_time_ms: 3000,
135 details: None,
136 error: None,
137 },
138 ];
139 let out = format_health_snapshots(&snapshots);
140 assert!(out.contains("[OK]"));
141 assert!(out.contains("[WARN]"));
142 assert!(out.contains('a'));
143 assert!(out.contains('b'));
144 }
145
146 // format_test_result
147
148 #[test]
149 fn test_result_passed() {
150 let run = TestRun {
151 id: None,
152 target: "mnw".to_string(),
153 started_at: "2026-03-10T00:00:00Z".to_string(),
154 finished_at: Some("2026-03-10T00:02:00Z".to_string()),
155 duration_secs: Some(120),
156 exit_code: Some(0),
157 passed: true,
158 summary: TestSummary {
159 steps: vec![
160 StepResult {
161 name: "cargo check".to_string(),
162 passed: true,
163 },
164 StepResult {
165 name: "cargo test".to_string(),
166 passed: true,
167 },
168 ],
169 total_passed: Some(759),
170 total_failed: Some(0),
171 details: vec![],
172 },
173 raw_output: String::new(),
174 filter: None,
175 };
176 let out = format_test_result("mnw", &run);
177 assert!(out.contains("mnw: PASSED"));
178 assert!(out.contains("Duration: 120s"));
179 assert!(out.contains("Tests: 759 passed, 0 failed"));
180 assert!(out.contains("PASS cargo check"));
181 assert!(out.contains("PASS cargo test"));
182 assert!(!out.contains("Raw output"));
183 }
184
185 #[test]
186 fn test_result_failed_shows_raw_output() {
187 let run = TestRun {
188 id: None,
189 target: "mnw".to_string(),
190 started_at: "2026-03-10T00:00:00Z".to_string(),
191 finished_at: Some("2026-03-10T00:01:00Z".to_string()),
192 duration_secs: Some(60),
193 exit_code: Some(1),
194 passed: false,
195 summary: TestSummary {
196 steps: vec![
197 StepResult {
198 name: "cargo check".to_string(),
199 passed: true,
200 },
201 StepResult {
202 name: "cargo test".to_string(),
203 passed: false,
204 },
205 ],
206 total_passed: Some(750),
207 total_failed: Some(9),
208 details: vec![],
209 },
210 raw_output: "thread 'test_foo' panicked at 'assertion failed'".to_string(),
211 filter: None,
212 };
213 let out = format_test_result("mnw", &run);
214 assert!(out.contains("mnw: FAILED"));
215 assert!(out.contains("PASS cargo check"));
216 assert!(out.contains("FAIL cargo test"));
217 assert!(out.contains("750 passed, 9 failed"));
218 assert!(out.contains("Raw output:"));
219 assert!(out.contains("assertion failed"));
220 }
221
222 #[test]
223 fn test_result_no_duration_or_counts() {
224 let run = TestRun {
225 id: None,
226 target: "svc".to_string(),
227 started_at: "2026-03-10T00:00:00Z".to_string(),
228 finished_at: None,
229 duration_secs: None,
230 exit_code: None,
231 passed: true,
232 summary: TestSummary {
233 steps: vec![],
234 total_passed: None,
235 total_failed: None,
236 details: vec![],
237 },
238 raw_output: String::new(),
239 filter: None,
240 };
241 let out = format_test_result("svc", &run);
242 assert!(out.contains("svc: PASSED"));
243 assert!(!out.contains("Duration:"));
244 assert!(!out.contains("Tests:"));
245 }
246
247 // format_status_target
248
249 #[test]
250 fn status_target_with_health_and_tests() {
251 let health = HealthSnapshot {
252 id: None,
253 target: "mnw".to_string(),
254 status: HealthStatus::Operational,
255 checked_at: "2026-03-10T00:00:00Z".to_string(),
256 response_time_ms: 95,
257 details: Some(HealthDetails {
258 version: Some("2.1.0".to_string()),
259 git_sha: None,
260 uptime: None,
261 checks: None,
262 monitoring: None,
263 }),
264 error: None,
265 };
266 let test = TestRun {
267 id: None,
268 target: "mnw".to_string(),
269 started_at: "2026-03-10T00:00:00Z".to_string(),
270 finished_at: Some("2026-03-10T00:01:00Z".to_string()),
271 duration_secs: Some(60),
272 exit_code: Some(0),
273 passed: true,
274 summary: TestSummary {
275 steps: vec![],
276 total_passed: Some(100),
277 total_failed: Some(0),
278 details: vec![],
279 },
280 raw_output: String::new(),
281 filter: None,
282 };
283 let out = format_status_target(
284 "mnw",
285 "MakeNotWork",
286 Some(&health),
287 None,
288 None,
289 None,
290 None,
291 None,
292 Some(&test),
293 None,
294 None,
295 );
296 assert!(out.contains("=== mnw (MakeNotWork) ==="));
297 assert!(out.contains("Health: [OK] operational (95ms) v2.1.0"));
298 assert!(out.contains("Tests: PASSED (60s)"));
299 assert!(out.contains("100 passed, 0 failed"));
300 }
301
302 #[test]
303 fn status_target_no_data() {
304 let out = format_status_target(
305 "mnw",
306 "MakeNotWork",
307 None,
308 None,
309 None,
310 None,
311 None,
312 None,
313 None,
314 None,
315 None,
316 );
317 assert!(out.contains("=== mnw (MakeNotWork) ==="));
318 assert!(out.contains("Health: no data"));
319 assert!(out.contains("Tests: no data"));
320 }
321
322 #[test]
323 fn status_target_health_only() {
324 let health = HealthSnapshot {
325 id: None,
326 target: "mnw".to_string(),
327 status: HealthStatus::Degraded,
328 checked_at: "2026-03-10T00:00:00Z".to_string(),
329 response_time_ms: 2000,
330 details: None,
331 error: None,
332 };
333 let out = format_status_target(
334 "mnw",
335 "MakeNotWork",
336 Some(&health),
337 None,
338 None,
339 None,
340 None,
341 None,
342 None,
343 None,
344 None,
345 );
346 assert!(out.contains("Health: [WARN] degraded (2000ms)"));
347 assert!(out.contains("Tests: no data"));
348 }
349
350 #[test]
351 fn status_target_failed_tests() {
352 let test = TestRun {
353 id: None,
354 target: "mnw".to_string(),
355 started_at: "2026-03-10T00:00:00Z".to_string(),
356 finished_at: None,
357 duration_secs: None,
358 exit_code: Some(1),
359 passed: false,
360 summary: TestSummary {
361 steps: vec![],
362 total_passed: Some(80),
363 total_failed: Some(5),
364 details: vec![],
365 },
366 raw_output: String::new(),
367 filter: None,
368 };
369 let out = format_status_target(
370 "mnw",
371 "MakeNotWork",
372 None,
373 None,
374 None,
375 None,
376 None,
377 None,
378 Some(&test),
379 None,
380 None,
381 );
382 assert!(out.contains("Tests: FAILED"));
383 assert!(out.contains("80 passed, 5 failed"));
384 }
385
386 // format_status_target with TLS
387
388 #[test]
389 fn status_target_tls_ok() {
390 let tls = TlsCheckRow {
391 id: 1,
392 target: "mnw".to_string(),
393 host: "makenot.work".to_string(),
394 valid: true,
395 days_remaining: 47,
396 not_before: "2026-01-10T00:00:00Z".to_string(),
397 not_after: "2026-04-27T00:00:00Z".to_string(),
398 subject: "CN=makenot.work".to_string(),
399 issuer: "CN=Let's Encrypt".to_string(),
400 checked_at: "2026-03-11T00:00:00Z".to_string(),
401 error: None,
402 webpki_trusted: Some(true),
403 platform_trusted: Some(true),
404 webpki_error: None,
405 platform_error: None,
406 };
407 let out = format_status_target(
408 "mnw",
409 "MakeNotWork",
410 None,
411 None,
412 Some(&tls),
413 None,
414 None,
415 None,
416 None,
417 None,
418 None,
419 );
420 assert!(out.contains("TLS: [OK] makenot.work"));
421 assert!(out.contains("47d remaining"));
422 assert!(out.contains("expires 2026-04-27"));
423 }
424
425 #[test]
426 fn status_target_tls_warning() {
427 let tls = TlsCheckRow {
428 id: 1,
429 target: "mnw".to_string(),
430 host: "makenot.work".to_string(),
431 valid: true,
432 days_remaining: 12,
433 not_before: "2026-01-10T00:00:00Z".to_string(),
434 not_after: "2026-03-23T00:00:00Z".to_string(),
435 subject: "CN=makenot.work".to_string(),
436 issuer: "CN=Let's Encrypt".to_string(),
437 checked_at: "2026-03-11T00:00:00Z".to_string(),
438 error: None,
439 webpki_trusted: Some(true),
440 platform_trusted: Some(true),
441 webpki_error: None,
442 platform_error: None,
443 };
444 let out = format_status_target(
445 "mnw",
446 "MakeNotWork",
447 None,
448 None,
449 Some(&tls),
450 None,
451 None,
452 None,
453 None,
454 None,
455 None,
456 );
457 assert!(out.contains("TLS: [WARN] makenot.work"));
458 assert!(out.contains("12d remaining"));
459 }
460
461 #[test]
462 fn status_target_tls_error() {
463 let tls = TlsCheckRow {
464 id: 1,
465 target: "mnw".to_string(),
466 host: "makenot.work".to_string(),
467 valid: false,
468 days_remaining: 0,
469 not_before: String::new(),
470 not_after: String::new(),
471 subject: String::new(),
472 issuer: String::new(),
473 checked_at: "2026-03-11T00:00:00Z".to_string(),
474 error: Some("connection refused".to_string()),
475 webpki_trusted: Some(false),
476 platform_trusted: Some(false),
477 webpki_error: Some("TCP connect failed".to_string()),
478 platform_error: Some("TCP connect failed".to_string()),
479 };
480 let out = format_status_target(
481 "mnw",
482 "MakeNotWork",
483 None,
484 None,
485 Some(&tls),
486 None,
487 None,
488 None,
489 None,
490 None,
491 None,
492 );
493 assert!(out.contains("TLS: [ERR] makenot.work"));
494 assert!(out.contains("connection refused"));
495 }
496
497 // format_status_target with incident
498
499 #[test]
500 fn status_target_with_active_incident() {
501 let incident = IncidentRow {
502 id: 1,
503 target: "mnw".to_string(),
504 started_at: "2026-03-11T14:30:00Z".to_string(),
505 ended_at: None,
506 duration_secs: None,
507 from_status: "operational".to_string(),
508 to_status: "degraded".to_string(),
509 };
510 let out = format_status_target(
511 "mnw",
512 "MakeNotWork",
513 None,
514 None,
515 None,
516 None,
517 None,
518 None,
519 None,
520 None,
521 Some(&incident),
522 );
523 assert!(out.contains("Incident: [ACTIVE] degraded since 2026-03-11T14:30:00Z"));
524 }
525
526 #[test]
527 fn status_target_no_incident() {
528 let out = format_status_target(
529 "mnw",
530 "MakeNotWork",
531 None,
532 None,
533 None,
534 None,
535 None,
536 None,
537 None,
538 None,
539 None,
540 );
541 assert!(!out.contains("Incident"));
542 }
543
544 // format_status_target with latency
545
546 #[test]
547 fn status_target_with_latency() {
548 let latency = LatencyStats {
549 min_ms: 95,
550 max_ms: 210,
551 avg_ms: 120.0,
552 p95_ms: 180,
553 sample_count: 288,
554 };
555 let out = format_status_target(
556 "mnw",
557 "MakeNotWork",
558 None,
559 Some(&latency),
560 None,
561 None,
562 None,
563 None,
564 None,
565 None,
566 None,
567 );
568 assert!(out.contains("Latency (24h): avg 120ms, p95 180ms, range 95-210ms (288 samples)"));
569 }
570
571 #[test]
572 fn status_target_without_latency() {
573 let out = format_status_target(
574 "mnw",
575 "MakeNotWork",
576 None,
577 None,
578 None,
579 None,
580 None,
581 None,
582 None,
583 None,
584 None,
585 );
586 assert!(!out.contains("Latency"));
587 }
588
589 // format_health_history
590
591 #[test]
592 fn health_history_empty() {
593 let out = format_health_history(&[]);
594 assert_eq!(out, "No health check history.\n");
595 }
596
597 #[test]
598 fn health_history_with_entries() {
599 let history = vec![
600 HealthSnapshot {
601 id: Some(2),
602 target: "mnw".to_string(),
603 status: HealthStatus::Operational,
604 checked_at: "2026-03-10T01:00:00Z".to_string(),
605 response_time_ms: 120,
606 details: None,
607 error: None,
608 },
609 HealthSnapshot {
610 id: Some(1),
611 target: "mnw".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 ];
619 let out = format_health_history(&history);
620 assert!(out.contains("[OK] mnw"));
621 assert!(out.contains("(120ms)"));
622 assert!(out.contains("2026-03-10T01:00:00Z"));
623 assert!(out.contains("[WARN] mnw"));
624 assert!(out.contains("(2500ms)"));
625 }
626
627 // format_test_history
628
629 #[test]
630 fn test_history_empty() {
631 let out = format_test_history(&[]);
632 assert_eq!(out, "No test run history.\n");
633 }
634
635 #[test]
636 fn test_history_with_entries() {
637 let history = vec![
638 TestRun {
639 id: Some(2),
640 target: "mnw".to_string(),
641 started_at: "2026-03-10T01:00:00Z".to_string(),
642 finished_at: None,
643 duration_secs: Some(120),
644 exit_code: Some(0),
645 passed: true,
646 summary: TestSummary {
647 steps: vec![],
648 total_passed: Some(810),
649 total_failed: Some(0),
650 details: vec![],
651 },
652 raw_output: String::new(),
653 filter: None,
654 },
655 TestRun {
656 id: Some(1),
657 target: "mnw".to_string(),
658 started_at: "2026-03-10T00:00:00Z".to_string(),
659 finished_at: None,
660 duration_secs: None,
661 exit_code: Some(1),
662 passed: false,
663 summary: TestSummary {
664 steps: vec![],
665 total_passed: None,
666 total_failed: None,
667 details: vec![],
668 },
669 raw_output: String::new(),
670 filter: None,
671 },
672 ];
673 let out = format_test_history(&history);
674 assert!(out.contains("[PASS] mnw (120s) 2026-03-10T01:00:00Z"));
675 assert!(out.contains("810 passed, 0 failed"));
676 assert!(out.contains("[FAIL] mnw 2026-03-10T00:00:00Z"));
677 }
678
679 #[test]
680 fn test_history_no_duration_no_counts() {
681 let history = vec![TestRun {
682 id: Some(1),
683 target: "svc".to_string(),
684 started_at: "2026-03-10T00:00:00Z".to_string(),
685 finished_at: None,
686 duration_secs: None,
687 exit_code: None,
688 passed: true,
689 summary: TestSummary {
690 steps: vec![],
691 total_passed: None,
692 total_failed: None,
693 details: vec![],
694 },
695 raw_output: String::new(),
696 filter: None,
697 }];
698 let out = format_test_history(&history);
699 // Should not have duration or counts
700 assert!(!out.contains('('));
701 assert!(out.contains("[PASS] svc 2026-03-10T00:00:00Z"));
702 }
703
704 // format_prune
705
706 #[test]
707 fn prune_formatting() {
708 let result = PruneResult {
709 health: 5,
710 tests: 3,
711 test_details: 15,
712 heartbeats: 10,
713 alerts: 2,
714 tls: 1,
715 incidents: 4,
716 routes: 0,
717 dns: 8,
718 whois: 2,
719 backups: 1,
720 systemd: 6,
721 ca_bundle: 2,
722 synckit_fleet: 7,
723 };
724 let out = format_prune(&result, 30);
725 assert_eq!(
726 out,
727 "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"
728 );
729 }
730
731 #[test]
732 fn prune_zero_records() {
733 let result = PruneResult {
734 health: 0,
735 tests: 0,
736 test_details: 0,
737 heartbeats: 0,
738 alerts: 0,
739 tls: 0,
740 incidents: 0,
741 routes: 0,
742 dns: 0,
743 whois: 0,
744 backups: 0,
745 systemd: 0,
746 ca_bundle: 0,
747 synckit_fleet: 0,
748 };
749 let out = format_prune(&result, 7);
750 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."));
751 }
752
753 // format_mesh
754
755 #[test]
756 fn mesh_no_instances() {
757 let data = serde_json::json!({});
758 let out = format_mesh(&data);
759 assert_eq!(out, "No mesh data available.\n");
760 }
761
762 #[test]
763 fn mesh_empty_instances() {
764 let data = serde_json::json!({ "instances": {} });
765 let out = format_mesh(&data);
766 // Empty map, no output lines beyond the empty string
767 assert!(out.is_empty());
768 }
769
770 #[test]
771 fn mesh_single_instance_with_targets_and_peers() {
772 let data = serde_json::json!({
773 "instances": {
774 "hetzner": {
775 "instance": {
776 "id": "uuid-123",
777 "version": "0.2.0"
778 },
779 "targets": {
780 "mnw": {
781 "status": "operational",
782 "response_time_ms": 95
783 }
784 },
785 "peers": {
786 "astra": {
787 "status": "online",
788 "latency_ms": 42
789 }
790 }
791 }
792 }
793 });
794 let out = format_mesh(&data);
795 assert!(out.contains("=== hetzner ==="));
796 assert!(out.contains("ID: uuid-123"));
797 assert!(out.contains("Version: 0.2.0"));
798 assert!(out.contains("Target mnw: operational (95ms)"));
799 assert!(out.contains("Peer astra: online (42ms)"));
800 }
801
802 #[test]
803 fn mesh_missing_instance_details() {
804 let data = serde_json::json!({
805 "instances": {
806 "node-1": {}
807 }
808 });
809 let out = format_mesh(&data);
810 assert!(out.contains("=== node-1 ==="));
811 assert!(out.contains("ID: ?"));
812 assert!(out.contains("Version: ?"));
813 }
814
815 #[test]
816 fn mesh_instance_with_error() {
817 let data = serde_json::json!({
818 "instances": {
819 "node-2": {
820 "error": "connection refused"
821 }
822 }
823 });
824 let out = format_mesh(&data);
825 assert!(out.contains("=== node-2 ==="));
826 assert!(out.contains("(connection refused)"));
827 }
828
829 #[test]
830 fn mesh_target_without_response_time() {
831 let data = serde_json::json!({
832 "instances": {
833 "node": {
834 "instance": { "id": "x", "version": "1.0" },
835 "targets": {
836 "svc": { "status": "unreachable" }
837 }
838 }
839 }
840 });
841 let out = format_mesh(&data);
842 assert!(out.contains("Target svc: unreachable"));
843 // No (Xms) suffix
844 assert!(!out.contains("Target svc: unreachable ("));
845 }
846
847 #[test]
848 fn mesh_peer_without_latency() {
849 let data = serde_json::json!({
850 "instances": {
851 "node": {
852 "instance": { "id": "x", "version": "1.0" },
853 "peers": {
854 "other": { "status": "missing" }
855 }
856 }
857 }
858 });
859 let out = format_mesh(&data);
860 assert!(out.contains("Peer other: missing"));
861 // No (Xms) suffix
862 assert!(!out.contains("Peer other: missing ("));
863 }
864
865 #[test]
866 fn mesh_multiple_instances() {
867 let data = serde_json::json!({
868 "instances": {
869 "alpha": {
870 "instance": { "id": "a1", "version": "0.1.0" }
871 },
872 "beta": {
873 "instance": { "id": "b2", "version": "0.2.0" }
874 }
875 }
876 });
877 let out = format_mesh(&data);
878 assert!(out.contains("=== alpha ==="));
879 assert!(out.contains("=== beta ==="));
880 assert!(out.contains("ID: a1"));
881 assert!(out.contains("ID: b2"));
882 }
883
884 // format_status_target with staleness
885
886 #[test]
887 fn status_target_stale_by_version() {
888 let staleness = TestStaleness {
889 stale: true,
890 reason: Some("version changed: 0.1.8 -> 0.1.9".to_string()),
891 current_version: Some("0.1.9".to_string()),
892 tested_version: Some("0.1.8".to_string()),
893 last_test_at: Some("2026-03-10T00:00:00Z".to_string()),
894 days_since_test: Some(1),
895 };
896 let out = format_status_target(
897 "mnw",
898 "MakeNotWork",
899 None,
900 None,
901 None,
902 None,
903 None,
904 None,
905 None,
906 Some(&staleness),
907 None,
908 );
909 assert!(out.contains("Tests: STALE"));
910 assert!(out.contains("version changed: 0.1.8 -> 0.1.9"));
911 }
912
913 #[test]
914 fn status_target_stale_by_age() {
915 let staleness = TestStaleness {
916 stale: true,
917 reason: Some("tests are 10 days old (threshold: 7d)".to_string()),
918 current_version: Some("0.1.9".to_string()),
919 tested_version: Some("0.1.9".to_string()),
920 last_test_at: Some("2026-03-01T00:00:00Z".to_string()),
921 days_since_test: Some(10),
922 };
923 let out = format_status_target(
924 "mnw",
925 "MakeNotWork",
926 None,
927 None,
928 None,
929 None,
930 None,
931 None,
932 None,
933 Some(&staleness),
934 None,
935 );
936 assert!(out.contains("Tests: STALE"));
937 assert!(out.contains("tests are 10 days old"));
938 }
939
940 #[test]
941 fn status_target_not_stale() {
942 let staleness = TestStaleness {
943 stale: false,
944 reason: None,
945 current_version: Some("0.1.9".to_string()),
946 tested_version: Some("0.1.9".to_string()),
947 last_test_at: Some("2026-03-10T00:00:00Z".to_string()),
948 days_since_test: Some(1),
949 };
950 let out = format_status_target(
951 "mnw",
952 "MakeNotWork",
953 None,
954 None,
955 None,
956 None,
957 None,
958 None,
959 None,
960 Some(&staleness),
961 None,
962 );
963 assert!(!out.contains("STALE"));
964 }
965
966 #[test]
967 fn status_target_no_staleness_data() {
968 let out = format_status_target(
969 "mnw",
970 "MakeNotWork",
971 None,
972 None,
973 None,
974 None,
975 None,
976 None,
977 None,
978 None,
979 None,
980 );
981 assert!(!out.contains("STALE"));
982 }
983
984 // format_status_target with routes
985
986 #[test]
987 fn status_target_all_routes_ok() {
988 let checks = vec![
989 RouteCheckRow {
990 id: 1,
991 target: "mnw".to_string(),
992 path: "/".to_string(),
993 status_code: 200,
994 ok: true,
995 response_time_ms: 50,
996 checked_at: "2026-03-13T00:00:00Z".to_string(),
997 error: None,
998 },
999 RouteCheckRow {
1000 id: 2,
1001 target: "mnw".to_string(),
1002 path: "/docs".to_string(),
1003 status_code: 200,
1004 ok: true,
1005 response_time_ms: 60,
1006 checked_at: "2026-03-13T00:00:00Z".to_string(),
1007 error: None,
1008 },
1009 ];
1010 let out = format_status_target(
1011 "mnw",
1012 "MakeNotWork",
1013 None,
1014 None,
1015 None,
1016 Some(&checks),
1017 None,
1018 None,
1019 None,
1020 None,
1021 None,
1022 );
1023 assert!(out.contains("Routes: 2/2 OK"));
1024 }
1025
1026 #[test]
1027 fn status_target_some_routes_failing() {
1028 let checks = vec![
1029 RouteCheckRow {
1030 id: 1,
1031 target: "mnw".to_string(),
1032 path: "/".to_string(),
1033 status_code: 200,
1034 ok: true,
1035 response_time_ms: 50,
1036 checked_at: "2026-03-13T00:00:00Z".to_string(),
1037 error: None,
1038 },
1039 RouteCheckRow {
1040 id: 2,
1041 target: "mnw".to_string(),
1042 path: "/docs/faq".to_string(),
1043 status_code: 404,
1044 ok: false,
1045 response_time_ms: 30,
1046 checked_at: "2026-03-13T00:00:00Z".to_string(),
1047 error: Some("HTTP 404".to_string()),
1048 },
1049 RouteCheckRow {
1050 id: 3,
1051 target: "mnw".to_string(),
1052 path: "/pricing".to_string(),
1053 status_code: 500,
1054 ok: false,
1055 response_time_ms: 20,
1056 checked_at: "2026-03-13T00:00:00Z".to_string(),
1057 error: Some("HTTP 500".to_string()),
1058 },
1059 ];
1060 let out = format_status_target(
1061 "mnw",
1062 "MakeNotWork",
1063 None,
1064 None,
1065 None,
1066 Some(&checks),
1067 None,
1068 None,
1069 None,
1070 None,
1071 None,
1072 );
1073 assert!(out.contains("Routes: 1/3 (FAIL: /docs/faq, /pricing)"));
1074 }
1075
1076 // format_versions
1077
1078 fn version_row(target: &str, version: Option<&str>, sha: Option<&str>) -> VersionRow {
1079 VersionRow {
1080 target: target.to_string(),
1081 label: format!("{target} label"),
1082 version: version.map(String::from),
1083 git_sha: sha.map(String::from),
1084 checked_at: Some("2026-07-29T18:04:37.123456+00:00".to_string()),
1085 version_since: Some("2026-07-28T09:00:00+00:00".to_string()),
1086 commits_behind: Some(8),
1087 behind_error: None,
1088 }
1089 }
1090
1091 #[test]
1092 fn versions_table_aligns_columns_and_shortens_the_sha() {
1093 let rows = vec![
1094 version_row("mnw", Some("0.11.0"), Some("6402bf4e9c1d2e3f4a5b")),
1095 version_row("multithreaded", Some("0.4.2"), Some("aaaabbbbcccc")),
1096 ];
1097 let out = format_versions(&rows);
1098 let lines: Vec<&str> = out.lines().collect();
1099
1100 assert!(lines[0].starts_with("TARGET"));
1101 // The header pads to the widest target, so every column starts at the
1102 // same offset on every line.
1103 let version_col = lines[0].find("VERSION").unwrap();
1104 assert_eq!(lines[1].find("0.11.0"), Some(version_col));
1105 assert_eq!(lines[2].find("0.4.2"), Some(version_col));
1106
1107 assert!(out.contains("6402bf4e "), "sha shortened to 8: {out}");
1108 assert!(!out.contains("6402bf4e9c1d"));
1109 assert!(
1110 out.contains("2026-07-29 18:04"),
1111 "timestamp to the minute: {out}"
1112 );
1113 }
1114
1115 #[test]
1116 fn versions_table_prints_a_dash_for_every_missing_value() {
1117 let rows = vec![VersionRow {
1118 target: "mt".to_string(),
1119 label: "Multithreaded".to_string(),
1120 version: None,
1121 git_sha: None,
1122 checked_at: None,
1123 version_since: None,
1124 commits_behind: None,
1125 behind_error: None,
1126 }];
1127 let out = format_versions(&rows);
1128 let row = out.lines().nth(1).unwrap();
1129 assert_eq!(
1130 row.split_whitespace().collect::<Vec<_>>(),
1131 ["mt", "-", "-", "-", "-", "-"]
1132 );
1133 }
1134
1135 #[test]
1136 fn versions_table_says_why_a_count_is_blank() {
1137 let mut row = version_row("mnw", Some("0.11.0"), Some("6402bf4e"));
1138 row.commits_behind = None;
1139 row.behind_error = Some("git rev-list exited 128: bad revision".to_string());
1140 let out = format_versions(&[row]);
1141 assert!(out.contains("behind: git rev-list exited 128"), "{out}");
1142 }
1143
1144 #[test]
1145 fn versions_table_scrubs_a_hostile_version_string() {
1146 // Same terminal-injection surface as every other display sink: the
1147 // version and sha are whatever the monitored target chose to send.
1148 let mut row = version_row("mnw", Some("1.0\u{1b}[2J FAKE"), Some("6402bf4e"));
1149 row.behind_error = Some("boom\u{1b}[2J".to_string());
1150 let out = format_versions(&[row]);
1151 assert!(!out.contains('\u{1b}'), "ESC must not reach the terminal");
1152 }
1153
1154 #[test]
1155 fn versions_empty_config_says_so() {
1156 assert_eq!(format_versions(&[]), "No targets configured.\n");
1157 }
1158
1159 #[test]
1160 fn minute_stamp_passes_through_anything_that_is_not_a_timestamp() {
1161 assert_eq!(minute_stamp("2026-07-29T18:04:37Z"), "2026-07-29 18:04");
1162 assert_eq!(minute_stamp("whenever"), "whenever");
1163 assert_eq!(minute_stamp("2026-07-29 18:04:37"), "2026-07-29 18:04:37");
1164 }
1165
1166 #[test]
1167 fn status_target_no_route_checks() {
1168 let out = format_status_target(
1169 "mnw",
1170 "MakeNotWork",
1171 None,
1172 None,
1173 None,
1174 None,
1175 None,
1176 None,
1177 None,
1178 None,
1179 None,
1180 );
1181 assert!(!out.contains("Routes"));
1182 }
1183