rho-coding-agent 1.24.0

A lightweight agent harness inspired by Pi
Documentation
use std::collections::BTreeMap;

use pretty_assertions::assert_eq;

use super::{AliasTarget, ModelAliasResolutionError, ModelAliases, ResolvedModelReference};

fn entries(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
    pairs
        .iter()
        .map(|(name, value)| (name.to_string(), value.to_string()))
        .collect()
}

#[test]
fn parses_provider_qualified_bare_and_slash_qualified_targets() {
    let aliases = ModelAliases::from_entries(entries(&[
        ("deep", "anthropic/claude-opus-4-8"),
        ("fast", "gpt-5.5"),
        ("open", "openrouter/anthropic/claude-sonnet-4"),
    ]))
    .unwrap();

    assert_eq!(
        aliases.get("deep"),
        Some(&AliasTarget {
            provider: Some("anthropic".into()),
            model: "claude-opus-4-8".into(),
        })
    );
    assert_eq!(
        aliases.get("fast"),
        Some(&AliasTarget {
            provider: None,
            model: "gpt-5.5".into(),
        })
    );
    assert_eq!(
        aliases.get("open"),
        Some(&AliasTarget {
            provider: Some("openrouter".into()),
            model: "anthropic/claude-sonnet-4".into(),
        })
    );
    assert_eq!(aliases.get("missing"), None);
}

#[test]
fn resolves_explicit_aliases_with_provenance() {
    let aliases = ModelAliases::from_entries(entries(&[
        ("deep", "anthropic/claude-opus-4-8"),
        ("fast", "gpt-5.5"),
    ]))
    .unwrap();

    assert_eq!(
        aliases.resolve("@deep"),
        Ok(ResolvedModelReference {
            alias: Some("deep".into()),
            provider: Some("anthropic".into()),
            model: "claude-opus-4-8".into(),
        })
    );
    assert_eq!(
        aliases.resolve("@fast"),
        Ok(ResolvedModelReference {
            alias: Some("fast".into()),
            provider: None,
            model: "gpt-5.5".into(),
        })
    );
}

#[test]
fn undefined_explicit_alias_returns_typed_actionable_error() {
    let aliases = ModelAliases::default();
    let error = aliases.resolve("@missing").unwrap_err();

    assert_eq!(
        error,
        ModelAliasResolutionError::UndefinedAlias {
            name: "missing".into(),
        }
    );
    assert_eq!(
        error.to_string(),
        "model alias '@missing' is not defined; define it in [model.aliases] or use a concrete model reference"
    );
}

#[test]
fn ordinary_model_references_stay_concrete_without_alias_resolution() {
    let aliases =
        ModelAliases::from_entries(entries(&[("deep", "anthropic/claude-opus-4-8")])).unwrap();

    for (input, model) in [
        ("deep", "deep"),
        (
            "openrouter/anthropic/claude-sonnet-4",
            "openrouter/anthropic/claude-sonnet-4",
        ),
    ] {
        assert_eq!(
            aliases.resolve(input),
            Ok(ResolvedModelReference {
                alias: None,
                provider: None,
                model: model.into(),
            }),
            "{input}"
        );
    }
}

#[test]
fn concrete_targets_may_share_alias_names_without_becoming_chains() {
    let aliases = ModelAliases::from_entries(entries(&[
        ("deep", "anthropic/claude-opus-4-8"),
        ("deeper", "deep"),
        ("qualified", "openrouter/deep"),
    ]))
    .unwrap();

    assert_eq!(
        aliases.resolve("@deeper"),
        Ok(ResolvedModelReference {
            alias: Some("deeper".into()),
            provider: None,
            model: "deep".into(),
        })
    );
    assert_eq!(
        aliases.resolve("@qualified"),
        Ok(ResolvedModelReference {
            alias: Some("qualified".into()),
            provider: Some("openrouter".into()),
            model: "deep".into(),
        })
    );
}

#[test]
fn rejects_malformed_names_and_values() {
    for (name, value) in [
        ("bad name", "gpt-5.5"),
        ("bad/name", "gpt-5.5"),
        ("bad@name", "gpt-5.5"),
        ("", "gpt-5.5"),
        ("deep", ""),
        ("deep", "anthropic/"),
        ("deep", "/claude-opus-4-8"),
        ("deep", "@other"),
        ("deep", "claude opus"),
    ] {
        let error = ModelAliases::from_entries(entries(&[(name, value)])).unwrap_err();
        assert!(error.contains("model alias"), "{name}={value}: {error}");
    }
}

#[test]
fn deserialization_rejects_alias_reference_values() {
    let error = toml::from_str::<ModelAliases>("deep = \"@other\"\n").unwrap_err();

    assert!(
        error
            .to_string()
            .contains("invalid model alias value '@other'"),
        "{error}"
    );
}