skailar 0.0.1

Official Rust SDK for the Skailar API
Documentation
mod common;

use common::*;
use skailar::{ChatCompletionRequest, ChatMessage, Error};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

async fn trigger_get_error(status: u16, body: serde_json::Value) -> Error {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/models"))
        .respond_with(ResponseTemplate::new(status).set_body_json(body))
        .mount(&server)
        .await;
    client(&server).models().list().await.unwrap_err()
}

#[tokio::test]
async fn maps_401_to_auth_error() {
    let err = trigger_get_error(401, sample_error("invalid_api_key", "bad key")).await;
    let api = err.as_api().expect("api error");
    assert!(api.is_auth());
    assert_eq!(api.code.as_deref(), Some("invalid_api_key"));
    assert_eq!(api.message, "bad key");
}

#[tokio::test]
async fn maps_404_to_not_found() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/models/missing"))
        .respond_with(
            ResponseTemplate::new(404).set_body_json(sample_error("not_found", "no such model")),
        )
        .mount(&server)
        .await;

    let err = client(&server)
        .models()
        .retrieve("missing")
        .await
        .unwrap_err();
    assert!(err.as_api().unwrap().is_not_found());
}

#[tokio::test]
async fn maps_400_to_bad_request() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/chat/completions"))
        .respond_with(
            ResponseTemplate::new(400).set_body_json(sample_error("bad_request", "missing model")),
        )
        .mount(&server)
        .await;

    let err = client(&server)
        .chat()
        .completions()
        .create(
            ChatCompletionRequest::builder()
                .model("m")
                .message(ChatMessage::user("hi"))
                .build()
                .unwrap(),
        )
        .await
        .unwrap_err();
    assert!(err.as_api().unwrap().is_bad_request());
}

#[tokio::test]
async fn maps_500_to_upstream() {
    let err = trigger_get_error(503, sample_error("upstream_error", "provider down")).await;
    assert!(err.as_api().unwrap().is_upstream());
}

#[tokio::test]
async fn rate_limit_exposes_retry_after() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/models"))
        .respond_with(
            ResponseTemplate::new(429)
                .insert_header("retry-after", "7")
                .set_body_json(sample_error("rate_limited", "slow down")),
        )
        .mount(&server)
        .await;

    let err = client(&server).models().list().await.unwrap_err();
    let api = err.as_api().unwrap();
    assert!(api.is_rate_limit());
    assert_eq!(api.retry_after, Some(7));
}

#[tokio::test]
async fn captures_request_id_header() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/models"))
        .respond_with(
            ResponseTemplate::new(500)
                .insert_header("x-request-id", "req_abc")
                .set_body_json(sample_error("upstream_error", "boom")),
        )
        .mount(&server)
        .await;

    let err = client(&server).models().list().await.unwrap_err();
    assert_eq!(err.as_api().unwrap().request_id.as_deref(), Some("req_abc"));
}

#[tokio::test]
async fn tolerates_non_json_error_body() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/models"))
        .respond_with(ResponseTemplate::new(502).set_body_string("upstream timeout"))
        .mount(&server)
        .await;

    let err = client(&server).models().list().await.unwrap_err();
    let api = err.as_api().unwrap();
    assert_eq!(api.status, 502);
    assert_eq!(api.message, "upstream timeout");
}

#[tokio::test]
async fn raw_body_is_preserved() {
    let err = trigger_get_error(400, sample_error("bad_request", "nope")).await;
    let api = err.as_api().unwrap();
    let raw = api.raw.as_ref().expect("raw body");
    assert_eq!(raw["error"]["type"], "bad_request");
}

#[tokio::test]
async fn error_display_includes_status_and_message() {
    let err = trigger_get_error(401, sample_error("invalid_api_key", "bad key")).await;
    let rendered = err.to_string();
    assert!(rendered.contains("401"));
    assert!(rendered.contains("bad key"));
}