use std::sync::Arc;
use serde::Serialize;
use tokio::sync::{mpsc, oneshot};
use crate::client::{AkribesClient, Inner};
use crate::error::{AkribesError, Result};
use crate::models::*;
use crate::sub::events::{EventSubscription, stream_bench_run_events};
#[derive(Clone, Debug)]
pub struct BenchClient {
pub(crate) inner: Arc<Inner>,
pub(crate) project_id: i64,
}
impl BenchClient {
pub(crate) fn new(inner: Arc<Inner>, project_id: i64) -> Self {
Self { inner, project_id }
}
fn c(&self) -> AkribesClient {
AkribesClient {
inner: Arc::clone(&self.inner),
}
}
fn bench_url(&self, script_name: &str) -> String {
format!(
"{}/projects/{}/scripts/{}/bench",
self.inner.base_url,
self.project_id,
urlencoding::encode(script_name),
)
}
pub async fn list_project_summaries(&self) -> Result<Vec<ProjectBenchSummary>> {
let url = format!(
"{}/projects/{}/benches",
self.inner.base_url, self.project_id
);
self.c().get_list(&url).await
}
pub async fn get(&self, script_name: &str) -> Result<Option<Bench>> {
self.c().get_opt(&self.bench_url(script_name)).await
}
pub async fn create_or_update(
&self,
script_name: &str,
req: &CreateOrUpdateBenchRequest,
) -> Result<Bench> {
self.c().post(&self.bench_url(script_name), req).await
}
pub async fn delete(&self, script_name: &str) -> Result<bool> {
self.c().delete(&self.bench_url(script_name)).await
}
pub async fn get_signature(&self, script_name: &str) -> Result<serde_json::Value> {
let url = format!(
"{}/projects/{}/scripts/{}/signature",
self.inner.base_url,
self.project_id,
urlencoding::encode(script_name),
);
Ok(self
.c()
.get_opt::<serde_json::Value>(&url)
.await?
.unwrap_or(serde_json::json!({})))
}
pub async fn contract_preview(
&self,
script_name: &str,
judge_script_id: i64,
channel: Option<&str>,
) -> Result<serde_json::Value> {
#[derive(Serialize)]
struct Q<'a> {
judge: i64,
#[serde(skip_serializing_if = "Option::is_none")]
channel: Option<&'a str>,
}
let base = format!("{}/contract-preview", self.bench_url(script_name));
let url = AkribesClient::url_with_query(
&base,
&Q {
judge: judge_script_id,
channel,
},
);
Ok(self
.c()
.get_opt::<serde_json::Value>(&url)
.await?
.unwrap_or(serde_json::json!({})))
}
pub async fn list_cases(&self, script_name: &str) -> Result<Vec<BenchCase>> {
let url = format!("{}/cases", self.bench_url(script_name));
self.c().get_list(&url).await
}
pub async fn create_case(
&self,
script_name: &str,
req: &CreateBenchCaseRequest,
) -> Result<BenchCase> {
let url = format!("{}/cases", self.bench_url(script_name));
self.c().post(&url, req).await
}
pub async fn case_contract_drift(&self, script_name: &str) -> Result<DriftReport> {
let url = format!("{}/cases/contract-drift", self.bench_url(script_name));
Ok(self
.c()
.get_opt::<DriftReport>(&url)
.await?
.unwrap_or(DriftReport {
drifted: Vec::new(),
script_version_id: None,
published_at: None,
published_by: None,
summary: String::new(),
}))
}
pub async fn list_runs(
&self,
script_name: &str,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<BenchRun>> {
#[derive(Serialize)]
struct Q {
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
offset: Option<i64>,
}
let base = format!("{}/runs", self.bench_url(script_name));
let url = AkribesClient::url_with_query(&base, &Q { limit, offset });
self.c().get_list(&url).await
}
pub async fn trigger_run(
&self,
script_name: &str,
req: &TriggerBenchRunRequest,
) -> Result<BenchRun> {
let url = format!("{}/runs", self.bench_url(script_name));
self.c().post(&url, req).await
}
}
#[derive(Clone, Debug)]
pub struct BenchRunsClient {
pub(crate) inner: Arc<Inner>,
}
impl BenchRunsClient {
pub(crate) fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
fn c(&self) -> AkribesClient {
AkribesClient {
inner: Arc::clone(&self.inner),
}
}
fn run_url(&self, run_id: i64) -> String {
format!("{}/bench-runs/{}", self.inner.base_url, run_id)
}
pub async fn get(&self, run_id: i64) -> Result<Option<BenchRun>> {
self.c().get_opt(&self.run_url(run_id)).await
}
pub async fn delete(&self, run_id: i64) -> Result<()> {
self.c().delete(&self.run_url(run_id)).await?;
Ok(())
}
pub async fn list_results(&self, run_id: i64) -> Result<Vec<BenchResult>> {
let url = format!("{}/results", self.run_url(run_id));
self.c().get_list(&url).await
}
pub async fn subscribe_run_events(
&self,
run_id: i64,
) -> Result<(mpsc::UnboundedReceiver<BenchRunEvent>, EventSubscription)> {
let (tx, rx) = mpsc::unbounded_channel();
let (ready_tx, ready_rx) = oneshot::channel::<Result<()>>();
let http = self.inner.http.clone();
let token = self.inner.token.clone();
let base_url = self.inner.base_url.clone();
let handle = tokio::spawn(async move {
let _ =
stream_bench_run_events(http, token, base_url, run_id, tx, Some(ready_tx)).await;
});
match ready_rx.await {
Ok(Ok(())) => Ok((rx, EventSubscription::from_handle(handle))),
Ok(Err(e)) => {
handle.abort();
Err(e)
}
Err(_) => {
handle.abort();
Err(AkribesError::Other(
"bench SSE listener died before subscription was confirmed".into(),
))
}
}
}
pub async fn cancel(&self, run_id: i64) -> Result<BenchRun> {
let url = format!("{}/cancel", self.run_url(run_id));
let empty: serde_json::Value = serde_json::json!({});
self.c().post(&url, &empty).await
}
pub async fn tag_session(
&self,
run_id: i64,
mcp_session_id: &str,
) -> Result<BenchRunTagSessionResponse> {
#[derive(Serialize)]
struct Body<'a> {
mcp_session_id: &'a str,
}
let url = format!("{}/tag-session", self.run_url(run_id));
self.c().patch(&url, &Body { mcp_session_id }).await
}
pub async fn promote_execution(
&self,
execution_id: &str,
req: &PromoteExecutionRequest,
) -> Result<BenchCase> {
let url = format!(
"{}/executions/{}/promote-to-case",
self.inner.base_url,
urlencoding::encode(execution_id),
);
self.c().post(&url, req).await
}
pub async fn compare(&self, run_a: i64, run_b: i64) -> Result<CompareReport> {
let url = format!(
"{}/bench-runs/{}/compare/{}",
self.inner.base_url, run_a, run_b,
);
self.c()
.get_opt::<CompareReport>(&url)
.await?
.ok_or_else(|| crate::error::AkribesError::HttpStatus {
status: 404,
message: format!("compare runs {}↔{} returned 404", run_a, run_b),
})
}
pub async fn get_case(&self, case_id: &str) -> Result<serde_json::Value> {
let url = format!(
"{}/executions/{}",
self.inner.base_url,
urlencoding::encode(case_id),
);
Ok(self
.c()
.get_opt::<serde_json::Value>(&url)
.await?
.unwrap_or(serde_json::Value::Null))
}
pub async fn patch_case(
&self,
case_id: &str,
req: &PatchBenchCaseRequest,
) -> Result<BenchCase> {
let url = format!(
"{}/cases/{}",
self.inner.base_url,
urlencoding::encode(case_id),
);
self.c().patch(&url, req).await
}
pub async fn delete_case(&self, case_id: &str) -> Result<()> {
let url = format!(
"{}/cases/{}",
self.inner.base_url,
urlencoding::encode(case_id),
);
self.c().delete(&url).await?;
Ok(())
}
pub async fn bench_by_id(&self, bench_id: i64) -> Result<Option<serde_json::Value>> {
let url = format!("{}/benches/{}", self.inner.base_url, bench_id);
self.c().get_json_value_opt(&url).await
}
pub async fn mcp_session_cost(&self, session_id: &str) -> Result<serde_json::Value> {
let url = format!(
"{}/mcp-sessions/{}/cost",
self.inner.base_url,
urlencoding::encode(session_id),
);
self.c().get_json_value(&url).await
}
}