#![cfg(feature = "server")]
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
use tower::ServiceExt;
#[derive(Default)]
struct Upstream {
hits: std::sync::Mutex<std::collections::HashMap<String, Arc<AtomicUsize>>>,
}
impl Upstream {
fn counter(&self, slug: &str) -> Arc<AtomicUsize> {
self.hits
.lock()
.unwrap()
.entry(slug.to_string())
.or_default()
.clone()
}
fn count(&self, slug: &str) -> usize {
self.counter(slug).load(Ordering::SeqCst)
}
}
static UPSTREAM: OnceLock<Arc<Upstream>> = OnceLock::new();
fn upstream() -> Arc<Upstream> {
UPSTREAM.get_or_init(build_upstream).clone()
}
fn build_upstream() -> Arc<Upstream> {
let counters = Arc::new(Upstream::default());
let for_route = counters.clone();
let app = axum::Router::new().route(
"/{slug}",
axum::routing::post(
move |axum::extract::Path(slug): axum::extract::Path<String>, _b: String| {
let c = for_route.clone();
async move {
let n = c.counter(&slug).fetch_add(1, Ordering::SeqCst) + 1;
if slug == "flaky" && n == 1 {
(
StatusCode::INTERNAL_SERVER_ERROR,
"upstream is having a bad minute".to_string(),
)
} else if slug == "flaky" {
(StatusCode::OK, "recovered".to_string())
} else {
(StatusCode::OK, format!("{slug}#{n}"))
}
}
},
),
);
let (addr_tx, addr_rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
addr_tx.send(listener.local_addr().unwrap()).unwrap();
axum::serve(listener, app).await.unwrap();
});
});
let addr = addr_rx.recv().expect("upstream bound");
std::env::set_var("AXON_TOOL_BASE_URL", format!("http://{addr}"));
counters
}
fn server_cfg() -> axon::axon_server::ServerConfig {
axon::axon_server::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 post(
app: &axum::Router,
uri: &str,
body: serde_json::Value,
tenant: Option<&str>,
) -> serde_json::Value {
let mut req = Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/json");
if let Some(t) = tenant {
req = req.header("X-Tenant-ID", t);
}
let resp = app
.clone()
.oneshot(req.body(Body::from(body.to_string())).unwrap())
.await
.unwrap();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
serde_json::from_slice(&bytes).unwrap_or_default()
}
async fn deploy(app: &axum::Router, src: &str) -> serde_json::Value {
post(
app,
"/v1/deploy",
serde_json::json!({ "source": src, "source_file": "cache.axon", "backend": "stub" }),
None,
)
.await
}
async fn execute(app: &axum::Router, flow: &str, tenant: Option<&str>) -> serde_json::Value {
post(
app,
"/v1/execute",
serde_json::json!({ "flow": flow, "backend": "stub" }),
tenant,
)
.await
}
fn assert_ran(out: &serde_json::Value) {
let result = out["result"].as_str().unwrap_or_default();
assert!(
!result.contains("connection failed"),
"the upstream must be reachable, or every call-counting assertion below is \
measuring failed connections instead of vendor hits: {out}"
);
assert_eq!(
out["step_audit"]["errors"], 0,
"the flow must complete without step errors: {out}"
);
}
async fn execute_sse(app: &axum::Router, flow: &str, tenant: &str) -> String {
let req = Request::builder()
.method("POST")
.uri("/v1/execute/sse")
.header("content-type", "application/json")
.header("X-Tenant-ID", tenant)
.body(Body::from(
serde_json::json!({ "flow_name": flow, "backend": "stub" }).to_string(),
))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes().to_vec();
let text = String::from_utf8(bytes).expect("utf-8 sse body");
assert!(
!text.contains("connection failed"),
"the upstream must be reachable on the SSE path too: {text}"
);
text
}
fn program(slug: &str, cache_block: &str, flow_body: &str) -> String {
let cache_block = cache_block.replace("Memo", &format!("Memo_{slug}"));
format!(
"tool T {{\n\
\x20 provider: http\n\
\x20 runtime: {slug}\n\
\x20 effects: <pure>\n\
\x20 output_type: String\n\
\x20 parameters: {{ id: String }}\n\
}}\n\
{cache_block}\n\
flow Run() -> Unit {{\n{flow_body}\n}}\n"
)
}
#[tokio::test(flavor = "multi_thread")]
async fn a_pure_tool_called_twice_is_computed_once() {
let up = upstream();
const SLUG: &str = "enrich";
let (app, _s) = axon::axon_server::build_router_with_state(server_cfg());
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/cache/memoised_pure_tool.axon"),
)
.expect("the fixture `advertised.rs` cites must exist");
let d = deploy(&app, &src).await;
assert_eq!(d["success"], true, "deploy must succeed: {d}");
let before = up.count(SLUG);
let out = execute(&app, "Run", Some("acme")).await;
assert_ran(&out);
let calls = up.count(SLUG) - before;
assert_eq!(
calls, 1,
"two identical calls to a `pure` tool under a default cache must reach the vendor \
ONCE. {calls} calls means the declaration is still inert — which is exactly what \
v2.89.0 measured and what shipped as `Real` in 2.88.0"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_same_program_with_cache_none_computes_twice() {
let up = upstream();
const SLUG: &str = "cache_none";
let (app, _s) = axon::axon_server::build_router_with_state(server_cfg());
let src = program(
SLUG,
"cache Memo { default: true }",
" use T(id = \"ada\")\n use T(id = \"ada\")",
)
.replace("provider: http", "provider: http\n cache: none");
let d = deploy(&app, &src).await;
assert_eq!(d["success"], true, "deploy must succeed: {d}");
let before = up.count(SLUG);
let out = execute(&app, "Run", Some("acme")).await;
assert_ran(&out);
let calls = up.count(SLUG) - before;
assert_eq!(
calls, 2,
"`cache: none` must opt out; {calls} calls means the runtime memoises regardless of \
the declaration, which is a different lie from the one v2.89.0 fixed"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_failed_call_is_not_memoised() {
let up = upstream();
const SLUG: &str = "flaky";
let (app, _s) = axon::axon_server::build_router_with_state(server_cfg());
let src = program(
SLUG,
"cache Memo { default: true }",
" use T(id = \"ada\")",
);
let d = deploy(&app, &src).await;
assert_eq!(d["success"], true, "{d}");
let before = up.count(SLUG);
let first = execute(&app, "Run", Some("acme")).await; let second = execute(&app, "Run", Some("acme")).await; let calls = up.count(SLUG) - before;
assert_eq!(
calls, 2,
"the failed first call must not have been stored: a second run has to reach the \
vendor again. {calls} call(s) means a transient failure got memoised"
);
assert!(
second["result"]
.as_str()
.unwrap_or_default()
.contains("recovered"),
"the second run must carry the RECOVERED body, proving the retry reached a live \
upstream rather than failing to connect twice.\nfirst: {first}\nsecond: {second}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_second_tenant_does_not_read_the_first_tenants_cached_result() {
let up = upstream();
const SLUG: &str = "tenant_iso";
let (app, _s) = axon::axon_server::build_router_with_state(server_cfg());
let src = program(
SLUG,
"cache Memo { default: true }",
" use T(id = \"shared\")",
);
let d = deploy(&app, &src).await;
assert_eq!(d["success"], true, "{d}");
let before = up.count(SLUG);
let a1 = execute(&app, "Run", Some("acme")).await;
assert_ran(&a1);
let after_a1 = up.count(SLUG);
assert_eq!(after_a1 - before, 1, "tenant acme's first call computes");
let a2 = execute(&app, "Run", Some("acme")).await;
assert_ran(&a2);
assert_eq!(
up.count(SLUG),
after_a1,
"the same tenant repeating the same call must HIT — otherwise this test could not \
distinguish tenant isolation from a cache that never hits at all"
);
let b1 = execute(&app, "Run", Some("globex")).await;
assert_ran(&b1);
assert_eq!(
up.count(SLUG) - after_a1,
1,
"tenant globex must reach the vendor itself. Serving it acme's memoised result is a \
cross-tenant read — the exact failure the design decision puts the tenant in the key to prevent, \
and the one that was reachable while `/v1/execute` lost the tenant across \
`spawn_blocking`"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_sse_door_memoises_and_keys_by_tenant_too() {
let up = upstream();
const SLUG: &str = "sse_door";
let (app, _s) = axon::axon_server::build_router_with_state(server_cfg());
let src = program(
SLUG,
"cache Memo { default: true }",
" use T(id = \"carol\")",
);
let d = deploy(&app, &src).await;
assert_eq!(d["success"], true, "deploy must succeed: {d}");
let before = up.count(SLUG);
execute_sse(&app, "Run", "acme").await;
let after_first = up.count(SLUG);
assert_eq!(after_first - before, 1, "the first SSE run computes");
execute_sse(&app, "Run", "acme").await;
assert_eq!(
up.count(SLUG),
after_first,
"the second SSE run with the same tenant and arguments must HIT. A miss here means \
the cache is mounted on the sync runner only — `cache` wired on one door and dead \
on the other, which is the shape v2.67.0/v2.69.0/v2.87.0 each found and named"
);
execute_sse(&app, "Run", "globex").await;
assert_eq!(
up.count(SLUG) - after_first,
1,
"a different tenant on the SSE path must MISS. Before v2.89.0 this path never \
called `with_tenant_id`, so `ctx.tenant_id` was the empty string for everyone and \
every SSE caller would have shared one cache namespace"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_value_computed_on_one_door_is_served_on_the_other() {
let up = upstream();
const SLUG: &str = "shared_tier";
let (app, _s) = axon::axon_server::build_router_with_state(server_cfg());
let src = program(
SLUG,
"cache Memo { default: true }",
" use T(id = \"dave\")",
);
let d = deploy(&app, &src).await;
assert_eq!(d["success"], true, "deploy must succeed: {d}");
let before = up.count(SLUG);
let sync_run = execute(&app, "Run", Some("acme")).await;
assert_ran(&sync_run);
assert_eq!(
up.count(SLUG) - before,
1,
"the sync run computes"
);
execute_sse(&app, "Run", "acme").await;
assert_eq!(
up.count(SLUG) - before,
1,
"the SSE run must be served the value the SYNC run computed. A second vendor call \
means each door holds its own tier, so nothing survives a request — memoisation \
that only works within a single run is the cache equivalent of a token bucket \
rebuilt per request (v2.69.0), and every test of it would pass"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_emit_on_the_declared_channel_flushes_the_cache() {
let up = upstream();
const SLUG: &str = "invalidation";
let (app, _s) = axon::axon_server::build_router_with_state(server_cfg());
let src = format!(
"channel Orders {{ message: String }}\n{}",
program(
SLUG,
"cache Memo { default: true invalidate_on: [Orders] }",
" use T(id = \"bob\")\n \
let payload = \"changed\"\n \
emit Orders(payload)\n \
use T(id = \"bob\")",
)
);
let d = deploy(&app, &src).await;
assert_eq!(d["success"], true, "deploy must succeed: {d}");
let before = up.count(SLUG);
let out = execute(&app, "Run", Some("acme")).await;
assert_ran(&out);
let calls = up.count(SLUG) - before;
assert_eq!(
calls, 2,
"the `emit Orders(…)` between the two identical calls must have flushed the \
namespace, so the second call recomputes. {calls} call(s) means `invalidate_on:` is \
still a typed reference to nothing"
);
}