use akribes_sdk::{AkribesClient, AkribesError};
use mockito::{Matcher, Server};
fn make_client(server: &Server) -> AkribesClient {
AkribesClient::builder(server.url())
.project_id(1)
.name("resolve-test")
.id("resolve-id")
.build()
}
#[tokio::test]
async fn projects_resolve_numeric_hits_get_by_id() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/10")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"id":10,"name":"Alpha","created_at":"2024-01-01T00:00:00Z"}"#)
.create_async()
.await;
let p = make_client(&server)
.projects()
.resolve("10")
.await
.unwrap()
.expect("resolved");
assert_eq!(p.id, 10);
assert_eq!(p.name, "Alpha");
}
#[tokio::test]
async fn projects_resolve_name_lists_and_filters() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"[{"id":10,"name":"Alpha","created_at":"2024-01-01T00:00:00Z"},
{"id":11,"name":"Beta","created_at":"2024-01-01T00:00:00Z"}]"#,
)
.create_async()
.await;
let p = make_client(&server)
.projects()
.resolve("Beta")
.await
.unwrap()
.expect("resolved");
assert_eq!(p.id, 11);
}
#[tokio::test]
async fn projects_resolve_name_no_match_is_none() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"[{"id":10,"name":"Alpha","created_at":"2024-01-01T00:00:00Z"}]"#)
.create_async()
.await;
assert!(
make_client(&server)
.projects()
.resolve("Gamma")
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn projects_resolve_numeric_missing_is_none() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/999")
.with_status(404)
.create_async()
.await;
assert!(
make_client(&server)
.projects()
.resolve("999")
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn scripts_resolve_name_uses_get_by_name() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/1/scripts/summarise")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"id":5,"project_id":1,"name":"summarise","created_at":"2024-01-01T00:00:00Z"}"#,
)
.create_async()
.await;
let s = make_client(&server)
.project(1)
.scripts()
.resolve("summarise")
.await
.unwrap()
.expect("resolved");
assert_eq!(s.id, 5);
assert_eq!(s.name, "summarise");
}
#[tokio::test]
async fn scripts_resolve_numeric_lists_and_filters_by_id() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/1/scripts")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"[{"id":5,"project_id":1,"name":"summarise","created_at":"2024-01-01T00:00:00Z"},
{"id":6,"project_id":1,"name":"classify","created_at":"2024-01-01T00:00:00Z"}]"#,
)
.create_async()
.await;
let s = make_client(&server)
.project(1)
.scripts()
.resolve("6")
.await
.unwrap()
.expect("resolved");
assert_eq!(s.name, "classify");
}
#[tokio::test]
async fn scripts_resolve_numeric_no_match_is_none() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/1/scripts")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"[{"id":5,"project_id":1,"name":"summarise","created_at":"2024-01-01T00:00:00Z"}]"#,
)
.create_async()
.await;
assert!(
make_client(&server)
.project(1)
.scripts()
.resolve("999")
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn publish_execute_returns_version_and_rebase_summary() {
let mut server = Server::new_async().await;
let _m = server
.mock("POST", "/projects/1/scripts/summarise/publish")
.match_body(Matcher::Json(serde_json::json!({
"channels": ["production"],
"label": "v1",
"published_by": "alice",
})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"version":{"id":42,"script_id":5,"source":"workflow {}","label":"v1",
"published_by":"alice","created_at":"2026-01-01T00:00:00Z"},
"rebased":[{"kind":"bench_case","count":3},{"kind":"judge","count":1}]}"#,
)
.create_async()
.await;
let outcome = make_client(&server)
.project(1)
.versions()
.publish("summarise")
.channels(vec!["production".into()])
.label("v1")
.published_by("alice")
.execute()
.await
.unwrap();
assert_eq!(outcome.version.id, 42);
let rebased = outcome.rebased.expect("rebase summary present");
assert_eq!(rebased.len(), 2);
assert_eq!(rebased[0].kind, "bench_case");
assert_eq!(rebased[0].count, 3);
}
#[tokio::test]
async fn publish_execute_version_only_drops_rebase() {
let mut server = Server::new_async().await;
let _m = server
.mock("POST", "/projects/1/scripts/summarise/publish")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"version":{"id":42,"script_id":5,"source":"workflow {}","label":null,
"published_by":null,"created_at":"2026-01-01T00:00:00Z"}}"#,
)
.create_async()
.await;
let version = make_client(&server)
.project(1)
.versions()
.publish("summarise")
.channels(vec!["dev".into()])
.execute_version_only()
.await
.unwrap();
assert_eq!(version.id, 42);
}
#[tokio::test]
async fn publish_dry_run_sends_dry_run_true_and_parses_breaks() {
let mut server = Server::new_async().await;
let _m = server
.mock("POST", "/projects/1/scripts/summarise/publish")
.match_body(Matcher::PartialJson(serde_json::json!({"dry_run": true})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"dry_run":true,"would_break":1,
"breaking_interests":[{"client_id":"c1","client_name":"puto","channel":"production",
"lifetime":"session","mismatch":{"missing":[["score","Number"]],
"wrong_type":[],"extra":[]}}]}"#,
)
.create_async()
.await;
let result = make_client(&server)
.project(1)
.versions()
.publish("summarise")
.channels(vec!["production".into()])
.execute_dry_run()
.await
.unwrap();
assert!(result.dry_run);
assert_eq!(result.would_break, 1);
assert_eq!(result.breaking_interests.len(), 1);
assert_eq!(result.breaking_interests[0].client_name, "puto");
}
#[tokio::test]
async fn publish_force_flag_serialised() {
let mut server = Server::new_async().await;
let _m = server
.mock("POST", "/projects/1/scripts/summarise/publish")
.match_body(Matcher::PartialJson(serde_json::json!({"force": true})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"version":{"id":43,"script_id":5,"source":"x","label":null,
"published_by":null,"created_at":"2026-01-01T00:00:00Z"}}"#,
)
.create_async()
.await;
make_client(&server)
.project(1)
.versions()
.publish("summarise")
.channels(vec!["production".into()])
.force(true)
.execute()
.await
.unwrap();
}
#[tokio::test]
async fn rate_limit_429_classifies_transient_with_retry_after() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/5")
.with_status(429)
.with_header("retry-after", "12")
.with_body("slow down")
.create_async()
.await;
let err = make_client(&server).projects().get(5).await.unwrap_err();
match err {
AkribesError::Transient {
status,
retry_after,
..
} => {
assert_eq!(status, Some(429));
assert_eq!(retry_after, Some(std::time::Duration::from_secs(12)));
}
other => panic!("expected Transient, got {other:?}"),
}
}
#[tokio::test]
async fn server_500_classifies_transient_with_status() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/5")
.with_status(503)
.with_body("unavailable")
.create_async()
.await;
let err = make_client(&server).projects().get(5).await.unwrap_err();
match err {
AkribesError::Transient { status, .. } => assert_eq!(status, Some(503)),
other => panic!("expected Transient, got {other:?}"),
}
}
#[tokio::test]
async fn unauthorized_401_classifies_fatal() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/5")
.with_status(401)
.with_body("token expired")
.create_async()
.await;
let err = make_client(&server).projects().get(5).await.unwrap_err();
assert!(matches!(err, AkribesError::Fatal { .. }), "got {err:?}");
}
#[tokio::test]
async fn malformed_json_body_surfaces_decode_error_with_snippet() {
let mut server = Server::new_async().await;
let _m = server
.mock("GET", "/projects/5")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"id":"not-a-number","name":42}"#)
.create_async()
.await;
let err = make_client(&server).projects().get(5).await.unwrap_err();
match err {
AkribesError::Other(msg) => {
assert!(
msg.contains("Project"),
"should name the target type: {msg}"
);
assert!(msg.contains("not-a-number"), "should quote body: {msg}");
}
other => panic!("expected Other decode error, got {other:?}"),
}
}
#[tokio::test]
async fn network_failure_surfaces_http_error() {
let client = AkribesClient::builder("http://127.0.0.1:1")
.project_id(1)
.name("net-test")
.build();
let err = client.projects().get(1).await.unwrap_err();
assert!(
matches!(err, AkribesError::Http(_)),
"expected transport error, got {err:?}"
);
}
#[tokio::test]
async fn conflict_409_already_exists_classified_from_structured_body() {
let mut server = Server::new_async().await;
let _m = server
.mock("POST", "/projects/1/scripts/summarise/bench")
.with_status(409)
.with_header("content-type", "application/json")
.with_body(
r#"{"error_type":"suite_already_exists","existing_suite_id":77,
"error":"a bench already exists for this script"}"#,
)
.create_async()
.await;
let req = akribes_sdk::CreateOrUpdateBenchRequest {
judge_script_id: Some(12),
judge_channel: None,
config: None,
};
let err = make_client(&server)
.project(1)
.bench()
.create_or_update("summarise", &req)
.await
.unwrap_err();
match err {
AkribesError::AlreadyExists { existing_id, .. } => assert_eq!(existing_id, 77),
other => panic!("expected AlreadyExists, got {other:?}"),
}
}