wsb-daemon 0.248.6265

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
//! What a worktree's own router sees when it is reached through the machine.

use axum::body::Body;
use axum::extract::Path;
use axum::http::{Request, StatusCode};
use axum::routing::{any, get};
use axum::Router;
use tower::ServiceExt;

/// The inner router: a worktree's, with a route that takes two segments of
/// its own — which is the shape that broke.
fn worktree_router() -> Router {
    Router::new()
        .route(
            "/api/v1/issues/:owner/:repo",
            get(|Path((owner, repo)): Path<(String, String)>| async move {
                format!("{owner}|{repo}")
            }),
        )
        .route("/api/v1/version", get(|| async { "version" }))
        .route(
            "/api/v1/query",
            get(|request: Request<Body>| async move {
                request.uri().query().unwrap_or_default().to_string()
            }),
        )
}

/// The machine's routing in front of it, exactly as the daemon mounts it:
/// the outer route captures the worktree and the rest, and the request goes
/// through [`super::inner_request`] — so the outer captures really are in the
/// request when it is rewritten.
async fn through_the_machine(path: &str) -> (StatusCode, String) {
    let machine = Router::new().route(
        "/api/v1/worktrees/:worktree/*rest",
        any(
            |Path((_worktree, rest)): Path<(String, String)>, request: Request<Body>| async move {
                let inner = super::inner_request(&rest, request).expect("a rewritable request");
                worktree_router().oneshot(inner).await.expect("the worktree router answers")
            },
        ),
    );
    let response = machine
        .oneshot(Request::builder().uri(path).body(Body::empty()).expect("a request"))
        .await
        .expect("the machine answers");
    let status = response.status();
    let body = hyper::body::to_bytes(response.into_body()).await.expect("a body");
    (status, String::from_utf8_lossy(&body).into_owned())
}

/// TEST10023: a worktree handler gets ITS path segments, not the machine's.
///
/// The bug: routing leaves its captures in the request's extensions, and the
/// inner router's `Path` extractor reads them from there. Forwarding the outer
/// request — with its URI swapped but its extensions intact — gave every
/// two-segment worktree route the OUTER captures. `/issues/:owner/:repo`
/// resolved to owner = the worktree id, repo = "issues/machinefabric/…", and
/// the handler ANSWERED: an empty list for a repository that does not exist,
/// beside a page insisting nothing had been read.
#[tokio::test]
async fn test10023_a_forwarded_request_carries_only_its_own_path() {
    let (status, body) =
        through_the_machine("/api/v1/worktrees/machinefabric:fix/issues/machinefabric/macfloom?state=all").await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(
        body, "machinefabric|macfloom",
        "the handler must see its own two segments, not the worktree id"
    );
}

/// TEST10024: the query survives the rewrite.
///
/// `?state=all` is how the page asks for closed issues too; losing it would
/// silently show only the open ones.
#[tokio::test]
async fn test10024_the_query_survives_the_rewrite() {
    let (status, body) = through_the_machine("/api/v1/worktrees/machinefabric:main/query?state=all&x=1").await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(body, "state=all&x=1", "every query parameter must reach the worktree");
}

/// TEST10025: a single-segment route still resolves.
#[tokio::test]
async fn test10025_a_plain_route_still_resolves() {
    let (status, body) = through_the_machine("/api/v1/worktrees/~0123456789abcdef/version").await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(body, "version");
}