From 95529ef153bd8eeabfe8d75b14344b66f58822d9 Mon Sep 17 00:00:00 2001 From: Dennis Alexis Valin Dittrich Date: Tue, 8 Sep 2026 04:04:44 +0200 Subject: [PATCH] fix(#4291): isolate ship:pre and empty-points e2e CLI tests from real HOME (#4321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#4291): isolate ship:pre and empty-points e2e CLI tests from real HOME tests/loop-hooks-ship-pre-e2e.test.cjs's runTools() and tests/loop-hooks-empty-points-e2e.test.cjs's spawnGsd() spawned gsd-tools without sandboxing HOME, so a capability genuinely installed on the host (e.g. beads, markdown-linting) leaked into the resolved registry and inflated their exact-count/empty-array assertions. Mirrors the installSpawnEnv() fix already applied to loop-hooks-verify-post-e2e.test.cjs in #4204/#4293. * fix(#4291): credit GSD_HOME blanking in spawnGsd docblock installSpawnEnv() blanks GSD_HOME as well as sandboxing HOME, and capability-loader's overlayRoots checks GSD_HOME before falling back to os.homedir() — sandboxing HOME alone would leave that env var reaching the real machine. The sibling loop-hooks-ship-pre-e2e.test.cjs docblock already states both halves; this one credited only HOME. Per trek-e's review on open-gsd/gsd-core#4321 (Nit, isolated adversarial pass). --------- Co-authored-by: Test Co-authored-by: Tom Boucher --- tests/loop-hooks-empty-points-e2e.test.cjs | 14 ++++++++++++-- tests/loop-hooks-ship-pre-e2e.test.cjs | 21 ++++++++++++--------- 2 files changed, 24 insertions(+), 11 deletions(-) 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; }