mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-31 12:55:21 +00:00
maint: integrate reviewed contribution #3778
This commit is contained in:
9 files changed
+78
-15
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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 || '' };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']
|
||||
});
|
||||
|
||||
|
||||
@@ -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 || '' };
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user