feat: add C++ language support and hook tests (#539)

- agents: cpp-build-resolver, cpp-reviewer
- commands: cpp-build, cpp-review, cpp-test
- rules: cpp/ (coding-style, hooks, patterns, security, testing)
- tests: 9 new hook test files with comprehensive coverage

Cherry-picked from PR #436.
This commit is contained in:
Affaan Mustafa
2026-03-16 14:31:49 -07:00
committed by GitHub
parent f12bb90924
commit b6595974c2
19 changed files with 2471 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
/**
* Tests for scripts/hooks/auto-tmux-dev.js
*
* Tests dev server command transformation for tmux wrapping.
*
* Run with: node tests/hooks/auto-tmux-dev.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'auto-tmux-dev.js');
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 runScript(input) {
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: typeof input === 'string' ? input : JSON.stringify(input),
timeout: 10000,
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function runTests() {
console.log('\n=== Testing auto-tmux-dev.js ===\n');
let passed = 0;
let failed = 0;
// Check if tmux is available for conditional tests
const tmuxAvailable = spawnSync('which', ['tmux'], { encoding: 'utf8' }).status === 0;
console.log('Dev server detection:');
if (test('transforms npm run dev command', () => {
const result = runScript({ tool_input: { command: 'npm run dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'), 'Should contain tmux');
assert.ok(output.tool_input.command.includes('npm run dev'), 'Should contain original command');
}
})) passed++; else failed++;
if (test('transforms pnpm dev command', () => {
const result = runScript({ tool_input: { command: 'pnpm dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'));
}
})) passed++; else failed++;
if (test('transforms yarn dev command', () => {
const result = runScript({ tool_input: { command: 'yarn dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'));
}
})) passed++; else failed++;
if (test('transforms bun run dev command', () => {
const result = runScript({ tool_input: { command: 'bun run dev' } });
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
if (process.platform !== 'win32' && tmuxAvailable) {
assert.ok(output.tool_input.command.includes('tmux'));
}
})) passed++; else failed++;
console.log('\nNon-dev commands (pass-through):');
if (test('does not transform npm install', () => {
const input = { tool_input: { command: 'npm install' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm install');
})) passed++; else failed++;
if (test('does not transform npm test', () => {
const input = { tool_input: { command: 'npm test' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm test');
})) passed++; else failed++;
if (test('does not transform npm run build', () => {
const input = { tool_input: { command: 'npm run build' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm run build');
})) passed++; else failed++;
if (test('does not transform npm run develop (partial match)', () => {
const input = { tool_input: { command: 'npm run develop' } };
const result = runScript(input);
assert.strictEqual(result.code, 0);
const output = JSON.parse(result.stdout);
assert.strictEqual(output.tool_input.command, 'npm run develop');
})) passed++; else failed++;
console.log('\nEdge cases:');
if (test('handles empty input gracefully', () => {
const result = runScript('{}');
assert.strictEqual(result.code, 0);
})) passed++; else failed++;
if (test('handles invalid JSON gracefully', () => {
const result = runScript('not json');
assert.strictEqual(result.code, 0);
assert.strictEqual(result.stdout, 'not json');
})) passed++; else failed++;
if (test('passes through missing command field', () => {
const input = { tool_input: {} };
const result = runScript(input);
assert.strictEqual(result.code, 0);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,108 @@
/**
* Tests for scripts/hooks/check-hook-enabled.js
*
* Tests the CLI wrapper around isHookEnabled.
*
* Run with: node tests/hooks/check-hook-enabled.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'check-hook-enabled.js');
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 runScript(args = [], envOverrides = {}) {
const env = { ...process.env, ...envOverrides };
// Remove potentially interfering env vars unless explicitly set
if (!envOverrides.ECC_HOOK_PROFILE) delete env.ECC_HOOK_PROFILE;
if (!envOverrides.ECC_DISABLED_HOOKS) delete env.ECC_DISABLED_HOOKS;
const result = spawnSync('node', [script, ...args], {
encoding: 'utf8',
timeout: 10000,
env,
});
return {
code: result.status || 0,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function runTests() {
console.log('\n=== Testing check-hook-enabled.js ===\n');
let passed = 0;
let failed = 0;
console.log('No arguments:');
if (test('returns yes when no hookId provided', () => {
const result = runScript([]);
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
console.log('\nDefault profile (standard):');
if (test('returns yes for hook with default profiles', () => {
const result = runScript(['my-hook']);
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
if (test('returns yes for hook with standard,strict profiles', () => {
const result = runScript(['my-hook', 'standard,strict']);
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
if (test('returns no for hook with only strict profile', () => {
const result = runScript(['my-hook', 'strict']);
assert.strictEqual(result.stdout, 'no');
})) passed++; else failed++;
if (test('returns no for hook with only minimal profile', () => {
const result = runScript(['my-hook', 'minimal']);
assert.strictEqual(result.stdout, 'no');
})) passed++; else failed++;
console.log('\nDisabled hooks:');
if (test('returns no when hook is disabled via env', () => {
const result = runScript(['my-hook'], { ECC_DISABLED_HOOKS: 'my-hook' });
assert.strictEqual(result.stdout, 'no');
})) passed++; else failed++;
if (test('returns yes when different hook is disabled', () => {
const result = runScript(['my-hook'], { ECC_DISABLED_HOOKS: 'other-hook' });
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
console.log('\nProfile overrides:');
if (test('returns yes for strict profile with strict-only hook', () => {
const result = runScript(['my-hook', 'strict'], { ECC_HOOK_PROFILE: 'strict' });
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
if (test('returns yes for minimal profile with minimal-only hook', () => {
const result = runScript(['my-hook', 'minimal'], { ECC_HOOK_PROFILE: 'minimal' });
assert.strictEqual(result.stdout, 'yes');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,131 @@
/**
* Tests for cost-tracker.js hook
*
* Run with: node tests/hooks/cost-tracker.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'cost-tracker.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function makeTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'cost-tracker-test-'));
}
function runScript(input, envOverrides = {}) {
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
env: { ...process.env, ...envOverrides },
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function runTests() {
console.log('\n=== Testing cost-tracker.js ===\n');
let passed = 0;
let failed = 0;
// 1. Passes through input on stdout
(test('passes through input on stdout', () => {
const input = {
model: 'claude-sonnet-4-20250514',
usage: { input_tokens: 100, output_tokens: 50 },
};
const inputStr = JSON.stringify(input);
const result = runScript(input);
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stdout, inputStr, 'Expected stdout to match original input');
}) ? passed++ : failed++);
// 2. Creates metrics file when given valid usage data
(test('creates metrics file when given valid usage data', () => {
const tmpHome = makeTempDir();
const input = {
model: 'claude-sonnet-4-20250514',
usage: { input_tokens: 1000, output_tokens: 500 },
};
const result = runScript(input, { HOME: tmpHome });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
assert.ok(fs.existsSync(metricsFile), `Expected metrics file to exist at ${metricsFile}`);
const content = fs.readFileSync(metricsFile, 'utf8').trim();
const row = JSON.parse(content);
assert.strictEqual(row.input_tokens, 1000, 'Expected input_tokens to be 1000');
assert.strictEqual(row.output_tokens, 500, 'Expected output_tokens to be 500');
assert.ok(row.timestamp, 'Expected timestamp to be present');
assert.ok(typeof row.estimated_cost_usd === 'number', 'Expected estimated_cost_usd to be a number');
assert.ok(row.estimated_cost_usd > 0, 'Expected estimated_cost_usd to be positive');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
// 3. Handles empty input gracefully
(test('handles empty input gracefully', () => {
const tmpHome = makeTempDir();
const result = runScript('', { HOME: tmpHome });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
// stdout should be empty since input was empty
assert.strictEqual(result.stdout, '', 'Expected empty stdout for empty input');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
// 4. Handles invalid JSON gracefully
(test('handles invalid JSON gracefully', () => {
const tmpHome = makeTempDir();
const invalidInput = 'not valid json {{{';
const result = runScript(invalidInput, { HOME: tmpHome });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
// Should still pass through the raw input on stdout
assert.strictEqual(result.stdout, invalidInput, 'Expected stdout to contain original invalid input');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
// 5. Handles missing usage fields gracefully
(test('handles missing usage fields gracefully', () => {
const tmpHome = makeTempDir();
const input = { model: 'claude-sonnet-4-20250514' };
const inputStr = JSON.stringify(input);
const result = runScript(input, { HOME: tmpHome });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stdout, inputStr, 'Expected stdout to match original input');
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
assert.ok(fs.existsSync(metricsFile), 'Expected metrics file to exist even with missing usage');
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
assert.strictEqual(row.input_tokens, 0, 'Expected input_tokens to be 0 when missing');
assert.strictEqual(row.output_tokens, 0, 'Expected output_tokens to be 0 when missing');
assert.strictEqual(row.estimated_cost_usd, 0, 'Expected estimated_cost_usd to be 0 when no tokens');
fs.rmSync(tmpHome, { recursive: true, force: true });
}) ? passed++ : failed++);
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env node
'use strict';
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'doc-file-warning.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(input) {
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: JSON.stringify(input),
timeout: 10000,
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function runTests() {
console.log('\n=== Testing doc-file-warning.js ===\n');
let passed = 0;
let failed = 0;
// 1. Allowed standard doc files - no warning in stderr
const standardFiles = [
'README.md',
'CLAUDE.md',
'AGENTS.md',
'CONTRIBUTING.md',
'CHANGELOG.md',
'LICENSE.md',
'SKILL.md',
'MEMORY.md',
'WORKLOG.md',
];
for (const file of standardFiles) {
(test(`allows standard doc file: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0, `expected exit code 0, got ${code}`);
assert.strictEqual(stderr, '', `expected no warning for ${file}, got: ${stderr}`);
}) ? passed++ : failed++);
}
// 2. Allowed directory paths - no warning
const allowedDirPaths = [
'docs/foo.md',
'docs/guide/setup.md',
'skills/bar.md',
'skills/testing/tdd.md',
'.history/session.md',
'memory/patterns.md',
'.claude/commands/deploy.md',
'.claude/plans/roadmap.md',
'.claude/projects/myproject.md',
];
for (const file of allowedDirPaths) {
(test(`allows directory path: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0, `expected exit code 0, got ${code}`);
assert.strictEqual(stderr, '', `expected no warning for ${file}, got: ${stderr}`);
}) ? passed++ : failed++);
}
// 3. Allowed .plan.md files - no warning
(test('allows .plan.md files', () => {
const { code, stderr } = runScript({ tool_input: { file_path: 'feature.plan.md' } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for .plan.md, got: ${stderr}`);
}) ? passed++ : failed++);
(test('allows nested .plan.md files', () => {
const { code, stderr } = runScript({ tool_input: { file_path: 'src/refactor.plan.md' } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for nested .plan.md, got: ${stderr}`);
}) ? passed++ : failed++);
// 4. Non-md/txt files always pass - no warning
const nonDocFiles = ['foo.js', 'app.py', 'styles.css', 'data.json', 'image.png'];
for (const file of nonDocFiles) {
(test(`allows non-doc file: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for ${file}, got: ${stderr}`);
}) ? passed++ : failed++);
}
// 5. Non-standard doc files - warning in stderr
const nonStandardFiles = ['random-notes.md', 'TODO.md', 'notes.txt', 'scratch.md', 'ideas.txt'];
for (const file of nonStandardFiles) {
(test(`warns on non-standard doc file: ${file}`, () => {
const { code, stderr } = runScript({ tool_input: { file_path: file } });
assert.strictEqual(code, 0, 'should still exit 0 (warn only)');
assert.ok(stderr.includes('WARNING'), `expected warning in stderr for ${file}, got: ${stderr}`);
assert.ok(stderr.includes(file), `expected file path in stderr for ${file}`);
}) ? passed++ : failed++);
}
// 6. Invalid/empty input - passes through without error
(test('handles empty object input without error', () => {
const { code, stderr } = runScript({});
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for empty input, got: ${stderr}`);
}) ? passed++ : failed++);
(test('handles missing file_path without error', () => {
const { code, stderr } = runScript({ tool_input: {} });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for missing file_path, got: ${stderr}`);
}) ? passed++ : failed++);
(test('handles empty file_path without error', () => {
const { code, stderr } = runScript({ tool_input: { file_path: '' } });
assert.strictEqual(code, 0);
assert.strictEqual(stderr, '', `expected no warning for empty file_path, got: ${stderr}`);
}) ? passed++ : failed++);
// 7. Stdout always contains the original input (pass-through)
(test('passes through input to stdout for allowed file', () => {
const input = { tool_input: { file_path: 'README.md' } };
const { stdout } = runScript(input);
assert.strictEqual(stdout, JSON.stringify(input));
}) ? passed++ : failed++);
(test('passes through input to stdout for warned file', () => {
const input = { tool_input: { file_path: 'random-notes.md' } };
const { stdout } = runScript(input);
assert.strictEqual(stdout, JSON.stringify(input));
}) ? passed++ : failed++);
(test('passes through input to stdout for empty input', () => {
const input = {};
const { stdout } = runScript(input);
assert.strictEqual(stdout, JSON.stringify(input));
}) ? passed++ : failed++);
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,397 @@
/**
* Tests for scripts/lib/hook-flags.js
*
* Run with: node tests/hooks/hook-flags.test.js
*/
const assert = require('assert');
// Import the module
const {
VALID_PROFILES,
normalizeId,
getHookProfile,
getDisabledHookIds,
parseProfiles,
isHookEnabled,
} = require('../../scripts/lib/hook-flags');
// Test helper
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
// Helper to save and restore env vars
function withEnv(vars, fn) {
const saved = {};
for (const key of Object.keys(vars)) {
saved[key] = process.env[key];
if (vars[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = vars[key];
}
}
try {
fn();
} finally {
for (const key of Object.keys(saved)) {
if (saved[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = saved[key];
}
}
}
}
// Test suite
function runTests() {
console.log('\n=== Testing hook-flags.js ===\n');
let passed = 0;
let failed = 0;
// VALID_PROFILES tests
console.log('VALID_PROFILES:');
if (test('is a Set', () => {
assert.ok(VALID_PROFILES instanceof Set);
})) passed++; else failed++;
if (test('contains minimal, standard, strict', () => {
assert.ok(VALID_PROFILES.has('minimal'));
assert.ok(VALID_PROFILES.has('standard'));
assert.ok(VALID_PROFILES.has('strict'));
})) passed++; else failed++;
if (test('contains exactly 3 profiles', () => {
assert.strictEqual(VALID_PROFILES.size, 3);
})) passed++; else failed++;
// normalizeId tests
console.log('\nnormalizeId:');
if (test('returns empty string for null', () => {
assert.strictEqual(normalizeId(null), '');
})) passed++; else failed++;
if (test('returns empty string for undefined', () => {
assert.strictEqual(normalizeId(undefined), '');
})) passed++; else failed++;
if (test('returns empty string for empty string', () => {
assert.strictEqual(normalizeId(''), '');
})) passed++; else failed++;
if (test('trims whitespace', () => {
assert.strictEqual(normalizeId(' hello '), 'hello');
})) passed++; else failed++;
if (test('converts to lowercase', () => {
assert.strictEqual(normalizeId('MyHook'), 'myhook');
})) passed++; else failed++;
if (test('handles mixed case with whitespace', () => {
assert.strictEqual(normalizeId(' My-Hook-ID '), 'my-hook-id');
})) passed++; else failed++;
if (test('converts numbers to string', () => {
assert.strictEqual(normalizeId(123), '123');
})) passed++; else failed++;
if (test('returns empty string for whitespace-only input', () => {
assert.strictEqual(normalizeId(' '), '');
})) passed++; else failed++;
// getHookProfile tests
console.log('\ngetHookProfile:');
if (test('defaults to standard when env var not set', () => {
withEnv({ ECC_HOOK_PROFILE: undefined }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
if (test('returns minimal when set to minimal', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal' }, () => {
assert.strictEqual(getHookProfile(), 'minimal');
});
})) passed++; else failed++;
if (test('returns standard when set to standard', () => {
withEnv({ ECC_HOOK_PROFILE: 'standard' }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
if (test('returns strict when set to strict', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict' }, () => {
assert.strictEqual(getHookProfile(), 'strict');
});
})) passed++; else failed++;
if (test('is case-insensitive', () => {
withEnv({ ECC_HOOK_PROFILE: 'STRICT' }, () => {
assert.strictEqual(getHookProfile(), 'strict');
});
})) passed++; else failed++;
if (test('trims whitespace from env var', () => {
withEnv({ ECC_HOOK_PROFILE: ' minimal ' }, () => {
assert.strictEqual(getHookProfile(), 'minimal');
});
})) passed++; else failed++;
if (test('defaults to standard for invalid value', () => {
withEnv({ ECC_HOOK_PROFILE: 'invalid' }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
if (test('defaults to standard for empty string', () => {
withEnv({ ECC_HOOK_PROFILE: '' }, () => {
assert.strictEqual(getHookProfile(), 'standard');
});
})) passed++; else failed++;
// getDisabledHookIds tests
console.log('\ngetDisabledHookIds:');
if (test('returns empty Set when env var not set', () => {
withEnv({ ECC_DISABLED_HOOKS: undefined }, () => {
const result = getDisabledHookIds();
assert.ok(result instanceof Set);
assert.strictEqual(result.size, 0);
});
})) passed++; else failed++;
if (test('returns empty Set for empty string', () => {
withEnv({ ECC_DISABLED_HOOKS: '' }, () => {
assert.strictEqual(getDisabledHookIds().size, 0);
});
})) passed++; else failed++;
if (test('returns empty Set for whitespace-only string', () => {
withEnv({ ECC_DISABLED_HOOKS: ' ' }, () => {
assert.strictEqual(getDisabledHookIds().size, 0);
});
})) passed++; else failed++;
if (test('parses single hook id', () => {
withEnv({ ECC_DISABLED_HOOKS: 'my-hook' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 1);
assert.ok(result.has('my-hook'));
});
})) passed++; else failed++;
if (test('parses multiple comma-separated hook ids', () => {
withEnv({ ECC_DISABLED_HOOKS: 'hook-a,hook-b,hook-c' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 3);
assert.ok(result.has('hook-a'));
assert.ok(result.has('hook-b'));
assert.ok(result.has('hook-c'));
});
})) passed++; else failed++;
if (test('trims whitespace around hook ids', () => {
withEnv({ ECC_DISABLED_HOOKS: ' hook-a , hook-b ' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 2);
assert.ok(result.has('hook-a'));
assert.ok(result.has('hook-b'));
});
})) passed++; else failed++;
if (test('normalizes hook ids to lowercase', () => {
withEnv({ ECC_DISABLED_HOOKS: 'MyHook,ANOTHER' }, () => {
const result = getDisabledHookIds();
assert.ok(result.has('myhook'));
assert.ok(result.has('another'));
});
})) passed++; else failed++;
if (test('filters out empty entries from trailing commas', () => {
withEnv({ ECC_DISABLED_HOOKS: 'hook-a,,hook-b,' }, () => {
const result = getDisabledHookIds();
assert.strictEqual(result.size, 2);
assert.ok(result.has('hook-a'));
assert.ok(result.has('hook-b'));
});
})) passed++; else failed++;
// parseProfiles tests
console.log('\nparseProfiles:');
if (test('returns fallback for null input', () => {
const result = parseProfiles(null);
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('returns fallback for undefined input', () => {
const result = parseProfiles(undefined);
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('uses custom fallback when provided', () => {
const result = parseProfiles(null, ['minimal']);
assert.deepStrictEqual(result, ['minimal']);
})) passed++; else failed++;
if (test('parses comma-separated string', () => {
const result = parseProfiles('minimal,strict');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('parses single string value', () => {
const result = parseProfiles('strict');
assert.deepStrictEqual(result, ['strict']);
})) passed++; else failed++;
if (test('parses array of profiles', () => {
const result = parseProfiles(['minimal', 'standard']);
assert.deepStrictEqual(result, ['minimal', 'standard']);
})) passed++; else failed++;
if (test('filters invalid profiles from string', () => {
const result = parseProfiles('minimal,invalid,strict');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('filters invalid profiles from array', () => {
const result = parseProfiles(['minimal', 'bogus', 'strict']);
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('returns fallback when all string values are invalid', () => {
const result = parseProfiles('invalid,bogus');
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('returns fallback when all array values are invalid', () => {
const result = parseProfiles(['invalid', 'bogus']);
assert.deepStrictEqual(result, ['standard', 'strict']);
})) passed++; else failed++;
if (test('is case-insensitive for string input', () => {
const result = parseProfiles('MINIMAL,STRICT');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('is case-insensitive for array input', () => {
const result = parseProfiles(['MINIMAL', 'STRICT']);
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('trims whitespace in string input', () => {
const result = parseProfiles(' minimal , strict ');
assert.deepStrictEqual(result, ['minimal', 'strict']);
})) passed++; else failed++;
if (test('handles null values in array', () => {
const result = parseProfiles([null, 'strict']);
assert.deepStrictEqual(result, ['strict']);
})) passed++; else failed++;
// isHookEnabled tests
console.log('\nisHookEnabled:');
if (test('returns true by default for a hook (standard profile)', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('returns true for empty hookId', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled(''), true);
});
})) passed++; else failed++;
if (test('returns true for null hookId', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled(null), true);
});
})) passed++; else failed++;
if (test('returns false when hook is in disabled list', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: 'my-hook' }, () => {
assert.strictEqual(isHookEnabled('my-hook'), false);
});
})) passed++; else failed++;
if (test('disabled check is case-insensitive', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: 'MY-HOOK' }, () => {
assert.strictEqual(isHookEnabled('my-hook'), false);
});
})) passed++; else failed++;
if (test('returns true when hook is not in disabled list', () => {
withEnv({ ECC_HOOK_PROFILE: undefined, ECC_DISABLED_HOOKS: 'other-hook' }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('returns false when current profile is not in allowed profiles', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'strict' }), false);
});
})) passed++; else failed++;
if (test('returns true when current profile is in allowed profiles', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'standard,strict' }), true);
});
})) passed++; else failed++;
if (test('returns true when current profile matches single allowed profile', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'minimal' }), true);
});
})) passed++; else failed++;
if (test('disabled hooks take precedence over profile match', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict', ECC_DISABLED_HOOKS: 'my-hook' }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: 'strict' }), false);
});
})) passed++; else failed++;
if (test('uses default profiles (standard, strict) when none specified', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), false);
});
})) passed++; else failed++;
if (test('allows standard profile by default', () => {
withEnv({ ECC_HOOK_PROFILE: 'standard', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('allows strict profile by default', () => {
withEnv({ ECC_HOOK_PROFILE: 'strict', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook'), true);
});
})) passed++; else failed++;
if (test('accepts array profiles option', () => {
withEnv({ ECC_HOOK_PROFILE: 'minimal', ECC_DISABLED_HOOKS: undefined }, () => {
assert.strictEqual(isHookEnabled('my-hook', { profiles: ['minimal', 'standard'] }), true);
});
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,207 @@
/**
* Tests for post-bash-build-complete.js and post-bash-pr-created.js
*
* Run with: node tests/hooks/post-bash-hooks.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const buildCompleteScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'post-bash-build-complete.js');
const prCreatedScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'post-bash-pr-created.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(scriptPath, input) {
return spawnSync('node', [scriptPath], {
encoding: 'utf8',
input,
stdio: ['pipe', 'pipe', 'pipe']
});
}
let passed = 0;
let failed = 0;
// ── post-bash-build-complete.js ──────────────────────────────────
console.log('\nPost-Bash Build Complete Hook Tests');
console.log('====================================\n');
console.log('Build command detection:');
if (test('stderr contains "Build completed" for npm run build command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm run build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('Build completed'), `stderr should contain "Build completed", got: ${result.stderr}`);
})) passed++; else failed++;
if (test('stderr contains "Build completed" for pnpm build command', () => {
const input = JSON.stringify({ tool_input: { command: 'pnpm build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('Build completed'), `stderr should contain "Build completed", got: ${result.stderr}`);
})) passed++; else failed++;
if (test('stderr contains "Build completed" for yarn build command', () => {
const input = JSON.stringify({ tool_input: { command: 'yarn build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('Build completed'), `stderr should contain "Build completed", got: ${result.stderr}`);
})) passed++; else failed++;
console.log('\nNon-build command detection:');
if (test('no stderr message for npm test command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm test' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-build command');
})) passed++; else failed++;
if (test('no stderr message for ls command', () => {
const input = JSON.stringify({ tool_input: { command: 'ls -la' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-build command');
})) passed++; else failed++;
if (test('no stderr message for git status command', () => {
const input = JSON.stringify({ tool_input: { command: 'git status' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-build command');
})) passed++; else failed++;
console.log('\nStdout pass-through:');
if (test('stdout passes through input for build command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm run build' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for non-build command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm test' } });
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for invalid JSON', () => {
const input = 'not valid json';
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through empty input', () => {
const input = '';
const result = runScript(buildCompleteScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
// ── post-bash-pr-created.js ──────────────────────────────────────
console.log('\n\nPost-Bash PR Created Hook Tests');
console.log('================================\n');
console.log('PR creation detection:');
if (test('stderr contains PR URL when gh pr create output has PR URL', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create --title "Fix bug" --body "desc"' },
tool_output: { output: 'https://github.com/owner/repo/pull/42\n' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('https://github.com/owner/repo/pull/42'), `stderr should contain PR URL, got: ${result.stderr}`);
assert.ok(result.stderr.includes('[Hook] PR created:'), 'stderr should contain PR created message');
assert.ok(result.stderr.includes('gh pr review 42'), 'stderr should contain review command');
})) passed++; else failed++;
if (test('stderr contains correct repo in review command', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create' },
tool_output: { output: 'Created PR\nhttps://github.com/my-org/my-repo/pull/123\nDone' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.ok(result.stderr.includes('--repo my-org/my-repo'), `stderr should contain correct repo, got: ${result.stderr}`);
assert.ok(result.stderr.includes('gh pr review 123'), 'stderr should contain correct PR number');
})) passed++; else failed++;
console.log('\nNon-PR command detection:');
if (test('no stderr about PR for non-gh command', () => {
const input = JSON.stringify({ tool_input: { command: 'npm test' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-PR command');
})) passed++; else failed++;
if (test('no stderr about PR for gh issue command', () => {
const input = JSON.stringify({ tool_input: { command: 'gh issue list' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for non-PR create command');
})) passed++; else failed++;
if (test('no stderr about PR for gh pr create without PR URL in output', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create' },
tool_output: { output: 'Error: could not create PR' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty when no PR URL in output');
})) passed++; else failed++;
if (test('no stderr about PR for gh pr list command', () => {
const input = JSON.stringify({ tool_input: { command: 'gh pr list' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.status, 0, 'Should exit with code 0');
assert.strictEqual(result.stderr, '', 'stderr should be empty for gh pr list');
})) passed++; else failed++;
console.log('\nStdout pass-through:');
if (test('stdout passes through input for PR create command', () => {
const input = JSON.stringify({
tool_input: { command: 'gh pr create' },
tool_output: { output: 'https://github.com/owner/repo/pull/1' }
});
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for non-PR command', () => {
const input = JSON.stringify({ tool_input: { command: 'echo hello' } });
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through input for invalid JSON', () => {
const input = 'not valid json';
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
if (test('stdout passes through empty input', () => {
const input = '';
const result = runScript(prCreatedScript, input);
assert.strictEqual(result.stdout, input, 'stdout should be the original input');
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);

View File

@@ -0,0 +1,121 @@
/**
* Tests for pre-bash-dev-server-block.js hook
*
* Run with: node tests/hooks/pre-bash-dev-server-block.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-dev-server-block.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(command) {
const input = { tool_input: { command } };
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: JSON.stringify(input),
timeout: 10000,
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' };
}
function runTests() {
console.log('\n=== Testing pre-bash-dev-server-block.js ===\n');
let passed = 0;
let failed = 0;
const isWindows = process.platform === 'win32';
// --- Blocking tests (non-Windows only) ---
if (!isWindows) {
(test('blocks npm run dev (exit code 2, stderr contains BLOCKED)', () => {
const result = runScript('npm run dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
assert.ok(result.stderr.includes('BLOCKED'), `Expected stderr to contain BLOCKED, got: ${result.stderr}`);
}) ? passed++ : failed++);
(test('blocks pnpm dev (exit code 2)', () => {
const result = runScript('pnpm dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks yarn dev (exit code 2)', () => {
const result = runScript('yarn dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
(test('blocks bun run dev (exit code 2)', () => {
const result = runScript('bun run dev');
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
}) ? passed++ : failed++);
} else {
console.log(' (skipping blocking tests on Windows)\n');
}
// --- Allow tests ---
(test('allows tmux-wrapped npm run dev (exit code 0)', () => {
const result = runScript('tmux new-session -d -s dev "npm run dev"');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows npm install (exit code 0)', () => {
const result = runScript('npm install');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows npm test (exit code 0)', () => {
const result = runScript('npm test');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
(test('allows npm run build (exit code 0)', () => {
const result = runScript('npm run build');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
}) ? passed++ : failed++);
// --- Edge cases ---
(test('empty/invalid input passes through (exit code 0)', () => {
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: '',
timeout: 10000,
});
assert.strictEqual(result.status || 0, 0, `Expected exit code 0, got ${result.status}`);
}) ? passed++ : failed++);
(test('stdout contains original input on pass-through', () => {
const input = { tool_input: { command: 'npm install' } };
const inputStr = JSON.stringify(input);
const result = spawnSync('node', [script], {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
});
assert.strictEqual(result.status || 0, 0);
assert.strictEqual(result.stdout.trim(), inputStr, `Expected stdout to contain original input`);
}) ? passed++ : failed++);
// --- Summary ---
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,104 @@
/**
* Tests for pre-bash-git-push-reminder.js and pre-bash-tmux-reminder.js hooks
*
* Run with: node tests/hooks/pre-bash-reminders.test.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const gitPushScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-git-push-reminder.js');
const tmuxScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-tmux-reminder.js');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function runScript(scriptPath, command, envOverrides = {}) {
const input = { tool_input: { command } };
const inputStr = JSON.stringify(input);
const result = spawnSync('node', [scriptPath], {
encoding: 'utf8',
input: inputStr,
timeout: 10000,
env: { ...process.env, ...envOverrides },
});
return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '', inputStr };
}
function runTests() {
console.log('\n=== Testing pre-bash-git-push-reminder.js & pre-bash-tmux-reminder.js ===\n');
let passed = 0;
let failed = 0;
// --- git-push-reminder tests ---
console.log(' git-push-reminder:');
(test('git push triggers stderr warning', () => {
const result = runScript(gitPushScript, 'git push origin main');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.ok(result.stderr.includes('[Hook]'), `Expected stderr to contain [Hook], got: ${result.stderr}`);
assert.ok(result.stderr.includes('Review changes before push'), `Expected stderr to mention review`);
}) ? passed++ : failed++);
(test('git status has no warning', () => {
const result = runScript(gitPushScript, 'git status');
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stderr, '', `Expected no stderr, got: ${result.stderr}`);
}) ? passed++ : failed++);
(test('git push always passes through input on stdout', () => {
const result = runScript(gitPushScript, 'git push');
assert.strictEqual(result.stdout, result.inputStr, 'Expected stdout to match original input');
}) ? passed++ : failed++);
// --- tmux-reminder tests (non-Windows only) ---
const isWindows = process.platform === 'win32';
if (!isWindows) {
console.log('\n tmux-reminder:');
(test('npm install triggers tmux suggestion', () => {
const result = runScript(tmuxScript, 'npm install', { TMUX: '' });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.ok(result.stderr.includes('[Hook]'), `Expected stderr to contain [Hook], got: ${result.stderr}`);
assert.ok(result.stderr.includes('tmux'), `Expected stderr to mention tmux`);
}) ? passed++ : failed++);
(test('npm test triggers tmux suggestion', () => {
const result = runScript(tmuxScript, 'npm test', { TMUX: '' });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.ok(result.stderr.includes('tmux'), `Expected stderr to mention tmux`);
}) ? passed++ : failed++);
(test('regular command like ls has no tmux suggestion', () => {
const result = runScript(tmuxScript, 'ls -la', { TMUX: '' });
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
assert.strictEqual(result.stderr, '', `Expected no stderr for ls, got: ${result.stderr}`);
}) ? passed++ : failed++);
(test('tmux reminder always passes through input on stdout', () => {
const result = runScript(tmuxScript, 'npm install', { TMUX: '' });
assert.strictEqual(result.stdout, result.inputStr, 'Expected stdout to match original input');
}) ? passed++ : failed++);
} else {
console.log('\n (skipping tmux-reminder tests on Windows)\n');
}
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -0,0 +1,159 @@
/**
* 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 qualityGate = require('../../scripts/hooks/quality-gate');
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++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);