Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-cli/tests/twin_push_is_framed.rs

#![cfg(feature = "server")]
//! **A pushed pack is a transaction** — through the real receive path, not `put_pack`.
//!
//! The first KVM twin drill (2026-09-15) found the push path never framed its pack: a client's
//! pack enters `Engine::ingest_push`, which opens the engine by `git_dir` and never passes the
//! `OpenRepo::logged` tee, while the ref update after it does. The twin and a restored primary
//! then both refused `refs/heads/main would point at …, which this repository does not have`.
//! The native twin tests had stayed green because they call `put_pack` directly. This test
//! pushes with stock git over SSH into a live server, follows its log into a twin, restores the
//! twin's log into a fresh data root, and asks each for the pushed commit.

mod common;

use std::sync::Arc;

use common::{local_repo, tools_present, Harness};
use gunnar_cli::server::twin::{restore, Follower, Progress, TwinConfig};
use gunnar_cli::server::ServerStores;
use gunnar_core::txlog::Meta;
use gunnar_core::{HashKind, RepoRef};

const NS: &str = "team";
const REPO: &str = "project";

/// Does `stores` hold `oid_hex` in `team/project`?
fn holds(stores: &ServerStores, oid_hex: &str) -> bool {
    let Some(row) = stores
        .catalog()
        .get(&RepoRef::new(NS, REPO).unwrap())
        .unwrap()
    else {
        return false;
    };
    let store = stores.repo_store(&row).unwrap();
    let oid = gix_hash::ObjectId::from_hex(oid_hex.as_bytes()).unwrap();
    store.serve().header(oid.as_bytes()).unwrap().is_some()
}

/// What `refs/heads/main` of `team/project` points at on `stores`.
fn main_of(stores: &ServerStores) -> Option<Vec<u8>> {
    let row = stores
        .catalog()
        .get(&RepoRef::new(NS, REPO).unwrap())
        .unwrap()?;
    let store = stores.repo_store(&row).unwrap();
    store
        .serve()
        .refs()
        .unwrap()
        .into_iter()
        .find(|r| r.name == "refs/heads/main")
        .and_then(|r| r.oid)
}

#[test]
fn a_pushed_pack_is_a_frame_before_its_refs_and_the_twin_and_a_restore_hold_it() {
    if !tools_present() {
        eprintln!("skipping: no git or ssh on PATH");
        return;
    }
    let harness = Harness::start(NS, REPO, HashKind::Sha256);
    let work = harness.dir.path().join("work");
    let head = local_repo(&harness, &work, HashKind::Sha256, "framed over the wire\n");
    harness
        .git(
            &work,
            &["push", &harness.url(NS, REPO), "HEAD:refs/heads/main"],
        )
        .expect_ok("git push");
    let head_bytes = gix_hash::ObjectId::from_hex(head.as_bytes())
        .unwrap()
        .as_bytes()
        .to_vec();

    // The primary's log: a Pack frame for the repository BEFORE the Refs frame that moves main.
    let log = Arc::clone(harness.stores.txlog());
    let entries = log.range(log.first_seq(), log.head_seq()).unwrap();
    let key = format!("{NS}/{REPO}");
    let refs_at = entries
        .iter()
        .position(|e| match e.meta() {
            Ok(Meta::Refs { repo, edits, .. }) => {
                repo == key
                    && edits.iter().any(|ed| {
                        ed.name == "refs/heads/main" && ed.new.as_deref() == Some(head.as_str())
                    })
            }
            _ => false,
        })
        .expect("a Refs frame moves refs/heads/main to the pushed commit");
    let pack_at = entries
        .iter()
        .position(|e| matches!(e.meta(), Ok(Meta::Pack { ref repo, .. }) if *repo == key))
        .expect("the pushed pack is a Pack frame — the receive path framed it");
    assert!(
        pack_at < refs_at,
        "the pack (index {pack_at}) is framed before the ref update (index {refs_at})"
    );

    // The twin follows that log and holds the commit and the ref.
    let twin_dir = tempfile::tempdir().unwrap();
    TwinConfig {
        primary: "harness".into(),
        joined_at_unix_ms: 0,
    }
    .save(twin_dir.path())
    .unwrap();
    let twin = Arc::new(ServerStores::open(twin_dir.path()).unwrap());
    let progress = Follower::new(Arc::clone(&twin), Box::new(Arc::clone(&log)))
        .step()
        .unwrap();
    assert!(matches!(progress, Progress::Applied { .. }), "{progress:?}");
    // **The twin only stores** (ruling 2026-09-22): the pushed pack and the ref update are in
    // its LOG, frame for frame, and it opened no repository store of its own. The restore below
    // is where they become a repository again.
    let tlog = twin.txlog();
    assert_eq!(
        tlog.head_seq(),
        log.head_seq(),
        "the twin's log is at the primary's head"
    );
    for (i, e) in entries.iter().enumerate() {
        let t = tlog.read(e.seq).unwrap();
        assert!(
            t.kind == e.kind && t.meta == e.meta && t.blob == e.blob,
            "frame {i} (seq {}) is byte for byte the primary's",
            e.seq
        );
    }
    assert!(
        !twin
            .repo_dir(&RepoRef::new(NS, REPO).unwrap())
            .unwrap()
            .exists(),
        "a store-only twin opens no repository store"
    );

    // A fresh data root restored from the twin's log, reopened as the primary it was.
    let fresh_dir = tempfile::tempdir().unwrap();
    {
        let fresh = Arc::new(ServerStores::open(fresh_dir.path()).unwrap());
        // `""` = "the stream names no log id", the same value every other call site in
        // the tree passes (`twin/tests.rs`). T4 added the parameter in `f80c867` and
        // these two gunnar-cli tests were not updated with it, so the crate has not
        // compiled its tests since that merge. Not this lane's change; fixed here
        // because a lane cannot run its own gate past somebody else's build error.
        restore(Arc::clone(twin.txlog()), fresh, "").unwrap();
    }
    let fresh = ServerStores::open(fresh_dir.path()).unwrap();
    assert!(
        holds(&fresh, &head),
        "the restored primary holds the pushed commit {head}"
    );
    assert_eq!(
        main_of(&fresh).as_deref(),
        Some(head_bytes.as_slice()),
        "the restored primary's main is the pushed commit"
    );
}