rho-coding-agent 1.6.0

A lightweight agent harness inspired by Pi
Documentation
use std::{sync::Arc, time::Duration};

use rho_sdk::SecretString;
use url::Url;

use super::{ProviderBuildOptions, ProviderBuilder, ProviderCredential};
use crate::{
    credentials::MemoryCredentialStore,
    providers::{openai::auth::Auth, openai_compatible::CompatibleAuth},
    reasoning::ReasoningLevel,
};

#[test]
fn options_reject_invalid_states_and_accept_typed_overrides() {
    assert!(ProviderBuildOptions::new("", "model", ReasoningLevel::Off).is_err());
    assert!(ProviderBuildOptions::new("openai", "", ReasoningLevel::Off).is_err());
    assert!(ProviderBuildOptions::new("unknown", "model", ReasoningLevel::Off).is_err());

    let options = ProviderBuildOptions::new("openai", "model", ReasoningLevel::Low)
        .unwrap()
        .endpoint(Url::parse("https://example.test/v1").unwrap())
        .unwrap()
        .request_timeout(Duration::from_secs(30))
        .unwrap();

    assert_eq!(options.provider(), "openai");
    assert_eq!(options.model(), "model");
    assert!(
        ProviderBuildOptions::new("openai", "model", ReasoningLevel::Off)
            .unwrap()
            .endpoint(Url::parse("file:///tmp/provider").unwrap())
            .is_err()
    );
    assert!(
        ProviderBuildOptions::new("openai", "model", ReasoningLevel::Off)
            .unwrap()
            .request_timeout(Duration::ZERO)
            .is_err()
    );
}

#[test]
fn credentials_are_redacted_and_mismatches_fail_before_execution() {
    let secret = "sk-provider-secret";
    let credential = ProviderCredential::AnthropicApiKey(SecretString::new(secret));
    let debug = format!("{credential:?}");
    assert!(debug.contains("anthropic-api-key"));
    assert!(debug.contains("[REDACTED]"));
    assert!(!debug.contains(secret));

    let options = ProviderBuildOptions::new("openai", "gpt-test", ReasoningLevel::Off).unwrap();
    let error = match ProviderBuilder::new(options, credential).build() {
        Ok(_) => panic!("mismatched credential unexpectedly built a provider"),
        Err(error) => error,
    };
    assert!(error
        .to_string()
        .contains("credential kind does not match provider"));
    assert!(!format!("{error:?}").contains(secret));
}

#[test]
fn explicit_builder_constructs_provider_without_environment_or_keychain_lookup() {
    let options = ProviderBuildOptions::new("openai", "gpt-test", ReasoningLevel::Medium).unwrap();
    let credential = ProviderCredential::OpenAi {
        auth: Auth::ApiKey("explicit-secret".into()),
        refresh_store: Arc::new(MemoryCredentialStore::default()),
    };

    let provider = ProviderBuilder::new(options, credential).build().unwrap();

    assert_eq!(provider.identity().provider, "openai");
    assert_eq!(provider.identity().model, "gpt-test");
}

#[test]
fn explicit_builder_constructs_openrouter_provider() {
    let options = ProviderBuildOptions::new(
        "openrouter",
        "anthropic/claude-sonnet-4",
        ReasoningLevel::Medium,
    )
    .unwrap();
    let credential =
        ProviderCredential::OpenAiCompatible(CompatibleAuth::ApiKey("explicit-secret".into()));

    let provider = ProviderBuilder::new(options, credential).build().unwrap();

    assert_eq!(provider.identity().provider, "openrouter");
    assert_eq!(provider.identity().model, "anthropic/claude-sonnet-4");
}

#[test]
fn public_provider_objects_are_send_sync() {
    fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<Arc<dyn rho_sdk::provider::ModelProvider>>();
}