arcature 2026.2.1

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! Pipeline zone ordering tests — instrumentation-based execution proof.
//!
//! Verifies the lifecycle-zone ordering (engine spec §7/§28) with
//! `AtomicUsize` counters and behavioral observation: pre-routing layers run
//! before route selection, post-routing layers run after, and a
//! short-circuited request skips work that is not needed.
//!
//! Per AGENTS.md §27, these tests run real Axum servers on `127.0.0.1:0` and
//! make real HTTP requests. Per AGENTS.md §28, ordering is proven with
//! `AtomicUsize` counters (handler execution) and a marker-header layer
//! (post-routing execution).

use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

use arcature::prelude::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

const HANG_GUARD: Duration = Duration::from_secs(10);

struct RunningApp {
    addr: SocketAddr,
    shutdown: tokio::sync::oneshot::Sender<()>,
    join: tokio::task::JoinHandle<()>,
}

impl RunningApp {
    async fn start(app: Application<()>) -> RunningApp {
        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .expect("bind ephemeral listener");
        let addr = listener.local_addr().expect("read bound address");
        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
        let join = tokio::spawn(async move {
            app.serve_with_shutdown(listener, async {
                let _ = shutdown_rx.await;
            })
            .await
            .expect("application served without engine error");
        });
        RunningApp {
            addr,
            shutdown: shutdown_tx,
            join,
        }
    }

    fn addr(&self) -> SocketAddr {
        self.addr
    }

    async fn stop(self) {
        self.shutdown
            .send(())
            .expect("server task still alive to receive shutdown");
        tokio::time::timeout(HANG_GUARD, self.join)
            .await
            .expect("server did not hang on shutdown")
            .expect("server task did not panic");
    }
}

async fn http_get(addr: SocketAddr, path: &str) -> String {
    let mut stream = tokio::time::timeout(HANG_GUARD, TcpStream::connect(addr))
        .await
        .expect("connect did not hang")
        .expect("connect succeeds");
    let request = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
    stream
        .write_all(request.as_bytes())
        .await
        .expect("write request");
    let mut buffer = Vec::new();
    tokio::time::timeout(HANG_GUARD, stream.read_to_end(&mut buffer))
        .await
        .expect("read did not hang")
        .expect("read succeeds");
    String::from_utf8_lossy(&buffer).into_owned()
}

fn header<'a>(response: &'a str, name: &str) -> Option<&'a str> {
    let lower = name.to_ascii_lowercase();
    for line in response.split("\r\n") {
        if let Some((k, v)) = line.split_once(": ")
            && k.to_ascii_lowercase() == lower
        {
            return Some(v.trim());
        }
    }
    None
}

/// A `from_fn` middleware that adds a marker response header. Applied via
/// `Router::layer` (post-routing). Its presence on the response proves the
/// post-routing layer ran; its absence proves it was skipped.
async fn post_routing_marker(
    req: arcature::axum::extract::Request,
    next: arcature::axum::middleware::Next,
) -> arcature::axum::response::Response {
    let mut response = next.run(req).await;
    response.headers_mut().insert(
        "x-post-routing-ran",
        "yes".parse().expect("valid header value"),
    );
    response
}

#[tokio::test]
async fn pre_routing_short_circuit_skips_handler() {
    // The proxy (pre-routing) short-circuits with 503. The handler must NOT
    // run — the proxy runs before route selection, so the request never
    // reaches the handler.
    let handler_called = Arc::new(AtomicUsize::new(0));
    let h_counter = handler_called.clone();

    let app = Application::new()
        .routes(Routes::new().route(
            "/",
            get(move || {
                let c = h_counter.clone();
                async move {
                    c.fetch_add(1, Ordering::SeqCst);
                    "handler ran"
                }
            }),
        ))
        .proxy(|_req| ProxyAction::ShortCircuit {
            status: StatusCode::SERVICE_UNAVAILABLE,
            response: None,
        })
        .build();

    let server = RunningApp::start(app).await;
    let response = http_get(server.addr(), "/").await;
    assert!(response.starts_with("HTTP/1.1 503"), "short-circuit 503");
    assert_eq!(
        handler_called.load(Ordering::SeqCst),
        0,
        "handler must not run when proxy short-circuits (pre-routing skips routing)"
    );
    server.stop().await;
}

#[tokio::test]
async fn continue_proxy_runs_handler_exactly_once() {
    // When the proxy returns Continue, the handler runs normally — exactly
    // once. This proves the pre-routing layer delegates to routing (does not
    // short-circuit on Continue) and does not re-run the handler.
    let handler_called = Arc::new(AtomicUsize::new(0));
    let h_counter = handler_called.clone();

    let app = Application::new()
        .routes(Routes::new().route(
            "/",
            get(move || {
                let c = h_counter.clone();
                async move {
                    c.fetch_add(1, Ordering::SeqCst);
                    "ok"
                }
            }),
        ))
        .proxy(|_req| ProxyAction::continue_default())
        .build();

    let server = RunningApp::start(app).await;
    let response = http_get(server.addr(), "/").await;
    assert!(response.starts_with("HTTP/1.1 200 OK"));
    assert_eq!(
        handler_called.load(Ordering::SeqCst),
        1,
        "handler must run exactly once when proxy returns Continue"
    );
    server.stop().await;
}

#[tokio::test]
async fn pre_routing_short_circuit_skips_post_routing_layer() {
    // The architectural proof: a post-routing layer (`from_fn` applied via
    // `Router::layer`) adds `x-post-routing-ran: yes` to the response.
    //
    // - When the proxy *continues*, routing happens, the handler runs, and the
    //   post-routing layer wraps the response → the marker header IS present.
    // - When the proxy *short-circuits*, routing is skipped entirely, so the
    //   post-routing layer (which wraps the router) never sees the request →
    //   the marker header is ABSENT.
    //
    // This is impossible if the proxy were post-routing (via `Router::layer`):
    // there the proxy and the marker layer would be in the same stack, so a
    // short-circuit would still pass through the outer layers. The absence of
    // the marker proves the proxy runs in a *different* (pre-routing) zone.

    // Case 1: Continue → marker present.
    let app_continue = Application::new()
        .routes(
            Routes::new()
                .route("/", get(|| async { "ok" }))
                .layer(arcature::axum::middleware::from_fn(post_routing_marker)),
        )
        .proxy(|_req| ProxyAction::continue_default())
        .build();
    let server = RunningApp::start(app_continue).await;
    let response = http_get(server.addr(), "/").await;
    assert!(response.starts_with("HTTP/1.1 200 OK"));
    assert_eq!(
        header(&response, "x-post-routing-ran"),
        Some("yes"),
        "post-routing layer must run when proxy continues (marker present)"
    );
    server.stop().await;

    // Case 2: ShortCircuit → marker absent.
    let app_short = Application::new()
        .routes(
            Routes::new()
                .route("/", get(|| async { "ok" }))
                .layer(arcature::axum::middleware::from_fn(post_routing_marker)),
        )
        .proxy(|_req| ProxyAction::ShortCircuit {
            status: StatusCode::SERVICE_UNAVAILABLE,
            response: None,
        })
        .build();
    let server = RunningApp::start(app_short).await;
    let response = http_get(server.addr(), "/").await;
    assert!(response.starts_with("HTTP/1.1 503"), "short-circuit 503");
    assert_eq!(
        header(&response, "x-post-routing-ran"),
        None,
        "post-routing layer must NOT run when proxy short-circuits (marker absent — pre-routing is a separate zone)"
    );
    server.stop().await;
}