From 51511461f60c3f94ba4db3b4da20d3e4ab8efae5 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 29 Apr 2026 18:28:56 -0400 Subject: [PATCH] test: cover pre-bash commit quality edges --- tests/hooks/pre-bash-commit-quality.test.js | 196 ++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/tests/hooks/pre-bash-commit-quality.test.js b/tests/hooks/pre-bash-commit-quality.test.js index 478d34d2..ba5c7ec2 100644 --- a/tests/hooks/pre-bash-commit-quality.test.js +++ b/tests/hooks/pre-bash-commit-quality.test.js @@ -40,6 +40,48 @@ function inTempRepo(fn) { } } +function captureConsoleError(fn) { + const previousError = console.error; + const lines = []; + console.error = (...args) => { + lines.push(args.join(' ')); + }; + + try { + const result = fn(); + return { result, stderr: lines.join('\n') }; + } finally { + console.error = previousError; + } +} + +function writeAndStage(repoDir, relativePath, content) { + const filePath = path.join(repoDir, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf8'); + spawnSync('git', ['add', relativePath], { cwd: repoDir, stdio: 'pipe', encoding: 'utf8' }); +} + +function withEnv(overrides, fn) { + const previous = {}; + for (const key of Object.keys(overrides)) { + previous[key] = process.env[key]; + process.env[key] = overrides[key]; + } + + try { + return fn(); + } finally { + for (const key of Object.keys(overrides)) { + if (typeof previous[key] === 'string') { + process.env[key] = previous[key]; + } else { + delete process.env[key]; + } + } + } +} + let passed = 0; let failed = 0; @@ -77,5 +119,159 @@ if (test('evaluate inspects staged snapshot instead of newer working tree conten }); })) passed++; else failed++; +if (test('passes through non-commit amend malformed JSON and run wrapper paths', () => { + const readInput = JSON.stringify({ tool_input: { command: 'git status --short' } }); + assert.deepStrictEqual(hook.evaluate(readInput), { output: readInput, exitCode: 0 }); + + const amendInput = JSON.stringify({ tool_input: { command: 'git commit --amend -m "fix: update"' } }); + assert.deepStrictEqual(hook.evaluate(amendInput), { output: amendInput, exitCode: 0 }); + + const malformed = 'not json {{{'; + const malformedResult = captureConsoleError(() => hook.run(malformed)); + assert.deepStrictEqual(malformedResult.result, { stdout: malformed, exitCode: 0 }); + assert.ok(malformedResult.stderr.includes('[Hook] Error:'), 'should log JSON parse errors without blocking'); +})) passed++; else failed++; + +if (test('allows git commit when no files are staged', () => { + inTempRepo(() => { + const input = JSON.stringify({ tool_input: { command: 'git commit -m "fix: no staged files"' } }); + const { result, stderr } = captureConsoleError(() => hook.evaluate(input)); + + assert.strictEqual(result.output, input); + assert.strictEqual(result.exitCode, 0); + assert.ok(stderr.includes('No staged files found'), `expected no-staged warning, got: ${stderr}`); + }); +})) passed++; else failed++; + +if (test('allows warning-only issues while reporting console TODO and message warnings', () => { + inTempRepo(repoDir => { + writeAndStage(repoDir, 'index.js', [ + 'console.log("debug only");', + '// TODO: clean this up', + '// TODO: tracked in issue #123', + '// console.log("commented out");', + '* console.log("doc comment");', + 'const ok = true;', + '' + ].join('\n')); + + const input = JSON.stringify({ + tool_input: { + command: 'git commit -m "fix: Uppercase subject."' + } + }); + const { result, stderr } = captureConsoleError(() => hook.evaluate(input)); + + assert.strictEqual(result.output, input); + assert.strictEqual(result.exitCode, 0, 'warning-only issues should not block'); + assert.ok(stderr.includes('WARNING Line 1'), `expected console warning, got: ${stderr}`); + assert.ok(stderr.includes('INFO Line 2'), `expected TODO info warning, got: ${stderr}`); + assert.ok(stderr.includes('Subject should start with lowercase'), `expected capitalization warning, got: ${stderr}`); + assert.ok(stderr.includes('should not end with a period'), `expected punctuation warning, got: ${stderr}`); + assert.ok(stderr.includes('Warnings found'), `expected warning summary, got: ${stderr}`); + }); +})) passed++; else failed++; + +if (test('reports invalid and long commit messages without blocking when files are clean', () => { + inTempRepo(repoDir => { + writeAndStage(repoDir, 'index.js', 'const clean = true;\n'); + + const longMessage = `Bad message ${'x'.repeat(80)}`; + const input = JSON.stringify({ + tool_input: { + command: `git commit --message="${longMessage}"` + } + }); + const { result, stderr } = captureConsoleError(() => hook.evaluate(input)); + + assert.strictEqual(result.output, input); + assert.strictEqual(result.exitCode, 0); + assert.ok(stderr.includes('does not follow conventional commit format'), `expected format warning, got: ${stderr}`); + assert.ok(stderr.includes('Commit message too long'), `expected length warning, got: ${stderr}`); + }); +})) passed++; else failed++; + +if (test('blocks commits with staged secret patterns across checkable files', () => { + inTempRepo(repoDir => { + writeAndStage(repoDir, 'index.js', [ + "const openai = 'sk-abcdefghijklmnopqrstuvwxyz';", + "const token = 'ghp_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ';", + '' + ].join('\n')); + writeAndStage(repoDir, 'app.py', [ + 'aws = "AKIAABCDEFGHIJKLMNOP"', + 'api_key = "secret-value"', + '' + ].join('\n')); + + const input = JSON.stringify({ tool_input: { command: 'git commit -m "fix: block secrets"' } }); + const { result, stderr } = captureConsoleError(() => hook.evaluate(input)); + + assert.strictEqual(result.output, input); + assert.strictEqual(result.exitCode, 2); + assert.ok(stderr.includes('Potential OpenAI API key'), `expected OpenAI secret warning, got: ${stderr}`); + assert.ok(stderr.includes('Potential GitHub PAT'), `expected GitHub PAT warning, got: ${stderr}`); + assert.ok(stderr.includes('Potential AWS Access Key'), `expected AWS key warning, got: ${stderr}`); + assert.ok(stderr.includes('Potential API key'), `expected generic API key warning, got: ${stderr}`); + }); +})) passed++; else failed++; + +if (test('reports eslint pylint and golint failures from staged files', () => { + inTempRepo(repoDir => { + writeAndStage(repoDir, 'index.js', 'const lint = true;\n'); + writeAndStage(repoDir, 'app.py', 'print("lint")\n'); + writeAndStage(repoDir, 'main.go', 'package main\n'); + + const eslintPath = path.join(repoDir, 'node_modules', '.bin', process.platform === 'win32' ? 'eslint.cmd' : 'eslint'); + fs.mkdirSync(path.dirname(eslintPath), { recursive: true }); + fs.writeFileSync(eslintPath, '#!/bin/sh\necho "eslint failed"\nexit 1\n', 'utf8'); + fs.chmodSync(eslintPath, 0o755); + + const binDir = path.join(repoDir, 'fake-bin'); + fs.mkdirSync(binDir, { recursive: true }); + const pylintPath = path.join(binDir, 'pylint'); + const golintPath = path.join(binDir, 'golint'); + fs.writeFileSync(pylintPath, '#!/bin/sh\necho "pylint failed"\nexit 1\n', 'utf8'); + fs.writeFileSync(golintPath, '#!/bin/sh\necho "main.go:1: lint failed"\nexit 0\n', 'utf8'); + fs.chmodSync(pylintPath, 0o755); + fs.chmodSync(golintPath, 0o755); + + withEnv({ PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}` }, () => { + const input = JSON.stringify({ tool_input: { command: 'git commit -m "fix: lint failures"' } }); + const { result, stderr } = captureConsoleError(() => hook.evaluate(input)); + + assert.strictEqual(result.output, input); + assert.strictEqual(result.exitCode, 2); + assert.ok(stderr.includes('ESLint Issues'), `expected ESLint output, got: ${stderr}`); + assert.ok(stderr.includes('eslint failed'), `expected ESLint failure text, got: ${stderr}`); + assert.ok(stderr.includes('Pylint Issues'), `expected Pylint output, got: ${stderr}`); + assert.ok(stderr.includes('pylint failed'), `expected Pylint failure text, got: ${stderr}`); + assert.ok(stderr.includes('golint Issues'), `expected golint output, got: ${stderr}`); + assert.ok(stderr.includes('main.go:1: lint failed'), `expected golint failure text, got: ${stderr}`); + }); + }); +})) passed++; else failed++; + +if (test('stdin entry point truncates oversized input and preserves pass-through output', () => { + const oversized = JSON.stringify({ + tool_input: { + command: 'git status', + filler: 'x'.repeat(1024 * 1024 + 1024) + } + }); + const result = spawnSync('node', [path.join(__dirname, '..', '..', 'scripts', 'hooks', 'pre-bash-commit-quality.js')], { + input: oversized, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 10000 + }); + + assert.strictEqual(result.status, 0); + assert.ok(result.stdout.length > 0, 'expected truncated payload to pass through'); + assert.ok(result.stdout.length <= 1024 * 1024, 'expected stdout to stay within hook input limit'); + assert.strictEqual(result.stdout, oversized.slice(0, result.stdout.length)); + assert.ok(result.stderr.includes('[Hook] Error:'), 'truncated JSON should be logged and allowed'); +})) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);