Skip to content

Commit 92fd0de

Browse files
aksOpsclaude
andcommitted
fix(test): RepositoryIdentityTest no longer depends on local git state
The two git-backed tests (resolve_gitRepoWithCommit_commitShaPresent, resolve_detachedHead_branchIsNull) failed when the developer's global gitconfig forced commit signing (commit.gpgsign=true, signingkey set) on a machine without a usable signing key — git commit exited non-zero, the original run() helper ignored the exit code, no commit was made, and rev-parse HEAD returned null. Made the git invocations hermetic: * repo-local config overrides commit.gpgsign / tag.gpgsign to false, unsets core.hooksPath, core.autocrlf, init.templateDir * explicit --no-gpg-sign on the commit (belt-and-braces) * scrub GIT_* env vars on every child process so no ambient CI / worktree state leaks in * run() now asserts the process exited 0 — silent failures become loud test failures * new requireGit() uses Assumptions.assumeTrue to skip cleanly when the git binary is absent (product still covered by non-git tests + RepositoryIdentity's own swallow-on-error path) Verified by running the pre-fix test against a hostile HOME with forced GPG signing — reproduces the 2 failures. Post-fix: 8/8 pass under the same hostile environment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a1c3636 commit 92fd0de

1 file changed

Lines changed: 77 additions & 6 deletions

File tree

src/test/java/io/github/randomcodespace/iq/intelligence/RepositoryIdentityTest.java

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@
44
import org.junit.jupiter.api.io.TempDir;
55

66
import java.nio.file.Path;
7+
import java.util.Map;
78

89
import static org.assertj.core.api.Assertions.assertThat;
910
import static org.assertj.core.api.Assertions.assertThatCode;
11+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
1012

1113
/**
1214
* Unit tests for {@link RepositoryIdentity}.
1315
* Validates graceful degradation when git metadata is unavailable.
16+
*
17+
* <p>Git invocations in these tests are hermetic: we override every global-config
18+
* setting that can cause {@code git commit} / {@code git init} to fail silently
19+
* on a developer machine but pass on CI (or vice-versa) — most notably
20+
* {@code commit.gpgsign}, {@code tag.gpgsign}, {@code core.hooksPath},
21+
* {@code init.templateDir}, and {@code core.autocrlf}. We also scrub
22+
* {@code GIT_CONFIG_*} / {@code GIT_DIR} / {@code GIT_WORK_TREE} env vars so the
23+
* invoked processes inherit no ambient git state from the parent.
1424
*/
1525
class RepositoryIdentityTest {
1626

@@ -109,31 +119,92 @@ void record_nullFieldsAllowed() {
109119
// Helpers
110120
// ------------------------------------------------------------------
111121

122+
/**
123+
* Pre-flight: skip git-dependent tests when the {@code git} binary is not available.
124+
* Treat absence-of-git as an environment gap, not a product bug — still covered by the
125+
* non-git tests above, and by the {@code RepositoryIdentity.runGit} swallow-all-errors path.
126+
*/
127+
private static void requireGit() {
128+
try {
129+
Process p = new ProcessBuilder("git", "--version")
130+
.redirectErrorStream(true).start();
131+
boolean ok = p.waitFor() == 0;
132+
assumeTrue(ok, "git binary not available on PATH");
133+
} catch (Exception e) {
134+
assumeTrue(false, "git binary not available on PATH: " + e.getMessage());
135+
}
136+
}
137+
112138
private static void initGitRepo(Path dir) throws Exception {
113-
run(dir, "git", "init");
139+
requireGit();
140+
// -c overrides are applied to THIS invocation only and cannot be shadowed by a user's
141+
// global gitconfig (unlike `git config` writes into the new .git/config).
142+
run(dir, "git",
143+
"-c", "init.defaultBranch=main",
144+
"-c", "init.templateDir=",
145+
"init");
114146
run(dir, "git", "config", "user.email", "test@test.com");
115147
run(dir, "git", "config", "user.name", "Test");
148+
// Kill every global knob that can make `git commit` fail on an otherwise-clean repo.
149+
run(dir, "git", "config", "commit.gpgsign", "false");
150+
run(dir, "git", "config", "tag.gpgsign", "false");
151+
run(dir, "git", "config", "core.hooksPath", "/dev/null");
152+
run(dir, "git", "config", "core.autocrlf", "false");
116153
}
117154

118155
private static void makeInitialCommit(Path dir) throws Exception {
119156
Path readme = dir.resolve("README.md");
120157
java.nio.file.Files.writeString(readme, "# Test");
121158
run(dir, "git", "add", ".");
122-
run(dir, "git", "commit", "-m", "init");
159+
// --no-gpg-sign is belt-and-braces over the repo-local commit.gpgsign=false set above;
160+
// --allow-empty-message keeps the test robust if a commit.template hook injects content.
161+
run(dir, "git", "-c", "commit.gpgsign=false", "commit",
162+
"--no-gpg-sign", "-m", "init");
123163
}
124164

125165
private static String runGit(Path dir, String... args) throws Exception {
166+
requireGit();
126167
var cmd = new java.util.ArrayList<String>();
127168
cmd.add("git");
128169
cmd.addAll(java.util.Arrays.asList(args));
129-
var proc = new ProcessBuilder(cmd).directory(dir.toFile()).start();
130-
String out = new String(proc.getInputStream().readAllBytes()).trim();
170+
var pb = new ProcessBuilder(cmd).directory(dir.toFile());
171+
scrubGitEnv(pb.environment());
172+
var proc = pb.start();
173+
String out;
174+
try (var is = proc.getInputStream()) {
175+
out = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8).trim();
176+
}
131177
proc.waitFor();
132178
return out;
133179
}
134180

181+
/**
182+
* Execute a git sub-command and assert it exited 0. Fails the test loudly (not silently)
183+
* if setup cannot complete — preferred over ignoring the exit code, which is what let the
184+
* GPG-signing failure slip through before.
185+
*/
135186
private static void run(Path dir, String... cmd) throws Exception {
136-
new ProcessBuilder(cmd).directory(dir.toFile())
137-
.redirectErrorStream(true).start().waitFor();
187+
var pb = new ProcessBuilder(cmd).directory(dir.toFile()).redirectErrorStream(true);
188+
scrubGitEnv(pb.environment());
189+
Process proc = pb.start();
190+
String stderr;
191+
try (var is = proc.getInputStream()) {
192+
stderr = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
193+
}
194+
int exit = proc.waitFor();
195+
if (exit != 0) {
196+
throw new IllegalStateException(
197+
"Command failed (exit " + exit + "): " + String.join(" ", cmd)
198+
+ "\n" + stderr);
199+
}
200+
}
201+
202+
/**
203+
* Remove every ambient git env var that could leak the parent shell's context into the
204+
* child process (most commonly {@code GIT_DIR}/{@code GIT_WORK_TREE} in a worktree-based
205+
* setup, or {@code GIT_CONFIG_*} injected by CI runners).
206+
*/
207+
private static void scrubGitEnv(Map<String, String> env) {
208+
env.keySet().removeIf(k -> k.startsWith("GIT_"));
138209
}
139210
}

0 commit comments

Comments
 (0)