#![cfg(feature = "server")]
use axon::axon_server::{build_router, ServerConfig};
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
fn server_cfg(strict: bool) -> 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: strict,
default_backend: None,
schemas_dir: None,
}
}
async fn deploy(app: axum::Router, src: &str) {
let body = serde_json::json!({
"source": src,
"source_file": "33e.axon",
"backend": "stub",
});
let req = Request::builder()
.method("POST")
.uri("/v1/deploy")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
fn parse_sse_events(body: &str) -> Vec<(String, serde_json::Value)> {
let mut out = Vec::new();
for block in body.split("\n\n") {
let mut event = None;
let mut data = None;
for line in block.lines() {
if let Some(rest) = line.strip_prefix("event: ") {
event = Some(rest.to_string());
} else if let Some(rest) = line.strip_prefix("data: ") {
data = Some(rest.to_string());
}
}
if let (Some(e), Some(d)) = (event, data) {
let parsed: serde_json::Value =
serde_json::from_str(&d).expect("valid JSON data");
out.push((e, parsed));
}
}
out
}
fn axon_complete_data(body: &str) -> serde_json::Value {
let events = parse_sse_events(body);
events
.into_iter()
.find_map(|(e, d)| if e == "axon.complete" { Some(d) } else { None })
.expect("axon.complete event present in wire body")
}
async fn fire_dynamic_route_for_policy(policy_slug: &str) -> serde_json::Value {
let src = format!(
"tool chat_stream {{ effects: <stream:{policy}> }}\n\
flow Chat() -> Unit {{\n\
step Generate {{ ask: \"hi\" apply: chat_stream output: Stream<Token> }}\n\
}}\n\
axonendpoint ChatEndpoint {{ public: true method: POST path: \"/chat\" execute: Chat transport: sse(axon) }}",
policy = policy_slug,
);
let app = build_router(server_cfg(true));
deploy(app.clone(), &src).await;
let req = Request::builder()
.method("POST")
.uri("/chat")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&bytes).to_string();
axon_complete_data(&body)
}
#[tokio::test]
async fn drop_oldest_effect_surfaces_on_wire() {
let data = fire_dynamic_route_for_policy("drop_oldest").await;
let policies = data["stream_policies"]
.as_array()
.expect("stream_policies array present");
assert_eq!(policies.len(), 1, "exactly one step has policy");
assert_eq!(policies[0]["step"].as_str(), Some("Generate"));
assert_eq!(policies[0]["policy"].as_str(), Some("drop_oldest"));
}
#[tokio::test]
async fn degrade_quality_effect_surfaces_on_wire() {
let data = fire_dynamic_route_for_policy("degrade_quality").await;
let policies = data["stream_policies"].as_array().expect("array present");
assert_eq!(policies[0]["policy"].as_str(), Some("degrade_quality"));
}
#[tokio::test]
async fn pause_upstream_effect_surfaces_on_wire() {
let data = fire_dynamic_route_for_policy("pause_upstream").await;
let policies = data["stream_policies"].as_array().expect("array present");
assert_eq!(policies[0]["policy"].as_str(), Some("pause_upstream"));
}
#[tokio::test]
async fn fail_effect_surfaces_on_wire() {
let data = fire_dynamic_route_for_policy("fail").await;
let policies = data["stream_policies"].as_array().expect("array present");
assert_eq!(policies[0]["policy"].as_str(), Some("fail"));
}
#[tokio::test]
async fn flow_without_stream_effects_omits_stream_policies_field() {
let src =
"flow Chat() -> Unit {\n\
step Generate { ask: \"hi\" output: Stream<Token> }\n\
}\n\
axonendpoint ChatEndpoint { public: true method: POST path: \"/chat\" execute: Chat transport: sse }";
let app = build_router(server_cfg(true));
deploy(app.clone(), src).await;
let req = Request::builder()
.method("POST")
.uri("/chat")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&bytes);
let data = axon_complete_data(&body);
assert!(
data.get("stream_policies").is_none(),
"stream_policies must be elided when no effects declared; got: {data}"
);
assert_eq!(data["steps_executed"].as_u64(), Some(1));
}
#[tokio::test]
async fn multi_step_flow_surfaces_per_step_policies_independently() {
let src =
"tool planner { effects: <stream:fail> }\n\
tool chat_stream { effects: <stream:drop_oldest> }\n\
tool audit_tool { }\n\
flow Pipeline() -> Unit {\n\
step Plan { ask: \"plan\" apply: planner output: Stream<Token> }\n\
step Generate { ask: \"do\" apply: chat_stream output: Stream<Token> }\n\
step Audit { ask: \"verify\" apply: audit_tool output: Stream<Token> }\n\
}\n\
axonendpoint PipelineEndpoint { public: true method: POST path: \"/pipe\" execute: Pipeline transport: sse(axon) }";
let app = build_router(server_cfg(true));
deploy(app.clone(), src).await;
let req = Request::builder()
.method("POST")
.uri("/pipe")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&bytes);
let data = axon_complete_data(&body);
let policies = data["stream_policies"]
.as_array()
.expect("stream_policies present for multi-step flow");
assert_eq!(policies.len(), 2, "only 2 of 3 steps declare effects");
let pairs: Vec<(String, String)> = policies
.iter()
.map(|p| {
(
p["step"].as_str().unwrap_or("").to_string(),
p["policy"].as_str().unwrap_or("").to_string(),
)
})
.collect();
assert!(
pairs.contains(&("Plan".to_string(), "fail".to_string())),
"expected Plan → fail in {pairs:?}"
);
assert!(
pairs.contains(&("Generate".to_string(), "drop_oldest".to_string())),
"expected Generate → drop_oldest in {pairs:?}"
);
assert!(
!pairs.iter().any(|(s, _)| s == "Audit"),
"Audit has no <stream:> effect; must be absent from policies"
);
}
#[tokio::test]
async fn pre_33e_canonical_wire_body_byte_compat_with_no_effects() {
let src =
"flow Chat() -> Unit {\n\
step Generate { ask: \"hi\" output: Stream<Token> }\n\
}\n\
axonendpoint ChatEndpoint { public: true method: POST path: \"/chat\" execute: Chat transport: sse }";
let app = build_router(server_cfg(true));
deploy(app.clone(), src).await;
let req = Request::builder()
.method("POST")
.uri("/chat")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&bytes);
let events = parse_sse_events(&body);
let tokens = events.iter().filter(|(e, _)| e == "axon.token").count();
let completes = events.iter().filter(|(e, _)| e == "axon.complete").count();
assert_eq!(tokens, 1, "exactly one axon.token expected; body:\n{body}");
assert_eq!(completes, 1, "exactly one axon.complete expected; body:\n{body}");
let complete_data = &events.iter().find(|(e, _)| e == "axon.complete").unwrap().1;
assert!(
complete_data.get("stream_policies").is_none(),
"no-effect flow's complete data MUST NOT carry stream_policies; got: {complete_data}"
);
}
#[tokio::test]
async fn malformed_source_resolver_falls_back_to_empty_policies() {
let src = "flow Plain() -> Unit {\n\
step Step1 { ask: \"hi\" }\n\
step Step2 { ask: \"there\" }\n\
}\n\
axonendpoint P { public: true method: POST path: \"/plain\" execute: Plain transport: sse }";
let app = build_router(server_cfg(true));
deploy(app.clone(), src).await;
let req = Request::builder()
.method("POST")
.uri("/plain")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&bytes);
let data = axon_complete_data(&body);
assert!(data.get("stream_policies").is_none());
}
#[tokio::test]
async fn every_closed_catalog_policy_is_reachable_via_wire() {
use axon::stream_effect::BackpressurePolicy;
for &policy in BackpressurePolicy::ALL {
let data = fire_dynamic_route_for_policy(policy.slug()).await;
let policies = data["stream_policies"]
.as_array()
.unwrap_or_else(|| panic!(
"policy {} must surface on wire; data: {data}",
policy.slug()
));
assert_eq!(
policies[0]["policy"].as_str(),
Some(policy.slug()),
"wire policy slug must match closed-catalog slug for {policy}"
);
}
}