use clap::Parser;
use pretty_assertions::assert_eq;
use serde_json::json;
use super::*;
use crate::doctor::{DoctorCheck, DoctorCheckId, DoctorStatus};
#[test]
fn json_document_carries_summary_and_sections() {
let report = DoctorReport::from_checks(vec![
DoctorCheck::new(
DoctorCheckId::ProviderAuth {
auth_mode: "openai-api-key".into(),
},
"OpenAI API key",
DoctorStatus::Ok,
"authenticated",
),
DoctorCheck::new(DoctorCheckId::Mcp, "MCP", DoctorStatus::Warn, "degraded")
.with_hint("run /mcp for details"),
]);
let document = serde_json::to_value(DoctorDocument::new(&report)).unwrap();
assert_eq!(
document,
json!({
"summary": { "ok": 1, "info": 0, "warn": 1, "fail": 0, "checking": 0 },
"sections": [
{
"id": "authentication",
"checks": [{
"id": { "kind": "provider_auth", "auth_mode": "openai-api-key" },
"label": "OpenAI API key",
"status": "ok",
"summary": "authenticated"
}]
},
{
"id": "extensions",
"checks": [{
"id": { "kind": "mcp" },
"label": "MCP",
"status": "warn",
"summary": "degraded",
"hint": "run /mcp for details"
}]
}
]
})
);
}
#[tokio::test(start_paused = true)]
async fn collect_probes_times_out_slow_tasks() {
let fast = tokio::spawn(async { DoctorProbeOutcome::Rtk { available: true } });
let slow = tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(60)).await;
DoctorProbeOutcome::Rtk { available: true }
});
let outcomes = collect_probes(
vec![(DoctorProbeId::Rtk, fast), (DoctorProbeId::Claude, slow)],
Duration::from_secs(1),
)
.await;
assert!(matches!(
outcomes[0],
DoctorProbeOutcome::Rtk { available: true }
));
assert!(matches!(
outcomes[1],
DoctorProbeOutcome::TimedOut(DoctorProbeId::Claude)
));
}
#[test]
fn provider_override_with_empty_cache_still_selects_the_host() {
let mut config = crate::config::Config::default();
let cli = Cli::try_parse_from(["rho", "--provider", "ollama", "doctor"]).unwrap();
apply_doctor_overrides(&mut config, &cli).unwrap();
assert_eq!(config.provider, "ollama");
}
#[test]
fn unknown_provider_override_still_errors() {
let mut config = crate::config::Config::default();
let cli = Cli::try_parse_from(["rho", "--provider", "not-a-real-host", "doctor"]).unwrap();
let err = apply_doctor_overrides(&mut config, &cli).unwrap_err();
assert!(
err.to_string().contains("unknown provider"),
"expected unknown provider, got {err:#}"
);
}
fn model_aliases(pairs: &[(&str, &str)]) -> crate::model_aliases::ModelAliases {
crate::model_aliases::ModelAliases::from_entries(
pairs
.iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect(),
)
.unwrap()
}
#[test]
fn doctor_overrides_reject_errors_that_run_rejects() {
for (name, args, aliases, raw_model) in [
(
"unknown model",
&["rho", "--model", "definitely-not-a-model", "doctor"] as &[&str],
&[] as &[(&str, &str)],
"definitely-not-a-model",
),
(
"undefined alias",
&["rho", "--model", "@missing", "doctor"],
&[],
"@missing",
),
(
"provider conflicts with alias host",
&["rho", "--provider", "openai", "--model", "@other", "doctor"],
&[("other", "anthropic/claude-sonnet-4-5")],
"@other",
),
] {
let mut config = crate::config::Config {
model_aliases: model_aliases(aliases),
..crate::config::Config::default()
};
let cli = Cli::try_parse_from(args).unwrap();
assert!(
apply_doctor_overrides(&mut config, &cli).is_err(),
"{name} must fail"
);
assert_ne!(
config.model, raw_model,
"{name} must not store the raw --model flag"
);
}
}