use std::sync::Arc;
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use http_body_util::BodyExt;
use serde_json::{json, Value};
use tensor_wasm_api::{build_router_with_config, AppState, AuthConfig, TenantConfig};
use tower::ServiceExt;
async fn body_bytes(body: Body) -> Vec<u8> {
body.collect()
.await
.expect("collect body")
.to_bytes()
.to_vec()
}
async fn body_json(body: Body) -> Value {
let bytes = body_bytes(body).await;
serde_json::from_slice(&bytes).expect("body is JSON")
}
fn router() -> axum::Router {
build_router_with_config(
Arc::new(AppState::default()),
AuthConfig::default(),
TenantConfig::default(),
)
}
fn json_post(uri: &str, body: Value) -> Request<Body> {
Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap()
}
async fn deploy_module(router: &axum::Router, wat: &str) -> String {
let wasm_bytes = wat::parse_str(wat).expect("WAT parses");
let wasm_b64 = BASE64.encode(&wasm_bytes);
let deploy_req = json_post(
"/functions",
json!({ "name": "invoke_envelope_shape", "wasm_b64": wasm_b64 }),
);
let deploy_resp = router
.clone()
.oneshot(deploy_req)
.await
.expect("deploy oneshot");
assert_eq!(deploy_resp.status(), StatusCode::OK, "deploy failed");
body_json(deploy_resp.into_body())
.await
.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.expect("deploy response has id")
}
async fn deploy_min_module(router: &axum::Router) -> String {
deploy_module(router, r#"(module (func (export "_start")))"#).await
}
#[tokio::test]
async fn invoke_envelope_matches_empty_body_response() {
let router = router();
let id = deploy_min_module(&router).await;
let envelope = json!({
"export": "_start",
"args": [],
});
let envelope_resp = router
.clone()
.oneshot(json_post(&format!("/functions/{id}/invoke"), envelope))
.await
.expect("envelope oneshot");
let envelope_status = envelope_resp.status();
let envelope_body = body_json(envelope_resp.into_body()).await;
let empty_resp = router
.clone()
.oneshot(json_post(&format!("/functions/{id}/invoke"), json!({})))
.await
.expect("empty oneshot");
let empty_status = empty_resp.status();
let empty_body = body_json(empty_resp.into_body()).await;
assert_eq!(
envelope_status, empty_status,
"envelope and empty-body responses must share a status: \
envelope={envelope_status}, empty={empty_status}"
);
assert_eq!(
envelope_status,
StatusCode::OK,
"envelope shape must produce 200, got {envelope_status}"
);
assert_eq!(
envelope_body, empty_body,
"envelope and empty-body responses must be JSON-identical \
today (the executor ignores `export`/`args` until S-31 \
wires them through). envelope={envelope_body}, empty={empty_body}"
);
}
#[tokio::test]
async fn invoke_envelope_with_only_args_succeeds() {
let router = router();
let id = deploy_module(
&router,
r#"(module (func (export "_start") (param i32 i32)))"#,
)
.await;
let body = json!({ "args": [1, 2] });
let resp = router
.oneshot(json_post(&format!("/functions/{id}/invoke"), body))
.await
.expect("oneshot");
assert_eq!(
resp.status(),
StatusCode::OK,
"envelope with only `args` must succeed"
);
}
#[tokio::test]
async fn invoke_async_envelope_produces_accepted() {
let router = router();
let id = deploy_min_module(&router).await;
let envelope = json!({
"export": "_start",
"args": [],
});
let resp = router
.oneshot(json_post(
&format!("/functions/{id}/invoke-async"),
envelope,
))
.await
.expect("envelope oneshot");
assert_eq!(
resp.status(),
StatusCode::ACCEPTED,
"envelope on async path must produce 202"
);
let body = body_json(resp.into_body()).await;
assert!(
body.get("job_id").and_then(Value::as_str).is_some(),
"async envelope response must include a job_id, got: {body}"
);
}