malvin 0.2.5

Non-interactive research and coding agent
use crate::acp::AuthError;

pub fn ensure_pi_authenticated(model: &str) -> Result<(), AuthError> {
    let parsed = crate::model_id::parse_model_id(model).map_err(AuthError)?;
    let Some((provider, _)) = parsed.pi_provider_and_model() else {
        return Err(AuthError(format!(
            "pi model id must be `pi:<provider>/<model>` (got `{model}`)"
        )));
    };
    if provider_has_access(provider) {
        return Ok(());
    }
    provider_auth_env_keys(provider).map_or_else(
        || {
            Err(AuthError(format!(
                "pi backend is not authenticated for provider `{provider}`. Store credentials in Pi’s auth file ({}).",
                pi::sdk::Config::auth_path().display()
            )))
        },
        |keys| {
            Err(AuthError(format!(
                "pi backend is not authenticated for provider `{provider}`. Set {} or store credentials in Pi’s auth file ({}).",
                keys.join(" or "),
                pi::sdk::Config::auth_path().display()
            )))
        },
    )
}

pub fn is_provider_authenticated(provider: &str) -> bool {
    provider_has_access(provider)
}

fn provider_has_access(provider: &str) -> bool {
    match provider_auth_env_keys(provider) {
        None => stored_credential_present(provider),
        Some(keys) if keys.iter().any(|k| crate::acp::env_key_nonempty(k)) => true,
        Some(_) => stored_credential_present(provider),
    }
}

fn stored_credential_present(provider: &str) -> bool {
    let Ok(auth) = pi::auth::AuthStorage::load(pi::sdk::Config::auth_path()) else {
        return false;
    };
    !matches!(
        auth.credential_status(provider),
        pi::auth::CredentialStatus::Missing
    ) || auth.has_stored_credential(provider)
}

#[must_use]
pub(crate) fn provider_auth_env_keys(provider: &str) -> Option<&'static [&'static str]> {
    let keys = pi::provider_metadata::provider_auth_env_keys(provider);
    if keys.is_empty() { None } else { Some(keys) }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn mapped_provider_requires_key() {
        crate::acp::with_env("OPENAI_API_KEY", None, || {
            if !stored_credential_present("openai") {
                assert!(ensure_pi_authenticated("pi:openai/gpt-4o").is_err());
            }
        });
    }

    #[test]
    fn unknown_provider_requires_stored_credential() {
        crate::acp::with_env("OPENAI_API_KEY", None, || {
            assert!(!is_provider_authenticated("some-unknown"));
            let err = ensure_pi_authenticated("pi:some-unknown/foo").expect_err("must fail");
            assert!(err.0.contains("some-unknown"));
        });
    }

    #[test]
    fn finish_after_channel_closed_maps_dropped_reply_to_error() {
        let err = crate::pi_sdk::session::finish_after_channel_closed(Err(
            "pi sdk runtime stopped".into(),
        ))
        .expect_err("dropped reply must fail");
        assert!(err.message.contains("runtime stopped"));
    }

    #[test]
    fn is_provider_authenticated_checks_known_and_unknown_providers() {
        crate::acp::with_env("OPENAI_API_KEY", None, || {
            if !stored_credential_present("openai") {
                assert!(!is_provider_authenticated("openai"));
            }
        });
        crate::acp::with_env("OPENAI_API_KEY", Some("test-key"), || {
            assert!(is_provider_authenticated("openai"));
        });
        assert!(!is_provider_authenticated("some-unknown"));
    }

    #[test]
    fn is_provider_authenticated_checks_primary_and_secondary() {
        crate::acp::with_env("DEEPSEEK_API_KEY", Some("test-key"), || {
            assert!(is_provider_authenticated("deepseek"));
            assert!(is_provider_authenticated("deep-seek"));
        });
        crate::acp::with_env("DEEPSEEK_API_KEY", None, || {
            if !stored_credential_present("deepseek") && !stored_credential_present("deep-seek") {
                assert!(!is_provider_authenticated("deepseek"));
            }
        });
    }
}