Files
ECC/tests/scripts/dsh-generate-hooks-config.test.js
T
phungthien269 8fca8ef4aa fix: address CodeRabbit and Greptile review findings
- Reject missing values for --ecc-root/--out with exit code 2 (CodeRabbit) | - Preserve regex matcher tokens with metacharacters or mcp__ prefixes from lower-casing (CodeRabbit) | - Shell-quote eccRoot in the env prefix to close CWE-78 command substitution (CodeRabbit, Greptile P1) | - Fix last_verified_at future date in the deepseek-harness record (CodeRabbit) | - Guide and skill: clarify skill-name placeholders and make the compose check verify the entry instead of echoing OK on exit status alone (Greptile P2 x2) | - Tests: 11 passing including new coverage for quoting, regex preservation, and flag validation
2026-10-02 00:49:31 +07:00

127 lines
5.4 KiB
JavaScript

/**
* Tests for scripts/dsh/generate-hooks-config.js
*
* Run with: node tests/scripts/dsh-generate-hooks-config.test.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const gen = require('../../scripts/dsh/generate-hooks-config');
function test(name, fn) {
try {
fn();
console.log(' \u2713 ' + name);
return true;
} catch (err) {
console.log(' \u2717 ' + name);
console.log(' Error: ' + err.message);
return false;
}
}
function runTests() {
let passed = 0;
let failed = 0;
if (test('mapMatcher maps Claude tool names to DSH lower-case names', () => {
assert.strictEqual(gen.mapMatcher('Bash'), 'bash');
assert.strictEqual(gen.mapMatcher('Edit|MultiEdit'), 'edit');
assert.strictEqual(gen.mapMatcher('Write|Edit|MultiEdit'), 'write|edit');
})) passed++; else failed++;
if (test('mapMatcher keeps regex passthrough tokens untouched', () => {
assert.strictEqual(gen.mapMatcher('.*'), '.*');
assert.strictEqual(gen.mapMatcher('^mcp__'), '^mcp__');
assert.strictEqual(gen.mapMatcher('.*|Task'), '.*|task');
})) passed++; else failed++;
if (test('mapCommand prefixes plugin-root env vars and keeps the resolver intact', () => {
const cmd = 'node -e "resolver" pretooluse:bash:check-console-log scripts/hooks/check-console-log.js standard';
const out = gen.mapCommand(cmd, '/ecc');
assert.strictEqual(out, 'CLAUDE_PLUGIN_ROOT=\'/ecc\' ECC_PLUGIN_ROOT=\'/ecc\' ' + cmd);
})) passed++; else failed++;
if (test('mapCommand passes through non-resolver commands with prefix only', () => {
const out = gen.mapCommand('echo hi', '/ecc');
assert.strictEqual(out, 'CLAUDE_PLUGIN_ROOT=\'/ecc\' ECC_PLUGIN_ROOT=\'/ecc\' echo hi');
})) passed++; else failed++;
if (test('generateConfig skips unsupported events and reports them', () => {
const doc = { hooks: {
PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: 'node -e "x" node scripts/hooks/a.js' }] }],
PreCompact: [{ matcher: '.*', hooks: [{ type: 'command', command: 'echo hi' }] }],
SessionEnd: [{ matcher: '.*', hooks: [{ type: 'command', command: 'echo hi' }] }],
} };
const { config, report } = gen.generateConfig('/ecc', doc);
assert.deepStrictEqual(Object.keys(config.hooks), ['PreToolUse']);
assert.ok(report.some(r => r.includes('PreCompact')));
assert.ok(report.some(r => r.includes('SessionEnd')));
})) passed++; else failed++;
if (test('generateConfig preserves timeout and maps matcher groups', () => {
const doc = { hooks: {
PostToolUse: [{ matcher: 'Edit|MultiEdit|Write', hooks: [
{ type: 'command', command: 'node -e "x" node scripts/hooks/b.js', timeout: 30 },
] }],
} };
const { config } = gen.generateConfig('/ecc', doc);
const g = config.hooks.PostToolUse[0];
assert.strictEqual(g.matcher, 'edit|write');
assert.strictEqual(g.hooks[0].timeout, 30);
})) passed++; else failed++;
if (test('generateConfig supports the bridge-only Subagent events', () => {
assert.ok(gen.SUPPORTED_EVENTS.has('SubagentStart'));
assert.ok(gen.SUPPORTED_EVENTS.has('SubagentStop'));
})) passed++; else failed++;
if (test('mapMatcher preserves case-sensitive regex tokens with metacharacters', () => {
assert.strictEqual(gen.mapMatcher('^mcp__GitHub__.*'), '^mcp__GitHub__.*');
assert.strictEqual(gen.mapMatcher('\\S'), '\\S');
})) passed++; else failed++;
if (test('mapCommand shell-quotes dangerous path characters', () => {
const dollar = gen.mapCommand('echo hi', '/home/u/$proj');
assert.ok(dollar.startsWith("CLAUDE_PLUGIN_ROOT='/home/u/$proj' "), 'literal inside single quotes: ' + dollar);
const quoted = gen.mapCommand('echo hi', "/home/u'z");
assert.ok(quoted.startsWith("CLAUDE_PLUGIN_ROOT='/home/u'\\''z' "), 'embedded quote escaped: ' + quoted);
})) passed++; else failed++;
if (test('CLI rejects missing flag values with exit code 2', () => {
const repoRoot = path.resolve(__dirname, '..', '..');
const res = spawnSync('node', [
path.join(repoRoot, 'scripts', 'dsh', 'generate-hooks-config.js'),
'--ecc-root',
], { encoding: 'utf8' });
assert.strictEqual(res.status, 2, 'exit 2 on missing value, stderr: ' + res.stderr);
assert.ok(res.stderr.includes('Missing value for --ecc-root'));
})) passed++; else failed++;
if (test('CLI end-to-end writes valid JSON to --out', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-dsh-'));
const outPath = path.join(tmp, 'hooks.dsh.json');
const repoRoot = path.resolve(__dirname, '..', '..');
const res = spawnSync('node', [
path.join(repoRoot, 'scripts', 'dsh', 'generate-hooks-config.js'),
'--ecc-root', repoRoot, '--out', outPath,
], { encoding: 'utf8' });
assert.strictEqual(res.status, 0, 'CLI exit 0, stderr: ' + res.stderr);
const doc = JSON.parse(fs.readFileSync(outPath, 'utf8'));
assert.ok(doc.hooks.PreToolUse.length > 0, 'PreToolUse groups present');
assert.ok(Object.keys(doc.hooks).includes('SessionStart'));
assert.ok(!Object.keys(doc.hooks).includes('PreCompact'), 'unsupported event excluded');
fs.rmSync(tmp, { recursive: true, force: true });
})) passed++; else failed++;
console.log('');
console.log('Passed: ' + passed + ', Failed: ' + failed);
return failed === 0;
}
process.exit(runTests() ? 0 : 1);