1use std::collections::HashSet;
53use std::fmt;
54use std::io::IsTerminal;
55
56use colored::Colorize;
57use dialoguer::{Confirm, MultiSelect, theme::ColorfulTheme};
58
59use super::probe::{CassStatus, HostProbeResult};
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum HostState {
70 ReadyToSync,
72 NeedsIndexing,
74 NeedsInstall,
76 Unreachable,
78 AlreadyConfigured,
80}
81
82impl HostState {
83 pub fn status_badge(&self) -> String {
85 match self {
86 HostState::ReadyToSync => format!("{} Ready to sync", "✓".green()),
87 HostState::NeedsIndexing => format!("{} Needs indexing", "⚡".yellow()),
88 HostState::NeedsInstall => format!("{} Needs install", "⚠".yellow()),
89 HostState::Unreachable => format!("{} Unreachable", "✗".red()),
90 HostState::AlreadyConfigured => format!("{} Already setup", "═".cyan()),
91 }
92 }
93
94 pub fn is_selectable(&self) -> bool {
96 matches!(
97 self,
98 HostState::ReadyToSync | HostState::NeedsIndexing | HostState::NeedsInstall
99 )
100 }
101
102 pub fn should_preselect(&self) -> bool {
104 matches!(self, HostState::ReadyToSync | HostState::NeedsIndexing)
106 }
107}
108
109#[derive(Debug, Clone)]
111pub struct HostDisplayInfo {
112 pub name: String,
114 pub hostname: String,
116 pub username: String,
118 pub cass_status: CassStatusDisplay,
120 pub detected_agents: Vec<String>,
122 pub reachable: bool,
124 pub error: Option<String>,
126 pub state: HostState,
128 pub system_info: Option<String>,
130}
131
132#[derive(Debug, Clone)]
134pub enum CassStatusDisplay {
135 Installed { version: String, sessions: u64 },
137 InstalledNotIndexed { version: String },
139 NotInstalled,
141 Unknown,
143}
144
145#[derive(Debug, Clone)]
147pub struct HostSelectionResult {
148 pub selected_indices: Vec<usize>,
150 pub needs_install: Vec<usize>,
152 pub needs_indexing: Vec<usize>,
154 pub ready_for_sync: Vec<usize>,
156}
157
158pub struct HostSelector {
164 hosts: Vec<HostDisplayInfo>,
165 theme: ColorfulTheme,
166}
167
168impl HostSelector {
169 pub fn new(hosts: Vec<HostDisplayInfo>) -> Self {
171 Self {
172 hosts,
173 theme: ColorfulTheme::default(),
174 }
175 }
176
177 fn format_host(&self, host: &HostDisplayInfo) -> String {
188 let mut lines = Vec::new();
189
190 let status_badge = host.state.status_badge();
193 let name_line = format!("{} {}", host.name.bold(), status_badge);
194 lines.push(name_line);
195
196 let system_info = host.system_info.as_deref().unwrap_or("");
198 let host_info = if system_info.is_empty() {
199 format!(
200 " {} • {}",
201 host.hostname.dimmed(),
202 host.username.dimmed()
203 )
204 } else {
205 format!(
206 " {} • {} • {}",
207 host.hostname.dimmed(),
208 host.username.dimmed(),
209 system_info.dimmed()
210 )
211 };
212 lines.push(host_info);
213
214 let status_line = match &host.cass_status {
216 CassStatusDisplay::Installed { version, sessions } => {
217 format!(
218 " {} cass v{} • {} sessions indexed",
219 "✓".green(),
220 version,
221 sessions
222 )
223 }
224 CassStatusDisplay::InstalledNotIndexed { version } => {
225 format!(
226 " {} cass v{} • {} (will index)",
227 "⚡".yellow(),
228 version,
229 "not indexed".yellow()
230 )
231 }
232 CassStatusDisplay::NotInstalled => {
233 format!(
234 " {} cass not installed (will install via cargo)",
235 "✗".yellow()
236 )
237 }
238 CassStatusDisplay::Unknown => {
239 format!(" {} status unknown", "?".dimmed())
240 }
241 };
242 lines.push(status_line);
243
244 if !host.detected_agents.is_empty() {
246 let agents: Vec<String> = host
247 .detected_agents
248 .iter()
249 .map(|a| {
250 let display_name = if a.is_empty() {
252 a.clone()
253 } else {
254 let mut chars = a.chars();
255 match chars.next() {
256 Some(first) => first.to_uppercase().chain(chars).collect(),
257 None => a.clone(),
258 }
259 };
260 format!("{} {}", display_name.cyan(), "✓".green())
261 })
262 .collect();
263 let agents_line = format!(" {}", agents.join(" "));
264 lines.push(agents_line);
265 }
266
267 if !host.reachable {
269 let error_msg = host.error.as_deref().unwrap_or("unreachable");
270 let error_line = format!(" {} {}", "⚠".red(), error_msg.red());
271 lines.push(error_line);
272 }
273
274 if host.state == HostState::AlreadyConfigured {
276 lines.push(format!(
277 " {}",
278 "Use 'cass sources edit' to modify".dimmed()
279 ));
280 }
281
282 lines.join("\n")
283 }
284
285 pub fn prompt(&self) -> Result<HostSelectionResult, InteractiveError> {
289 if self.hosts.is_empty() {
290 return Err(InteractiveError::NoHosts);
291 }
292
293 let selectable_hosts: Vec<(usize, &HostDisplayInfo)> = self
295 .hosts
296 .iter()
297 .enumerate()
298 .filter(|(_, h)| h.state.is_selectable())
299 .collect();
300
301 if selectable_hosts.is_empty() {
302 return Err(InteractiveError::NoSelectableHosts);
303 }
304
305 let items: Vec<String> = selectable_hosts
307 .iter()
308 .map(|(_, h)| self.format_host(h))
309 .collect();
310
311 let defaults: Vec<bool> = selectable_hosts
313 .iter()
314 .map(|(_, h)| h.state.should_preselect())
315 .collect();
316
317 println!();
319 println!(
320 "{}",
321 "Select hosts to configure as sources:".bold().underline()
322 );
323 println!(
324 "{}",
325 "[space] toggle [a] all [enter] confirm [q] quit".dimmed()
326 );
327 println!();
328
329 let selected_in_filtered = MultiSelect::with_theme(&self.theme)
330 .items(&items)
331 .defaults(&defaults)
332 .interact_opt()
333 .map_err(|e| InteractiveError::IoError(e.to_string()))?
334 .ok_or(InteractiveError::Cancelled)?;
335
336 let selected: Vec<usize> = selected_in_filtered
338 .iter()
339 .filter_map(|&i| selectable_hosts.get(i).map(|(orig_idx, _)| *orig_idx))
340 .collect();
341
342 let mut needs_install = Vec::new();
344 let mut needs_indexing = Vec::new();
345 let mut ready_for_sync = Vec::new();
346
347 for &idx in &selected {
348 if let Some(host) = self.hosts.get(idx) {
349 match host.state {
350 HostState::ReadyToSync => ready_for_sync.push(idx),
351 HostState::NeedsIndexing => needs_indexing.push(idx),
352 HostState::NeedsInstall => needs_install.push(idx),
353 _ => {} }
355 }
356 }
357
358 Ok(HostSelectionResult {
359 selected_indices: selected,
360 needs_install,
361 needs_indexing,
362 ready_for_sync,
363 })
364 }
365
366 pub fn get_host(&self, index: usize) -> Option<&HostDisplayInfo> {
368 self.hosts.get(index)
369 }
370}
371
372pub fn confirm_action(message: &str, default: bool) -> Result<bool, InteractiveError> {
378 Confirm::with_theme(&ColorfulTheme::default())
379 .with_prompt(message)
380 .default(default)
381 .interact()
382 .map_err(|e| InteractiveError::IoError(e.to_string()))
383}
384
385pub fn confirm_with_details(
387 action: &str,
388 details: &[&str],
389 default: bool,
390) -> Result<bool, InteractiveError> {
391 println!();
392 println!("{}", action.bold());
393 for detail in details {
394 println!(" • {}", detail);
395 }
396 println!();
397
398 confirm_action("Proceed?", default)
399}
400
401pub fn probe_to_display_info(
411 probe: &HostProbeResult,
412 already_configured: &HashSet<String>,
413) -> HostDisplayInfo {
414 let generated_name = super::config::normalize_generated_remote_source_name(&probe.host_name);
415
416 let state = if already_configured.contains(&super::config::source_name_key(&generated_name)) {
418 HostState::AlreadyConfigured
419 } else if !probe.reachable {
420 HostState::Unreachable
421 } else {
422 match &probe.cass_status {
423 CassStatus::Indexed { session_count, .. } if *session_count > 0 => {
424 HostState::ReadyToSync
425 }
426 CassStatus::Indexed { .. } => HostState::NeedsIndexing, CassStatus::InstalledNotIndexed { .. } => HostState::NeedsIndexing,
428 CassStatus::NotFound | CassStatus::Unknown => HostState::NeedsInstall,
429 }
430 };
431
432 let cass_status = match &probe.cass_status {
434 CassStatus::Indexed {
435 version,
436 session_count,
437 ..
438 } => CassStatusDisplay::Installed {
439 version: version.clone(),
440 sessions: *session_count,
441 },
442 CassStatus::InstalledNotIndexed { version } => CassStatusDisplay::InstalledNotIndexed {
443 version: version.clone(),
444 },
445 CassStatus::NotFound => CassStatusDisplay::NotInstalled,
446 CassStatus::Unknown => CassStatusDisplay::Unknown,
447 };
448
449 let system_info = probe.system_info.as_ref().map(|si| {
451 let os_info = si
453 .distro
454 .as_deref()
455 .filter(|d| !d.is_empty())
456 .unwrap_or(&si.os);
457 if let Some(res) = &probe.resources {
458 let disk_gb = res.disk_available_mb / 1024;
459 format!("{} • {}GB free", os_info, disk_gb)
460 } else {
461 os_info.to_string()
462 }
463 });
464
465 let detected_agents: Vec<String> = probe
467 .detected_agents
468 .iter()
469 .map(|a| a.agent_type.clone())
470 .collect();
471
472 let hostname = probe.host_name.clone();
474
475 let username = probe
476 .system_info
477 .as_ref()
478 .and_then(|si| {
479 si.remote_home
482 .rsplit('/')
483 .find(|s| !s.is_empty())
484 .map(String::from)
485 })
486 .unwrap_or_else(|| "user".to_string());
487
488 HostDisplayInfo {
489 name: probe.host_name.clone(),
490 hostname,
491 username,
492 cass_status,
493 detected_agents,
494 reachable: probe.reachable,
495 error: probe.error.clone(),
496 state,
497 system_info,
498 }
499}
500
501pub fn run_host_selection(
515 probed_hosts: &[HostProbeResult],
516 already_configured: &HashSet<String>,
517) -> Result<(HostSelectionResult, Vec<HostDisplayInfo>), InteractiveError> {
518 if !std::io::stdin().is_terminal() {
520 return Err(InteractiveError::NotATty);
521 }
522
523 let hosts: Vec<HostDisplayInfo> = probed_hosts
525 .iter()
526 .map(|p| probe_to_display_info(p, already_configured))
527 .collect();
528
529 let unreachable_count = hosts
531 .iter()
532 .filter(|h| h.state == HostState::Unreachable)
533 .count();
534 let configured_count = hosts
535 .iter()
536 .filter(|h| h.state == HostState::AlreadyConfigured)
537 .count();
538
539 if unreachable_count > 0 || configured_count > 0 {
540 println!();
541 if unreachable_count > 0 {
542 println!(
543 "{}",
544 format!(
545 " {} {} unreachable (check SSH config)",
546 "⚠".yellow(),
547 unreachable_count
548 )
549 .dimmed()
550 );
551 }
552 if configured_count > 0 {
553 println!(
554 "{}",
555 format!(" {} {} already configured", "═".cyan(), configured_count).dimmed()
556 );
557 }
558 }
559
560 let selector = HostSelector::new(hosts.clone());
562 let result = selector.prompt()?;
563
564 let install_count = result.needs_install.len();
566 let index_count = result.needs_indexing.len();
567 let sync_count = result.ready_for_sync.len();
568 let total = result.selected_indices.len();
569
570 if total > 0 {
571 println!();
572 let mut parts = Vec::new();
573 if sync_count > 0 {
574 parts.push(format!("{} ready to sync", sync_count));
575 }
576 if index_count > 0 {
577 parts.push(format!("{} needs indexing", index_count));
578 }
579 if install_count > 0 {
580 let est_mins = install_count * 3;
582 parts.push(format!(
583 "{} needs install (~{} min)",
584 install_count, est_mins
585 ));
586 }
587 println!(
588 " {} selected: {}",
589 total.to_string().bold(),
590 parts.join(", ")
591 );
592 }
593
594 Ok((result, hosts))
595}
596
597#[derive(Debug)]
603pub enum InteractiveError {
604 Cancelled,
606 NoHosts,
608 NoSelectableHosts,
610 NotATty,
612 IoError(String),
614}
615
616impl fmt::Display for InteractiveError {
617 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
618 match self {
619 InteractiveError::Cancelled => write!(f, "Operation cancelled by user"),
620 InteractiveError::NoHosts => write!(f, "No hosts available for selection"),
621 InteractiveError::NoSelectableHosts => {
622 write!(
623 f,
624 "No selectable hosts (all unreachable or already configured)"
625 )
626 }
627 InteractiveError::NotATty => {
628 write!(
629 f,
630 "Interactive selection requires a terminal.\n\n\
631 For non-interactive use:\n \
632 cass sources setup --hosts css,csd,yto\n \
633 cass sources setup --non-interactive # select all reachable"
634 )
635 }
636 InteractiveError::IoError(msg) => write!(f, "IO error: {}", msg),
637 }
638 }
639}
640
641impl std::error::Error for InteractiveError {}
642
643#[cfg(test)]
648mod tests {
649 use super::*;
650
651 #[test]
652 fn test_host_display_info_creation() {
653 let host = HostDisplayInfo {
654 name: "laptop".into(),
655 hostname: "192.168.1.100".into(),
656 username: "user".into(),
657 cass_status: CassStatusDisplay::Installed {
658 version: "0.1.50".into(),
659 sessions: 123,
660 },
661 detected_agents: vec!["claude".into(), "codex".into()],
662 reachable: true,
663 error: None,
664 state: HostState::ReadyToSync,
665 system_info: Some("ubuntu 22.04 • 45GB free".into()),
666 };
667
668 assert_eq!(host.name, "laptop");
669 assert!(host.reachable);
670 assert!(matches!(
671 host.cass_status,
672 CassStatusDisplay::Installed { .. }
673 ));
674 assert_eq!(host.state, HostState::ReadyToSync);
675 }
676
677 #[test]
678 fn test_host_selector_format() {
679 let hosts = vec![HostDisplayInfo {
680 name: "test-host".into(),
681 hostname: "10.0.0.1".into(),
682 username: "testuser".into(),
683 cass_status: CassStatusDisplay::NotInstalled,
684 detected_agents: vec!["claude".into()],
685 reachable: true,
686 error: None,
687 state: HostState::NeedsInstall,
688 system_info: None,
689 }];
690
691 let selector = HostSelector::new(hosts);
692 let formatted = selector.format_host(&selector.hosts[0]);
693
694 assert!(formatted.contains("test-host"));
696 assert!(formatted.contains("10.0.0.1"));
697 assert!(formatted.contains("testuser"));
698 assert!(formatted.contains("cass not installed"));
699 assert!(formatted.contains("Claude"));
701 assert!(formatted.contains("Needs install"));
703 }
704
705 #[test]
706 fn test_host_selector_empty() {
707 let selector = HostSelector::new(vec![]);
708 assert!(selector.hosts.is_empty());
710 }
711
712 #[test]
713 fn test_cass_status_display_variants() {
714 let installed = CassStatusDisplay::Installed {
715 version: "0.1.50".into(),
716 sessions: 100,
717 };
718 let not_installed = CassStatusDisplay::NotInstalled;
719 let unknown = CassStatusDisplay::Unknown;
720
721 assert!(matches!(installed, CassStatusDisplay::Installed { .. }));
722 assert!(matches!(not_installed, CassStatusDisplay::NotInstalled));
723 assert!(matches!(unknown, CassStatusDisplay::Unknown));
724 }
725
726 #[test]
727 fn test_host_selection_result() {
728 let result = HostSelectionResult {
729 selected_indices: vec![0, 2, 3],
730 needs_install: vec![2],
731 needs_indexing: vec![],
732 ready_for_sync: vec![0, 3],
733 };
734
735 assert_eq!(result.selected_indices.len(), 3);
736 assert_eq!(result.needs_install.len(), 1);
737 assert_eq!(result.needs_indexing.len(), 0);
738 assert_eq!(result.ready_for_sync.len(), 2);
739 }
740
741 #[test]
742 fn test_interactive_error_display() {
743 let cancelled = InteractiveError::Cancelled;
744 let no_hosts = InteractiveError::NoHosts;
745 let io_error = InteractiveError::IoError("test error".into());
746
747 assert!(cancelled.to_string().contains("cancelled"));
748 assert!(no_hosts.to_string().contains("No hosts"));
749 assert!(io_error.to_string().contains("test error"));
750 }
751
752 #[test]
753 fn test_unreachable_host_format() {
754 let hosts = vec![HostDisplayInfo {
755 name: "unreachable-host".into(),
756 hostname: "10.0.0.99".into(),
757 username: "user".into(),
758 cass_status: CassStatusDisplay::Unknown,
759 detected_agents: vec![],
760 reachable: false,
761 error: Some("Connection timed out".into()),
762 state: HostState::Unreachable,
763 system_info: None,
764 }];
765
766 let selector = HostSelector::new(hosts);
767 let formatted = selector.format_host(&selector.hosts[0]);
768
769 assert!(formatted.contains("unreachable-host"));
770 assert!(formatted.contains("Connection timed out"));
771 assert!(formatted.contains("Unreachable"));
772 }
773
774 #[test]
775 fn test_host_state_properties() {
776 assert!(HostState::ReadyToSync.is_selectable());
778 assert!(HostState::NeedsIndexing.is_selectable());
779 assert!(HostState::NeedsInstall.is_selectable());
780 assert!(!HostState::Unreachable.is_selectable());
781 assert!(!HostState::AlreadyConfigured.is_selectable());
782
783 assert!(HostState::ReadyToSync.should_preselect());
785 assert!(HostState::NeedsIndexing.should_preselect());
786 assert!(!HostState::NeedsInstall.should_preselect());
787 assert!(!HostState::Unreachable.should_preselect());
788 assert!(!HostState::AlreadyConfigured.should_preselect());
789 }
790
791 #[test]
792 fn test_host_state_status_badges() {
793 let badge = HostState::ReadyToSync.status_badge();
794 assert!(badge.contains("Ready to sync"));
795
796 let badge = HostState::NeedsIndexing.status_badge();
797 assert!(badge.contains("Needs indexing"));
798
799 let badge = HostState::NeedsInstall.status_badge();
800 assert!(badge.contains("Needs install"));
801
802 let badge = HostState::Unreachable.status_badge();
803 assert!(badge.contains("Unreachable"));
804
805 let badge = HostState::AlreadyConfigured.status_badge();
806 assert!(badge.contains("Already setup"));
807 }
808
809 #[test]
810 fn test_probe_to_display_info() {
811 let probe = HostProbeResult {
812 host_name: "test-server".into(),
813 reachable: true,
814 connection_time_ms: 50,
815 cass_status: CassStatus::Indexed {
816 version: "0.1.50".into(),
817 session_count: 100,
818 last_indexed: None,
819 },
820 detected_agents: vec![],
821 system_info: None,
822 resources: None,
823 error: None,
824 };
825
826 let already_configured = HashSet::new();
827 let display = probe_to_display_info(&probe, &already_configured);
828
829 assert_eq!(display.name, "test-server");
830 assert_eq!(display.state, HostState::ReadyToSync);
831 assert!(matches!(
832 display.cass_status,
833 CassStatusDisplay::Installed { sessions: 100, .. }
834 ));
835 }
836
837 #[test]
838 fn test_probe_to_display_info_already_configured() {
839 let probe = HostProbeResult {
840 host_name: "configured-host".into(),
841 reachable: true,
842 connection_time_ms: 50,
843 cass_status: CassStatus::Indexed {
844 version: "0.1.50".into(),
845 session_count: 100,
846 last_indexed: None,
847 },
848 detected_agents: vec![],
849 system_info: None,
850 resources: None,
851 error: None,
852 };
853
854 let mut already_configured = HashSet::new();
855 already_configured.insert("configured-host".into());
856 let display = probe_to_display_info(&probe, &already_configured);
857
858 assert_eq!(display.state, HostState::AlreadyConfigured);
859 }
860
861 #[test]
862 fn test_probe_to_display_info_already_configured_case_insensitive() {
863 let probe = HostProbeResult {
864 host_name: "Configured-Host".into(),
865 reachable: true,
866 connection_time_ms: 50,
867 cass_status: CassStatus::Indexed {
868 version: "0.1.50".into(),
869 session_count: 100,
870 last_indexed: None,
871 },
872 detected_agents: vec![],
873 system_info: None,
874 resources: None,
875 error: None,
876 };
877
878 let mut already_configured = HashSet::new();
879 already_configured.insert(super::super::config::source_name_key("configured-host"));
880 let display = probe_to_display_info(&probe, &already_configured);
881
882 assert_eq!(display.state, HostState::AlreadyConfigured);
883 }
884
885 #[test]
886 fn test_probe_to_display_info_reserved_local_ssh_alias_already_configured() {
887 let probe = HostProbeResult {
888 host_name: "local".into(),
889 reachable: true,
890 connection_time_ms: 50,
891 cass_status: CassStatus::Indexed {
892 version: "0.1.50".into(),
893 session_count: 100,
894 last_indexed: None,
895 },
896 detected_agents: vec![],
897 system_info: None,
898 resources: None,
899 error: None,
900 };
901
902 let mut already_configured = HashSet::new();
903 already_configured.insert(super::super::config::source_name_key("local-ssh"));
904 let display = probe_to_display_info(&probe, &already_configured);
905
906 assert_eq!(display.state, HostState::AlreadyConfigured);
907 }
908
909 #[test]
910 fn test_installed_not_indexed_status() {
911 let status = CassStatusDisplay::InstalledNotIndexed {
912 version: "0.1.50".into(),
913 };
914 assert!(matches!(
915 status,
916 CassStatusDisplay::InstalledNotIndexed { .. }
917 ));
918 }
919
920 #[test]
921 fn test_probe_to_display_info_username_extraction() {
922 use super::super::probe::SystemInfo;
923
924 let probe = HostProbeResult {
926 host_name: "test".into(),
927 reachable: true,
928 connection_time_ms: 50,
929 cass_status: CassStatus::NotFound,
930 detected_agents: vec![],
931 system_info: Some(SystemInfo {
932 os: "Linux".into(),
933 arch: "x86_64".into(),
934 distro: None,
935 has_cargo: false,
936 has_cargo_binstall: false,
937 has_curl: false,
938 has_wget: false,
939 remote_home: "/home/ubuntu".into(),
940 machine_id: None,
941 }),
942 resources: None,
943 error: None,
944 };
945 let display = probe_to_display_info(&probe, &HashSet::new());
946 assert_eq!(display.username, "ubuntu");
947
948 let probe_root = HostProbeResult {
950 host_name: "test".into(),
951 reachable: true,
952 connection_time_ms: 50,
953 cass_status: CassStatus::NotFound,
954 detected_agents: vec![],
955 system_info: Some(SystemInfo {
956 os: "Linux".into(),
957 arch: "x86_64".into(),
958 distro: None,
959 has_cargo: false,
960 has_cargo_binstall: false,
961 has_curl: false,
962 has_wget: false,
963 remote_home: "/".into(),
964 machine_id: None,
965 }),
966 resources: None,
967 error: None,
968 };
969 let display_root = probe_to_display_info(&probe_root, &HashSet::new());
970 assert_eq!(display_root.username, "user");
971
972 let probe_empty = HostProbeResult {
974 host_name: "test".into(),
975 reachable: true,
976 connection_time_ms: 50,
977 cass_status: CassStatus::NotFound,
978 detected_agents: vec![],
979 system_info: Some(SystemInfo {
980 os: "Linux".into(),
981 arch: "x86_64".into(),
982 distro: None,
983 has_cargo: false,
984 has_cargo_binstall: false,
985 has_curl: false,
986 has_wget: false,
987 remote_home: "".into(),
988 machine_id: None,
989 }),
990 resources: None,
991 error: None,
992 };
993 let display_empty = probe_to_display_info(&probe_empty, &HashSet::new());
994 assert_eq!(display_empty.username, "user");
995 }
996
997 #[test]
998 fn test_probe_to_display_info_empty_distro_fallback() {
999 use super::super::probe::SystemInfo;
1000
1001 let probe = HostProbeResult {
1003 host_name: "test".into(),
1004 reachable: true,
1005 connection_time_ms: 50,
1006 cass_status: CassStatus::NotFound,
1007 detected_agents: vec![],
1008 system_info: Some(SystemInfo {
1009 os: "Linux".into(),
1010 arch: "x86_64".into(),
1011 distro: Some("".into()), has_cargo: false,
1013 has_cargo_binstall: false,
1014 has_curl: false,
1015 has_wget: false,
1016 remote_home: "/home/user".into(),
1017 machine_id: None,
1018 }),
1019 resources: None,
1020 error: None,
1021 };
1022 let display = probe_to_display_info(&probe, &HashSet::new());
1023 assert!(display.system_info.as_ref().unwrap().contains("Linux"));
1025 }
1026}