use pretty_assertions::assert_eq;
use rho_providers::{model::provider_models::ProviderModelHealth, provider};
use super::*;
#[test]
fn plan_follows_gate_and_selected_or_configured_hosts() {
let config = Config::default();
assert!(plan_probes(&config, &config.provider, DoctorProbeGate::Disabled).is_empty());
assert_eq!(
plan_probes(&config, &config.provider, DoctorProbeGate::Live),
vec![DoctorProbeId::Claude, DoctorProbeId::Rtk]
);
let active = Config {
provider: "ollama".into(),
..Default::default()
};
assert_endpoint_then_binaries(&active, &active.provider, "ollama");
let mut configured = Config::default();
configured
.providers
.set_endpoint("ollama", rho_providers::provider::OLLAMA_API_BASE)
.unwrap();
assert_endpoint_then_binaries(&configured, &configured.provider, "ollama");
let unsaved = Config::default();
assert_endpoint_then_binaries(&unsaved, "ollama", "ollama");
}
fn assert_endpoint_then_binaries(config: &Config, active_provider: &str, provider: &str) {
let probes = plan_probes(config, active_provider, DoctorProbeGate::Live);
let DoctorProbeId::ProviderEndpoint {
provider: planned,
endpoint,
} = &probes[0]
else {
panic!("expected an endpoint probe first, got {:?}", probes[0]);
};
assert_eq!(planned, provider);
assert!(
provider::provider_descriptor(provider)
.is_some_and(|descriptor| descriptor.probes_configured_endpoint()),
"{provider} does not expose a model endpoint"
);
assert_eq!(
config.resolved_provider_endpoint(provider).as_ref(),
Some(endpoint)
);
assert_eq!(&probes[1..], &[DoctorProbeId::Claude, DoctorProbeId::Rtk]);
}
#[test]
fn placeholders_and_results_cover_the_same_rows() {
let ids = |checks: &[DoctorCheck]| {
checks
.iter()
.map(|check| check.id.clone())
.collect::<Vec<_>>()
};
let claude_ids = vec![DoctorCheckId::ClaudeAuth, DoctorCheckId::ClaudeBinary];
let checking = placeholder_checks(&DoctorProbeId::Claude);
assert_eq!(ids(&checking), claude_ids);
assert!(checking
.iter()
.all(|check| check.status == DoctorStatus::Checking && check.summary == "checking"));
let timed_out = probe_checks(
&DoctorProbeOutcome::TimedOut(DoctorProbeId::Claude),
"openai",
);
assert_eq!(ids(&timed_out), claude_ids);
assert!(timed_out
.iter()
.all(|check| check.status == DoctorStatus::Warn && check.summary == "timed out"));
let failed = probe_checks(&DoctorProbeOutcome::Failed(DoctorProbeId::Rtk), "openai");
assert_eq!(ids(&failed), vec![DoctorCheckId::Rtk]);
assert_eq!(
(failed[0].status, failed[0].summary.as_str()),
(DoctorStatus::Warn, "probe failed")
);
}
fn ollama_endpoint_id() -> DoctorProbeId {
DoctorProbeId::ProviderEndpoint {
provider: "ollama".into(),
endpoint: rho_providers::provider::OLLAMA_API_BASE.parse().unwrap(),
}
}
#[test]
fn timed_out_or_failed_active_endpoint_fails_ci() {
let id = ollama_endpoint_id();
for (name, outcome, summary) in [
(
"active timeout",
DoctorProbeOutcome::TimedOut(id.clone()),
"timed out",
),
(
"active failure",
DoctorProbeOutcome::Failed(id.clone()),
"probe failed",
),
] {
let checks = probe_checks(&outcome, "ollama");
assert_eq!(
(checks[0].status, checks[0].summary.as_str()),
(DoctorStatus::Fail, summary),
"{name}"
);
}
let unused = probe_checks(
&DoctorProbeOutcome::TimedOut(ollama_endpoint_id()),
"openai",
);
assert_eq!(unused[0].status, DoctorStatus::Info);
}
#[test]
fn outcomes_map_to_rows() {
let rtk = probe_checks(&DoctorProbeOutcome::Rtk { available: false }, "openai");
assert_eq!(
rtk,
vec![
DoctorCheck::new(DoctorCheckId::Rtk, "rtk", DoctorStatus::Info, "unavailable")
.with_hint("optional shell-command rewriting helper")
]
);
let endpoint = probe_checks(
&DoctorProbeOutcome::ProviderEndpoint {
provider: "ollama".into(),
health: ProviderModelHealth::ReachableWithModels { model_count: 1 },
},
"ollama",
);
assert_eq!(
endpoint,
vec![DoctorCheck::new(
DoctorCheckId::ProviderEndpoint {
provider: "ollama".into()
},
"Ollama connection",
DoctorStatus::Ok,
"reachable, 1 model",
)]
);
}
#[cfg(unix)]
#[tokio::test]
async fn hung_rtk_probe_is_killed_within_budget() {
let mut command = tokio::process::Command::new("sh");
command.args(["-c", "sleep 30"]);
let started = std::time::Instant::now();
assert!(!super::probe_rtk_command(command).await);
assert!(
started.elapsed() < std::time::Duration::from_secs(4),
"hung probe must not exceed the 2s budget by much, elapsed {:?}",
started.elapsed()
);
}