akribes-sdk 0.22.1

Rust client SDK for the Akribes workflow server
Documentation
//! Integration tests for the `ExecutionsClient` sub-client read paths that
//! aren't already covered elsewhere: the per-task cost/token breakdown
//! (`GET /executions/{id}/tasks`) and the document-markdown read
//! (`GET /documents/{id}/markdown`).
//!
//! Each test pins the request path against the corresponding akribes-server
//! route and asserts the SDK's decode behaviour, including the failure modes
//! (404 → `None` for `tasks`; malformed response → error for
//! `get_document_markdown`).

use akribes_sdk::{AkribesClient, AkribesError};
use mockito::Server;

fn make_client(server: &Server) -> AkribesClient {
    AkribesClient::builder(server.url())
        .name("exec-test")
        .id("exec-id")
        .build()
}

// ── tasks() ──────────────────────────────────────────────────────────────────

#[tokio::test]
async fn tasks_hits_route_and_parses_envelope() {
    let mut server = Server::new_async().await;
    let _m = server
        .mock("GET", "/executions/exec_1/tasks")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(
            r#"{
                "execution_id": "exec_1",
                "tasks": [
                    {
                        "task_name": "classify",
                        "model": "gpt-5.4",
                        "provider": "openai",
                        "input_tokens": 120,
                        "output_tokens": 45,
                        "cached_input_tokens": 10,
                        "cache_write_input_tokens": 0,
                        "cost_usd": 0.0021,
                        "duration_ms": 812,
                        "attempt": 1,
                        "finished_at": "2026-01-01T00:00:00Z"
                    }
                ]
            }"#,
        )
        .create_async()
        .await;

    let resp = make_client(&server)
        .executions()
        .tasks("exec_1")
        .await
        .unwrap()
        .expect("200 should yield Some");
    assert_eq!(resp.execution_id, "exec_1");
    assert_eq!(resp.tasks.len(), 1);
    let t = &resp.tasks[0];
    assert_eq!(t.task_name, "classify");
    assert_eq!(t.model.as_deref(), Some("gpt-5.4"));
    assert_eq!(t.provider.as_deref(), Some("openai"));
    assert_eq!(t.input_tokens, 120);
    assert_eq!(t.output_tokens, 45);
    assert_eq!(t.cached_input_tokens, 10);
    assert_eq!(t.cache_write_input_tokens, 0);
    assert_eq!(t.cost_usd, Some(0.0021));
    assert_eq!(t.duration_ms, Some(812));
    assert_eq!(t.attempt, 1);
    assert_eq!(t.finished_at, "2026-01-01T00:00:00Z");
}

#[tokio::test]
async fn tasks_404_yields_none() {
    let mut server = Server::new_async().await;
    let _m = server
        .mock("GET", "/executions/missing/tasks")
        .with_status(404)
        .create_async()
        .await;

    let resp = make_client(&server)
        .executions()
        .tasks("missing")
        .await
        .unwrap();
    assert!(resp.is_none(), "404 should map to Ok(None)");
}

// ── get_document_markdown() ───────────────────────────────────────────────────

#[tokio::test]
async fn document_markdown_returns_string() {
    let mut server = Server::new_async().await;
    let _m = server
        .mock("GET", "/documents/doc_1/markdown")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(r##"{"markdown":"# Title body"}"##)
        .create_async()
        .await;

    let md = make_client(&server)
        .executions()
        .get_document_markdown("doc_1")
        .await
        .unwrap();
    assert_eq!(md, "# Title body");
}

/// A missing `markdown` field is a server-contract violation, not an "empty
/// document". The SDK must surface it rather than silently returning "".
#[tokio::test]
async fn document_markdown_missing_field_errors() {
    let mut server = Server::new_async().await;
    let _m = server
        .mock("GET", "/documents/doc_2/markdown")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(r#"{"not_markdown":"oops"}"#)
        .create_async()
        .await;

    let err = make_client(&server)
        .executions()
        .get_document_markdown("doc_2")
        .await
        .expect_err("missing markdown field should error");
    match err {
        AkribesError::Other(msg) => {
            assert!(msg.contains("doc_2"), "error should mention doc id: {msg}");
            assert!(
                msg.contains("malformed"),
                "error should describe the contract violation: {msg}"
            );
        }
        other => panic!("expected AkribesError::Other, got {other:?}"),
    }
}

/// A non-string `markdown` field (e.g. `null`) is equally malformed.
#[tokio::test]
async fn document_markdown_wrong_type_errors() {
    let mut server = Server::new_async().await;
    let _m = server
        .mock("GET", "/documents/doc_3/markdown")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(r#"{"markdown":null}"#)
        .create_async()
        .await;

    let err = make_client(&server)
        .executions()
        .get_document_markdown("doc_3")
        .await
        .expect_err("non-string markdown field should error");
    assert!(matches!(err, AkribesError::Other(_)));
}