crates/gunnar-cli/tests/repo_verbs_against_the_door.rs
#![cfg(feature = "server")]
//! **`gunnar settings`, `gunnar create` and a sha256 tree's first `gunnar push`,
//! against the real door** (OX-WIRED 2026-10-28: the verbs were mounted, but only a
//! stub door ever answered them, in `gunnar-ssh/tests/repo_verb.rs`).
//!
//! Here the `gunnar` binary runs as a person runs it: over stock `ssh` to a real
//! `gunnar serve` whose control plane is up, so the forge's `gunnar-repo` verb is
//! answered by the server's own `Repos` handlers. Every answer is checked twice:
//! by what the verb prints, and by the catalog it changed.
mod common;
use std::path::Path;
use std::process::Command;
use common::{local_repo, tools_present, Harness, HarnessOptions, Seed};
use gunnar_core::{HashKind, RepoRef, Visibility};
/// The installed appliance (the bootstrap admin, no repository) with its control plane up
/// (the repository door answers only once the control plane is), and the namespace
/// `nordisk`: `Repos.Create` makes a repository in a namespace that exists.
fn appliance() -> Harness {
assert!(
tools_present(),
"this test requires `git` and `ssh` on PATH"
);
let h = Harness::start_with_options(
"unused",
"unused",
HashKind::Sha256,
HarnessOptions {
grpc: true,
seed: Seed::BootstrapAdminOnly,
..HarnessOptions::default()
},
);
h.stores
.catalog()
.create_namespace("nordisk")
.expect("the namespace nordisk");
h
}
/// `gunnar <args>` in `cwd`, signed in with the harness key by the `ssh` git would run.
fn gunnar(h: &Harness, cwd: &Path, args: &[&str]) -> (bool, String, String) {
let out = Command::new(env!("CARGO_BIN_EXE_gunnar-cli"))
.current_dir(cwd)
.env("HOME", h.dir.path())
.env("GIT_SSH_COMMAND", h.ssh_command())
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("LC_ALL", "C")
.env_remove("SSH_AUTH_SOCK")
.args(args)
.output()
.expect("the gunnar binary runs");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn host(h: &Harness) -> String {
format!("ssh://git@127.0.0.1:{}", h.ssh_port)
}
fn catalog(h: &Harness, ns: &str, repo: &str) -> Option<gunnar_core::Repo> {
h.stores
.catalog()
.get(&RepoRef::new(ns, repo).expect("legal"))
.expect("read the catalog")
}
/// ★ `gunnar create` makes an empty repository over the door, sha256 unless `--sha1`.
#[test]
fn gunnar_create_makes_a_sha256_repository_and_a_sha1_one_when_asked() {
let h = appliance();
let key = h.key_path.to_str().expect("utf-8").to_owned();
let (ok, out, err) = gunnar(
&h,
h.dir.path(),
&["create", "nordisk/knut", "--host", &host(&h), "-i", &key],
);
assert!(
ok,
"gunnar create nordisk/knut\n stdout: {out}\n stderr: {err}"
);
assert!(
out.contains("repository nordisk/knut") && out.contains("object-format sha256"),
"{out}"
);
assert_eq!(
catalog(&h, "nordisk", "knut").expect("created").hash_kind,
HashKind::Sha256
);
let (ok, out, err) = gunnar(
&h,
h.dir.path(),
&[
"create",
"nordisk/old",
"--sha1",
"--host",
&host(&h),
"-i",
&key,
],
);
assert!(ok, "gunnar create --sha1\n stdout: {out}\n stderr: {err}");
assert!(out.contains("object-format sha1"), "{out}");
assert_eq!(
catalog(&h, "nordisk", "old").expect("created").hash_kind,
HashKind::Sha1
);
// A second create of the same name is the server's refusal, said, and changes nothing.
let (ok, out, err) = gunnar(
&h,
h.dir.path(),
&[
"create",
"nordisk/knut",
"--sha1",
"--host",
&host(&h),
"-i",
&key,
],
);
assert!(
!ok,
"a second create of nordisk/knut must be refused\n stdout: {out}\n stderr: {err}"
);
assert_eq!(
catalog(&h, "nordisk", "knut")
.expect("still there")
.hash_kind,
HashKind::Sha256
);
// A namespace that does not exist is the server's sentence too.
let (ok, _, err) = gunnar(
&h,
h.dir.path(),
&["create", "nowhere/x", "--host", &host(&h), "-i", &key],
);
assert!(!ok && err.contains("namespace nowhere not found"), "{err}");
assert!(catalog(&h, "nowhere", "x").is_none());
}
/// ★ `gunnar settings` shows a repository, and changes its visibility and default branch.
#[test]
fn gunnar_settings_shows_and_changes_a_repository_through_the_forges_door() {
let h = appliance();
let key = h.key_path.to_str().expect("utf-8").to_owned();
let (ok, out, err) = gunnar(
&h,
h.dir.path(),
&["create", "nordisk/knut", "--host", &host(&h), "-i", &key],
);
assert!(ok, "gunnar create\n stdout: {out}\n stderr: {err}");
let (ok, out, err) = gunnar(
&h,
h.dir.path(),
&["settings", "nordisk/knut", "--host", &host(&h), "-i", &key],
);
assert!(
ok,
"gunnar settings (show)\n stdout: {out}\n stderr: {err}"
);
assert!(
out.contains("repository nordisk/knut") && out.contains("visibility private"),
"{out}"
);
let (ok, out, err) = gunnar(
&h,
h.dir.path(),
&[
"settings",
"nordisk/knut",
"--visibility",
"public",
"--default-branch",
"trunk",
"--host",
&host(&h),
"-i",
&key,
],
);
assert!(ok, "gunnar settings --visibility public --default-branch trunk\n stdout: {out}\n stderr: {err}");
assert!(
out.contains("visibility public") && out.contains("default-branch refs/heads/trunk"),
"{out}"
);
let repo = catalog(&h, "nordisk", "knut").expect("there");
assert_eq!(
repo.visibility,
Visibility::Public,
"the catalog says public"
);
assert_eq!(
repo.default_branch, "refs/heads/trunk",
"the catalog holds the new default branch"
);
// Back to private, read back by a plain show.
let (ok, _, err) = gunnar(
&h,
h.dir.path(),
&[
"settings",
"nordisk/knut",
"--visibility",
"private",
"--host",
&host(&h),
"-i",
&key,
],
);
assert!(ok, "{err}");
assert_eq!(
catalog(&h, "nordisk", "knut").expect("there").visibility,
Visibility::Private
);
// A value the verb does not know is refused before any connection.
let (ok, _, err) = gunnar(
&h,
h.dir.path(),
&[
"settings",
"nordisk/knut",
"--visibility",
"secret",
"--host",
&host(&h),
"-i",
&key,
],
);
assert!(
!ok && err.contains("public") && err.contains("private"),
"{err}"
);
// A repository that does not exist is the server's refusal, said (the door does not
// tell a missing repository from one the caller may not read).
let (ok, out, err) = gunnar(
&h,
h.dir.path(),
&[
"settings",
"nordisk/nothing",
"--host",
&host(&h),
"-i",
&key,
],
);
assert!(
!ok && err.contains("nordisk/nothing") && out.is_empty(),
"stdout: {out}\nstderr: {err}"
);
}
/// ★ **A sha256 tree's first `gunnar push` creates its repository as sha256** (ef4ec6f6):
/// a push-made repository is sha1 (the server announces its format first), so `gunnar
/// push` creates the remote over the door in sha256 before it pushes. The bytes come
/// back out through stock git.
#[test]
fn a_sha256_trees_first_gunnar_push_creates_the_repository_in_sha256() {
let h = appliance();
let key = h.key_path.to_str().expect("utf-8").to_owned();
let work = h.dir.path().join("work-sha256");
local_repo(&h, &work, HashKind::Sha256, "a sha256 tree\n");
let url = h.url("nordisk", "s256");
let (ok, out, err) = gunnar(
&h,
&work,
&["push", &url, "refs/heads/main:refs/heads/main", "-i", &key],
);
assert!(ok, "gunnar push of a sha256 tree to a repository that does not exist\n stdout: {out}\n stderr: {err}");
assert!(
err.contains("created nordisk/s256") && err.contains("sha256"),
"the push says it created the repository: {err}"
);
assert_eq!(
catalog(&h, "nordisk", "s256")
.expect("the push created it")
.hash_kind,
HashKind::Sha256
);
let back = h.dir.path().join("back");
h.git(
h.dir.path(),
&["clone", "--quiet", &url, back.to_str().expect("utf-8")],
)
.expect_ok("clone the sha256 repository back");
assert_eq!(
std::fs::read_to_string(back.join("hello.txt")).expect("the cloned file"),
"a sha256 tree\n"
);
let format = h.git(&back, &["rev-parse", "--show-object-format"]);
format.expect_ok("rev-parse --show-object-format");
assert_eq!(format.stdout.trim(), "sha256", "the clone is sha256");
// The second push finds the repository and leaves it alone.
std::fs::write(work.join("hello.txt"), "two\n").expect("edit");
h.git(&work, &["commit", "--quiet", "-am", "two"])
.expect_ok("commit");
let (ok, out, err) = gunnar(
&h,
&work,
&["push", &url, "refs/heads/main:refs/heads/main", "-i", &key],
);
assert!(
ok,
"the second gunnar push\n stdout: {out}\n stderr: {err}"
);
assert!(
!err.contains("created"),
"a repository that exists is not created again: {err}"
);
}