rho-coding-agent 2.4.0

A fast Rust agent harness with a small footprint and opinionated defaults
use clap::Parser;
use pretty_assertions::assert_eq;
use serde_json::json;

use super::*;
use crate::doctor::{DoctorCheck, DoctorCheckId, DoctorStatus};

// Covers: `rho doctor --json` emits summary counts plus sections with
// snake_case status and tagged check ids.
// Owner: CLI (pure serialization)
#[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"
                    }]
                }
            ]
        })
    );
}

// Covers: a probe that outlives the deadline is aborted and reported as timed
// out; finished probes keep their results.
// Owner: CLI (paused tokio clock, no child processes)
#[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)
    ));
}

// Covers: `rho --provider ollama doctor` must print a report even when the
// model cache is empty; catalog resolution cannot abort the command.
// Owner: CLI (pure selection)
#[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");
}

// Covers: unknown --provider still fails instead of producing a silent report.
// Owner: CLI (pure selection)
#[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()
}

// Covers: doctor must reject the same override errors as `rho run` instead
// of storing the raw `--model` string. Empty cache is the only exception.
// Owner: CLI (pure selection)
#[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"
        );
    }
}