crates/gunnar-cli/tests/first_push_door.rs
#![cfg(feature = "server")]
//! **The first-repository door, end to end: stock `git`, a real SSH
//! connection, and a server in the installed appliance's exact state.**
//!
//! The state is the one measured under KVM on 2026-08-25: `--admin-key`
//! enrolled the bootstrap admin (48/48 guests authenticated), and then the
//! admin could do NOTHING — three git verbs, every one of them wanting a
//! repository, an empty catalog nobody could ever put a repository into,
//! gRPC off by design, no shell by construction. This file drives the
//! fix at the highest level that runs without KVM: the admin's first
//! `git push` names a repository that does not exist, and that push — not a
//! new verb, not a control-plane call — creates it.
//!
//! Measured, both halves, in one file: the push is proven by cloning the bytes
//! back out; and the same door is proven SHUT for a registered non-admin,
//! with the exact fused sentence the appliance answered on 2026-08-25, so
//! the wire still does not reveal which repositories exist.
mod common;
use common::{local_repo, tools_present, Harness};
use gunnar_core::{Grant, HashKind, Level, Principal, PrincipalKind, RepoRef};
/// The appliance scenario: enroll (at boot, from `--admin-key`), push to a
/// repository that has never existed, clone it back, push again.
#[test]
fn the_bootstrap_admins_first_push_creates_the_first_repository() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = Harness::start_appliance_bootstrap();
// Nothing exists. This is the brick the door unbricks.
assert!(
harness
.stores
.catalog()
.namespaces()
.expect("list")
.is_empty(),
"the appliance harness must start with an empty catalog"
);
// ★ The first-push PROSPECT is sha1, so the working tree is sha1.
//
// It was sha256 here until 2026-09-20, when the door was changed with the
// measurement in its own comment (`gunnar-ssh/src/control.rs`,
// `push_create_spec`): git advertises the repository's object format
// BEFORE the client says anything, so a prospect nobody asked for can only
// be the format an un-negotiated client assumes — sha1. On gunnar.rs the
// sha256 prospect refused every real client the estate has. These tests
// kept pushing sha256 trees and had been red ever since.
//
// A caller who WANTS sha256 still gets it from `repo create`, where the
// hash is a decision somebody made rather than one nobody could.
let work = harness.dir.path().join("work");
local_repo(
&harness,
&work,
HashKind::Sha1,
"the first bytes on the appliance\n",
);
harness
.git(
&work,
&[
"push",
&harness.url("nordisk", "first"),
"HEAD:refs/heads/main",
],
)
.expect_ok("the admin's first push, to a repository that does not exist yet");
// The push is real only if the bytes come back out (measure, never assert).
let out = harness.dir.path().join("out");
harness
.git(
harness.dir.path(),
&["clone", "--quiet", &harness.url("nordisk", "first"), "out"],
)
.expect_ok("clone the repository the push created");
assert_eq!(
std::fs::read_to_string(out.join("hello.txt")).expect("the cloned file"),
"the first bytes on the appliance\n",
"the clone must return the pushed bytes"
);
// The second push takes the ordinary path: the repository now resolves.
std::fs::write(work.join("hello.txt"), "the second commit\n").expect("edit");
harness
.git(&work, &["commit", "--quiet", "-am", "two"])
.expect_ok("commit");
harness
.git(
&work,
&[
"push",
&harness.url("nordisk", "first"),
"HEAD:refs/heads/main",
],
)
.expect_ok("the second push, through the ordinary resolve");
}
/// The door is shut for everyone else, and shut INDISTINGUISHABLY: a
/// registered principal with Write on a real repository, pushing to a name
/// that does not exist, gets exactly the sentence the appliance measured —
/// not a hint that creation is a thing that exists for someone else.
#[test]
fn a_non_admins_push_to_a_missing_repository_is_refused_with_the_measured_sentence() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
// The default harness: `tester` holds Write on team/project and a
// server-wide READ — deliberately not Admin, so this is the strongest
// non-admin there is.
let harness = Harness::start("team", "project", HashKind::Sha256);
let work = harness.dir.path().join("work");
local_repo(&harness, &work, HashKind::Sha256, "seed\n");
let out = harness.git(
&work,
&[
"push",
&harness.url("team", "never-created"),
"HEAD:refs/heads/main",
],
);
out.expect_failure("a non-admin push to a missing repository");
assert!(
out.stderr
.contains("repository not found, or you do not have access"),
"the sentence must stay exactly as measured on the appliance: {}",
out.stderr
);
assert!(
harness
.stores
.catalog()
.get(&RepoRef::new("team", "never-created").expect("legal"))
.expect("read the catalog")
.is_none(),
"the refused push must not have created anything"
);
}
/// A second admin, made the ordinary way (`Access.Grant --server … admin`),
/// holds the same door — the credential is the grant row, not the bootstrap
/// name. This is the test that keeps the door from being hardcoded to the
/// string "admin".
#[test]
fn a_granted_server_admin_can_also_create_by_pushing() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = Harness::start("team", "project", HashKind::Sha256);
harness
.stores
.auth()
.add_principal(Principal::new("ops", "ops", PrincipalKind::User).expect("valid"))
.expect("add ops");
// Reuse the harness key for the new principal? No — one key, one
// principal, is the identity model. Mint a second key.
let private = gunnar_core::mint_ed25519().expect("mint");
let parsed = gunnar_core::ssh_key::PrivateKey::from_openssh(&private).expect("parse");
let public = parsed.public_key().to_openssh().expect("render");
let key_path = harness.dir.path().join("id_ops");
std::fs::write(&key_path, &private).expect("write");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600)).unwrap();
}
harness
.stores
.auth()
.register_key("ops", &public, None)
.expect("register");
harness
.stores
.auth()
.grant(Grant::server("ops", Level::Admin))
.expect("grant server admin");
let ssh = format!(
"ssh -i {} -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR",
key_path.display()
);
let work = harness.dir.path().join("work-ops");
// sha1: the first-push prospect's format since 2026-09-20 — see the note on
// the first test in this file.
local_repo(&harness, &work, HashKind::Sha1, "ops content\n");
harness
.git_with_ssh(
&work,
&ssh,
&[
"push",
&harness.url("infra", "tooling"),
"HEAD:refs/heads/main",
],
)
.expect_ok("a granted server admin pushes a repository into existence");
assert!(
harness
.stores
.catalog()
.get(&RepoRef::new("infra", "tooling").expect("legal"))
.expect("read")
.is_some(),
"the push must have created infra/tooling"
);
}
/// **A refused push-create leaves NO repository behind** (the site test,
/// 2026-08-30, two FAIL rows). A working tree whose hash is not the door's
/// prospect reads the advertised `object-format`, says *"the receiving end
/// does not support this repository's hash algorithm"* and hangs up without
/// sending a command. On v1.1.3-166 the door had already written the catalog
/// row at exec time, so `repo get` succeeded and `ls-remote` answered for a
/// repository nobody pushed to. The door is prospective now: the catalog, the
/// namespace and the disk are untouched by a push that never sent a command.
///
/// ★ **The refused side is now the SHA-256 tree.** The prospect became sha1 on
/// 2026-09-20 (`push_create_spec`, with the gunnar.rs measurement in its own
/// comment), so the client git refuses is the other one. The property this test
/// exists for is unchanged and is not about a hash: a push that never sent a
/// command leaves nothing behind, and the same door still creates for a client
/// that CAN speak to it.
#[test]
fn a_refused_push_create_leaves_no_repository_behind() {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let harness = Harness::start_appliance_bootstrap();
let work = harness.dir.path().join("work-sha256");
local_repo(&harness, &work, HashKind::Sha256, "a sha256 tree\n");
// Also a namespace that does not exist: the admin's door would conjure
// it for a real push, and must not for an abandoned one.
let out = harness.git(
&work,
&["push", &harness.url("nobody", "x"), "HEAD:refs/heads/main"],
);
out.expect_failure("a sha256 push into the sha1 first-push door");
assert!(
out.stderr.contains("hash algorithm"),
"git must refuse on the advertised object-format, got: {}",
out.stderr
);
let path = RepoRef::new("nobody", "x").expect("legal");
assert!(
harness.stores.catalog().get(&path).expect("read").is_none(),
"the refused push-create left a catalog row behind"
);
assert!(
harness
.stores
.catalog()
.namespace("nobody")
.expect("read")
.is_none(),
"the refused push-create left a conjured namespace behind"
);
let dir = harness.stores.repo_dir(&path).expect("dir");
assert!(
!dir.exists(),
"the refused push-create left a directory behind: {}",
dir.display()
);
// And `ls-remote` — the row the site test measured — must not answer.
let probe = harness.git(
harness.dir.path(),
&["ls-remote", &harness.url("nobody", "x")],
);
probe.expect_failure("ls-remote of a repository that was never created");
assert!(
probe
.stderr
.contains("repository not found, or you do not have access"),
"{}",
probe.stderr
);
// The identity-value guard: the same door, a sha1 tree, creates.
let ok = harness.dir.path().join("work-sha1");
local_repo(&harness, &ok, HashKind::Sha1, "a sha1 tree\n");
harness
.git(
&ok,
&["push", &harness.url("nobody", "x"), "HEAD:refs/heads/main"],
)
.expect_ok("the sha1 push through the same door");
assert!(harness.stores.catalog().get(&path).expect("read").is_some());
assert!(dir.exists());
}
/// **The first push of a tree with an LFS object creates the repository**
/// (2026-09-28), judged by the real `git-lfs` over the real `ssh`.
///
/// MEASURED on a real box before the fix: git-lfs's pre-push hook runs
/// BEFORE git's receive-pack sends a command and asks `git-lfs-transfer
/// <repo> upload` first; the repository did not exist, the exec was refused,
/// and the hook died — `batch request: exec request failed on channel 0:
/// exit status 255` — so neither the repository nor the ref was created,
/// while a precreated repository worked. Now the upload opens the
/// push-create door under the push's own rules, the first object makes the
/// repository, and the push that follows lands in it. Proven by the bytes:
/// the object on the server's disk, and a fresh clone that materialises it.
///
/// Hard-fails without `git-lfs` on PATH, like `gunnar-lfs/tests/interop.rs`:
/// a skip would be a green nobody earned.
#[test]
fn the_first_push_of_a_tree_with_an_lfs_object_creates_the_repository() {
let lfs = std::process::Command::new("git-lfs")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
assert!(
tools_present() && lfs,
"git, ssh and git-lfs are required on PATH; a suite that skipped would report a pass"
);
let harness = Harness::start_appliance_bootstrap();
let work = harness.dir.path().join("lfs-work");
std::fs::create_dir_all(&work).expect("the working tree");
harness
.git(&work, &["init", "-q", "--object-format=sha1", "-b", "main"])
.expect_ok("git init");
harness
.git(&work, &["lfs", "install"])
.expect_ok("git lfs install (HOME is the harness dir)");
harness
.git(&work, &["lfs", "track", "*.bin"])
.expect_ok("git lfs track");
let content: Vec<u8> = (0..300_000u32)
.map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
.collect();
std::fs::write(work.join("big.bin"), &content).expect("the payload");
harness
.git(&work, &["add", ".gitattributes", "big.bin"])
.expect_ok("git add");
harness
.git(&work, &["commit", "-q", "-m", "one big file"])
.expect_ok("git commit");
let listed = harness.git(&work, &["lfs", "ls-files", "--long"]);
listed.expect_ok("git lfs ls-files");
let oid = listed
.stdout
.split_whitespace()
.next()
.expect("the fixture is an LFS object")
.to_owned();
let url = harness.url("nordisk", "lfsfirst");
harness
.git(&work, &["push", &url, "HEAD:refs/heads/main"])
.expect_ok("the first push, with an LFS object, to a repository that does not exist");
let stored = harness
.git_dir("nordisk", "lfsfirst")
.join("lfs")
.join("objects")
.join(&oid[0..2])
.join(&oid[2..4])
.join(&oid);
assert_eq!(
std::fs::read(&stored).map(|b| b.len()).unwrap_or(0),
content.len(),
"the object is on the server's disk at {}",
stored.display()
);
let out = harness.dir.path().join("lfs-out");
harness
.git(
harness.dir.path(),
&["clone", "-q", &url, out.to_str().expect("utf-8")],
)
.expect_ok("clone the push-created repository back");
let got = std::fs::read(out.join("big.bin")).expect("the clone has the file");
assert!(
got == content,
"the clone materialised the LFS object ({} bytes, want {})",
got.len(),
content.len()
);
}