diff --git a/tests/loop-hooks-empty-points-e2e.test.cjs b/tests/loop-hooks-empty-points-e2e.test.cjs index 6c28f4f27..579ad91ee 100644 --- a/tests/loop-hooks-empty-points-e2e.test.cjs +++ b/tests/loop-hooks-empty-points-e2e.test.cjs @@ -27,7 +27,7 @@ const os = require('node:os'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); -const { cleanup } = require('./helpers.cjs'); +const { cleanup, installSpawnEnv } = require('./helpers.cjs'); const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs'); const realRegistry = require('../gsd-core/bin/lib/capability-registry.cjs'); @@ -54,12 +54,22 @@ function makeBareDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-loop-bare-')); } -/** Spawn gsd-tools via raw spawnSync; returns { status, stdout, stderr } */ +/** + * Spawn gsd-tools via raw spawnSync; returns { status, stdout, stderr }. + * + * #4291: env: installSpawnEnv() sandboxes HOME AND blanks GSD_HOME, which + * capability-loader's overlayRoots checks BEFORE falling back to + * os.homedir() — sandboxing HOME alone would leave that env var reaching + * the real machine. Without both, a capability genuinely installed on the + * host running the suite (e.g. beads, markdown-linting) leaked into these + * empty-point/exact-shape assertions. + */ function spawnGsd(args, cwd) { return spawnSync(process.execPath, [GSD_TOOLS, ...args], { cwd: cwd || os.tmpdir(), encoding: 'utf8', timeout: 60000, + env: installSpawnEnv(), }); } diff --git a/tests/loop-hooks-ship-pre-e2e.test.cjs b/tests/loop-hooks-ship-pre-e2e.test.cjs index 05b3683fc..37a54c0f5 100644 --- a/tests/loop-hooks-ship-pre-e2e.test.cjs +++ b/tests/loop-hooks-ship-pre-e2e.test.cjs @@ -26,7 +26,7 @@ const os = require('node:os'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); -const { cleanup } = require('./helpers.cjs'); +const { cleanup, installSpawnEnv } = require('./helpers.cjs'); const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs'); @@ -41,20 +41,23 @@ const realRegistry = require('../gsd-core/bin/lib/capability-registry.cjs'); /** * Run gsd-tools synchronously via spawnSync. Returns { status, stdout, stderr }. * Does NOT throw on non-zero exit — callers must assert status themselves. + * + * #4291: uses helpers.cjs's installSpawnEnv() rather than a hand-rolled env, + * because it sandboxes HOME (so capability-loader's overlayRoots, which + * falls back to os.homedir(), never reaches the real machine) AND clears + * the full config-location env list, not just three session-identity keys. + * Without it, a capability genuinely installed on the host running the + * suite (e.g. beads, markdown-linting) leaked into the ship:pre registry + * and inflated the exact-count assertions below. opts.env is spread last so + * the two deliberate overlay-fixture call sites below (GSD_HOME: fixture.home) + * still opt in to a specific third-party capability root. */ function runTools(args, opts = {}) { const result = spawnSync(process.execPath, [GSD_TOOLS, ...args], { encoding: 'utf8', timeout: 60000, cwd: opts.cwd || process.cwd(), - env: { - ...process.env, - // Clear ambient session vars that can redirect config paths - GSD_SESSION_KEY: '', - CODEX_THREAD_ID: '', - CLAUDE_SESSION_ID: '', - ...opts.env, - }, + env: installSpawnEnv(opts.env), }); return result; }