use axum::body::Body;
use axum::http::Request;
use otelite_api::{DashboardConfig, DashboardServer};
use otelite_core::telemetry::trace::{Span, SpanKind, SpanStatus, StatusCode as SpanStatusCode};
use otelite_storage::sqlite::SqliteBackend;
use otelite_storage::{StorageBackend, StorageConfig};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use tower::ServiceExt;
#[derive(Deserialize)]
struct Fixture {
version: u32,
window: Window,
spans: Vec<FixtureSpan>,
api: serde_json::Value,
api_empty: serde_json::Value,
}
#[derive(Deserialize)]
struct Window {
start_ns: i64,
end_ns: i64,
}
#[derive(Deserialize)]
struct FixtureSpan {
span_id: String,
model: String,
system: String,
hour: i64,
#[serde(default)]
offset_s: i64,
duration_ms: i64,
input: i64,
#[serde(default)]
output: Option<i64>,
#[serde(default)]
ttft_ms: Option<i64>,
#[serde(default)]
cache_read: Option<i64>,
#[serde(default)]
cache_creation: Option<i64>,
#[serde(default)]
response_model: Option<String>,
}
fn span_from_fixture(s: &FixtureSpan, window: &Window) -> Span {
let mut attributes: HashMap<String, String> = HashMap::new();
attributes.insert("model".into(), s.model.clone());
attributes.insert("gen_ai.request.model".into(), s.model.clone());
attributes.insert("gen_ai.system".into(), s.system.clone());
attributes.insert("gen_ai.usage.input_tokens".into(), s.input.to_string());
if let Some(o) = s.output {
attributes.insert("gen_ai.usage.output_tokens".into(), o.to_string());
}
if let Some(t) = s.ttft_ms {
attributes.insert("ttft_ms".into(), t.to_string());
}
if let Some(c) = s.cache_read {
attributes.insert("gen_ai.usage.cache_read.input_tokens".into(), c.to_string());
}
if let Some(c) = s.cache_creation {
attributes.insert(
"gen_ai.usage.cache_creation.input_tokens".into(),
c.to_string(),
);
}
if let Some(rm) = &s.response_model {
attributes.insert("gen_ai.response.model".into(), rm.clone());
}
let start = window.start_ns + (s.hour * 3600 + s.offset_s) * 1_000_000_000;
Span {
resource: None,
trace_id: "t".into(),
span_id: s.span_id.clone(),
parent_span_id: None,
name: "claude_code.llm_request".into(),
kind: SpanKind::Internal,
start_time: start,
end_time: start + s.duration_ms * 1_000_000,
attributes,
events: vec![],
status: SpanStatus {
code: SpanStatusCode::Ok,
message: None,
},
}
}
async fn get_json(app: &axum::Router, uri: &str) -> serde_json::Value {
let response = app
.clone()
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
.await
.unwrap();
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap_or_else(|e| panic!("parse {uri}: {e} (status {status})"))
}
#[tokio::test]
async fn api_matches_parity_fixture() {
let raw = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/throughput_parity_v1.json"
))
.unwrap();
let fixture: Fixture = serde_json::from_str(&raw).unwrap();
assert_eq!(
fixture.version, 1,
"bump the fixture version and re-freeze expected JSON"
);
let temp_dir = tempfile::TempDir::new().unwrap();
let storage_config = StorageConfig::default().with_data_dir(temp_dir.path().to_path_buf());
let mut storage = SqliteBackend::new(storage_config);
storage.initialize().await.unwrap();
for s in &fixture.spans {
storage
.write_span(&span_from_fixture(s, &fixture.window))
.await
.unwrap();
}
let storage: Arc<dyn StorageBackend> = Arc::new(storage);
let server = DashboardServer::new(DashboardConfig::default(), storage);
let app = server.build_router();
let start = fixture.window.start_ns;
let end = fixture.window.end_ns;
let q = |extra: &str| format!("/api/genai/{extra}?start_time={start}&end_time={end}");
let actual = serde_json::json!({
"token_usage": get_json(&app, &q("usage")).await,
"latency_stats": get_json(&app, &q("latency_stats")).await,
"latency_percentiles_rolling": get_json(&app, &(q("latency_percentiles") + "&bucket_secs=3600&metrics=duration,ttft")).await,
"latency_percentiles_calendar": get_json(&app, &(q("latency_percentiles") + "&bucket_secs=3600&metrics=duration&calendar_day=1&timezone=UTC")).await,
"latency_series": get_json(&app, &(q("latency_series") + "&bucket_secs=3600")).await,
});
assert_eq!(
actual, fixture.api,
"API JSON drifted from the v1 parity fixture — regenerate it only after a deliberate change"
);
let empty_actual = serde_json::json!({
"token_usage": get_json(&app, "/api/genai/usage?start_time=100&end_time=200").await,
"latency_stats": get_json(&app, "/api/genai/latency_stats?start_time=100&end_time=200").await,
"latency_percentiles_calendar": get_json(&app, "/api/genai/latency_percentiles?start_time=1787702400000000000&end_time=1787788800000000000&bucket_secs=3600&metrics=duration&calendar_day=1&timezone=UTC").await,
"latency_series": get_json(&app, "/api/genai/latency_series?start_time=100&end_time=200&bucket_secs=3600").await,
});
assert_eq!(
empty_actual, fixture.api_empty,
"empty-window API JSON drifted from the v1 parity fixture"
);
let api = &fixture.api;
let summary = &api["token_usage"]["summary"];
assert_eq!(summary["total_requests"], 37);
let pop = api["token_usage"]["by_model"]
.as_array()
.unwrap()
.iter()
.find(|m| m["model"] == "pop-sys/pop-model")
.unwrap();
assert_eq!(pop["rerouted_count"], 1);
assert_eq!(pop["response_model"], "pop-alias");
assert_eq!(summary["total_cache_read_tokens"], 150);
assert_eq!(summary["total_cache_creation_tokens"], 30);
let no_out = api["latency_stats"]["items"]
.as_array()
.unwrap()
.iter()
.find(|m| m["model"] == "pop-sys/no-out-model")
.unwrap();
assert_eq!(no_out["throughput_sample_count"], 0);
let ttft = api["latency_stats"]["items"]
.as_array()
.unwrap()
.iter()
.find(|m| m["model"] == "pop-sys/ttft-model")
.unwrap();
assert_eq!(ttft["ttft_degenerate"], true);
assert_eq!(ttft["ttft_degenerate_count"], 12);
let calendar = &api["latency_percentiles_calendar"]["metrics"]["duration"]["models"];
let low = &calendar["pop-sys/low-model"];
assert_eq!(low[0]["count"], 0, "D0 must be an explicit empty grid day");
assert_eq!(low[1]["count"], 7);
}