rho-coding-agent 1.19.2

A lightweight agent harness inspired by Pi
Documentation
use pretty_assertions::assert_eq;
use rho_providers::{
    credentials::{
        save_openrouter_oauth_key, save_provider_api_key, CredentialStore, MemoryCredentialStore,
    },
    model::catalog,
    provider::XAI_TOKENS_ACCOUNT,
};

use super::{
    auth_mode_picker, login_group_next, login_group_picker, login_method_picker,
    refresh_model_list_picker, LoginGroupNext, ALL_REFRESHABLE_PROVIDERS,
};

#[test]
fn login_picker_lists_poolside() {
    let picker = login_group_picker();
    let poolside = picker
        .items
        .iter()
        .find(|item| item.value == "poolside")
        .expect("Poolside should be available for login");

    assert_eq!(poolside.label, "Poolside");
}

#[test]
fn login_picker_lists_ollama_cloud() {
    let picker = login_group_picker();
    let ollama_cloud = picker
        .items
        .iter()
        .find(|item| item.value == "ollama-cloud")
        .expect("Ollama Cloud should be available for login");

    assert_eq!(ollama_cloud.label, "Ollama Cloud");

    let methods = login_method_picker(
        catalog::login_group("ollama-cloud").expect("Ollama Cloud login group"),
    );
    let values = methods
        .items
        .iter()
        .map(|item| item.value.as_str())
        .collect::<Vec<_>>();
    assert_eq!(values, vec!["ollama-cloud-api-key", "ollama-cloud-device"]);
}

#[test]
fn auth_mode_picker_lists_only_usable_modes_and_marks_active_mode() {
    let store = MemoryCredentialStore::default();
    save_provider_api_key(&store, "openrouter-api-key", "api-secret").unwrap();

    let picker = auth_mode_picker(&store, "openrouter", "openrouter-api-key").unwrap();
    assert_eq!(picker.items.len(), 1);
    assert_eq!(picker.items[0].value, "openrouter-api-key");
    assert_eq!(
        picker.items[0]
            .badge
            .as_ref()
            .map(|badge| badge.text.as_str()),
        Some("active")
    );

    save_openrouter_oauth_key(&store, "oauth-secret").unwrap();
    let picker = auth_mode_picker(&store, "openrouter", "openrouter-api-key").unwrap();
    let modes = picker
        .items
        .iter()
        .map(|item| {
            (
                item.value.as_str(),
                item.badge.as_ref().map(|badge| badge.text.as_str()),
            )
        })
        .collect::<Vec<_>>();

    assert_eq!(
        modes,
        vec![
            ("openrouter-api-key", Some("active")),
            ("openrouter-oauth", None),
        ]
    );
}

#[test]
fn auth_mode_picker_keeps_valid_mode_when_sibling_credentials_are_invalid() {
    let store = MemoryCredentialStore::default();
    save_provider_api_key(&store, "xai-api-key", "api-secret").unwrap();
    store.set_secret(XAI_TOKENS_ACCOUNT, "not-json").unwrap();

    let picker = auth_mode_picker(&store, "xai", "xai-oauth").unwrap();
    let modes = picker
        .items
        .iter()
        .map(|item| item.value.as_str())
        .collect::<Vec<_>>();

    assert_eq!(modes, vec!["xai-api-key"]);
}

#[test]
fn refresh_picker_lists_all_and_available_refreshable_providers() {
    let picker = refresh_model_list_picker(&[
        "api-key".into(),
        "anthropic-api-key".into(),
        "xai-api-key".into(),
    ]);
    let values = picker
        .items
        .iter()
        .map(|item| item.value.as_str())
        .collect::<Vec<_>>();

    assert_eq!(
        values,
        vec![ALL_REFRESHABLE_PROVIDERS, "anthropic", "openai"]
    );
}

#[test]
fn login_picker_lists_providers_alphabetically_by_label() {
    let picker = login_group_picker();
    let labels = picker
        .items
        .iter()
        .map(|item| item.label.as_str())
        .collect::<Vec<_>>();
    assert!(labels
        .windows(2)
        .all(|pair| { pair[0].to_ascii_lowercase() <= pair[1].to_ascii_lowercase() }));
}

#[test]
fn refresh_picker_lists_openrouter_once_for_multiple_auth_modes() {
    let picker =
        refresh_model_list_picker(&["openrouter-api-key".into(), "openrouter-oauth".into()]);
    let openrouter = picker
        .items
        .iter()
        .filter(|item| item.label == "OpenRouter")
        .map(|item| (item.value.as_str(), item.detail.as_deref()))
        .collect::<Vec<_>>();

    assert_eq!(
        openrouter,
        vec![("openrouter", Some("Refresh cached OpenRouter models."),)]
    );
}

#[test]
fn refresh_picker_always_offers_all_configured_providers() {
    let picker = refresh_model_list_picker(&[]);

    assert_eq!(picker.items.len(), 1);
    assert_eq!(picker.items[0].value, ALL_REFRESHABLE_PROVIDERS);
}

#[test]
fn login_group_picker_keeps_claude_code_out_of_top_level() {
    let picker = login_group_picker();
    assert!(
        picker.items.iter().all(|item| item.value != "claude-code"),
        "claude-code belongs under Anthropic methods, not as its own group: {:?}",
        picker
            .items
            .iter()
            .map(|item| item.value.as_str())
            .collect::<Vec<_>>()
    );
    assert!(
        picker.items.iter().any(|item| item.value == "anthropic"),
        "Anthropic group should remain available"
    );
    assert!(
        picker.items.iter().all(|item| {
            let label = item.label.to_ascii_lowercase();
            !label.contains("external runtime")
                && item.section.as_deref() != Some("External runtimes")
        }),
        "login groups must not expose a standalone external-runtimes section: {:?}",
        picker
            .items
            .iter()
            .map(|item| (item.section.as_deref(), item.label.as_str()))
            .collect::<Vec<_>>()
    );
}

#[test]
fn anthropic_login_methods_include_claude_code_for_delegation() {
    let picker = login_method_picker(catalog::login_group("anthropic").expect("anthropic group"));
    let values = picker
        .items
        .iter()
        .map(|item| item.value.as_str())
        .collect::<Vec<_>>();
    assert_eq!(values, vec!["anthropic-api-key", "claude-code"]);

    let api_key = &picker.items[0];
    assert_eq!(api_key.label, "API Key");
    assert_eq!(api_key.value, "anthropic-api-key");

    let claude = &picker.items[1];
    assert_eq!(claude.label, "Claude Code (delegation only)");
    assert_eq!(claude.value, "claude-code");
    let detail = claude
        .detail
        .as_deref()
        .expect("claude code method should explain ownership");
    assert!(
        detail.contains("External Claude binary") || detail.contains("subscription"),
        "detail should mark the external/subscription runtime: {detail}"
    );
    assert!(
        detail.contains("not Anthropic API billing"),
        "detail should distinguish Claude Code from Anthropic API billing: {detail}"
    );
    assert!(
        detail.contains("managed by Claude Code") && detail.contains("not Rho"),
        "detail should say Claude Code owns credentials, not Rho: {detail}"
    );
}

#[test]
fn login_group_next_opens_anthropic_methods_including_claude_code() {
    match login_group_next(catalog::login_group("anthropic").expect("anthropic group")) {
        LoginGroupNext::MethodPicker(picker) => {
            assert_eq!(picker.title, "select Anthropic login method");
            let values = picker
                .items
                .iter()
                .map(|item| item.value.as_str())
                .collect::<Vec<_>>();
            assert_eq!(values, vec!["anthropic-api-key", "claude-code"]);
        }
        LoginGroupNext::Provider(provider) => {
            panic!("Anthropic should open a method picker, got direct provider {provider}")
        }
    }
}

#[test]
fn login_group_next_keeps_single_catalog_method_direct() {
    match login_group_next(catalog::login_group("poolside").expect("poolside group")) {
        LoginGroupNext::Provider(provider) => assert_eq!(provider, "poolside-api-key"),
        LoginGroupNext::MethodPicker(picker) => panic!(
            "single-method groups should stay direct, got {:?}",
            picker
                .items
                .iter()
                .map(|item| item.value.as_str())
                .collect::<Vec<_>>()
        ),
    }
}