jacquardSnapshot

← snapshot

19171 bytes
//! Integration tests driving the router directly, no socket.

#![expect(
    clippy::unwrap_used,
    reason = "test assertions read better with unwrap"
)]

use std::sync::Arc;

use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use http_body_util::BodyExt as _;
use jac_serve::state::AppState;
use serde_json::{Value, json};
use tower::ServiceExt as _;

fn app() -> Router {
    jac_serve::app(Arc::new(AppState::new()))
}

async fn call(app: &Router, method: &str, uri: &str, body: Option<Value>) -> (StatusCode, Value) {
    let request = match body {
        Some(v) => Request::builder()
            .method(method)
            .uri(uri)
            .header(header::CONTENT_TYPE, "application/json")
            .body(Body::from(v.to_string()))
            .unwrap(),
        None => Request::builder()
            .method(method)
            .uri(uri)
            .body(Body::empty())
            .unwrap(),
    };
    let response = app.clone().oneshot(request).await.unwrap();
    let status = response.status();
    let bytes = response.into_body().collect().await.unwrap().to_bytes();
    let value = if bytes.is_empty() {
        Value::Null
    } else {
        serde_json::from_slice(&bytes).unwrap()
    };
    (status, value)
}

fn init_body() -> Value {
    json!({
        "name": "Meridian Systems",
        "founder": { "display_name": "Ada" },
        "agents": [ { "model": "loom-bot" } ],
        "initial_commit": {
            "message": "auth: initial login flow",
            "files": [ { "path": "src/auth/login.rs", "content": "fn login() {}" } ]
        },
        "founding_decision": {
            "title": "auth retry fails closed",
            "rationale": "401/403 are terminal",
            "families": ["constraints"],
            "scope": ["src/auth"]
        }
    })
}

#[tokio::test]
async fn init_roundtrip() {
    let app = app();
    let (status, body) = call(&app, "POST", "/api/repos", Some(init_body())).await;
    assert_eq!(status, StatusCode::CREATED, "{body}");
    assert_eq!(body["slug"], "meridian-systems");
    assert_eq!(body["founder"]["kind"], "human");
    assert_eq!(body["founder"]["display_name"], "Ada");
    assert_eq!(body["default_ref"], "main");
    assert_eq!(body["head"]["provenance"], "human");
    assert_eq!(body["founding_decision"]["state"], "unsettled");
    assert!(
        body["honesty"]
            .as_str()
            .unwrap()
            .contains("not authenticated")
    );

    // The repo shows up in the listing with its unsettled count.
    let (status, body) = call(&app, "GET", "/api/repos", None).await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(body["repos"][0]["decisions"]["unsettled"], 1);

    // A duplicate name collides on the slug.
    let (status, _) = call(&app, "POST", "/api/repos", Some(init_body())).await;
    assert_eq!(status, StatusCode::CONFLICT);
}

#[tokio::test]
async fn commit_log_detail_blob() {
    let app = app();
    let (_, init) = call(&app, "POST", "/api/repos", Some(init_body())).await;
    let agent = init["agents"][0]["id"].as_str().unwrap().to_owned();

    let (status, commit) = call(
        &app,
        "POST",
        "/api/repos/meridian-systems/commits",
        Some(json!({
            "ref": "main",
            "actor": { "kind": "agent", "id": agent },
            "provenance": "agent",
            "message": "auth: retry",
            "files": [
                { "path": "src/auth/login.rs", "content": "fn login() {}" },
                { "path": "src/auth/backoff.rs", "content": "fn retry() {}" }
            ]
        })),
    )
    .await;
    assert_eq!(status, StatusCode::CREATED, "{commit}");
    assert_eq!(commit["snapshot"]["provenance"], "agent");
    assert_eq!(commit["snapshot"]["author"]["display_name"], "loom-bot");
    let snap_id = commit["snapshot"]["id"].as_str().unwrap().to_owned();

    let (status, log) = call(
        &app,
        "GET",
        "/api/repos/meridian-systems/log?ref=main",
        None,
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    let entries = log["entries"].as_array().unwrap();
    assert_eq!(entries.len(), 2, "initial + retry commit");
    assert_eq!(entries[0]["id"], snap_id.as_str());

    let (status, detail) = call(
        &app,
        "GET",
        &format!("/api/repos/meridian-systems/snapshots/{snap_id}"),
        None,
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    let changed: Vec<&str> = detail["changed_from_parent"]
        .as_array()
        .unwrap()
        .iter()
        .map(|v| v.as_str().unwrap())
        .collect();
    assert_eq!(changed, ["src/auth/backoff.rs"]);
    let blob = detail["files"]
        .as_array()
        .unwrap()
        .iter()
        .find(|f| f["path"] == "src/auth/backoff.rs")
        .unwrap()["blob"]
        .as_str()
        .unwrap()
        .to_owned();

    let (status, blob_body) = call(
        &app,
        "GET",
        &format!("/api/repos/meridian-systems/blobs/{blob}"),
        None,
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(blob_body["content"], "fn retry() {}");
    assert_eq!(blob_body["binary"], false);
}

#[tokio::test]
async fn the_gate_story_over_http() {
    let app = app();
    let (_, init) = call(&app, "POST", "/api/repos", Some(init_body())).await;
    let agent = init["agents"][0]["id"].as_str().unwrap().to_owned();
    let founder = init["founder"]["id"].as_str().unwrap().to_owned();
    let decision = init["founding_decision"]["id"].as_str().unwrap().to_owned();

    // Branch and commit into the governed scope.
    let (status, _) = call(
        &app,
        "POST",
        "/api/repos/meridian-systems/refs",
        Some(json!({ "name": "feature/backoff", "from": { "ref": "main" } })),
    )
    .await;
    assert_eq!(status, StatusCode::CREATED);
    call(
        &app,
        "POST",
        "/api/repos/meridian-systems/commits",
        Some(json!({
            "ref": "feature/backoff",
            "actor": { "kind": "agent", "id": agent },
            "provenance": "agent",
            "message": "auth: retry",
            "files": [
                { "path": "src/auth/login.rs", "content": "fn login() {}" },
                { "path": "src/auth/backoff.rs", "content": "fn retry() {}" }
            ]
        })),
    )
    .await;

    // Preview: blocked, and nothing moved.
    let (status, preview) = call(
        &app,
        "GET",
        "/api/repos/meridian-systems/promote/preview?from=feature/backoff&into=main",
        None,
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(preview["verdict"]["verdict"], "blocked");
    assert_eq!(preview["fast_forward"], true);

    // The real promotion: 200 with a blocked verdict — a result, not an error.
    let (status, verdict) = call(
        &app,
        "POST",
        "/api/repos/meridian-systems/promote",
        Some(json!({ "from": "feature/backoff", "into": "main" })),
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(verdict["verdict"], "blocked");
    assert_eq!(verdict["reason"], "unsettled-decisions");
    assert_eq!(verdict["ref_moved"], false);
    assert_eq!(verdict["unsettled"][0], decision.as_str());

    // A whitespace statement is refused with the engine's exact message.
    let (status, err) = call(
        &app,
        "POST",
        &format!("/api/repos/meridian-systems/decisions/{decision}/attest"),
        Some(json!({ "by": founder, "statement": "   " })),
    )
    .await;
    assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
    assert!(
        err["error"]["message"]
            .as_str()
            .unwrap()
            .contains("1..=2000")
    );

    // Ada attests; the decision settles with her verbatim words.
    let (status, settled) = call(
        &app,
        "POST",
        &format!("/api/repos/meridian-systems/decisions/{decision}/attest"),
        Some(json!({ "by": founder, "statement": "I walked the 401 path; it fails closed." })),
    )
    .await;
    assert_eq!(status, StatusCode::OK, "{settled}");
    assert_eq!(settled["state"], "settled");
    assert_eq!(settled["attestation"]["attestor"]["display_name"], "Ada");

    // The same promotion, admitted, one decision checked, ref moved.
    let (status, verdict) = call(
        &app,
        "POST",
        "/api/repos/meridian-systems/promote",
        Some(json!({ "from": "feature/backoff", "into": "main" })),
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(verdict["verdict"], "admitted");
    assert_eq!(verdict["decisions_checked"], 1);
    assert_eq!(verdict["ungoverned"], false);
    assert_eq!(verdict["ref_moved"], true);
}

#[tokio::test]
async fn ungoverned_admission_says_so() {
    let app = app();
    let (_, init) = call(&app, "POST", "/api/repos", Some(init_body())).await;
    let founder = init["founder"]["id"].as_str().unwrap().to_owned();

    call(
        &app,
        "POST",
        "/api/repos/meridian-systems/refs",
        Some(json!({ "name": "docs/pass", "from": { "ref": "main" } })),
    )
    .await;
    call(
        &app,
        "POST",
        "/api/repos/meridian-systems/commits",
        Some(json!({
            "ref": "docs/pass",
            "actor": { "kind": "human", "id": founder },
            "provenance": "human",
            "message": "docs only",
            "files": [
                { "path": "src/auth/login.rs", "content": "fn login() {}" },
                { "path": "docs/readme.md", "content": "# readme" }
            ]
        })),
    )
    .await;

    let (status, verdict) = call(
        &app,
        "POST",
        "/api/repos/meridian-systems/promote",
        Some(json!({ "from": "docs/pass", "into": "main" })),
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(verdict["verdict"], "admitted");
    assert_eq!(verdict["decisions_checked"], 0);
    assert_eq!(
        verdict["ungoverned"], true,
        "zero checked must be reported as ungoverned, not as verification"
    );
}

#[tokio::test]
async fn non_fast_forward_is_refused() {
    let app = app();
    let (_, init) = call(&app, "POST", "/api/repos", Some(init_body())).await;
    let founder = init["founder"]["id"].as_str().unwrap().to_owned();

    // A stray ref with no shared history.
    call(
        &app,
        "POST",
        "/api/repos/meridian-systems/commits",
        Some(json!({
            "ref": "stray",
            "actor": { "kind": "human", "id": founder },
            "provenance": "human",
            "message": "unrelated",
            "files": [ { "path": "b.rs", "content": "2" } ]
        })),
    )
    .await;

    let (status, err) = call(
        &app,
        "POST",
        "/api/repos/meridian-systems/promote",
        Some(json!({ "from": "stray", "into": "main" })),
    )
    .await;
    assert_eq!(status, StatusCode::CONFLICT);
    assert_eq!(err["error"]["code"], "non-fast-forward");
}

#[tokio::test]
async fn rendezvous_across_repos() {
    let app = app();
    let (_, seed) = call(&app, "POST", "/api/demo", None).await;
    assert_eq!(seed["repos"][0], "meridian-systems");

    // The board holds three publications, none carrying content.
    let (status, board) = call(&app, "GET", "/api/rendezvous/publications", None).await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(board["publications"].as_array().unwrap().len(), 3);

    // Halcyon's probe matched Meridian's decision sketch during the seed.
    assert!(
        !seed["rendezvous"]["matches"].as_array().unwrap().is_empty(),
        "the twin decisions should rendezvous"
    );
    let (_, intros) = call(&app, "GET", "/api/rendezvous/introductions", None).await;
    assert_eq!(intros["introductions"].as_array().unwrap().len(), 1);

    // The live gate: the parked branch previews blocked.
    let (status, preview) = call(
        &app,
        "GET",
        "/api/repos/meridian-systems/promote/preview?from=feature/session-cache&into=main",
        None,
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(preview["verdict"]["verdict"], "blocked");

    // Seeding twice collides, honestly.
    let (status, _) = call(&app, "POST", "/api/demo", None).await;
    assert_eq!(status, StatusCode::CONFLICT);
}

/// A remark is raised against a real claim, and settles exactly once.
#[tokio::test]
async fn remarks_anchor_and_settle() {
    let app = app();
    let (_, seeded) = call(&app, "POST", "/api/demo", None).await;
    let slug = seeded["repos"][0].as_str().unwrap().to_owned();
    let (_, detail) = call(&app, "GET", &format!("/api/repos/{slug}"), None).await;
    let founder = detail["humans"][0]["id"].as_str().unwrap().to_owned();

    // Nothing to act on yet.
    let (status, empty) = call(&app, "GET", &format!("/api/repos/{slug}/remarks"), None).await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(empty["open"], 0);

    let raise = json!({
        "anchor": {
            "kind": "decision",
            "id": "whatever",
            "quote": "Exponential backoff is for transient network errors only."
        },
        "body": "Use randomness to avoid a thundering herd problem.",
        "kind": "suggestion",
        "by": founder,
    });
    let (status, remark) = call(
        &app,
        "POST",
        &format!("/api/repos/{slug}/remarks"),
        Some(raise),
    )
    .await;
    assert_eq!(status, StatusCode::CREATED);
    assert_eq!(remark["state"], "open");
    assert_eq!(remark["kind"], "suggestion");
    // The quote travels with it: the suggestion is unreadable without the
    // sentence that prompted it.
    assert!(
        remark["anchor"]["quote"]
            .as_str()
            .unwrap()
            .contains("transient network errors")
    );
    let id = remark["id"].as_str().unwrap().to_owned();

    let (_, open) = call(
        &app,
        "GET",
        &format!("/api/repos/{slug}/remarks?state=open"),
        None,
    )
    .await;
    assert_eq!(open["open"], 1);

    // Drafting has to name what it became.
    let (status, _) = call(
        &app,
        "POST",
        &format!("/api/repos/{slug}/remarks/{id}/settle"),
        Some(json!({ "outcome": "drafted" })),
    )
    .await;
    assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);

    let (status, settled) = call(
        &app,
        "POST",
        &format!("/api/repos/{slug}/remarks/{id}/settle"),
        Some(json!({ "outcome": "drafted", "decision": "abc123" })),
    )
    .await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(settled["state"], "drafted");
    assert_eq!(settled["outcome"]["decision"], "abc123");

    // Settling twice is a conflict, not a silent overwrite.
    let (status, _) = call(
        &app,
        "POST",
        &format!("/api/repos/{slug}/remarks/{id}/settle"),
        Some(json!({ "outcome": "declined" })),
    )
    .await;
    assert_eq!(status, StatusCode::CONFLICT);

    let (_, after) = call(
        &app,
        "GET",
        &format!("/api/repos/{slug}/remarks?state=open"),
        None,
    )
    .await;
    assert_eq!(after["open"], 0);
}

/// An empty remark, an unknown kind, and a stranger are all refused.
#[tokio::test]
async fn remarks_refuse_nonsense() {
    let app = app();
    let (_, seeded) = call(&app, "POST", "/api/demo", None).await;
    let slug = seeded["repos"][0].as_str().unwrap().to_owned();
    let (_, detail) = call(&app, "GET", &format!("/api/repos/{slug}"), None).await;
    let founder = detail["humans"][0]["id"].as_str().unwrap().to_owned();
    let anchor = json!({ "kind": "file", "id": "src/net/retry.rs" });

    for body in [
        json!({ "anchor": anchor, "body": "   ", "kind": "suggestion", "by": founder }),
        json!({ "anchor": anchor, "body": "ok", "kind": "shout", "by": founder }),
        json!({ "anchor": anchor, "body": "ok", "kind": "note", "by": "hum-0000000000009999" }),
        json!({
            "anchor": json!({ "kind": "elsewhere", "id": "x" }),
            "body": "ok", "kind": "note", "by": founder
        }),
    ] {
        let (status, _) = call(
            &app,
            "POST",
            &format!("/api/repos/{slug}/remarks"),
            Some(body),
        )
        .await;
        assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
    }
}

/// Transcription answers honestly when no model is configured.
#[tokio::test]
async fn transcribe_says_when_unavailable() {
    // The test process has no WHISPER_MODEL, which is the unconfigured case.
    let app = app();
    let (status, body) = call(&app, "GET", "/api/transcribe", None).await;
    assert_eq!(status, StatusCode::OK);
    assert_eq!(body["configured"], false);
    // Availability is a 200 saying "no", never an error — the caller is
    // expected to fall back, not to treat this as a failure.
    assert_eq!(body["engine"], "whisper.cpp");
}

/// A repository written to disk and read back reproduces the same addresses.
///
/// This is the only real test that an export means anything: a snapshot's id
/// has its timestamp, author, provenance, and tree hashed into it, so if any
/// of those is re-derived rather than replayed the ids drift and what is on
/// disk describes a history the engine can no longer produce.
#[tokio::test]
async fn a_repo_round_trips_through_disk() {
    use jac_serve::gitimport::Bounds;

    // Import this very repository — it is the git working copy the tests run
    // inside, so no fixture has to be fabricated.
    let Some(source) = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(std::path::Path::parent)
        .map(std::path::Path::to_path_buf)
    else {
        return;
    };
    if !source.join(".git").exists() {
        // Vendored checkouts have no .git; nothing to assert against.
        return;
    }

    let out = std::env::temp_dir().join(format!("jac-roundtrip-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&out);
    let bounds = Bounds {
        commits: 3,
        paths: 12,
    };
    let Ok(manifest) = jac_serve::batch::one(&source, &out, &bounds) else {
        // A shallow clone or an empty window is not a failure of this claim.
        return;
    };

    let slug = manifest["slug"].as_str().unwrap_or_default().to_owned();
    assert!(!slug.is_empty(), "the import wrote no slug");
    let dir = out.join(&slug);
    let raw = std::fs::read(dir.join("snapshots.json")).unwrap_or_default();
    let written: Vec<Value> = serde_json::from_slice(&raw).unwrap_or_default();
    assert!(!written.is_empty(), "the import wrote nothing");

    let read_back = jac_serve::store::load(&dir);
    assert!(
        read_back.is_ok(),
        "reading the repo back failed: {:?}",
        read_back.as_ref().err().map(ToString::to_string),
    );
    let Ok(loaded) = read_back else { return };
    assert!(
        loaded.mismatched.is_empty(),
        "reading a repo back changed {} of {} addresses: {:?}",
        loaded.mismatched.len(),
        written.len(),
        loaded.mismatched,
    );

    let _ = std::fs::remove_dir_all(&out);
}