use std::{fs, path::Path};
use axum::{
body::{Body, to_bytes},
http::{Request, StatusCode},
};
use codex_cost::{models::UsageSummary, provider::LocalCodexProvider, server::create_app};
use tempfile::tempdir;
use tower::util::ServiceExt;
fn write_session(path: &Path, session_id: &str, cwd: &str, tokens: u64) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture directory");
}
let session_meta = serde_json::json!({
"timestamp": "2026-07-06T08:00:00.000Z",
"type": "session_meta",
"payload": {
"id": session_id,
"cwd": cwd,
"thread_name": format!("Thread {session_id}"),
},
});
let turn_context = serde_json::json!({
"timestamp": "2026-07-06T08:00:30.000Z",
"type": "turn_context",
"payload": {
"cwd": cwd,
"model": "gpt-5",
"effort": "xhigh",
},
});
let token_count = serde_json::json!({
"timestamp": "2026-07-06T08:01:00.000Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"last_token_usage": {
"input_tokens": tokens / 2,
"cached_input_tokens": 1,
"output_tokens": tokens / 4,
"reasoning_output_tokens": 2,
"total_tokens": tokens,
},
"model_context_window": 258400,
},
},
});
fs::write(
path,
format!("{session_meta}\n{turn_context}\n{token_count}\n"),
)
.expect("write fixture session");
}
async fn response_json(app: axum::Router, method: &str, uri: &str) -> (StatusCode, UsageSummary) {
let response = app
.oneshot(
Request::builder()
.method(method)
.uri(uri)
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let status = response.status();
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("read body");
let summary = serde_json::from_slice::<UsageSummary>(&body).expect("summary json");
(status, summary)
}
#[tokio::test]
async fn health_route_returns_ok() {
let root = tempdir().expect("create codex home");
let app = create_app(LocalCodexProvider::new(Some(root.path().to_path_buf())));
let response = app
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn index_route_uses_codex_cost_brand() {
let root = tempdir().expect("create codex home");
let app = create_app(LocalCodexProvider::new(Some(root.path().to_path_buf())));
let response = app
.oneshot(
Request::builder()
.uri("/")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let status = response.status();
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("read body");
let html = String::from_utf8(body.to_vec()).expect("utf-8 html");
assert_eq!(status, StatusCode::OK);
assert!(html.contains("<title>Codex Cost</title>"));
assert!(html.contains("<h1>Codex Cost</h1>"));
}
#[tokio::test]
async fn summary_route_returns_local_codex_cost_usage() {
let root = tempdir().expect("create codex home");
write_session(
&root.path().join("sessions/2026/07/06/rollout-active.jsonl"),
"active",
"C:\\work\\active",
21,
);
write_session(
&root.path().join("archived_sessions/rollout-archived.jsonl"),
"archived",
"C:\\work\\archived",
9,
);
let app = create_app(LocalCodexProvider::new(Some(root.path().to_path_buf())));
let (status, summary) = response_json(app, "GET", "/api/summary").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(summary.totals.all_time_tokens, 30);
assert_eq!(summary.totals.event_count, 2);
assert_eq!(summary.totals.session_count, 2);
assert_eq!(summary.distributions.models[0].label, "gpt-5");
assert_eq!(summary.distributions.context_windows[0].label, "258400");
}
#[tokio::test]
async fn refresh_route_rescans_files() {
let root = tempdir().expect("create codex home");
let app = create_app(LocalCodexProvider::new(Some(root.path().to_path_buf())));
let (_, before) = response_json(app.clone(), "GET", "/api/summary").await;
assert_eq!(before.totals.all_time_tokens, 0);
write_session(
&root.path().join("sessions/2026/07/06/rollout-late.jsonl"),
"late",
"C:\\work\\late",
13,
);
let (status, after) = response_json(app, "POST", "/api/refresh").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(after.totals.all_time_tokens, 13);
assert_eq!(after.totals.event_count, 1);
}