Files
everything-claude-code/tests/hooks/block-no-verify.test.js
Ke Wang 809e0fa0a9 fix: address PR review comments on block-no-verify hook
- Add `minimal` profile so the security hook runs in all profiles
- Scope -n/--no-verify flag check to the detected subcommand region,
  preventing false positives on chained commands (e.g. `git log -n 10`)
- Guard stdin listeners with `require.main === module` so require()
  from run-with-flags.js does not register unnecessary listeners
- Verify subcommand token is preceded only by flags/flag-args after
  "git", preventing misclassification of argument values as subcommands
- Add integration tests for block-no-verify hook

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:29:01 -05:00

133 lines
5.0 KiB
JavaScript

/**
* Tests for scripts/hooks/block-no-verify.js via run-with-flags.js
*/
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runHook(input, env = {}) {
const rawInput = typeof input === 'string' ? input : JSON.stringify(input);
const result = spawnSync('node', [runner, 'pre:bash:block-no-verify', 'scripts/hooks/block-no-verify.js', 'minimal,standard,strict'], {
input: rawInput,
encoding: 'utf8',
env: {
...process.env,
ECC_HOOK_PROFILE: 'standard',
...env
},
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe']
});
return {
code: Number.isInteger(result.status) ? result.status : 1,
stdout: result.stdout || '',
stderr: result.stderr || ''
};
}
let passed = 0;
let failed = 0;
console.log('\nblock-no-verify hook tests');
console.log('─'.repeat(50));
// --- Basic allow/block ---
if (test('allows plain git commit', () => {
const r = runHook({ tool_input: { command: 'git commit -m "hello"' } });
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
})) passed++; else failed++;
if (test('blocks --no-verify on git commit', () => {
const r = runHook({ tool_input: { command: 'git commit --no-verify -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
assert.ok(r.stderr.includes('BLOCKED'), `stderr should contain BLOCKED: ${r.stderr}`);
})) passed++; else failed++;
if (test('blocks -n shorthand on git commit', () => {
const r = runHook({ tool_input: { command: 'git commit -n -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
assert.ok(r.stderr.includes('BLOCKED'), `stderr should contain BLOCKED: ${r.stderr}`);
})) passed++; else failed++;
if (test('blocks core.hooksPath override', () => {
const r = runHook({ tool_input: { command: 'git -c core.hooksPath=/dev/null commit -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
assert.ok(r.stderr.includes('core.hooksPath'), `stderr should mention core.hooksPath: ${r.stderr}`);
})) passed++; else failed++;
// --- Chained command false positive prevention (Comment 2) ---
if (test('does not false-positive on -n belonging to git log in a chain', () => {
const r = runHook({ tool_input: { command: 'git log -n 10 && git commit -m "msg"' } });
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
})) passed++; else failed++;
if (test('does not false-positive on --no-verify in a prior non-git command', () => {
const r = runHook({ tool_input: { command: 'echo --no-verify && git commit -m "msg"' } });
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
})) passed++; else failed++;
if (test('still blocks --no-verify on the git commit part of a chain', () => {
const r = runHook({ tool_input: { command: 'git log -n 5 && git commit --no-verify -m "msg"' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
})) passed++; else failed++;
// --- Subcommand detection (Comment 4) ---
if (test('does not misclassify "commit" as subcommand when it is an argument to push', () => {
// "git push origin commit" — "commit" is a refspec arg, not the subcommand
const r = runHook({ tool_input: { command: 'git push origin commit' } });
// This should detect "push" as the subcommand, not "commit"
// Either way it should not block since there's no --no-verify
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
})) passed++; else failed++;
// --- Blocks on push --no-verify ---
if (test('blocks --no-verify on git push', () => {
const r = runHook({ tool_input: { command: 'git push --no-verify' } });
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
assert.ok(r.stderr.includes('git push'), `stderr should mention git push: ${r.stderr}`);
})) passed++; else failed++;
// --- Non-git commands pass through ---
if (test('allows non-git commands', () => {
const r = runHook({ tool_input: { command: 'npm test' } });
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
})) passed++; else failed++;
// --- Plain text input (not JSON) ---
if (test('handles plain text input', () => {
const r = runHook('git commit -m "hello"');
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
})) passed++; else failed++;
if (test('blocks plain text input with --no-verify', () => {
const r = runHook('git commit --no-verify -m "msg"');
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
})) passed++; else failed++;
console.log('─'.repeat(50));
console.log(`Passed: ${passed} Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);