maint: integrate reviewed contribution #3778

This commit is contained in:
haelyra committed 2026-10-29 11:23:21 -07:00
commit cbdd9be209
9 files changed
+78 -15

No files matched your search

+8
View File
@@ -11,6 +11,13 @@ const { spawnSync } = require('child_process');
const preDispatcher = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-dispatcher.js');
const postDispatcher = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'post-bash-dispatcher.js');
// The post-Bash dispatcher appends to ~/.claude/bash-commands.log and
// ~/.claude/cost-tracker.log, so runs get an empty home instead of the
// developer's real one unless a test passes its own HOME.
const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-bash-dispatcher-home-'));
const ISOLATED_HOME_ENV = { HOME: ISOLATED_HOME, USERPROFILE: ISOLATED_HOME };
process.on('exit', () => fs.rmSync(ISOLATED_HOME, { recursive: true, force: true }));
function test(name, fn) {
try {
fn();
@@ -29,6 +36,7 @@ function runScript(scriptPath, input, env = {}) {
encoding: 'utf8',
env: {
...process.env,
...ISOLATED_HOME_ENV,
...env,
},
timeout: 10000,
+7 -1
View File
@@ -15,6 +15,12 @@ const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'cost-tracke
// Harness caches belong to a real private root shared by parent and hook child.
const cacheRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-cost-cache-')));
// cost-tracker.js appends to ~/.claude/metrics/costs.jsonl and writes cost
// snapshots there, so runs get an empty home instead of the developer's real
// one unless a test passes withTempHome().
const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-cost-home-'));
const ISOLATED_HOME_ENV = { HOME: ISOLATED_HOME, USERPROFILE: ISOLATED_HOME };
process.on('exit', () => fs.rmSync(ISOLATED_HOME, { recursive: true, force: true }));
const savedTempEnvironment = Object.fromEntries(['TMPDIR', 'TMP', 'TEMP']
.map(name => [name, process.env[name]]));
for (const name of Object.keys(savedTempEnvironment)) process.env[name] = cacheRoot;
@@ -56,7 +62,7 @@ function runScript(input, envOverrides = {}) {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
env: { ...process.env, ...envOverrides, TMPDIR: cacheRoot, TMP: cacheRoot, TEMP: cacheRoot },
env: { ...process.env, ...ISOLATED_HOME_ENV, ...envOverrides, TMPDIR: cacheRoot, TMP: cacheRoot, TEMP: cacheRoot },
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
}
+16 -3
View File
@@ -15,6 +15,16 @@ const { spawnSync } = require('child_process');
const evaluateScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'evaluate-session.js');
// evaluate-session.js creates its learned-skills directory under the home
// directory, so every run gets an empty home instead of the developer's real one.
const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'eval-session-home-'));
const ISOLATED_HOME_ENV = { HOME: ISOLATED_HOME, USERPROFILE: ISOLATED_HOME };
process.on('exit', () => fs.rmSync(ISOLATED_HOME, { recursive: true, force: true }));
function isolatedEnv(extra = {}) {
return { ...process.env, ...ISOLATED_HOME_ENV, ...extra };
}
// Test helpers
function test(name, fn) {
try {
@@ -62,6 +72,7 @@ function runEvaluate(stdinJson) {
encoding: 'utf8',
input: JSON.stringify(stdinJson),
timeout: 10000,
env: isolatedEnv(),
});
return {
code: result.status || 0,
@@ -135,6 +146,7 @@ function runTests() {
encoding: 'utf8',
input: 'not valid json at all',
timeout: 10000,
env: isolatedEnv(),
});
assert.strictEqual(result.status, 0, 'Should exit 0 even on bad stdin');
})) passed++; else failed++;
@@ -253,6 +265,7 @@ function runTests() {
encoding: 'utf8',
input: '',
timeout: 10000,
env: isolatedEnv(),
});
// Empty stdin → JSON.parse('') throws → fallback to env var (unset) → null → exit 0
assert.strictEqual(result.status, 0, 'Should exit 0 on empty stdin');
@@ -269,7 +282,7 @@ function runTests() {
encoding: 'utf8',
input: 'invalid json {{{',
timeout: 10000,
env: { ...process.env, CLAUDE_TRANSCRIPT_PATH: transcript }
env: isolatedEnv({ CLAUDE_TRANSCRIPT_PATH: transcript })
});
assert.strictEqual(result.status, 0, 'Should exit 0');
@@ -396,8 +409,8 @@ function runTests() {
'Should NOT contain literal ~ in output (should be expanded)');
assert.ok(result.stderr.includes('custom-learned-skills-dir'),
`Should reference the custom learned skills dir. Got: ${result.stderr}`);
// The ~ should have been replaced with os.homedir()
assert.ok(result.stderr.includes(os.homedir()),
// The ~ should have been replaced with the child's home directory
assert.ok(result.stderr.includes(ISOLATED_HOME),
`Should contain expanded home directory. Got: ${result.stderr}`);
cleanupTestDir(testDir);
+11 -4
View File
@@ -177,12 +177,19 @@ async function asyncTest(name, fn) {
}
}
// Hook scripts write session files, compaction logs, learned skills and
// homunculus data under the home directory, so script runs get an empty home
// instead of the developer's real one unless a test passes its own HOME.
const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-test-home-'));
const ISOLATED_HOME_ENV = { HOME: ISOLATED_HOME, USERPROFILE: ISOLATED_HOME };
process.on('exit', () => fs.rmSync(ISOLATED_HOME, { recursive: true, force: true }));
// Run a script and capture output
function runScript(scriptPath, input = '', env = {}, cwd = process.cwd()) {
return new Promise((resolve, reject) => {
const proc = spawn('node', [scriptPath], {
cwd,
env: { ...process.env, ...env },
env: { ...process.env, ...ISOLATED_HOME_ENV, ...env },
stdio: ['pipe', 'pipe', 'pipe']
});
@@ -209,7 +216,7 @@ function runShellScript(scriptPath, args = [], input = '', env = {}, cwd = proce
return new Promise((resolve, reject) => {
const proc = spawn('bash', [toBashPath(scriptPath), ...args], {
cwd,
env: { ...process.env, ...env },
env: { ...process.env, ...ISOLATED_HOME_ENV, ...env },
stdio: ['pipe', 'pipe', 'pipe']
});
@@ -1515,7 +1522,7 @@ async function runTests() {
if (
await asyncTest('creates compaction log', async () => {
await runScript(path.join(scriptsDir, 'pre-compact.js'));
const logFile = path.join(getCanonicalSessionsDir(os.homedir()), 'compaction-log.txt');
const logFile = path.join(getCanonicalSessionsDir(ISOLATED_HOME), 'compaction-log.txt');
assert.ok(fs.existsSync(logFile), 'Compaction log should exist');
})
)
@@ -3700,7 +3707,7 @@ async function runTests() {
const shell = process.platform === 'win32' ? 'bash' : 'bash';
const proc = spawn(shell, ['-lc', shellCommand], {
env: process.env,
env: { ...process.env, ...ISOLATED_HOME_ENV },
stdio: ['ignore', 'pipe', 'pipe']
});
+8 -1
View File
@@ -47,13 +47,20 @@ function withTempHome(homeDir) {
};
}
// session-activity-tracker.js appends to ~/.claude/metrics/tool-usage.jsonl, so
// runs get an empty home instead of the developer's real one unless a test
// passes withTempHome().
const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'session-activity-home-'));
const ISOLATED_HOME_ENV = { HOME: ISOLATED_HOME, USERPROFILE: ISOLATED_HOME };
process.on('exit', () => fs.rmSync(ISOLATED_HOME, { recursive: true, force: true }));
function runScript(input, envOverrides = {}, options = {}) {
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
env: { ...process.env, ...envOverrides },
env: { ...process.env, ...ISOLATED_HOME_ENV, ...envOverrides },
cwd: options.cwd,
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
+6
View File
@@ -31,6 +31,9 @@ const SUBPROCESS_TIMEOUT_MS = process.platform === 'darwin' && process.env.CI ==
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-stop-stdout-')); // non-git cwd
const dataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-stop-data-'));
// Stop hooks also write learned skills under the home directory; give them
// an empty one instead of the developer's real one.
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-stop-home-'));
function test(name, fn) {
try {
@@ -58,6 +61,8 @@ function stopPayload(messageCharacters, character = 'm') {
function hookEnv() {
const env = {
...process.env,
HOME: homeDir,
USERPROFILE: homeDir,
ECC_HOOK_PROFILE: 'standard',
ECC_AGENT_DATA_HOME: dataHome,
CLAUDE_SESSION_ID: `stop-stdout-test-${process.pid}`
@@ -448,6 +453,7 @@ else failed++;
try {
fs.rmSync(workDir, { recursive: true, force: true });
fs.rmSync(dataHome, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
} catch {
/* best-effort cleanup */
}
+11 -2
View File
@@ -41,6 +41,13 @@ async function asyncTest(name, fn) {
}
}
// Hook scripts write session data, learned skills and homunculus observations
// under the home directory, so runs get an empty home instead of the developer's
// real one unless a test passes its own HOME.
const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-integration-home-'));
const ISOLATED_HOME_ENV = { HOME: ISOLATED_HOME, USERPROFILE: ISOLATED_HOME };
process.on('exit', () => fs.rmSync(ISOLATED_HOME, { recursive: true, force: true }));
/**
* Run a hook script with simulated Claude Code input
* @param {string} scriptPath - Path to the hook script
@@ -51,7 +58,7 @@ async function asyncTest(name, fn) {
function runHookWithInput(scriptPath, input = {}, env = {}, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
const proc = spawn('node', [scriptPath], {
env: { ...process.env, ...env },
env: { ...process.env, ...ISOLATED_HOME_ENV, ...env },
stdio: ['pipe', 'pipe', 'pipe']
});
@@ -110,7 +117,7 @@ function getSessionStartPayload(stdout) {
function runHookCommand(command, input = {}, env = {}, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
const isWindows = process.platform === 'win32';
const mergedEnv = { ...process.env, CLAUDE_PLUGIN_ROOT: REPO_ROOT, ...env };
const mergedEnv = { ...process.env, ...ISOLATED_HOME_ENV, CLAUDE_PLUGIN_ROOT: REPO_ROOT, ...env };
if (Array.isArray(command)) {
const [program, ...args] = command;
const proc = spawn(program, args, { env: mergedEnv, stdio: ['pipe', 'pipe', 'pipe'] });
@@ -297,6 +304,7 @@ async function runTests() {
if (await asyncTest('hooks handle malformed JSON input', async () => {
const proc = spawn('node', [path.join(scriptsDir, 'session-start.js')], {
env: { ...process.env, ...ISOLATED_HOME_ENV },
stdio: ['pipe', 'pipe', 'pipe']
});
@@ -887,6 +895,7 @@ async function runTests() {
// session-end parses stdin JSON. If input is > 1MB and truncated mid-JSON,
// JSON.parse should fail and fall back to env var
const proc = spawn('node', [path.join(scriptsDir, 'session-end.js')], {
env: { ...process.env, ...ISOLATED_HOME_ENV },
stdio: ['pipe', 'pipe', 'pipe']
});
+3 -3
View File
@@ -767,8 +767,8 @@ function runTests() {
// setPreferredPackageManager success
console.log('\nsetPreferredPackageManager (success):');
if (test('successfully saves preferred package manager', () => {
// This writes to ~/.claude/package-manager.json — read original to restore
if (test('successfully saves preferred package manager', () => withIsolatedHome(() => {
// Writes ~/.claude/package-manager.json under the isolated home, not the real one
const utils = require('../../scripts/lib/utils');
const configPath = path.join(utils.getClaudeDir(), 'package-manager.json');
const original = utils.readFile(configPath);
@@ -791,7 +791,7 @@ function runTests() {
}
}
}
})) passed++;
}))) passed++;
else failed++;
// getCommandPattern completeness
+8 -1
View File
@@ -73,6 +73,13 @@ function stripComments(source) {
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "")
}
// Hook scripts write homunculus observations under the home directory, so
// runner calls get an empty home instead of the developer's real one unless a
// test passes its own HOME.
const ISOLATED_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-pi-home-"))
const ISOLATED_HOME_ENV = { HOME: ISOLATED_HOME, USERPROFILE: ISOLATED_HOME }
process.on("exit", () => fs.rmSync(ISOLATED_HOME, { recursive: true, force: true }))
/**
* Invokes ECC's hook runner like the adapter (`runEccHook` in
* .pi/extensions/index.ts): it uses the test host's Node executable with the
@@ -85,7 +92,7 @@ function runHookRunner(eccRoot, hookId, relScript, profiles, payload, extraEnv,
encoding: "utf8",
cwd: cwd || eccRoot,
timeout: 30000,
env: { ...process.env, CLAUDE_PLUGIN_ROOT: eccRoot, ECC_PLUGIN_ROOT: eccRoot, ...extraEnv },
env: { ...process.env, ...ISOLATED_HOME_ENV, CLAUDE_PLUGIN_ROOT: eccRoot, ECC_PLUGIN_ROOT: eccRoot, ...extraEnv },
})
}