#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::Arc;
use std::time::Duration;
use axum::response::{IntoResponse, Response};
use axum::routing::post;
use axum::{Json, Router};
use bytes::Bytes;
use firstpass_core::{GENESIS_HASH, ServedFrom, Verdict, verify_chain};
use firstpass_proxy::provider::ProviderRegistry;
use firstpass_proxy::proxy::AppState;
use firstpass_proxy::{ProxyConfig, app, store};
use serde_json::{Value, json};
async fn spawn_upstream() -> String {
let router = Router::new()
.route("/v1/messages", post(anthropic_messages))
.route("/v1/chat/completions", post(openai_chat));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, router).await.unwrap();
});
format!("http://{addr}")
}
fn requested_model(body: &Bytes) -> String {
serde_json::from_slice::<Value>(body)
.ok()
.and_then(|v| v.get("model").and_then(Value::as_str).map(str::to_owned))
.unwrap_or_default()
}
async fn anthropic_messages(body: Bytes) -> Response {
let model = requested_model(&body);
if model.contains("fail5xx") {
return (axum::http::StatusCode::SERVICE_UNAVAILABLE, "upstream down").into_response();
}
let text = if model.contains("haiku") {
""
} else {
"fn main() {} // compiles"
};
Json(json!({
"id": "msg_e2e",
"type": "message",
"role": "assistant",
"model": model,
"content": [{ "type": "text", "text": text }],
"usage": { "input_tokens": 1000, "output_tokens": 200 },
}))
.into_response()
}
async fn openai_chat(body: Bytes) -> Response {
let model = requested_model(&body);
Json(json!({
"id": "chatcmpl_e2e",
"object": "chat.completion",
"model": model,
"choices": [{ "index": 0, "message": { "role": "assistant", "content": "answer via openai" }, "finish_reason": "stop" }],
"usage": { "prompt_tokens": 1000, "completion_tokens": 200 },
}))
.into_response()
}
async fn spawn_proxy(upstream: &str, ladder: &[&str]) -> (String, std::path::PathBuf) {
let db_path = std::env::temp_dir().join(format!("firstpass-e2e-{}.db", uuid::Uuid::now_v7()));
let ladder_toml = ladder
.iter()
.map(|m| format!("\"{m}\""))
.collect::<Vec<_>>()
.join(", ");
let routing = format!(
"[[route]]\nmatch = {{}}\nmode = \"enforce\"\nladder = [{ladder_toml}]\ngates = [\"non-empty\"]\n"
);
let upstream = upstream.to_owned();
let db_str = db_path.to_string_lossy().into_owned();
let config = ProxyConfig::from_lookup(move |key| match key {
"FIRSTPASS_UPSTREAM_ANTHROPIC" => Some(upstream.clone()),
"FIRSTPASS_UPSTREAM_OPENAI" => Some(upstream.clone()),
"FIRSTPASS_MODE" => Some("enforce".to_owned()),
"FIRSTPASS_CONFIG_TOML" => Some(routing.clone()),
"FIRSTPASS_DB" => Some(db_str.clone()),
_ => None,
})
.unwrap();
let providers = ProviderRegistry::new(&config.upstream_anthropic, &config.upstream_openai);
let (traces, _writer) = store::open(&db_path).unwrap();
let state = AppState {
config: Arc::new(config),
http: reqwest::Client::new(),
providers,
gate_health: Arc::new(firstpass_proxy::gate::GateHealthRegistry::new()),
traces,
adaptive: None,
};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, app(state).unwrap()).await.unwrap();
});
(format!("http://{addr}"), db_path)
}
async fn wait_for_traces(db_path: &std::path::Path, want: usize) -> Vec<firstpass_core::Trace> {
let deadline = std::time::Instant::now() + Duration::from_secs(3);
loop {
let traces = store::load_all_traces(db_path).unwrap_or_default();
if traces.len() >= want || std::time::Instant::now() >= deadline {
return traces;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
}
#[tokio::test]
async fn enforce_escalates_over_real_http_then_feedback_attaches() {
let upstream = spawn_upstream().await;
let (proxy, db) = spawn_proxy(
&upstream,
&[
"anthropic/claude-haiku-4-5",
"anthropic/claude-sonnet-5",
"anthropic/claude-opus-4-8",
],
)
.await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{proxy}/v1/messages"))
.header("x-api-key", "byok-test")
.json(&json!({
"model": "claude-haiku-4-5",
"max_tokens": 256,
"messages": [{ "role": "user", "content": "write a hello world" }],
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let served: Value = resp.json().await.unwrap();
assert_eq!(served["type"], "message");
assert_eq!(served["content"][0]["text"], "fn main() {} // compiles");
assert_eq!(served["model"], "anthropic/claude-sonnet-5");
let traces = wait_for_traces(&db, 1).await;
assert_eq!(traces.len(), 1);
let trace = &traces[0];
assert_eq!(trace.attempts.len(), 2, "haiku failed, sonnet passed");
assert_eq!(trace.attempts[0].verdict, Verdict::Fail);
assert_eq!(trace.attempts[1].verdict, Verdict::Pass);
assert_eq!(trace.final_.served_rung, Some(1));
assert_eq!(trace.final_.served_from, ServedFrom::Attempt);
assert!(
trace.final_.savings_usd > 0.0,
"served below top rung => real savings"
);
assert_eq!(trace.attempts[1].in_tokens, 1000);
assert_eq!(trace.attempts[1].out_tokens, 200);
verify_chain(&traces, GENESIS_HASH).unwrap();
let trace_id = trace.trace_id.to_string();
let fb = client
.post(format!("{proxy}/v1/feedback"))
.json(&json!({
"trace_id": trace_id,
"gate_id": "tests",
"verdict": "pass",
"score": 1.0,
"reporter": "ci",
}))
.send()
.await
.unwrap();
assert_eq!(fb.status(), 202);
let view = store::load_trace_view(&db, &trace_id).unwrap().unwrap();
assert_eq!(view.deferred.len(), 1);
assert_eq!(view.deferred[0].gate_id, "tests");
verify_chain(&store::load_all_traces(&db).unwrap(), GENESIS_HASH).unwrap();
let _ = std::fs::remove_file(&db);
}
#[tokio::test]
async fn cross_provider_failover_over_real_http() {
let upstream = spawn_upstream().await;
let (proxy, db) = spawn_proxy(&upstream, &["anthropic/claude-fail5xx", "openai/gpt-5.5"]).await;
let resp = reqwest::Client::new()
.post(format!("{proxy}/v1/messages"))
.header("x-api-key", "byok-test")
.json(&json!({
"model": "claude-fail5xx",
"max_tokens": 128,
"messages": [{ "role": "user", "content": "hi" }],
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let served: Value = resp.json().await.unwrap();
assert_eq!(served["content"][0]["text"], "answer via openai");
assert_eq!(served["model"], "openai/gpt-5.5");
let traces = wait_for_traces(&db, 1).await;
let trace = &traces[0];
assert_eq!(
trace.attempts[0].verdict,
Verdict::Abstain,
"anthropic 503 → abstain"
);
assert_eq!(trace.attempts[1].verdict, Verdict::Pass, "openai served");
assert_eq!(trace.attempts[1].provider, "openai");
assert_eq!(trace.final_.served_rung, Some(1));
verify_chain(&traces, GENESIS_HASH).unwrap();
let _ = std::fs::remove_file(&db);
}