test(grasp-audit): route git subprocesses through hermetic git_command

Fixture behaviour and the hard-coded deterministic commit hashes were
only stable because the host's git configuration happened to be benign.
Empirically, a failing pre-commit hook delivered via core.hooksPath or
init.templateDir (including from the XDG config location) breaks every
fixture commit, and init.defaultBranch renames branches fixtures rely
on; per-site "-c" flags and GIT_TERMINAL_PROMPT covered only fragments
of that surface.

Add grasp_audit::git_command(), which masks the global and system
config files via GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM=/dev/null
(requires git >= 2.32; the flake pins 2.54), disables terminal
prompts, and clears the GIT_CONFIG_COUNT/GIT_CONFIG_PARAMETERS
injection vectors plus inherited GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE.
Every git invocation in fixtures and specs now goes through it, so an
invocation sees only configuration it supplies itself (repo-local
config, -c flags, or env vars at the call site). Redundant per-site
GIT_TERMINAL_PROMPT pins are dropped.

A regression test first proves a deliberately hostile global config
(defaultBranch=main, failing hooks via hooksPath and templateDir,
gpgsign, autocrlf) breaks a non-hermetic commit, then proves the
hermetic recipe still yields DETERMINISTIC_COMMIT_HASH with that
config exposed through the process environment. The test waits only
on process exit, no sleeps.

Excluded scope: ngit-grasp's tests/ adopt the helper separately, and
swallowed git exit statuses are fixed separately; behaviour of the
relay itself is unchanged.

Validated: cargo clippy -p grasp-audit --all-targets -D warnings;
cargo test -p grasp-audit --lib (37 passed).
This commit is contained in:
DanConwayDev
2026-08-01 16:13:47 +00:00
parent 172aea9ef1
commit be8dce6e9c
8 changed files with 273 additions and 86 deletions
+32 -35
View File
@@ -1314,7 +1314,7 @@ impl<'a> TestContext<'a> {
// -B creates or resets main at HEAD, tolerating a pre-existing local
// main (e.g. when the host sets init.defaultBranch=main)
let _ = Command::new("git")
let _ = git_command()
.args(["checkout", "-B", "main"])
.current_dir(&clone_path)
.output();
@@ -1416,7 +1416,7 @@ impl<'a> TestContext<'a> {
// Create or reset main at our deterministic commit and check it out.
// -B tolerates a pre-existing local main (e.g. when the host sets
// init.defaultBranch=main)
let checkout_output = Command::new("git")
let checkout_output = git_command()
.args(["checkout", "-B", "main"])
.current_dir(&clone_path)
.output();
@@ -1553,13 +1553,13 @@ impl<'a> TestContext<'a> {
// Reset to orphan state and create deterministic root commit
// Step 1: Create orphan branch (removes all history)
let _ = Command::new("git")
let _ = git_command()
.args(["checkout", "--orphan", "main-new"])
.current_dir(&clone_path)
.output();
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
let _ = Command::new("git")
let _ = git_command()
.args(["rm", "-rf", "--cached", "."])
.current_dir(&clone_path)
.output();
@@ -1577,12 +1577,12 @@ impl<'a> TestContext<'a> {
};
// Step 4: Replace main branch with our new orphan branch
let _ = Command::new("git")
let _ = git_command()
.args(["branch", "-D", "main"])
.current_dir(&clone_path)
.output();
let _ = Command::new("git")
let _ = git_command()
.args(["branch", "-m", "main"])
.current_dir(&clone_path)
.output();
@@ -1730,13 +1730,13 @@ impl<'a> TestContext<'a> {
// Reset to orphan state and create deterministic root commit
// Step 1: Create orphan branch (removes all history)
let _ = Command::new("git")
let _ = git_command()
.args(["checkout", "--orphan", "main-new"])
.current_dir(&clone_path)
.output();
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
let _ = Command::new("git")
let _ = git_command()
.args(["rm", "-rf", "--cached", "."])
.current_dir(&clone_path)
.output();
@@ -1757,12 +1757,12 @@ impl<'a> TestContext<'a> {
};
// Step 4: Replace main branch with our new orphan branch
let _ = Command::new("git")
let _ = git_command()
.args(["branch", "-D", "main"])
.current_dir(&clone_path)
.output();
let _ = Command::new("git")
let _ = git_command()
.args(["branch", "-m", "main"])
.current_dir(&clone_path)
.output();
@@ -1937,12 +1937,12 @@ impl<'a> TestContext<'a> {
}
// Create master branch if needed and push to refs/nostr/<pr-event-id>
let _ = Command::new("git")
let _ = git_command()
.args(["branch", "-M", "master"])
.current_dir(&clone_path)
.output();
let push_output = Command::new("git")
let push_output = git_command()
.args([
"push",
"origin",
@@ -2070,13 +2070,13 @@ impl<'a> TestContext<'a> {
// Reset to orphan state and create deterministic root commit
// Step 1: Create orphan branch (removes all history)
let _ = Command::new("git")
let _ = git_command()
.args(["checkout", "--orphan", "pr-branch"])
.current_dir(&clone_path)
.output();
// Step 2: Clear staged files (orphan keeps files staged from previous branch)
let _ = Command::new("git")
let _ = git_command()
.args(["rm", "-rf", "--cached", "."])
.current_dir(&clone_path)
.output();
@@ -2112,7 +2112,7 @@ impl<'a> TestContext<'a> {
));
}
let push_output = Command::new("git")
let push_output = git_command()
.args([
"push",
"origin",
@@ -2335,9 +2335,9 @@ pub async fn send_and_verify_rejected(
// Git Operation Helpers
// ============================================================
use crate::git_command;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
/// Clone a repository from the relay and return the path
@@ -2368,9 +2368,8 @@ pub fn clone_repo(relay_domain: &str, npub: &str, repo_id: &str) -> Result<PathB
let _ = fs::remove_dir_all(&clone_path);
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
let output = Command::new("git")
let output = git_command()
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.map_err(|e| format!("Failed to execute git clone: {}", e))?;
@@ -2380,11 +2379,11 @@ pub fn clone_repo(relay_domain: &str, npub: &str, repo_id: &str) -> Result<PathB
}
// Configure git user
let _ = Command::new("git")
let _ = git_command()
.args(["config", "user.email", "test@grasp-audit.local"])
.current_dir(&clone_path)
.output();
let _ = Command::new("git")
let _ = git_command()
.args(["config", "user.name", "GRASP Audit Test"])
.current_dir(&clone_path)
.output();
@@ -2417,7 +2416,7 @@ pub fn create_commit(clone_path: &Path, message: &str) -> Result<String, String>
fs::write(&test_file, message).map_err(|e| format!("Failed to write file: {}", e))?;
let filename = test_file.file_name().unwrap().to_str().unwrap();
let output = Command::new("git")
let output = git_command()
.args(["add", filename])
.current_dir(clone_path)
.output()
@@ -2427,7 +2426,7 @@ pub fn create_commit(clone_path: &Path, message: &str) -> Result<String, String>
return Err("Git add failed".to_string());
}
let output = Command::new("git")
let output = git_command()
.args(["-c", "commit.gpgsign=false", "commit", "-m", message])
.current_dir(clone_path)
.output()
@@ -2437,7 +2436,7 @@ pub fn create_commit(clone_path: &Path, message: &str) -> Result<String, String>
return Err("Git commit failed".to_string());
}
let output = Command::new("git")
let output = git_command()
.args(["rev-parse", "HEAD"])
.current_dir(clone_path)
.output()
@@ -2514,7 +2513,7 @@ pub fn create_deterministic_commit_with_variant(
fs::write(&test_file, content).map_err(|e| format!("Failed to write file: {}", e))?;
let output = Command::new("git")
let output = git_command()
.args(["add", "test.txt"])
.current_dir(clone_path)
.output()
@@ -2525,7 +2524,7 @@ pub fn create_deterministic_commit_with_variant(
}
// Create deterministic commit with fixed dates and GPG disabled
let output = Command::new("git")
let output = git_command()
.args(["-c", "commit.gpgsign=false", "commit", "-m", message])
.env("GIT_AUTHOR_DATE", "2024-01-01T00:00:00Z")
.env("GIT_COMMITTER_DATE", "2024-01-01T00:00:00Z")
@@ -2538,7 +2537,7 @@ pub fn create_deterministic_commit_with_variant(
return Err(format!("Git commit failed: {}", stderr));
}
let output = Command::new("git")
let output = git_command()
.args(["rev-parse", "HEAD"])
.current_dir(clone_path)
.output()
@@ -2594,10 +2593,9 @@ pub fn create_deterministic_commit(clone_path: &Path, _message: &str) -> Result<
/// # }
/// ```
pub fn try_push(clone_path: &Path) -> Result<bool, String> {
let output = Command::new("git")
let output = git_command()
.args(["push", "origin", "main", "--force"])
.current_dir(clone_path)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.map_err(|e| format!("Failed to execute git push: {}", e))?;
@@ -2632,10 +2630,9 @@ pub fn try_push(clone_path: &Path) -> Result<bool, String> {
/// # }
/// ```
pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
let output = Command::new("git")
let output = git_command()
.args(["push", "origin", &format!("HEAD:{}", ref_name)])
.current_dir(clone_path)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.map_err(|e| format!("Failed to execute git push: {}", e))?;
@@ -2653,7 +2650,7 @@ pub fn try_push_to_ref(clone_path: &Path, ref_name: &str) -> Result<bool, String
/// * `remote_url` - The remote URL to add as `origin`
pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
// Step 1: git init <path>
let output = Command::new("git")
let output = git_command()
.args(["init", path.to_str().unwrap_or(".")])
.output()
.map_err(|e| format!("Failed to execute git init: {}", e))?;
@@ -2664,7 +2661,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
}
// Step 2: git config user.email
let output = Command::new("git")
let output = git_command()
.args(["config", "user.email", "probe@grasp-audit.local"])
.current_dir(path)
.output()
@@ -2676,7 +2673,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
}
// Step 3: git config user.name
let output = Command::new("git")
let output = git_command()
.args(["config", "user.name", "GRASP Probe"])
.current_dir(path)
.output()
@@ -2688,7 +2685,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
}
// Step 4: git symbolic-ref HEAD refs/heads/main (sets default branch to main)
let output = Command::new("git")
let output = git_command()
.args(["symbolic-ref", "HEAD", "refs/heads/main"])
.current_dir(path)
.output()
@@ -2700,7 +2697,7 @@ pub fn init_local_repo(path: &Path, remote_url: &str) -> Result<(), String> {
}
// Step 5: git remote add origin <remote_url>
let output = Command::new("git")
let output = git_command()
.args(["remote", "add", "origin", remote_url])
.current_dir(path)
.output()
+204
View File
@@ -0,0 +1,204 @@
//! Hermetic git subprocess construction.
//!
//! Fixtures and tests spawn many `git` subprocesses and several of them feed
//! hard-coded deterministic commit hashes. Those hashes (and the surrounding
//! control flow) are only meaningful if every input git reads is supplied by
//! the fixture itself rather than inherited from whatever the host happens to
//! have configured.
use std::process::Command;
/// Build a `git` [`Command`] that is hermetic with respect to ambient git
/// configuration.
///
/// Ambient configuration — the system `/etc/gitconfig`, the user's
/// `~/.gitconfig` or `$XDG_CONFIG_HOME/git/config`, and config injected via
/// the `GIT_CONFIG_COUNT` / `GIT_CONFIG_PARAMETERS` environment — can change
/// fixture behaviour: a failing hook reached through `core.hooksPath` or
/// `init.templateDir` breaks every commit, `init.defaultBranch` renames
/// branches fixtures rely on, and credential helpers can stall network
/// operations waiting for input.
///
/// Every git subprocess spawned by fixtures or tests must be built through
/// this helper. The invocation then sees only configuration it supplies
/// itself: repo-local `git config`, `-c` flags, or `GIT_*` environment
/// variables set at the call site.
///
/// Requires git >= 2.32 (June 2021) for `GIT_CONFIG_GLOBAL` /
/// `GIT_CONFIG_SYSTEM`; the flake pins a far newer git.
pub fn git_command() -> Command {
let mut cmd = Command::new("git");
// Mask the system config and the user-global config in both of its
// locations (~/.gitconfig and $XDG_CONFIG_HOME/git/config).
cmd.env("GIT_CONFIG_GLOBAL", "/dev/null");
cmd.env("GIT_CONFIG_SYSTEM", "/dev/null");
// Fail fast instead of stalling the suite on a credential prompt.
cmd.env("GIT_TERMINAL_PROMPT", "0");
// Config can also be injected through the environment. GIT_CONFIG_KEY_n /
// GIT_CONFIG_VALUE_n are only honoured while GIT_CONFIG_COUNT is set, so
// removing the count neutralises the whole family.
cmd.env_remove("GIT_CONFIG_COUNT");
cmd.env_remove("GIT_CONFIG_PARAMETERS");
// Repository discovery must come from `current_dir`, not from a parent
// process that happens to run inside a git hook, alias, or rebase.
cmd.env_remove("GIT_DIR");
cmd.env_remove("GIT_WORK_TREE");
cmd.env_remove("GIT_INDEX_FILE");
cmd
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures::{
create_deterministic_commit_with_variant, CommitVariant, DETERMINISTIC_COMMIT_HASH,
};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
/// Write a global gitconfig whose settings demonstrably break the fixture
/// recipe when they leak in: `core.hooksPath` and `init.templateDir` both
/// deliver a pre-commit hook that always fails, `init.defaultBranch`
/// renames the initial branch, and gpgsign/autocrlf cover the remaining
/// commit inputs.
fn write_hostile_gitconfig(dir: &Path) -> PathBuf {
let hooks = dir.join("hooks");
fs::create_dir_all(&hooks).expect("create hooks dir");
let hook = hooks.join("pre-commit");
fs::write(
&hook,
"#!/bin/sh\necho 'hostile ambient git config leaked into a fixture' >&2\nexit 1\n",
)
.expect("write pre-commit hook");
fs::set_permissions(&hook, fs::Permissions::from_mode(0o755)).expect("chmod hook");
let template_hooks = dir.join("template").join("hooks");
fs::create_dir_all(&template_hooks).expect("create template hooks dir");
fs::copy(&hook, template_hooks.join("pre-commit")).expect("copy hook into template");
let config = dir.join("gitconfig");
fs::write(
&config,
format!(
"[init]\n\
\tdefaultBranch = main\n\
\ttemplateDir = {template}\n\
[core]\n\
\thooksPath = {hooks}\n\
\tautocrlf = true\n\
[commit]\n\
\tgpgsign = true\n",
template = dir.join("template").display(),
hooks = hooks.display(),
),
)
.expect("write hostile gitconfig");
config
}
/// Restores the mutated process environment even if the test panics.
struct EnvGuard {
saved: Vec<(&'static str, Option<std::ffi::OsString>)>,
}
impl EnvGuard {
fn set(vars: &[(&'static str, &Path)]) -> Self {
let saved = vars
.iter()
.map(|(key, value)| {
let previous = std::env::var_os(key);
std::env::set_var(key, value);
(*key, previous)
})
.collect();
Self { saved }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, previous) in self.saved.drain(..) {
match previous {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
}
fn init_repo_with_identity(git: impl Fn() -> Command, repo: &Path) {
let init = git()
.args(["init", repo.to_str().expect("utf-8 repo path")])
.output()
.expect("spawn git init");
assert!(
init.status.success(),
"git init failed: {}",
String::from_utf8_lossy(&init.stderr)
);
for (key, value) in [
("user.email", "test@grasp-audit.local"),
("user.name", "GRASP Audit Test"),
] {
let config = git()
.args(["config", key, value])
.current_dir(repo)
.output()
.expect("spawn git config");
assert!(
config.status.success(),
"git config {key} failed: {}",
String::from_utf8_lossy(&config.stderr)
);
}
}
/// Regression test for hermeticity: the deterministic commit fixture must
/// produce its pinned hash even when the surrounding process advertises a
/// hostile git configuration.
///
/// The test first proves the hostile configuration is potent (a plain git
/// invocation honouring it cannot commit at all), then proves
/// [`git_command`]-based fixtures neutralise it.
#[test]
fn deterministic_commit_survives_hostile_ambient_git_config() {
let dir = tempfile::tempdir().expect("create temp dir");
let hostile_config = write_hostile_gitconfig(dir.path());
// Potency check: a non-hermetic git honouring the hostile config must
// fail to commit (the pre-commit hook fires). If this stops failing,
// the hostile config has rotted and no longer guards anything.
let raw_repo = dir.path().join("raw");
let raw_git = || {
let mut cmd = Command::new("git");
cmd.env("GIT_CONFIG_GLOBAL", &hostile_config);
cmd.env("GIT_CONFIG_SYSTEM", "/dev/null");
cmd
};
init_repo_with_identity(raw_git, &raw_repo);
let err = create_deterministic_commit_with_variant(&raw_repo, CommitVariant::Owner)
.expect_err("hostile config should break a commit that inherits it");
assert!(
err.contains("hostile ambient git config leaked"),
"expected the hostile hook to fire, got: {err}"
);
// Hermetic check: with the hostile config exposed the way a real host
// exposes it (global/system config paths in the environment), the
// fixture recipe must still produce the pinned deterministic hash.
let _guard = EnvGuard::set(&[
("HOME", dir.path()),
("GIT_CONFIG_GLOBAL", &hostile_config),
("GIT_CONFIG_SYSTEM", &hostile_config),
]);
let hermetic_repo = dir.path().join("hermetic");
init_repo_with_identity(git_command, &hermetic_repo);
let hash = create_deterministic_commit_with_variant(&hermetic_repo, CommitVariant::Owner)
.expect("hermetic fixture commit should succeed under hostile ambient config");
assert_eq!(
hash, DETERMINISTIC_COMMIT_HASH,
"deterministic hash must not depend on ambient git configuration"
);
}
}
+2
View File
@@ -31,6 +31,7 @@
pub mod audit;
pub mod client;
pub mod fixtures;
pub mod git;
pub mod isolation;
pub mod probe;
pub mod result;
@@ -62,6 +63,7 @@ pub use fixtures::{
PR_TEST_COMMIT_HASH,
RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
};
pub use git::git_command;
pub use probe::{run_probe, ProbeCheck, ProbeReport};
pub use result::{AuditResult, TestResult};
+3 -4
View File
@@ -15,11 +15,11 @@
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
//! ```
use crate::git_command;
use crate::specs::grasp01::SpecRef;
use crate::{AuditClient, FixtureKind, TestContext, TestResult};
use nostr_sdk::prelude::*;
use std::fs;
use std::process::Command;
/// Test suite for Git clone operations
pub struct GitCloneTests;
@@ -106,7 +106,7 @@ impl GitCloneTests {
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
// Attempt to clone the repository
let output = Command::new("git")
let output = git_command()
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
.env("GIT_TERMINAL_PROMPT", "0") // Disable password prompts
.output();
@@ -229,9 +229,8 @@ impl GitCloneTests {
let invalid_url = format!("http://{}/invalid/path", relay_domain);
let output = Command::new("git")
let output = git_command()
.args(["clone", &invalid_url, clone_path.to_str().unwrap()])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.unwrap();
+4 -7
View File
@@ -22,11 +22,11 @@
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
//! ```
use crate::git_command;
use crate::specs::grasp01::SpecRef;
use crate::{AuditClient, FixtureKind, TestContext, TestResult};
use nostr_sdk::prelude::*;
use std::fs;
use std::process::Command;
/// Test suite for Git filter capability operations
pub struct GitFilterTests;
@@ -223,14 +223,13 @@ impl GitFilterTests {
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
// Attempt filtered clone with blob:none
let output = Command::new("git")
let output = git_command()
.args([
"clone",
"--filter=blob:none",
&clone_url,
clone_path.to_str().unwrap(),
])
.env("GIT_TERMINAL_PROMPT", "0")
.output();
// Clean up
@@ -334,14 +333,13 @@ impl GitFilterTests {
let clone_url = format!("http://{}/{}/{}.git", relay_domain, npub, repo_id);
// First do a shallow clone to have a repository to fetch into
let clone_output = Command::new("git")
let clone_output = git_command()
.args([
"clone",
"--depth=1",
&clone_url,
clone_path.to_str().unwrap(),
])
.env("GIT_TERMINAL_PROMPT", "0")
.output();
// Clean up
@@ -360,10 +358,9 @@ impl GitFilterTests {
}
// Now attempt a filtered fetch
let output = Command::new("git")
let output = git_command()
.args(["fetch", "--filter=tree:0", "origin"])
.current_dir(&clone_path)
.env("GIT_TERMINAL_PROMPT", "0")
.output();
let output = match output {
@@ -31,6 +31,7 @@
#[allow(dead_code)]
const PR_TEST_COMMIT_HASH: &str = "5a51b30e4615b572dcd5b9e487861b58605a5c21";
use crate::git_command;
use crate::specs::grasp01::SpecRef;
use crate::{
clone_repo, create_commit, create_deterministic_commit_with_variant, try_push, try_push_to_ref,
@@ -40,7 +41,6 @@ use crate::{
use nostr_sdk::prelude::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
// ============================================================
@@ -66,13 +66,13 @@ use std::time::Duration;
fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
// Step 1: Clean up any tracked files in the working directory
// This ensures we start with a clean slate
let _ = Command::new("git")
let _ = git_command()
.args(["clean", "-fd"])
.current_dir(clone_path)
.output();
// Step 2: Create orphan branch (removes all history)
let output = Command::new("git")
let output = git_command()
.args(["checkout", "--orphan", "pr-test-branch"])
.current_dir(clone_path)
.output()
@@ -86,7 +86,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
}
// Step 3: Remove ALL files from the index (staging area)
let output = Command::new("git")
let output = git_command()
.args(["rm", "-rf", "--cached", "."])
.current_dir(clone_path)
.output()
@@ -123,7 +123,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
create_deterministic_commit_with_variant(clone_path, CommitVariant::PRTestCommit)?;
// Step 6: Verify this is actually a root commit (no parent)
let output = Command::new("git")
let output = git_command()
.args(["rev-list", "--max-parents=0", "HEAD"])
.current_dir(clone_path)
.output()
@@ -139,12 +139,12 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
}
// Step 7: Replace main branch with our new orphan branch
let _ = Command::new("git")
let _ = git_command()
.args(["branch", "-D", "main"])
.current_dir(clone_path)
.output();
let output = Command::new("git")
let output = git_command()
.args(["branch", "-m", "main"])
.current_dir(clone_path)
.output()
@@ -160,7 +160,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
// Step 8: Verify commit hash matches expected
if commit_hash != PR_TEST_COMMIT_HASH {
// Debug: Show what's in the commit
let tree_output = Command::new("git")
let tree_output = git_command()
.args(["ls-tree", "-r", "HEAD"])
.current_dir(clone_path)
.output();
@@ -168,7 +168,7 @@ fn create_pr_test_commit(clone_path: &Path) -> Result<String, String> {
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_else(|_| "Failed to get tree".to_string());
let cat_output = Command::new("git")
let cat_output = git_command()
.args(["cat-file", "-p", "HEAD"])
.current_dir(clone_path)
.output();
@@ -254,7 +254,7 @@ fn reset_to_correct_pr_commit(clone_path: &Path) -> Result<String, String> {
/// Returns Ok(true) if push succeeded, Ok(false) if rejected, Err on git error.
#[allow(dead_code)]
fn push_to_pr_ref(clone_path: &Path, pr_event_id: &str) -> Result<bool, String> {
let push_output = Command::new("git")
let push_output = git_command()
.args([
"push",
"--force",
@@ -332,7 +332,7 @@ async fn get_default_branch_from_info_refs(
/// Checks if a ref exists on the remote.
#[allow(dead_code)]
fn ref_exists_on_remote(clone_path: &Path, ref_name: &str) -> Result<bool, String> {
let output = Command::new("git")
let output = git_command()
.args(["ls-remote", "origin", ref_name])
.current_dir(clone_path)
.output()
@@ -541,8 +541,6 @@ impl PushAuthorizationTests {
client: &AuditClient,
relay_domain: &str,
) -> TestResult {
use std::process::Command;
let test_name = "test_push_rejected_wrong_commit";
// ============================================================
@@ -617,7 +615,7 @@ impl PushAuthorizationTests {
};
// Create/checkout main branch
let branch_output = Command::new("git")
let branch_output = git_command()
.args(["checkout", "-B", "main"])
.current_dir(&clone_path)
.output();
@@ -1568,7 +1566,7 @@ impl PushAuthorizationTests {
// ============================================================
// Step 4: Create and checkout develop1 branch, then create unique commit
// ============================================================
let output = Command::new("git")
let output = git_command()
.args(["checkout", "-b", "develop1"])
.current_dir(&clone_path)
.output();
@@ -1683,14 +1681,13 @@ mod tests {
#[test]
fn test_pr_test_commit_hash_discovery() {
use std::fs;
use std::process::Command;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path();
// Initialize git repo
let output = Command::new("git")
let output = git_command()
.args(["init"])
.current_dir(path)
.output()
@@ -1702,14 +1699,14 @@ mod tests {
);
// Configure git user - use same identity as clone_repo in fixtures.rs
let output = Command::new("git")
let output = git_command()
.args(["config", "user.email", "test@grasp-audit.local"])
.current_dir(path)
.output()
.expect("git config email failed");
assert!(output.status.success(), "git config email failed");
let output = Command::new("git")
let output = git_command()
.args(["config", "user.name", "GRASP Audit Test"])
.current_dir(path)
.output()
@@ -1721,7 +1718,7 @@ mod tests {
fs::write(&test_file, "PR test deterministic commit\n").expect("Failed to write test file");
// Add the file
let output = Command::new("git")
let output = git_command()
.args(["add", "test.txt"])
.current_dir(path)
.output()
@@ -1733,7 +1730,7 @@ mod tests {
);
// Create deterministic commit with fixed dates and GPG disabled
let output = Command::new("git")
let output = git_command()
.args([
"-c",
"commit.gpgsign=false",
@@ -1753,7 +1750,7 @@ mod tests {
);
// Get the commit hash
let output = Command::new("git")
let output = git_command()
.args(["rev-parse", "HEAD"])
.current_dir(path)
.output()
+3 -8
View File
@@ -48,6 +48,7 @@
//! sleep before the ref-mirror probe — keeps the total wait close to
//! the original 2 s while making the two failure modes attributable.
use crate::git_command;
use crate::specs::grasp06::SpecRef;
use crate::{
create_commit, init_local_repo, try_push_to_ref, AuditClient, FixtureKind, TestContext,
@@ -56,7 +57,6 @@ use crate::{
use nostr_sdk::prelude::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
pub struct MirroringTests;
@@ -619,9 +619,8 @@ impl MirroringTests {
// had ample time to run. A negative result here is a real
// "didn't happen, won't happen" signal.
let refname = format!("refs/nostr/{}", pr_event_id);
let out = Command::new("git")
let out = git_command()
.args(["ls-remote", &prs_url, &refname])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.map_err(|e| format!("Failed to execute git ls-remote {}: {}", prs_url, e))?;
@@ -707,9 +706,5 @@ impl Drop for TempPath {
/// Run a `git` command in `cwd` and return the [`std::process::Output`]
/// unchanged. The caller decides whether non-zero exit is failure.
fn run_git(cwd: &Path, args: &[&str]) -> std::io::Result<std::process::Output> {
Command::new("git")
.args(args)
.current_dir(cwd)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
git_command().args(args).current_dir(cwd).output()
}
+6 -10
View File
@@ -5,6 +5,7 @@
//! Each test here maps 1:1 to a MUST in the spec, or to an audit-derived
//! invariant that follows directly from NIP-11 discovery semantics.
use crate::git_command;
use crate::specs::grasp06::fixtures::advertises_grasp;
use crate::specs::grasp06::SpecRef;
use crate::{
@@ -13,7 +14,6 @@ use crate::{
use nostr_sdk::prelude::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
pub struct PrsEndpointTests;
@@ -164,9 +164,8 @@ impl PrsEndpointTests {
let _ = fs::remove_dir_all(&clone_path);
};
let clone_output = Command::new("git")
let clone_output = git_command()
.args(["clone", &clone_url, clone_path.to_str().unwrap()])
.env("GIT_TERMINAL_PROMPT", "0")
.output();
let clone_output = match clone_output {
@@ -186,7 +185,7 @@ impl PrsEndpointTests {
// 4. Verify the cloned repo has zero refs. `for-each-ref` lists
// every ref one-per-line; empty stdout means no refs at all,
// which is the spec's "empty bare repository" invariant.
let refs_output = Command::new("git")
let refs_output = git_command()
.args(["-C", clone_path.to_str().unwrap(), "for-each-ref"])
.output();
@@ -520,9 +519,8 @@ impl PushValidationTests {
// empty stdout means the ref was deleted (or never written).
// A failed ls-remote (404 / connection error) is a setup
// failure — the /prs/ endpoint must be reachable.
let ls_out = Command::new("git")
let ls_out = git_command()
.args(["ls-remote", &prs_url, &refname])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.map_err(|e| format!("Failed to execute git ls-remote {}: {}", prs_url, e))?;
@@ -670,10 +668,9 @@ impl Drop for LocalWorkspace2 {
/// Run a `git` command in `cwd` and require success. Returns the stderr text
/// on failure so the caller's error message contains the actual git output.
fn run_git(cwd: &Path, args: &[&str]) -> Result<(), String> {
let output = Command::new("git")
let output = git_command()
.args(args)
.current_dir(cwd)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.map_err(|e| format!("Failed to execute git {:?}: {}", args, e))?;
if !output.status.success() {
@@ -687,10 +684,9 @@ fn run_git(cwd: &Path, args: &[&str]) -> Result<(), String> {
/// about the exit code — the caller decides whether success or failure is
/// the spec-correct outcome.
fn git_push(cwd: &Path, url: &str, refname: &str) -> std::io::Result<std::process::Output> {
Command::new("git")
git_command()
.args(["push", url, &format!("HEAD:{}", refname)])
.current_dir(cwd)
.env("GIT_TERMINAL_PROMPT", "0")
.output()
}