diff --git a/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java b/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java index 09e266e..971eeb6 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/session/GitWorktreesTest.java @@ -1809,4 +1809,43 @@ class GitWorktreesTest { + "different count means a helper now bypasses the hermetic factory; route " + "it through gitProcessBuilder or document the new exception here"); } + + /** + * fleetd #369 review round 2. {@link #everyGitSubprocessGoesThroughTheHermeticFactory} counts + * call sites, not behaviour — it catches a NEW helper built the old, leak-prone way, but it + * cannot catch {@link #gitProcessBuilder} itself being gutted: deleting {@code + * pb.environment().putAll(hermeticEnv())} from inside the factory leaves every call site + * unchanged, the count stays 2, and the whole unpoisoned suite stays green — the exact leak + * this ticket fixed would come back silently, with nothing but a human remembering to re-run + * the poison command to catch it. This test instead inspects what the factory actually hands + * to {@link ProcessBuilder#start()}, so it fails the moment the hermetic environment stops + * being applied, on any machine, with no poison needed. + * + *

The property under test: every git subprocess this class starts must run with an + * environment that cannot see the operator's real git configuration. A call-site count is a + * proxy for that; this is the thing itself. + */ + @Test + void gitProcessBuilderCarriesTheFullHermeticEnvironment(@TempDir Path tmp) { + Map env = gitProcessBuilder(tmp, "status", "--porcelain").environment(); + + assertEquals("/dev/null", env.get("GIT_CONFIG_GLOBAL"), + "GIT_CONFIG_GLOBAL must be neutralized, or the operator's real ~/.gitconfig applies"); + assertEquals("/dev/null", env.get("GIT_CONFIG_SYSTEM"), + "GIT_CONFIG_SYSTEM must be neutralized, or the machine's real /etc/gitconfig applies"); + assertEquals("0", env.get("GIT_TERMINAL_PROMPT"), + "GIT_TERMINAL_PROMPT must be disabled, or a credential prompt can hang the subprocess"); + + String xdg = env.get("XDG_CONFIG_HOME"); + assertNotNull(xdg, + "XDG_CONFIG_HOME must be set — left unset, git falls back to the operator's real " + + "$HOME/.config/git/ignore (gitignore(5)), exactly fleetd #369's leak"); + assertFalse(xdg.isBlank(), "XDG_CONFIG_HOME must not be blank — blank behaves like unset"); + assertTrue(Path.of(xdg).startsWith(CLASS_TMP), + "XDG_CONFIG_HOME must point inside this test class's own throwaway directory, " + + "never the operator's real one or the JVM's inherited value — got: " + xdg); + assertFalse(Files.exists(Path.of(xdg, "git", "ignore")), + "the resolved XDG default excludes file must provably not exist, or its contents " + + "would silently apply to every git status this test class runs"); + } }