mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 20:25:26 +02:00
- 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
127 lines
5.4 KiB
JavaScript
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);
|