diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index 310e55e8e..a41ba1d11 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -4646,7 +4646,7 @@ const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); -const { cleanup } = require('./helpers.cjs'); +const { cleanup, TEST_ENV_BASE } = require('./helpers.cjs'); const ROOT = path.join(__dirname, '..'); const PROFILE_OUTPUT = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'profile-output.cjs'); @@ -4672,7 +4672,12 @@ describe('Bug #2973: dev-preferences default writer path is skills/gsd-dev-prefe m.cmdGenerateDevPreferences(${JSON.stringify(tmpHome)}, { analysis: ${JSON.stringify(analysisPath)} }, false); `); const result = cp.spawnSync(process.execPath, [driver], { - env: Object.assign({}, process.env, { HOME: tmpHome, USERPROFILE: tmpHome }), + // #2665: TEST_ENV_BASE must be merged in explicitly here. This is a RAW + // spawn, not runGsdTools, so nothing scrubs the config-location vars for + // it -- and the writer under test resolves them env-FIRST. Sandboxing + // HOME alone let an ambient CLAUDE_CONFIG_DIR win, and the SKILL.md + // landed in the developer's live config dir instead of tmpHome. + env: Object.assign({}, process.env, TEST_ENV_BASE, { HOME: tmpHome, USERPROFILE: tmpHome }), encoding: 'utf-8', // Bound the subprocess so a regression that hangs the writer // (or the dispatcher) cannot deadlock CI (PR #3003 CR feedback). diff --git a/tests/issue-766-plugin-manifest.test.cjs b/tests/issue-766-plugin-manifest.test.cjs index 329e29a9f..1a78e243c 100644 --- a/tests/issue-766-plugin-manifest.test.cjs +++ b/tests/issue-766-plugin-manifest.test.cjs @@ -24,7 +24,7 @@ const ROOT = path.resolve(__dirname, '..'); const identity = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'package-identity.cjs')); const pkg = require(path.join(ROOT, 'package.json')); const { MANAGED_HOOKS } = require(path.join(ROOT, 'hooks', 'managed-hooks-registry.cjs')); -const { cleanup } = require('./helpers.cjs'); +const { cleanup, TEST_ENV_BASE } = require('./helpers.cjs'); const PLUGIN_JSON_PATH = path.join(ROOT, '.claude-plugin', 'plugin.json'); const HOOKS_JSON_PATH = path.join(ROOT, 'hooks', 'hooks.json'); @@ -359,9 +359,36 @@ describe('C: plugin.json schema validation', () => { // ── C2: Opportunistic CLI integration (skipped when claude not on PATH) ────── + // #2665: the `claude` CLI is a THIRD-PARTY binary that bootstraps its own + // config (.claude.json plus a backups/ dir) into whatever CLAUDE_CONFIG_DIR + // names. Two things follow, and only the first is obvious: + // + // 1. Inheriting the developer's ambient CLAUDE_CONFIG_DIR makes even a bare + // `--version` probe write into their live config dir. That alone kept + // `CLAUDE_CONFIG_DIR= npm run test:unit; find -type f` from + // returning empty after every GSD-side leak was closed. + // 2. BLANKING it (TEST_ENV_BASE's '' convention) is not enough here. GSD's + // own resolvers treat '' as falsy and fall back to the home dir, but this + // binary is not ours and gives no such guarantee -- blanking it produced a + // stray backups/ directory in the REPO ROOT. + // + // So point it at a real throwaway dir rather than at nothing. + const claudeCliHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2665-claude-cli-')); + const claudeCliEnv = () => ({ + ...process.env, + ...TEST_ENV_BASE, + HOME: claudeCliHome, + USERPROFILE: claudeCliHome, + CLAUDE_CONFIG_DIR: path.join(claudeCliHome, '.claude'), + }); + const claudeAvailable = (() => { try { - const result = spawnSync('claude', ['--version'], { encoding: 'utf-8', timeout: 5000 }); + const result = spawnSync('claude', ['--version'], { + encoding: 'utf-8', + timeout: 5000, + env: claudeCliEnv(), + }); return result.status === 0; } catch (_) { return false; @@ -384,6 +411,7 @@ describe('C: plugin.json schema validation', () => { cwd: ROOT, encoding: 'utf-8', timeout: 15000, + env: claudeCliEnv(), }); assert.equal( result.status,