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;
}