Files
ECC/tests/hooks/quality-gate.test.js
T

281 lines
12 KiB
JavaScript

/**
* Tests for scripts/hooks/quality-gate.js
*
* Run with: node tests/hooks/quality-gate.test.js
*/
const assert = require('assert');
const path = require('path');
const os = require('os');
const fs = require('fs');
const { spawnSync } = require('child_process');
const qualityGate = require('../../scripts/hooks/quality-gate');
const hookPath = path.resolve(__dirname, '../../scripts/hooks/quality-gate.js');
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
return true;
} catch (err) {
console.log(` ✗ ${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
let passed = 0;
let failed = 0;
console.log('\nQuality Gate Hook Tests');
console.log('========================\n');
// --- run() returns original input for valid JSON ---
console.log('run() pass-through behavior:');
if (test('returns original input for valid JSON with file_path', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/nonexistent-file.js' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for valid JSON without file_path', () => {
const input = JSON.stringify({ tool_input: { command: 'ls' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for valid JSON with nested structure', () => {
const input = JSON.stringify({ tool_input: { file_path: '/some/path.ts', content: 'hello' }, other: [1, 2, 3] });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() returns original input for invalid JSON ---
console.log('\nInvalid JSON handling:');
if (test('returns original input for invalid JSON (no crash)', () => {
const input = 'this is not json at all {{{';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for partial JSON', () => {
const input = '{"tool_input": {';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input for JSON with trailing garbage', () => {
const input = '{"tool_input": {}}extra';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() returns original input when file does not exist ---
console.log('\nNon-existent file handling:');
if (test('returns original input when file_path points to non-existent file', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/does-not-exist-12345.js' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input when file_path is a non-existent .py file', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/does-not-exist-12345.py' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('returns original input when file_path is a non-existent .go file', () => {
const input = JSON.stringify({ tool_input: { file_path: '/tmp/does-not-exist-12345.go' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() returns original input for empty input ---
console.log('\nEmpty input handling:');
if (test('returns original input for empty string', () => {
const input = '';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return empty string unchanged');
})) passed++; else failed++;
if (test('returns original input for whitespace-only string', () => {
const input = ' ';
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return whitespace string unchanged');
})) passed++; else failed++;
// --- run() handles missing tool_input gracefully ---
console.log('\nMissing tool_input handling:');
if (test('handles missing tool_input gracefully', () => {
const input = JSON.stringify({ something_else: 'value' });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('handles null tool_input gracefully', () => {
const input = JSON.stringify({ tool_input: null });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('handles tool_input with empty file_path', () => {
const input = JSON.stringify({ tool_input: { file_path: '' } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
if (test('handles empty JSON object', () => {
const input = JSON.stringify({});
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
})) passed++; else failed++;
// --- run() with a real file (but no formatter installed) ---
console.log('\nReal file without formatter:');
if (test('returns original input for existing file with no formatter configured', () => {
const tmpFile = path.join(os.tmpdir(), `quality-gate-test-${Date.now()}.js`);
fs.writeFileSync(tmpFile, 'const x = 1;\n');
try {
const input = JSON.stringify({ tool_input: { file_path: tmpFile } });
const result = qualityGate.run(input);
assert.strictEqual(result, input, 'Should return original input unchanged');
} finally {
fs.unlinkSync(tmpFile);
}
})) passed++; else failed++;
// Exercise real executable shims: .cmd on Windows, executable shell scripts elsewhere.
function withFormatterFixture(formatter, fallback, fn) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'quality-gate project ')));
try {
const srcDir = path.join(root, 'source files');
const binDir = path.join(root, fallback ? 'runner bin' : 'node_modules/.bin');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(binDir, { recursive: true });
fs.writeFileSync(path.join(root, 'package.json'), '{}');
fs.writeFileSync(path.join(root, formatter === 'biome' ? 'biome.json' : '.prettierrc'), '{}');
const fileName = 'edited %ECC_TEST_LITERAL% !ECC_TEST_LITERAL! & file';
const filePath = path.join(srcDir, fileName + (formatter === 'biome' ? '.json' : '.js'));
const original = formatter === 'biome' ? '{"value":1}\n' : 'const value=1;\n';
fs.writeFileSync(filePath, original);
const capturePath = path.join(root, 'invocation.json');
const runnerPath = path.join(root, 'fake formatter.js');
fs.writeFileSync(runnerPath, [
"const fs = require('fs');",
"const path = require('path');",
'const args = process.argv.slice(2);',
'fs.writeFileSync(process.env.ECC_TEST_CAPTURE, JSON.stringify({ args, cwd: process.cwd() }));',
"if (args.includes('--write')) fs.appendFileSync(args.find(arg => path.isAbsolute(arg)), 'formatted\\n');",
'process.exit(Number(process.env.ECC_TEST_EXIT || 0));'
].join('\n'));
const binName = fallback ? 'npx' : formatter;
const shimPath = path.join(binDir, binName + (process.platform === 'win32' ? '.cmd' : ''));
const quoteSh = value => "'" + value.replace(/'/g, "'\\''") + "'";
const shim = process.platform === 'win32'
? `@echo off\r\n"${process.execPath}" "${runnerPath}" %*\r\n`
: `#!/bin/sh\nexec ${quoteSh(process.execPath)} ${quoteSh(runnerPath)} "$@"\n`;
fs.writeFileSync(shimPath, shim, { mode: 0o755 });
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toUpperCase() !== 'PATH'));
Object.assign(env, {
PATH: binDir,
CLAUDE_PACKAGE_MANAGER: 'npm',
ECC_QUALITY_GATE_FIX: 'false',
ECC_QUALITY_GATE_STRICT: 'true',
ECC_TEST_CAPTURE: capturePath,
ECC_TEST_LITERAL: 'must-not-expand',
ECC_TEST_EXIT: '0'
});
const input = JSON.stringify({ tool_input: { file_path: filePath } });
const runHook = (overrides, nodeArguments = []) => spawnSync(process.execPath, [...nodeArguments, hookPath], {
cwd: srcDir, input, encoding: 'utf8', timeout: 20000,
env: { ...env, ...overrides }
});
fn({ root, filePath, original, capturePath, input, runHook });
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
function assertInvocation(fixture, result, expectedArgs) {
assert.ifError(result.error);
assert.strictEqual(result.status, 0, result.stderr);
assert.strictEqual(result.stdout, fixture.input, 'Hook must preserve its input');
assert.ok(fs.existsSync(fixture.capturePath), 'Formatter shim was not executed');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(fixture.capturePath, 'utf8')), {
args: expectedArgs, cwd: fixture.root
});
}
console.log('\nReal formatter subprocesses:');
for (const formatter of ['prettier', 'biome']) {
for (const fix of [false, true]) {
if (test(`runs local ${formatter} in ${fix ? 'fix' : 'check'} mode with literal spaces and shell metacharacters`, () => {
withFormatterFixture(formatter, false, fixture => {
const result = fixture.runHook({ ECC_QUALITY_GATE_FIX: String(fix) });
const args = formatter === 'biome'
? ['check', fixture.filePath, ...(fix ? ['--write'] : [])]
: [fix ? '--write' : '--check', fixture.filePath];
assertInvocation(fixture, result, args);
assert.strictEqual(result.stderr, '', 'Successful formatter should not warn');
assert.strictEqual(fs.readFileSync(fixture.filePath, 'utf8'), fixture.original + (fix ? 'formatted\n' : ''));
});
})) passed++; else failed++;
}
if (test(`runs ${formatter} through the package runner when no local formatter exists`, () => {
withFormatterFixture(formatter, true, fixture => {
const args = formatter === 'biome'
? ['@biomejs/biome', 'check', fixture.filePath]
: ['prettier', '--check', fixture.filePath];
const result = fixture.runHook();
assertInvocation(fixture, result, args);
assert.strictEqual(result.stderr, '');
assert.strictEqual(fs.readFileSync(fixture.filePath, 'utf8'), fixture.original);
});
})) passed++; else failed++;
}
if (test('preserves literal formatter arguments even if invocation options drift to shell:true', () => {
withFormatterFixture('prettier', false, fixture => {
const preload = path.join(fixture.root, 'invocation-drift.cjs');
const helperPath = path.resolve(__dirname, '../../scripts/hooks/pre-bash-commit-quality.js');
fs.writeFileSync(preload, [
`const helper = require(${JSON.stringify(helperPath)});`,
'const original = helper.getLinterInvocation;',
'helper.getLinterInvocation = (...args) => {',
' const invocation = original(...args);',
' invocation.options.shell = true;',
' return invocation;',
'};'
].join('\n'));
const result = fixture.runHook({}, ['--require', preload]);
assertInvocation(fixture, result, ['--check', fixture.filePath]);
assert.strictEqual(result.stderr, '');
});
})) passed++; else failed++;
if (test('reports a real formatter failure in strict mode while preserving hook input', () => {
withFormatterFixture('prettier', false, fixture => {
const result = fixture.runHook({ ECC_TEST_EXIT: '2' });
assertInvocation(fixture, result, ['--check', fixture.filePath]);
assert.ok(result.stderr.includes(`[QualityGate] Prettier check failed for ${fixture.filePath}`));
assert.strictEqual(fs.readFileSync(fixture.filePath, 'utf8'), fixture.original);
});
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);