rho-coding-agent 1.16.0

A lightweight agent harness inspired by Pi
Documentation
use tempfile::TempDir;

use {super::*, crate::config::Config};

#[test]
fn formats_agent_metadata_with_prompt_extension() {
    let home = TempDir::new().unwrap();
    let cwd = TempDir::new().unwrap();
    let directory = home.path().join(".rho/agents");
    std::fs::create_dir_all(&directory).unwrap();
    std::fs::write(
        directory.join("release-reviewer.md"),
        "---\ndescription: Reviews releases before deployment.\nprompt: extend\nmodel-policy: require\nprovider: anthropic\nmodel: claude-sonnet\nreasoning: high\ntools: [read_file, bash]\n---\nSECRET PROMPT BODY\n",
    )
    .unwrap();

    let catalog = AgentCatalog::discover_with_home(cwd.path(), Some(home.path())).unwrap();
    let picker = agent_picker(catalog, AgentModelView::from(&Config::default()));
    let item = picker
        .items
        .iter()
        .find(|item| item.value == "release-reviewer")
        .unwrap();
    let detail = item.detail.as_deref().unwrap();

    assert_eq!(picker.layout, PickerLayout::Overlay);
    assert!(picker.is_overlay());
    let chrome = picker.overlay_chrome.as_ref().unwrap();
    assert_eq!(chrome.nav_label, " AGENTS");
    assert_eq!(chrome.detail_label.as_deref(), Some(" DETAILS"));
    assert_eq!(chrome.nav_keys_hint, "↑↓ agents");
    assert!(detail.contains("Reviews releases before deployment."));
    assert!(detail.contains("~/.rho/agents"));
    assert!(detail.contains("require anthropic/claude-sonnet"));
    assert!(detail.contains("high"));
    assert!(detail.contains("Runtime\nrho"));
    assert!(detail.contains("bash, read_file"));
    assert!(
        !detail.contains("Inherit Claude config"),
        "rho agents omit Claude config inheritance: {detail}"
    );
    assert!(detail.contains("extend system prompt"));
    assert!(detail.contains("Prompt extension"));
    assert!(detail.contains("SECRET PROMPT BODY"));
}

#[test]
fn marks_internal_agents_as_reserved() {
    let root = TempDir::new().unwrap();
    let catalog = AgentCatalog::discover_with_home(root.path(), None).unwrap();

    let picker = agent_picker(catalog, AgentModelView::from(&Config::default()));
    let internal_items = picker
        .items
        .iter()
        .filter(|item| matches!(item.value.as_str(), "goal-judge" | "session-title"))
        .collect::<Vec<_>>();

    assert_eq!(internal_items.len(), 2);
    for item in internal_items {
        let badge = item.badge.as_ref().unwrap();
        assert_eq!(badge.text, "(internal)");
        assert_eq!(badge.tone, PickerBadgeTone::Internal);
        let detail = item.detail.as_deref().unwrap();
        assert!(detail.contains("Source\ninternal"));
        assert!(detail.contains("reserved; cannot be overridden or delegated"));
        assert!(detail.contains("Model\nopenai/gpt-5.5"));
        assert!(detail.contains("Model source: conversation fallback"));
        assert!(detail.contains("Reasoning\nlow"));
        assert!(detail.contains("Tools\nnone"));
    }
}

#[test]
fn internal_agent_detail_shows_override_and_source() {
    let root = TempDir::new().unwrap();
    let catalog = AgentCatalog::discover_with_home(root.path(), None).unwrap();
    let mut config = Config::default();
    config.set_internal_agent_model(
        "goal-judge",
        "anthropic".into(),
        "claude-haiku-4-5".into(),
        "anthropic-api-key".into(),
    );

    let picker = agent_picker(catalog, AgentModelView::from(&config));
    let detail = picker
        .items
        .iter()
        .find(|item| item.value == "goal-judge")
        .unwrap()
        .detail
        .as_deref()
        .unwrap();

    assert!(detail.contains("Model\nanthropic/claude-haiku-4-5"));
    assert!(detail.contains("Model source: override"));
}

#[test]
fn internal_agent_model_picker_starts_with_conversation_choice() {
    let picker = crate::tui::model_picker::internal_agent_model_picker(
        "goal-judge",
        "openai",
        "gpt-5.5",
        true,
        &[],
        &[],
    );

    assert_eq!(picker.action, PickerAction::SelectInternalAgentModel);
    assert_eq!(picker.items[0].label, "Use conversation model");
    assert_eq!(
        picker.items[0].value,
        crate::tui::model_picker::USE_CONVERSATION_MODEL
    );
    assert_eq!(picker.selected, 0);
}

#[test]
fn unavailable_internal_agent_override_keeps_conversation_choice_selectable() {
    let picker = crate::tui::model_picker::internal_agent_model_picker(
        "goal-judge",
        "anthropic",
        "unavailable-model",
        false,
        &[],
        &[],
    );

    assert_eq!(picker.items.len(), 1);
    assert_eq!(picker.selected, 0);
    assert_eq!(
        picker.selected_item().unwrap().value,
        crate::tui::model_picker::USE_CONVERSATION_MODEL
    );
}