monochromatic-git-policy-cli 0.1.4

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! Controls for the starvation reservation on disposable registries:
//!  request bytes, the earned threshold, grant order, holders seen by others, release, and the grant
//!  after the configured number of lost races.

use super::*;
use crate::test_support::{fixture, remove};
use crate::transaction_owner::{current_transaction_owner, encode_transaction_owner};
use crate::transaction_registry::{OWNER_FILENAME, TRANSACTION_ROOT_NAME, ensure_transaction_root};

/// Transaction IDs of the fixtures, canonical UUIDs.
const FIRST: &str = "0b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
/// The second transaction of the fixtures.
const SECOND: &str = "1c7d3d2f-7a6c-4e1f-8b66-4a6e9f3f7b21";

/// A transaction directory under `registry` owned by this live process,
///  or by a foreign identity when `live` is false.
fn transaction(registry: &Path, id: &str, created_at: &str, live: bool) -> PathBuf {
    let directory: PathBuf = registry.join(id);
    std::fs::create_dir_all(&directory).expect("transaction directory");
    let mut owner = current_transaction_owner(id, created_at).expect("current owner");
    if !live {
        owner.owner_identity = String::from("linux:1");
    }
    std::fs::write(directory.join(OWNER_FILENAME), encode_transaction_owner(&owner)).expect("owner record");
    return directory;
}

/// A fixture registry root with the transaction root created.
fn registry_of(name: &str) -> (PathBuf, PathBuf) {
    let root: PathBuf = fixture(name);
    let registry: PathBuf = root.join(TRANSACTION_ROOT_NAME);
    ensure_transaction_root(registry.as_path()).expect("registry root");
    return (root, registry);
}

/// The request file names the incumbent's object with its lost-race count and a trailing newline.
#[test]
fn request_bytes_match_the_incumbent_object() {
    assert_eq!(
        reservation_request_bytes(2),
        b"{\"schemaVersion\":2,\"state\":\"reservation-request\",\"lostRaces\":2}\n".to_vec()
    );
}

/// A lost race earns the reservation from the configured count on, never before.
#[test]
fn the_reservation_is_earned_at_the_configured_count() {
    assert!(!reservation_earned(0, 1));
    assert!(reservation_earned(1, 1));
    assert!(!reservation_earned(2, 3));
    assert!(reservation_earned(3, 3));
    assert!(reservation_earned(4, 3));
}

/// Without any registry, nobody requests; a request without a live owner does not count.
#[test]
fn requesters_are_live_transactions_with_a_request() {
    let (root, registry) = registry_of("replay-reservation-requesters");
    assert!(live_requesters(registry.as_path()).expect("empty").is_empty());
    let live: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    assert!(live_requesters(registry.as_path()).expect("no request").is_empty());
    std::fs::write(live.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("request");
    assert_eq!(live_requesters(registry.as_path()).expect("requester"), vec![String::from(FIRST)]);
    let dead: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-09T00:00:00.000Z", false);
    std::fs::write(dead.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("request");
    assert_eq!(live_requesters(registry.as_path()).expect("dead excluded"), vec![String::from(FIRST)]);
    remove(root.as_path());
}

/// The oldest live requester is first in grant order.
#[test]
fn the_oldest_requester_is_granted_first() {
    let (root, registry) = registry_of("replay-reservation-order");
    for (id, created) in [(FIRST, "2026-10-10T00:00:05.000Z"), (SECOND, "2026-10-10T00:00:01.000Z")] {
        let directory: PathBuf = transaction(registry.as_path(), id, created, true);
        std::fs::write(directory.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("request");
    }
    assert_eq!(
        live_requesters(registry.as_path()).expect("order"),
        vec![String::from(SECOND), String::from(FIRST)]
    );
    remove(root.as_path());
}

/// A reservation held by one transaction is seen by the others until it is released;
///  release removes the request and the lock, and the grant also needs the oldest request.
#[test]
fn a_held_reservation_is_seen_by_others_until_released() {
    let (root, registry) = registry_of("replay-reservation-holder");
    let first_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    let second_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
    let mut first: LandingReservation = LandingReservation::open(registry.as_path(), first_directory.as_path(), FIRST, false);
    let second: LandingReservation = LandingReservation::open(registry.as_path(), second_directory.as_path(), SECOND, false);
    assert!(first.reserve(1).expect("granted"));
    assert!(first_directory.join(RESERVATION_REQUEST_FILENAME).exists());
    assert!(!first.held_by_another().expect("own view"));
    assert!(second.held_by_another().expect("other view"));
    first.release().expect("released");
    assert!(!first_directory.join(RESERVATION_REQUEST_FILENAME).exists());
    assert!(!second.held_by_another().expect("free"));
    remove(root.as_path());
}

/// An inherited landing lease neither requests nor waits: `reserve` returns false and writes nothing.
#[test]
fn a_nested_landing_neither_requests_nor_waits() {
    let (root, registry) = registry_of("replay-reservation-nested");
    let directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    let mut nested: LandingReservation = LandingReservation::open(registry.as_path(), directory.as_path(), FIRST, true);
    assert!(!nested.reserve(1).expect("not applicable"));
    assert!(!directory.join(RESERVATION_REQUEST_FILENAME).exists());
    assert!(!nested.held_by_another().expect("nested never waits"));
    nested.await_slot().expect("returns at once");
    remove(root.as_path());
}

/// The reservation is taken on the lost race the configuration names:
///  earlier races only count, the granted request carries the count, and the grant happens once.
#[test]
fn the_grant_follows_the_configured_lost_races() {
    let (root, registry) = registry_of("replay-reservation-threshold");
    let directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    let configured: u64 = 3;
    let mut reservation: LandingReservation = LandingReservation::open(registry.as_path(), directory.as_path(), FIRST, false);
    let mut granted_at: Option<u64> = None;
    for lost in 1..=configured {
        if reservation_earned(lost, configured) && reservation.reserve(lost).expect("reserve") {
            granted_at = Some(lost);
            break;
        }
    }
    assert_eq!(granted_at, Some(configured));
    let request: Vec<u8> = std::fs::read(directory.join(RESERVATION_REQUEST_FILENAME)).expect("request");
    assert_eq!(request, reservation_request_bytes(configured));
    reservation.release().expect("released");
    remove(root.as_path());
}

/// A landing waits for a released reservation and then takes the landing lock.
#[test]
fn a_landing_takes_the_landing_lock_once_the_reservation_is_free() {
    let (root, registry) = registry_of("replay-reservation-yield");
    let holder_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    let mut holder: LandingReservation = LandingReservation::open(registry.as_path(), holder_directory.as_path(), FIRST, false);
    assert!(holder.reserve(1).expect("granted"));
    holder.release().expect("released");
    let waiter_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
    let waiter: LandingReservation = LandingReservation::open(registry.as_path(), waiter_directory.as_path(), SECOND, false);
    let landing_lock: PathBuf = registry.join("landing.lock");
    let lock: OwnerLock = acquire_reserved_landing_lock(&waiter, || {
        let acquired = try_acquire_owner_lock(landing_lock.as_path(), Some(SECOND))
            .map_err(ReservationError::Lock)?;
        return acquired.ok_or_else(|| { return ReservationError::Registry(RecoveryError(String::from("landing lock busy"))); });
    })
    .expect("landing lock after the reservation is free");
    lock.release().expect("released landing lock");
    remove(root.as_path());
}

/// Run `work` on its own thread and fail the test when it has not finished after a ceiling,
///  so a wait that lost its limit fails instead of hanging the run.
fn finishes_in_time<Output: Send + 'static>(work: impl FnOnce() -> Output + Send + 'static) -> Output {
    let (sender, receiver) = std::sync::mpsc::channel::<Output>();
    std::thread::spawn(move || {
        let _sent = sender.send(work());
    });
    return receiver
        .recv_timeout(Duration::from_secs(10))
        .expect("the wait did not end at its limit");
}

/// A live reservation that is never released ends another transaction's wait at the limit,
///  naming the holder and leaving the reservation in place.
#[test]
fn a_reservation_held_past_the_limit_ends_the_wait() {
    let (root, registry) = registry_of("replay-reservation-await-timeout");
    let first_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    let second_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
    let mut holder: LandingReservation =
        LandingReservation::open(registry.as_path(), first_directory.as_path(), FIRST, false);
    assert!(holder.reserve(1).expect("granted"));
    let waiter: LandingReservation =
        LandingReservation::open(registry.as_path(), second_directory.as_path(), SECOND, false);
    let started: Instant = Instant::now();
    let failure: ReservationError = finishes_in_time(move || {
        return waiter.await_slot_for(Duration::from_millis(200)).expect_err("held past the limit");
    });
    assert!(started.elapsed() >= Duration::from_millis(200), "{:?}", started.elapsed());
    match failure {
        ReservationError::WaitTimedOut { waited, holder_pid } => {
            assert!(waited >= Duration::from_millis(200), "{waited:?}");
            assert_eq!(holder_pid, Some(i64::from(std::process::id())));
        }
        other => panic!("expected a refused wait, got {other}"),
    }
    assert!(holder.held_by_another().is_ok());
    holder.release().expect("released");
    remove(root.as_path());
}

/// A wait is over at its limit exactly, not before it.
#[test]
fn a_wait_is_over_at_its_limit_exactly() {
    let limit: Duration = Duration::from_millis(200);
    assert!(!limit_reached(Duration::from_millis(199), limit));
    assert!(limit_reached(limit, limit));
    assert!(limit_reached(Duration::from_millis(201), limit));
}

/// The default wait holds while another transaction's reservation stands,
///  and ends once it is released.
#[test]
fn the_default_wait_lasts_as_long_as_the_reservation() {
    let (root, registry) = registry_of("replay-reservation-await-default");
    let first_directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    let second_directory: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
    let mut holder: LandingReservation =
        LandingReservation::open(registry.as_path(), first_directory.as_path(), FIRST, false);
    assert!(holder.reserve(1).expect("granted"));
    let waiter: LandingReservation =
        LandingReservation::open(registry.as_path(), second_directory.as_path(), SECOND, false);
    let (sender, receiver) = std::sync::mpsc::channel::<Result<(), ReservationError>>();
    std::thread::spawn(move || {
        let _sent = sender.send(waiter.await_slot());
    });
    assert!(
        receiver.recv_timeout(Duration::from_millis(400)).is_err(),
        "the wait ended while the reservation stood"
    );
    holder.release().expect("released");
    receiver
        .recv_timeout(Duration::from_secs(10))
        .expect("the wait did not end after the release")
        .expect("the wait succeeds");
    remove(root.as_path());
}

/// Releasing a reservation that never wrote a request is not an error,
///  and a request that cannot be removed for any other reason is.
#[cfg(unix)]
#[test]
fn a_release_without_a_request_succeeds_and_other_failures_are_reported() {
    let (root, registry) = registry_of("replay-reservation-release-kinds");
    let directory: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    LandingReservation::open(registry.as_path(), directory.as_path(), FIRST, false)
        .release()
        .expect("no request to remove");
    let file: PathBuf = root.join("not-a-directory");
    std::fs::write(&file, b"x").expect("file");
    let failure: ReservationError = LandingReservation::open(registry.as_path(), file.as_path(), FIRST, false)
        .release()
        .expect_err("the request path cannot be inspected");
    assert!(matches!(failure, ReservationError::Write(_)), "{failure}");
    remove(root.as_path());
}

/// An older live request that is never granted ends a younger transaction's wait for the grant at the limit,
///  and the younger transaction's request is withdrawn by its release.
#[test]
fn an_older_request_held_past_the_limit_ends_the_wait_for_the_grant() {
    let (root, registry) = registry_of("replay-reservation-reserve-timeout");
    let older: PathBuf = transaction(registry.as_path(), FIRST, "2026-10-10T00:00:00.000Z", true);
    std::fs::write(older.join(RESERVATION_REQUEST_FILENAME), reservation_request_bytes(1)).expect("older request");
    let younger: PathBuf = transaction(registry.as_path(), SECOND, "2026-10-10T00:00:01.000Z", true);
    let registry_for_thread: PathBuf = registry.clone();
    let younger_for_thread: PathBuf = younger.clone();
    let (failure, reservation) = finishes_in_time(move || {
        let mut reservation: LandingReservation = LandingReservation::open(
            registry_for_thread.as_path(),
            younger_for_thread.as_path(),
            SECOND,
            false,
        );
        let failure: ReservationError = reservation
            .reserve_for(2, Duration::from_millis(200))
            .expect_err("never first in line");
        return (failure, reservation);
    });
    match failure {
        ReservationError::WaitTimedOut { waited, holder_pid } => {
            assert!(waited >= Duration::from_millis(200), "{waited:?}");
            assert_eq!(holder_pid, None);
        }
        other => panic!("expected a refused wait, got {other}"),
    }
    assert!(younger.join(RESERVATION_REQUEST_FILENAME).exists());
    reservation.release().expect("released");
    assert!(!younger.join(RESERVATION_REQUEST_FILENAME).exists());
    assert!(older.join(RESERVATION_REQUEST_FILENAME).exists(), "the older request is untouched");
    remove(root.as_path());
}

/// The refusal names the holder or the older request, the wait and the variable.
#[test]
fn a_refused_wait_is_described_and_other_failures_keep_their_debug_text() {
    assert_eq!(
        ReservationError::WaitTimedOut { waited: Duration::from_millis(61_900), holder_pid: Some(77) }.to_string(),
        "gave up after waiting 61 seconds for the next landing slot because process 77 holds the reservation; \
         CLI_GIT_LOCK_WAIT_SECONDS sets how long to wait, and nothing was taken from that process"
    );
    assert!(
        ReservationError::WaitTimedOut { waited: Duration::from_secs(2), holder_pid: None }
            .to_string()
            .contains("because an older live landing request holds the next slot")
    );
    let other: ReservationError = ReservationError::Identity(String::from("probe failed"));
    assert_eq!(other.to_string(), format!("{other:?}"));
}