From 697384145a0c7ee7aac216ad94354bc7d7850640 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 9 Sep 2026 10:16:58 +0700 Subject: [PATCH] test(cc-config): cover the env runClaudePlugin actually spawns with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `autoInstallSkillPlugins` takes `run` as a seam, so every existing test stops short of the real `spawnSync` — the line that decides whether CLAUDE_CONFIG_DIR is pinned was untested. Restoring the unconditional pin here (the bug that sent `.claude.json` to `~/.claude/.claude.json`) kept the whole suite green. Export `runClaudePlugin` and drive it against a fake `claude` on PATH that prints the variable back: one test for CLAUDE_HOME set, one for unset. Mutation-checked — the unconditional pin fails the second. --- server/__tests__/skill-plugins.test.js | 49 ++++++++++++++++++++++++++ server/lib/skill-plugins.js | 4 +++ 2 files changed, 53 insertions(+) diff --git a/server/__tests__/skill-plugins.test.js b/server/__tests__/skill-plugins.test.js index 61ed9aea..4a7ea256 100644 --- a/server/__tests__/skill-plugins.test.js +++ b/server/__tests__/skill-plugins.test.js @@ -238,3 +238,52 @@ describe("autoInstallSkillPlugins", () => { assert.deepEqual(run.calls, []); }); }); + +describe("runClaudePlugin: the env it spawns with", () => { + // Every other test in this file injects `run`, so nothing exercises the real + // spawn. That gap is how a wrong CLAUDE_CONFIG_DIR pin lived here: the CLI + // writes `.claude.json` INSIDE that dir, while cc-discovery reads it beside + // ~/.claude by default, so pinning it unconditionally aimed writes at a file + // nothing reads. cc-plugin-cli.js's runClaude has the mirror of this test. + const PRINT_ENV = 'printf "%s\\n" "CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR-}"'; + + function stubClaude(body) { + const bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skillplug-bin-")); + const file = path.join(bin, "claude"); + fs.writeFileSync(file, `#!/usr/bin/env bash\n${body}\n`); + fs.chmodSync(file, 0o755); + const originalPath = process.env.PATH; + process.env.PATH = `${bin}${path.delimiter}${originalPath}`; + return () => { + process.env.PATH = originalPath; + fs.rmSync(bin, { recursive: true, force: true }); + }; + } + + it("leaves CLAUDE_CONFIG_DIR unset when CLAUDE_HOME is not set", () => { + const original = process.env.CLAUDE_HOME; + delete process.env.CLAUDE_HOME; + const restore = stubClaude(PRINT_ENV); + try { + const res = skillPlugins.runClaudePlugin(["plugin", "list"], 10_000); + assert.equal(res.output.trim(), "CLAUDE_CONFIG_DIR="); + } finally { + restore(); + if (original !== undefined) process.env.CLAUDE_HOME = original; + } + }); + + it("pins CLAUDE_CONFIG_DIR to CLAUDE_HOME when the operator set it", () => { + const original = process.env.CLAUDE_HOME; + process.env.CLAUDE_HOME = "/tmp/some-other-claude-home"; + const restore = stubClaude(PRINT_ENV); + try { + const res = skillPlugins.runClaudePlugin(["plugin", "list"], 10_000); + assert.equal(res.output.trim(), "CLAUDE_CONFIG_DIR=/tmp/some-other-claude-home"); + } finally { + restore(); + if (original === undefined) delete process.env.CLAUDE_HOME; + else process.env.CLAUDE_HOME = original; + } + }); +}); diff --git a/server/lib/skill-plugins.js b/server/lib/skill-plugins.js index 3d8a66cc..21bbc85a 100644 --- a/server/lib/skill-plugins.js +++ b/server/lib/skill-plugins.js @@ -220,4 +220,8 @@ module.exports = { requiredSkillPlugins, missingSkillPlugins, autoInstallSkillPlugins, + // Exported only so a test can pin the env it spawns with. Every other test + // injects `run` and so never reaches the real spawn — which is how an + // unconditional CLAUDE_CONFIG_DIR pin could be restored here unnoticed. + runClaudePlugin, };