#![cfg(feature = "server")]
use axon::axon_server::{
build_router, collect_axonendpoint_routes, resolve_route_backend, DynamicEndpointRoute,
ServerConfig,
};
use axon::backend_resolution::BackendResolutionReason;
use axon::type_checker::compute_implicit_transports;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use std::collections::HashMap;
use tower::ServiceExt;
fn one_route(src: &str) -> DynamicEndpointRoute {
let tokens = axon::lexer::Lexer::new(src, "<test>").tokenize().unwrap();
let mut prog = axon::parser::Parser::new(tokens).parse().unwrap();
compute_implicit_transports(&mut prog);
let table: HashMap<_, _> = collect_axonendpoint_routes(&prog, src, "<test>").unwrap();
table.into_values().next().expect("one route expected")
}
const DECLARED: &str = "flow F() -> Unit { step S { ask: \"x\" } }\n\
axonendpoint E { public: true method: POST path: \"/chat\" execute: F backend: gemini }";
const UNDECLARED: &str = "flow F() -> Unit { step S { ask: \"x\" } }\n\
axonendpoint E { public: true method: POST path: \"/chat\" execute: F }";
#[test]
fn s1_declared_backend_resolves_via_rung_2() {
let route = one_route(DECLARED);
let r = resolve_route_backend(&route, vec![], vec![], None).unwrap();
assert_eq!(r.backend, "gemini");
assert_eq!(
r.reason,
BackendResolutionReason::EndpointDeclared,
"36.f D1: a route's declared `backend:` resolves on rung 2"
);
}
#[test]
fn s1_declared_backend_outranks_registry_and_env() {
let route = one_route(DECLARED);
let r = resolve_route_backend(
&route,
vec!["openai".into()],
vec!["anthropic".into()],
Some("kimi".into()),
)
.unwrap();
assert_eq!(
r.backend, "gemini",
"36.f D1: the declared `backend:` (rung 2) outranks the server \
default (rung 3), the registry (rung 4a) and the env (rung 4b)"
);
}
#[test]
fn s1_undeclared_route_falls_through_to_registry() {
let route = one_route(UNDECLARED);
let r = resolve_route_backend(&route, vec!["openai".into()], vec![], None).unwrap();
assert_eq!(r.backend, "openai");
assert_eq!(r.reason, BackendResolutionReason::RegistryRanked);
}
#[test]
fn s1_undeclared_route_falls_through_to_env() {
let route = one_route(UNDECLARED);
let r = resolve_route_backend(&route, vec![], vec!["kimi".into()], None).unwrap();
assert_eq!(r.backend, "kimi");
assert_eq!(r.reason, BackendResolutionReason::EnvironmentAvailable);
}
#[test]
fn s1_undeclared_route_uses_server_default_over_auto_rungs() {
let route = one_route(UNDECLARED);
let r = resolve_route_backend(
&route,
vec!["openai".into()],
vec!["kimi".into()],
Some("anthropic".into()),
)
.unwrap();
assert_eq!(r.backend, "anthropic");
assert_eq!(r.reason, BackendResolutionReason::ServerDefault);
}
#[test]
fn s1_undeclared_route_with_nothing_is_honest_failure() {
let route = one_route(UNDECLARED);
assert!(
resolve_route_backend(&route, vec![], vec![], None).is_err(),
"36.f D5: an undeclared route with no registry / no env / no \
server default is an honest failure — never a silent stub"
);
}
#[test]
fn s1_resolution_is_deterministic() {
let route = one_route(DECLARED);
let a = resolve_route_backend(&route, vec!["openai".into()], vec![], None).unwrap();
let b = resolve_route_backend(&route, vec!["openai".into()], vec![], None).unwrap();
assert_eq!(a, b);
}
fn server_cfg() -> ServerConfig {
ServerConfig {
host: "127.0.0.1".into(),
port: 0,
channel: "memory".into(),
auth_token: String::new(),
log_level: "INFO".into(),
log_format: "json".into(),
log_file: None,
database_url: None,
config_path: None,
strict_type_driven_transport: false,
default_backend: None,
schemas_dir: None,
}
}
async fn register_backend(app: &axum::Router, name: &str) {
let req = Request::builder()
.method("PUT")
.uri(format!("/v1/backends/{name}"))
.header("content-type", "application/json")
.body(Body::from(r#"{"enabled":true}"#))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK, "register {name} failed");
}
async fn deploy(app: &axum::Router, src: &str) {
let body = serde_json::json!({ "source": src, "source_file": "test.axon" });
let req = Request::builder()
.method("POST")
.uri("/v1/deploy")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
let status = resp.status();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap_or_default();
assert_eq!(status, StatusCode::OK, "deploy failed: {json}");
assert_eq!(json.get("success").and_then(|v| v.as_bool()), Some(true), "{json}");
}
async fn hit(app: &axum::Router, method: &str, path: &str, accept_sse: bool) -> (StatusCode, String, Vec<u8>) {
let mut builder = Request::builder()
.method(method)
.uri(path)
.header("content-type", "application/json");
if accept_sse {
builder = builder.header("accept", "text/event-stream");
}
let resp = app.clone().oneshot(builder.body(Body::from("{}")).unwrap()).await.unwrap();
let status = resp.status();
let ct = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let bytes = resp.into_body().collect().await.unwrap().to_bytes().to_vec();
(status, ct, bytes)
}
#[tokio::test]
async fn s2_json_branch_runs_the_declared_backend_outranking_registry() {
let app = build_router(server_cfg());
register_backend(&app, "openai").await;
deploy(
&app,
"flow Chat() -> Unit { step S { ask: \"hi\" } }\n\
axonendpoint E { public: true method: POST path: \"/chat\" execute: Chat backend: stub }",
)
.await;
let (status, ct, body) = hit(&app, "POST", "/chat", false).await;
assert_eq!(status, StatusCode::OK, "dynamic route must dispatch");
assert!(ct.starts_with("application/json"), "JSON branch, got {ct}");
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
json["execution_metrics"]["backend"], "stub",
"36.f D1+D3: the route's declared `backend: stub` (rung 2) must \
execute — outranking the `openai` registry entry the pre-36 \
hardcoded `\"auto\"` would have auto-ranked. Body: {json}"
);
assert!(
json["step_audit"]["steps_executed"].as_u64().unwrap_or(0) >= 1,
"36.f: a real step must run against the resolved backend: {json}"
);
}
#[tokio::test]
async fn s3_sse_branch_runs_the_declared_backend() {
let app = build_router(server_cfg());
register_backend(&app, "openai").await;
deploy(
&app,
"flow Stream() -> Unit { step S { ask: \"hi\" output: Stream<Token> } }\n\
axonendpoint E { public: true method: POST path: \"/stream\" execute: Stream \
backend: stub transport: sse }",
)
.await;
let (status, ct, body) = hit(&app, "POST", "/stream", true).await;
assert_eq!(status, StatusCode::OK);
assert!(
ct.starts_with("text/event-stream"),
"36.f: the SSE branch must serve `text/event-stream`, got {ct}"
);
let text = String::from_utf8_lossy(&body);
assert!(
text.contains("axon.complete") || text.contains("axon.token"),
"36.f: the SSE branch must stream the flow against the resolved \
`stub` backend and reach completion — no hardcoded `\"auto\"` \
dead-ending. Body: {text}"
);
}
#[tokio::test]
async fn s4_undeclared_route_still_dispatches() {
let app = build_router(server_cfg());
deploy(
&app,
"flow Chat() -> Unit { step S { ask: \"hi\" } }\n\
axonendpoint E { public: true method: POST path: \"/plain\" execute: Chat backend: stub }",
)
.await;
let (status, _ct, body) = hit(&app, "POST", "/plain", false).await;
assert_eq!(status, StatusCode::OK, "36.f D9: the route must dispatch");
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["execution_metrics"]["backend"], "stub");
assert!(json["step_audit"]["steps_executed"].as_u64().unwrap_or(0) >= 1);
}