From f9bb4893636fdcf892f53662ce820a33eac2c17d Mon Sep 17 00:00:00 2001
From: Dennis Alexis Valin Dittrich
Date: Sat, 5 Sep 2026 11:58:29 +0200
Subject: [PATCH] fix(#4204): isolate verify:post CLI test capability discovery
from real HOME (#4293)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
tests/loop-hooks-verify-post-e2e.test.cjs's runCli stripped ambient GSD_*
env vars but never sandboxed HOME, so the spawned gsd-tools CLI
subprocess fell through to os.homedir() (capability-loader.cts's
overlayRoots and capability-state.cts's resolveCapabilityRuntimeState
both thread process.env['GSD_HOME'], which falls back to os.homedir()
when unset). Any capability genuinely installed at ~/.gsd/capabilities
on the machine running the suite (e.g. beads, markdown-linting) leaked
into the verify:post registry and inflated the file's exact-count
assertions (3 -> 5 active hooks, 0 -> 2 on the all-off case, etc).
Switch runCli to helpers.cjs's installSpawnEnv(), the helper ~370 other
test files already use for this: it sandboxes HOME/USERPROFILE to a
per-file mkdtemp'd fixture and clears the full config-location env list
(GSD_HOME, GSD_RUNTIME, CLAUDE_CONFIG_DIR, etc.), so the CLI subprocess
sees only the core registry regardless of what's installed on the host.
The pure resolveLoopHooks() tests in the same file were already
unaffected — they call the resolver directly with realRegistry,
bypassing CLI env resolution entirely.
Fixes #4204
Co-authored-by: Test
Co-authored-by: Tom Boucher
---
tests/loop-hooks-verify-post-e2e.test.cjs | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/tests/loop-hooks-verify-post-e2e.test.cjs b/tests/loop-hooks-verify-post-e2e.test.cjs
index c4c5fa4c8..f00793b75 100644
--- a/tests/loop-hooks-verify-post-e2e.test.cjs
+++ b/tests/loop-hooks-verify-post-e2e.test.cjs
@@ -23,7 +23,7 @@
*/
const { describe, test, before, after, afterEach } = require('node:test');
-const { cleanup } = require('./helpers.cjs');
+const { cleanup, installSpawnEnv } = require('./helpers.cjs');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
@@ -40,20 +40,23 @@ const realRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
// ── CLI path ───────────────────────────────────────────────────────────────────
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
-// ── Env hermeticity (strip ambient GSD_ vars that skew planning dir lookups) ──
-const CLEAN_ENV = Object.fromEntries(
- Object.entries(process.env).filter(([k]) => !k.startsWith('GSD_')),
-);
-
/**
* Invoke gsd-tools CLI with spawnSync and return the parsed result.
- * Always use CLEAN_ENV to avoid ambient GSD_ env vars redirecting planning paths.
+ *
+ * #4204: uses helpers.cjs's installSpawnEnv() rather than a hand-rolled env,
+ * because it sandboxes HOME (so capability-loader's overlayRoots and
+ * resolveCapabilityRuntimeState's registry load — both of which fall back to
+ * os.homedir() — never reach the real machine) AND clears the full
+ * config-location env list (GSD_HOME, GSD_RUNTIME, CLAUDE_CONFIG_DIR, etc.),
+ * not just a GSD_-prefix strip. Without it, a capability genuinely installed
+ * on the host running the suite (e.g. beads, markdown-linting) leaked into
+ * the verify:post registry and inflated the exact-count assertions below.
*/
function runCli(args, cwd) {
const result = spawnSync(process.execPath, [GSD_TOOLS, ...args], {
cwd,
encoding: 'utf8',
- env: CLEAN_ENV,
+ env: installSpawnEnv(),
timeout: 60000,
});
return result;