AkurAI Build
Menu

AkurAI-Build

public

Latest change 23ae2d15410185bdd0a33888ddbbf8fc1a1d8fea - queue: integrate ref-guard, wire merge-queue worker/recovery, record CI run id by Ólafur Búi Ólafsson

//! Integration tests for the protected Git ref transaction guard.
//!
//! Owned slice: this file only. Included via `#[path]` so no edit to
//! `lib.rs` (owned by the primary integration task) is needed.

#[path = "../src/ref_guard.rs"]
mod ref_guard;

use ref_guard::compare_and_swap;
use std::fs;
use std::process::Command;

fn init_repo(dir: &std::path::Path) {
    assert!(
        Command::new("git")
            .arg("init")
            .args(["-b", "main"])
            .arg(dir)
            .status()
            .expect("run git init")
            .success(),
        "git init failed"
    );
}

fn commit(dir: &std::path::Path, message: &str) -> String {
    assert!(
        Command::new("git")
            .current_dir(dir)
            .args([
                "-c",
                "user.name=test",
                "-c",
                "user.email=test@test",
                "commit",
                "--allow-empty",
                "-m",
                message,
            ])
            .status()
            .expect("run git commit")
            .success(),
        "git commit failed"
    );
    let output = Command::new("git")
        .current_dir(dir)
        .args(["rev-parse", "HEAD"])
        .output()
        .expect("run git rev-parse");
    String::from_utf8(output.stdout)
        .expect("valid utf-8 sha")
        .trim()
        .to_owned()
}

#[test]
fn compare_and_swap_moves_ref_when_expected_matches() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);
    let first = commit(&repo, "first");
    let second = commit(&repo, "second");

    // main already points at `second` after two commits; reset it back
    // to `first` so we can exercise the CAS moving it forward again.
    assert!(
        Command::new("git")
            .current_dir(&repo)
            .args(["update-ref", "refs/heads/main", &first])
            .status()
            .expect("reset ref")
            .success()
    );

    let result = compare_and_swap(&repo, "refs/heads/main", &first, &second);
    assert!(result.is_ok(), "CAS should succeed: {result:?}");

    let output = Command::new("git")
        .current_dir(&repo)
        .args(["rev-parse", "refs/heads/main"])
        .output()
        .expect("read ref");
    let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
    assert_eq!(head, second, "ref should now point at second commit");
}

#[test]
fn compare_and_swap_fails_when_expected_does_not_match() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);
    let first = commit(&repo, "first");
    let second = commit(&repo, "second");
    let bogus = "cafecafecafecafecafecafecafecafecafecafe";

    let result = compare_and_swap(&repo, "refs/heads/main", &first, &second);
    assert!(
        result.is_err(),
        "CAS should fail: ref already moved to second, not first"
    );

    let result_bogus = compare_and_swap(&repo, "refs/heads/main", bogus, &second);
    assert!(result_bogus.is_err(), "CAS with wrong expected should fail");
}

#[test]
fn compare_and_swap_rejects_invalid_ref_name() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);
    let first = commit(&repo, "first");
    let zero = "0000000000000000000000000000000000000000";

    let result = compare_and_swap(&repo, "not..valid..ref", zero, &first);
    assert!(result.is_err(), "invalid ref name should be rejected");
}

#[test]
fn compare_and_swap_rejects_invalid_oid() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);

    let result = compare_and_swap(&repo, "refs/heads/main", "not-hex", "also-not-hex");
    assert!(result.is_err(), "invalid oid should be rejected");
}

#[test]
fn compare_and_swap_deletes_ref_when_expected_matches() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);
    let target = commit(&repo, "target");
    let zero = "0000000000000000000000000000000000000000";

    assert!(
        Command::new("git")
            .current_dir(&repo)
            .args(["branch", "throwaway"])
            .status()
            .expect("create branch")
            .success()
    );

    let result = compare_and_swap(&repo, "refs/heads/throwaway", &target, zero);
    assert!(result.is_ok(), "CAS delete should succeed: {result:?}");

    let output = Command::new("git")
        .current_dir(&repo)
        .args(["show-ref", "refs/heads/throwaway"])
        .output()
        .expect("show-ref");
    assert!(
        !output.status.success(),
        "ref should no longer exist after CAS delete"
    );
}

#[test]
fn compare_and_swap_delete_fails_when_expected_stale() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);
    let first = commit(&repo, "first");
    let second = commit(&repo, "second");
    let zero = "0000000000000000000000000000000000000000";

    // main is now at `second`; try to delete it under the stale
    // expectation that it is still at `first`.
    let result = compare_and_swap(&repo, "refs/heads/main", &first, zero);
    assert!(
        result.is_err(),
        "CAS delete with stale expected oid must fail"
    );

    let output = Command::new("git")
        .current_dir(&repo)
        .args(["rev-parse", "refs/heads/main"])
        .output()
        .expect("read ref");
    let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
    assert_eq!(
        head, second,
        "ref must be untouched after failed CAS delete"
    );
}

#[test]
fn concurrent_compare_and_swap_exactly_one_writer_wins() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);
    let base = commit(&repo, "base");

    // Two independent target shas both racing to CAS refs/heads/main
    // forward from the same observed `base`. Exactly one may win;
    // the loser must fail, and the ref must land on the winner's sha.
    assert!(
        Command::new("git")
            .current_dir(&repo)
            .args(["checkout", "-b", "side"])
            .status()
            .expect("checkout side branch")
            .success()
    );
    let side_target = commit(&repo, "side-commit");
    assert!(
        Command::new("git")
            .current_dir(&repo)
            .args(["checkout", "main"])
            .status()
            .expect("checkout main")
            .success()
    );
    let main_target = commit(&repo, "main-commit");

    // Reset main back to base so both CAS attempts race from the same
    // observed starting point.
    assert!(
        Command::new("git")
            .current_dir(&repo)
            .args(["update-ref", "refs/heads/main", &base])
            .status()
            .expect("reset ref")
            .success()
    );

    let repo_a = repo.clone();
    let repo_b = repo.clone();
    let base_a = base.clone();
    let base_b = base.clone();
    let target_a = main_target.clone();
    let target_b = side_target.clone();

    let handle_a = std::thread::spawn(move || {
        compare_and_swap(&repo_a, "refs/heads/main", &base_a, &target_a)
    });
    let handle_b = std::thread::spawn(move || {
        compare_and_swap(&repo_b, "refs/heads/main", &base_b, &target_b)
    });

    let result_a = handle_a.join().expect("thread a joins");
    let result_b = handle_b.join().expect("thread b joins");

    let ok_count = [&result_a, &result_b].iter().filter(|r| r.is_ok()).count();
    assert_eq!(
        ok_count, 1,
        "exactly one concurrent CAS writer must win: a={result_a:?} b={result_b:?}"
    );

    let output = Command::new("git")
        .current_dir(&repo)
        .args(["rev-parse", "refs/heads/main"])
        .output()
        .expect("read ref");
    let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
    let winner = if result_a.is_ok() {
        &main_target
    } else {
        &side_target
    };
    assert_eq!(&head, winner, "ref must land on the winning CAS target");
}

/// Capture the literal wire bytes a stock `git send-pack` sends for a
/// push, by substituting `--receive-pack` with a relay script that
/// tees stdin to disk before handing it to the real `git receive-pack`.
/// This is the durable equivalent of the throwaway `.tmp/protocol_probe`
/// used during review: it exercises the actual Git binary on this host
/// (2.55.0 at time of writing) rather than a synthetic byte layout, so
/// a future Git wire-format change would be caught by CI instead of
/// only being caught by manual review probing.
///
/// Bounded: the underlying `git push` is wall-clock timed and this
/// panics if it exceeds `CAPTURE_TIMEOUT`, so a hung subprocess (stuck
/// waiting on a credential prompt, an unreachable transport, etc.)
/// fails the test loudly instead of hanging the suite indefinitely.
/// `GIT_TERMINAL_PROMPT=0` additionally prevents interactive prompts
/// from ever blocking on stdin.
fn capture_real_send_pack_wire_bytes(branch: &str) -> Vec<u8> {
    const CAPTURE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);

    let dir = tempfile::tempdir().expect("create temp dir");
    let remote = dir.path().join("remote.git");
    assert!(
        Command::new("git")
            .args(["init", "--bare", "-b", "main"])
            .arg(&remote)
            .status()
            .expect("git init --bare")
            .success(),
        "bare remote init failed"
    );

    let client = dir.path().join("client");
    fs::create_dir(&client).expect("create client dir");
    init_repo(&client);
    commit(&client, "initial");
    if branch != "main" {
        assert!(
            Command::new("git")
                .current_dir(&client)
                .args(["checkout", "-b", branch])
                .status()
                .expect("checkout branch")
                .success(),
            "branch checkout failed"
        );
        commit(&client, "on branch");
    }

    let request_path = dir.path().join("request.bin");
    let relay_script = dir.path().join("relay-receive-pack.sh");
    fs::write(
        &relay_script,
        format!(
            "#!/bin/sh\nset -e\ntee \"{}\" | git receive-pack \"$1\"\n",
            request_path.display()
        ),
    )
    .expect("write relay script");
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&relay_script)
            .expect("stat relay script")
            .permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&relay_script, perms).expect("chmod relay script");
    }

    let start = std::time::Instant::now();
    let output = Command::new("git")
        .current_dir(&client)
        .env("GIT_TERMINAL_PROMPT", "0")
        .args([
            "push",
            &format!("--receive-pack={}", relay_script.display()),
            remote.to_str().expect("utf-8 remote path"),
            branch,
        ])
        .output()
        .expect("run git push through capture relay");
    let elapsed = start.elapsed();
    assert!(
        elapsed < CAPTURE_TIMEOUT,
        "capturing a real send-pack request must be bounded: took {elapsed:?}"
    );
    assert!(
        output.status.success(),
        "real git push through the capture relay must succeed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    fs::read(&request_path).expect("read captured wire bytes")
}

#[test]
fn real_send_pack_wire_request_for_unprotected_ref_is_accepted() {
    let body = capture_real_send_pack_wire_bytes("feature");
    let result = ref_guard::protected_receive_commands(&body, &[]);
    assert!(
        result.is_ok(),
        "a real stock git send-pack request to an unprotected ref must parse and pass: {result:?}"
    );
}

#[test]
fn real_send_pack_wire_request_denied_for_protected_ref() {
    let body = capture_real_send_pack_wire_bytes("main");
    let protected = vec!["refs/heads/main".to_owned()];
    let result = ref_guard::protected_receive_commands(&body, &protected);
    assert!(
        result.is_err(),
        "a real stock git send-pack request targeting a protected ref must be denied"
    );
}

#[test]
fn real_send_pack_wire_request_truncated_is_rejected() {
    let body = capture_real_send_pack_wire_bytes("feature");
    // Truncate mid-stream (well before the flush-pkt / packfile) to
    // simulate a cut connection or a malicious short write; parsing
    // real captured bytes this way must still fail closed rather than
    // panic or silently accept a partial command set.
    let truncated = &body[..body.len().min(40)];
    let result = ref_guard::protected_receive_commands(truncated, &[]);
    assert!(
        result.is_err(),
        "a truncated real wire request must be rejected, not silently accepted"
    );
}

#[test]
fn compare_and_swap_creates_new_ref_from_zero_oid() {
    let dir = tempfile::tempdir().expect("create temp dir");
    let repo = dir.path().join("repo");
    fs::create_dir(&repo).expect("create repo dir");
    init_repo(&repo);
    let target = commit(&repo, "target");
    let zero = "0000000000000000000000000000000000000000";

    let result = compare_and_swap(&repo, "refs/heads/newbranch", zero, &target);
    assert!(
        result.is_ok(),
        "creating a new ref via CAS should succeed: {result:?}"
    );

    let output = Command::new("git")
        .current_dir(&repo)
        .args(["rev-parse", "refs/heads/newbranch"])
        .output()
        .expect("read new ref");
    let head = String::from_utf8_lossy(&output.stdout).trim().to_owned();
    assert_eq!(head, target);
}