test(redact): fixture git and hook installs share gitEnv(), so a global core.hooksPath is never rewritten (#3353) (#3384)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-25 12:14:02 +00:00
1 parent e2fffee311
commit 0ea3dc8986
10 files changed
+127 -75

No files matched your search

+12
View File
@@ -1,5 +1,17 @@
# Changelog
## [1.91.209.0] - 2026-10-25
**The free test suite no longer rewrites a contributor's global git pre-push hook (#3353).** On a machine with a global `core.hooksPath`, `test/redact-prepush-target.test.ts` and `test/fs-utils.test.ts` ran `gstack-redact install-prepush-hook` with the parent environment. Git resolved the hooks directory to the global one, so the install moved the developer's own `pre-push` to `pre-push.local` and left a managed wrapper pointing into the test checkout, which breaks every later push once that checkout is gone. The same config also failed 4 tests in the target file, 5 in `test/redact-prepush-hook.test.ts` and 1 in `fs-utils`. CI has no global hooks path, so it stayed green.
### Fixed
- **`test/helpers/git-env.ts`** (new): `gitEnv()` is the one fixture environment for git and hook installs: `GIT_CONFIG_GLOBAL=/dev/null`, `GIT_CONFIG_NOSYSTEM=1`, `GIT_CONFIG_COUNT=0` and no `GIT_CONFIG_PARAMETERS`. Config written inside the fixture repo still applies.
- `test/redact-prepush-target.test.ts`, `test/redact-prepush-hook.test.ts` and `test/fs-utils.test.ts` pass it to every fixture git, install and hook spawn. `test/redact-prepush-scan-range.test.ts` and `test/redact-prepush-rebase-force-push.test.ts` drop their private copies (#2967) and import it.
### Tests
- A new case in `test/redact-prepush-target.test.ts` sets a global, system and environment `core.hooksPath` to a hook that fails, builds the fixture, and checks the developer's hooks directory is untouched, the managed hook landed in the fixture repo, and a push never runs the developer's hook. It fails without the shared environment.
- Rerun of the issue's repro with `HOME` pointing at a gitconfig with a global hooks dir: on main, two files rewrote it and three files had failures; with this change all six `redact-prepush-*` and `fs-utils` files pass and the directory is unchanged.
## [1.91.208.0] - 2026-10-25
**The Capy install guide says that the team-mode session-start updater also waits for live workflow leases.** With #3385 (#3363), `bin/gstack-session-update` skips its background fast-forward while another session holds a live workflow lease, the same leases `bin/gstack-upgrade` already honors. `docs/capy-install.md` described only the upgrade side.
+1 -1
View File
@@ -1 +1 @@
1.91.208.0
1.91.209.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.208.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.209.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.208",
"version": "1.91.209",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+3 -1
View File
@@ -14,6 +14,7 @@ import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { spawnSync } from "child_process";
import { gitEnv } from "./helpers/git-env";
import { mkdirpSync } from "../lib/fs-utils";
const REDACT = path.resolve(import.meta.dir, "..", "bin", "gstack-redact");
@@ -93,7 +94,7 @@ describe("install-prepush-hook under bun-on-Windows EEXIST semantics (#2635)", (
const base = tmpdir();
try {
const repo = path.join(base, "repo");
spawnSync("git", ["init", "-q", repo], { timeout: 30_000 });
spawnSync("git", ["init", "-q", repo], { timeout: 30_000, env: gitEnv() });
const hookDir = path.join(repo, ".git", "hooks");
fs.mkdirSync(hookDir, { recursive: true });
const hookPath = path.join(hookDir, "pre-push");
@@ -106,6 +107,7 @@ describe("install-prepush-hook under bun-on-Windows EEXIST semantics (#2635)", (
cwd: repo,
encoding: "utf8",
timeout: 30_000,
env: gitEnv(),
});
expect(r.status).toBe(0);
expect(r.stderr ?? "").not.toContain("EEXIST");
+20
View File
@@ -0,0 +1,20 @@
/**
* Environment for fixture git commands and `gstack-redact install-prepush-hook`.
*
* With the parent environment, a developer's global or system core.hooksPath
* (or the same key via GIT_CONFIG_COUNT / GIT_CONFIG_PARAMETERS) reaches the
* fixture: `git rev-parse --git-path hooks` resolves to that directory, so the
* install rewrites the developer's own pre-push (#3353), and their hook runs
* on fixture pushes and rejects the local remote (#2967). /dev/null drops the
* global file on every platform: Git for Windows rejects NUL (see
* EMPTY_GLOBAL_GIT_CONFIG in lib/cso/process.ts). Config written inside the
* fixture repo still applies.
*/
export function gitEnv(extra: Record<string, string | undefined> = {}): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...process.env, ...extra };
env.GIT_CONFIG_GLOBAL = "/dev/null";
env.GIT_CONFIG_NOSYSTEM = "1";
env.GIT_CONFIG_COUNT = "0";
delete env.GIT_CONFIG_PARAMETERS;
return env;
}
+17 -14
View File
@@ -13,6 +13,7 @@ import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { spawnSync } from "child_process";
import { gitEnv } from "./helpers/git-env";
const PREPUSH = path.resolve(import.meta.dir, "..", "bin", "gstack-redact-prepush");
const REDACT = path.resolve(import.meta.dir, "..", "bin", "gstack-redact");
@@ -21,7 +22,7 @@ let repo: string;
let stateHome: string;
function git(args: string[], cwd = repo): string {
const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000 });
const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000, env: gitEnv() });
return r.stdout?.trim() ?? "";
}
@@ -40,7 +41,7 @@ function runHook(
cwd: repo,
input: Buffer.from(stdinLines),
encoding: "utf8",
env: { ...process.env, ...env },
env: gitEnv(env),
timeout: 30_000,
});
return { code: r.status ?? 0, stderr: r.stderr ?? "" };
@@ -439,7 +440,7 @@ describe("install UX surfaces (#1946 / eng review D3+D10)", () => {
const r = spawnSync("bash", ["-c", script], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], // stdin not a TTY
env: { ...process.env, GSTACK_HOME: home },
env: gitEnv({ GSTACK_HOME: home }),
timeout: 15_000,
});
expect(r.status).toBe(0);
@@ -467,7 +468,7 @@ describe("install UX surfaces (#1946 / eng review D3+D10)", () => {
const r = spawnSync("bash", ["-c", script], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, GSTACK_HOME: home },
env: gitEnv({ GSTACK_HOME: home }),
timeout: 15_000,
});
expect(r.status).toBe(0);
@@ -502,7 +503,7 @@ describe("install / chaining", () => {
const existing = path.join(hookDir, "pre-push");
fs.writeFileSync(existing, "#!/usr/bin/env bash\necho mine\n", { mode: 0o755 });
const r = spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const r = spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(r.status).toBe(0);
const installed = fs.readFileSync(existing, "utf8");
expect(installed).toContain("gstack-redact pre-push (managed)");
@@ -516,7 +517,7 @@ describe("install / chaining", () => {
test("chained pre-push.local receives the final ref line (trailing newline preserved)", () => {
const hookDir = path.join(repo, ".git", "hooks");
fs.mkdirSync(hookDir, { recursive: true });
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000 });
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000, env: gitEnv() });
const seen = path.join(repo, "seen.txt");
fs.writeFileSync(
@@ -531,7 +532,7 @@ describe("install / chaining", () => {
cwd: repo,
input: Buffer.from(line),
encoding: "utf8",
env: { ...process.env, GSTACK_REDACT_PREPUSH: "skip", GSTACK_HOME: stateHome },
env: gitEnv({ GSTACK_REDACT_PREPUSH: "skip", GSTACK_HOME: stateHome }),
timeout: 30_000,
});
expect(r.status).toBe(0);
@@ -545,7 +546,7 @@ describe("install / chaining", () => {
test("a blocking pre-push.local still short-circuits the push", () => {
const hookDir = path.join(repo, ".git", "hooks");
fs.mkdirSync(hookDir, { recursive: true });
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000 });
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000, env: gitEnv() });
fs.writeFileSync(
path.join(hookDir, "pre-push.local"),
"#!/usr/bin/env bash\nwhile read -r _a _b _c _d || [ -n \"${_a:-}\" ]; do exit 1; done\nexit 0\n",
@@ -555,7 +556,7 @@ describe("install / chaining", () => {
cwd: repo,
input: Buffer.from(`refs/heads/main ${"b".repeat(40)} refs/heads/main ${ZERO}\n`),
encoding: "utf8",
env: { ...process.env, GSTACK_REDACT_PREPUSH: "skip", GSTACK_HOME: stateHome },
env: gitEnv({ GSTACK_REDACT_PREPUSH: "skip", GSTACK_HOME: stateHome }),
timeout: 30_000,
});
expect(r.status).toBe(1);
@@ -603,6 +604,7 @@ describe("install / chaining", () => {
timeout: 30_000,
cwd: repo,
encoding: "utf8",
env: gitEnv(),
});
expect(r.status).toBe(0);
expect(r.stdout).toContain("refreshed stale managed pre-push hook");
@@ -618,7 +620,7 @@ describe("install / chaining", () => {
cwd: repo,
input: Buffer.from(`refs/heads/main ${sha} refs/heads/main ${ZERO}\n`),
encoding: "utf8",
env: { ...process.env, GSTACK_REDACT_PREPUSH: "skip", GSTACK_HOME: stateHome },
env: gitEnv({ GSTACK_REDACT_PREPUSH: "skip", GSTACK_HOME: stateHome }),
});
expect(run.status).toBe(0);
expect(fs.readFileSync(path.join(stateHome, "security", "prepush-skip.jsonl"), "utf8")).toContain("env-skip");
@@ -630,13 +632,14 @@ describe("install / chaining", () => {
fs.mkdirSync(hookDir, { recursive: true });
const hook = path.join(hookDir, "pre-push");
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo });
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, env: gitEnv() });
const first = fs.readFileSync(hook, "utf8");
const stamp = fs.statSync(hook).mtimeMs;
const again = spawnSync("bun", [REDACT, "install-prepush-hook"], {
cwd: repo,
encoding: "utf8",
env: gitEnv(),
});
expect(again.status).toBe(0);
expect(again.stdout).toContain("already installed");
@@ -651,8 +654,8 @@ describe("install / chaining", () => {
fs.writeFileSync(path.join(hookDir, "pre-push"), "#!/usr/bin/env bash\necho mine\n", {
mode: 0o755,
});
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000 });
spawnSync("bun", [REDACT, "uninstall-prepush-hook"], { cwd: repo, timeout: 30_000 });
spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000, env: gitEnv() });
spawnSync("bun", [REDACT, "uninstall-prepush-hook"], { cwd: repo, timeout: 30_000, env: gitEnv() });
const restored = fs.readFileSync(path.join(hookDir, "pre-push"), "utf8");
expect(restored).toContain("echo mine");
expect(restored).not.toContain("managed");
@@ -671,7 +674,7 @@ describe("base resolution when the default branch is neither main nor master", (
// blocks the push having scanned NOTHING — the "scans more, never less"
// fallback inverting into "scans nothing".
const bare = fs.mkdtempSync(path.join(os.tmpdir(), "prepush-remote-"));
spawnSync("git", ["init", "-q", "--bare", "-b", "trunk", bare], { timeout: 30_000 });
spawnSync("git", ["init", "-q", "--bare", "-b", "trunk", bare], { timeout: 30_000, env: gitEnv() });
git(["branch", "-M", "trunk"]);
const old = commit("legacy.txt", FAKE_AWS_KEY + "\n", "secret already on the remote");
+1 -11
View File
@@ -31,23 +31,13 @@ import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { spawnSync } from "child_process";
import { gitEnv } from "./helpers/git-env";
const PREPUSH = path.resolve(import.meta.dir, "..", "bin", "gstack-redact-prepush");
let repo: string;
let remote: string;
// Same blanking as test/redact-prepush-scan-range.test.ts (#2967). A
// hooksPath written into the fixture repo still runs.
function gitEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...process.env };
env.GIT_CONFIG_GLOBAL = "/dev/null";
env.GIT_CONFIG_NOSYSTEM = "1";
env.GIT_CONFIG_COUNT = "0";
delete env.GIT_CONFIG_PARAMETERS;
return env;
}
function git(args: string[], cwd = repo): string {
const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000, env: gitEnv() });
if (r.status !== 0) throw new Error(`git ${args.join(" ")}\n${r.stderr}`);
+1 -15
View File
@@ -17,25 +17,11 @@
*/
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { spawnSync } from "child_process";
import { gitEnv } from "./helpers/git-env";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { dirname, join } from "path";
// Fixture git was spawned with the parent environment. A global or system
// core.hooksPath, or the same key via GIT_CONFIG_COUNT /
// GIT_CONFIG_PARAMETERS, runs on these pushes and rejects the local remote
// (#2967). /dev/null drops the global file on every platform: Git for
// Windows rejects NUL (see EMPTY_GLOBAL_GIT_CONFIG in lib/cso/process.ts).
// Config written inside the fixture repo still applies.
function gitEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...process.env };
env.GIT_CONFIG_GLOBAL = "/dev/null";
env.GIT_CONFIG_NOSYSTEM = "1";
env.GIT_CONFIG_COUNT = "0";
delete env.GIT_CONFIG_PARAMETERS;
return env;
}
let dir: string;
const run = (args: string[], cwd = dir): string => {
const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000, env: gitEnv() });
+70 -31
View File
@@ -4,13 +4,14 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { scan } from "../lib/redact-engine";
import { gitEnv } from "./helpers/git-env";
const REDACT = path.resolve(import.meta.dir, "../bin/gstack-redact");
const roots: string[] = [];
const key = ["AKIA", "1234567890ABCDEF"].join("");
function git(cwd: string, ...args: string[]): string {
const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000 });
const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000, env: gitEnv() });
if (result.status !== 0) throw new Error(`git ${args[0]} failed: ${result.stderr}`);
return result.stdout.trim();
}
@@ -35,7 +36,7 @@ function fixture(initialSecret = true, sha256 = false): { repo: string; origin:
git(repo, "remote", "add", "origin", origin);
git(repo, "push", "-q", "-u", "origin", "main");
git(repo, "remote", "add", "publish", publish);
const install = spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const install = spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(install.status).toBe(0);
return { repo, origin, publish, head };
}
@@ -58,11 +59,49 @@ afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
describe("fixture installs ignore the developer's hook config (#3353)", () => {
test("a global, system or environment core.hooksPath is neither rewritten nor run", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "prepush-target-global-"));
roots.push(root);
const hooks = path.join(root, "global-hooks");
fs.mkdirSync(hooks);
const original = "#!/bin/sh\necho developer-hook >&2\nexit 1\n";
fs.writeFileSync(path.join(hooks, "pre-push"), original, { mode: 0o755 });
const config = path.join(root, "gitconfig");
fs.writeFileSync(config, `[core]\n\thooksPath = ${JSON.stringify(hooks)}\n`);
const keys = ["GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_COUNT",
"GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0", "GIT_CONFIG_PARAMETERS"] as const;
const prev = Object.fromEntries(keys.map(key => [key, process.env[key]]));
try {
process.env.GIT_CONFIG_GLOBAL = config;
process.env.GIT_CONFIG_SYSTEM = config;
delete process.env.GIT_CONFIG_NOSYSTEM;
process.env.GIT_CONFIG_COUNT = "1";
process.env.GIT_CONFIG_KEY_0 = "core.hooksPath";
process.env.GIT_CONFIG_VALUE_0 = hooks;
process.env.GIT_CONFIG_PARAMETERS = `'core.hooksPath=${hooks}'`;
const { repo } = fixture(false);
expect(fs.readdirSync(hooks)).toEqual(["pre-push"]);
expect(fs.readFileSync(path.join(hooks, "pre-push"), "utf8")).toBe(original);
const installed = git(repo, "rev-parse", "--path-format=absolute", "--git-path", "hooks/pre-push");
expect(fs.readFileSync(installed, "utf8")).toContain("gstack-redact pre-push (managed)");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status, push.stderr).toBe(0);
expect(push.stderr).not.toContain("developer-hook");
} finally {
for (const [key, value] of Object.entries(prev)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
});
});
describe("installed pre-push guard uses the actual destination", () => {
test("origin tracking at HEAD cannot excuse the same credential on first push to publish", () => {
const { repo, publish, head } = fixture();
expect(git(repo, "rev-parse", "origin/main")).toBe(head);
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
@@ -71,7 +110,7 @@ describe("installed pre-push guard uses the actual destination", () => {
test("a configured remote's different push URL cannot reuse its fetch tracking tip", () => {
const { repo, publish } = fixture();
git(repo, "remote", "set-url", "--push", "origin", publish);
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
@@ -92,7 +131,7 @@ describe("installed pre-push guard uses the actual destination", () => {
git(repo, "update-ref", "refs/heads/publish/main", tip);
expect(git(repo, "rev-parse", "publish/main")).toBe(tip);
expect(git(repo, "rev-parse", "refs/remotes/publish/main")).toBe(base);
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
@@ -108,7 +147,7 @@ describe("installed pre-push guard uses the actual destination", () => {
expect(git(repo, "rev-parse", "refs/remotes/publish/main")).toBe(git(repo, "rev-parse", "HEAD"));
git(repo, "remote", "set-url", "--add", "publish", second);
expect(git(repo, "remote", "get-url", "publish")).toBe(publish);
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(second, "for-each-ref", "--format=%(refname)")).toBe("");
@@ -116,7 +155,7 @@ describe("installed pre-push guard uses the actual destination", () => {
test("direct URL push cannot borrow origin's tracking history", () => {
const { repo, publish } = fixture();
const push = spawnSync("git", ["push", publish, "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", publish, "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
@@ -125,8 +164,8 @@ describe("installed pre-push guard uses the actual destination", () => {
test("an advertised remote tip absent from local objects cannot justify a guessed base", () => {
const { repo, publish } = fixture();
const remoteTip = seedIndependentRemote(repo, publish);
expect(spawnSync("git", ["cat-file", "-e", remoteTip], { cwd: repo, timeout: 30_000 }).status).not.toBe(0);
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
expect(spawnSync("git", ["cat-file", "-e", remoteTip], { cwd: repo, timeout: 30_000, env: gitEnv() }).status).not.toBe(0);
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(publish, "rev-parse", "refs/heads/main")).toBe(remoteTip);
@@ -135,21 +174,21 @@ describe("installed pre-push guard uses the actual destination", () => {
test("an absent advertised tip permits a clean forced update after conservative scanning", () => {
const { repo, publish, head } = fixture(false);
const remoteTip = seedIndependentRemote(repo, publish);
expect(spawnSync("git", ["cat-file", "-e", remoteTip], { cwd: repo, timeout: 30_000 }).status).not.toBe(0);
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
expect(spawnSync("git", ["cat-file", "-e", remoteTip], { cwd: repo, timeout: 30_000, env: gitEnv() }).status).not.toBe(0);
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(git(publish, "rev-parse", "refs/heads/main")).toBe(head);
});
test("new SHA-256 branch scans the correct empty tree and allows a clean push", () => {
const { repo, publish } = fixture(false, true);
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(git(publish, "rev-parse", "refs/heads/main")).toHaveLength(64);
fs.writeFileSync(path.join(repo, "leak.txt"), `key ${key}\n`);
git(repo, "add", "leak.txt");
git(repo, "commit", "-qm", "new secret");
const blocked = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const blocked = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(blocked.status).toBe(1);
expect(blocked.stderr).toContain("aws.access_key");
});
@@ -164,7 +203,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "clean.txt"), "safe update\n");
git(repo, "add", "clean.txt");
git(repo, "commit", "-qm", "clean branch");
const push = spawnSync("git", ["push", "origin", "main", "leak"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main", "leak"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
@@ -183,7 +222,7 @@ describe("installed pre-push guard uses the actual destination", () => {
git(repo, "add", "clean.txt");
git(repo, "commit", "-qm", "clean update");
const head = git(repo, "rev-parse", "HEAD");
const push = spawnSync("git", ["push", "origin", "main", ":disposable"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main", ":disposable"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
expect(git(origin, "for-each-ref", "--format=%(refname)", "refs/heads/disposable")).toBe("");
@@ -193,7 +232,7 @@ describe("installed pre-push guard uses the actual destination", () => {
const { repo, origin, head } = fixture(false);
const hook = git(repo, "rev-parse", "--git-path", "hooks/pre-push");
const result = spawnSync("bash", [hook, "origin", origin], {
cwd: repo, input: "refs/heads/main malformed\n", encoding: "utf8", timeout: 30_000,
cwd: repo, input: "refs/heads/main malformed\n", encoding: "utf8", timeout: 30_000, env: gitEnv(),
});
expect(result.status, result.stderr || result.error?.message).toBe(1);
expect(result.stderr).toContain("could not parse a pre-push ref line");
@@ -208,7 +247,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "near seam");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.secret_key");
});
@@ -221,7 +260,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "unlabeled seam");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(push.stderr).not.toContain("aws.secret_key");
});
@@ -232,7 +271,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "one finding at seam");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr.match(/HIGH aws\.access_key/g)).toHaveLength(1);
});
@@ -247,7 +286,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "supplementary normalization seam");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr.match(/HIGH aws\.access_key/g)).toHaveLength(1);
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
@@ -264,7 +303,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "mid-line control");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(push.stderr).not.toContain("MEDIUM finding");
});
@@ -277,7 +316,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "embedded key control");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(push.stderr).not.toContain("aws.access_key");
expect(git(origin, "rev-parse", "refs/heads/main")).not.toBe(head);
@@ -294,7 +333,7 @@ describe("installed pre-push guard uses the actual destination", () => {
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "long bearer advisory");
const tip = git(repo, "rev-parse", "HEAD");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(push.stderr).toContain("MEDIUM finding");
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(tip);
@@ -308,7 +347,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "invisible seam");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.secret_key");
});
@@ -320,7 +359,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "unscannable context");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("engine.input_too_large");
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
@@ -339,7 +378,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
git(repo, "add", "payload.txt");
git(repo, "commit", "-qm", "normalization seam");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(expected ? 1 : 0);
expect(push.stderr.includes("aws.secret_key")).toBe(expected);
});
@@ -350,19 +389,19 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "large.txt"), "ordinary content\n".repeat(160_000));
git(repo, "add", "large.txt");
git(repo, "commit", "-qm", "large clean diff");
let push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000 });
let push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000, env: gitEnv() });
expect(push.status).toBe(0);
fs.writeFileSync(path.join(repo, "long.txt"), "x".repeat(900_000) + "\n");
git(repo, "add", "long.txt");
git(repo, "commit", "-qm", "long clean line");
push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000 });
push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000, env: gitEnv() });
expect(push.status).toBe(0);
const priorTip = git(origin, "rev-parse", "refs/heads/main");
expect(priorTip).not.toBe(head);
fs.writeFileSync(path.join(repo, "too-long.txt"), "x".repeat(1_100_000) + "\n");
git(repo, "add", "too-long.txt");
git(repo, "commit", "-qm", "over cap line");
push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000 });
push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("engine.input_too_large");
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(priorTip);
@@ -373,7 +412,7 @@ describe("installed pre-push guard uses the actual destination", () => {
fs.writeFileSync(path.join(repo, "long.txt"), `${"x".repeat(900_000)} key ${key}\n`);
git(repo, "add", "long.txt");
git(repo, "commit", "-qm", "long line with key");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(1);
expect(push.stderr).toContain("aws.access_key");
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
@@ -385,7 +424,7 @@ describe("installed pre-push guard uses the actual destination", () => {
git(repo, "add", "long.txt");
git(repo, "commit", "-qm", "neighboring long lines");
const tip = git(repo, "rev-parse", "HEAD");
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
expect(push.status).toBe(0);
expect(push.stderr).not.toContain("engine.input_too_large");
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(tip);