From 90ea2f327ce8c6fefe2d8802099d55b7c9fc39a1 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 16:02:31 -0800 Subject: [PATCH] fix: 2 bugs fixed, 17 tests added for hook scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fixes: - evaluate-session.js: whitespace-tolerant regex for counting user messages in JSONL transcripts (/"type":"user"/ → /"type"\s*:\s*"user"/) - session-end.js: guard against null elements in content arrays (c.text → (c && c.text) to prevent TypeError) New tests (17): - evaluate-session: whitespace JSON regression test - session-end: null content array elements regression test - post-edit-console-warn: 5 tests (warn, skip non-JS, clean files, missing file, stdout passthrough) - post-edit-format: 3 tests (empty stdin, non-JS skip, invalid JSON) - post-edit-typecheck: 4 tests (empty stdin, non-TS skip, missing file, no tsconfig) Total test count: 191 (up from 164) --- scripts/hooks/evaluate-session.js | 4 +- scripts/hooks/session-end.js | 2 +- tests/hooks/hooks.test.js | 149 ++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/scripts/hooks/evaluate-session.js b/scripts/hooks/evaluate-session.js index 0dcfd37..1aaa67d 100644 --- a/scripts/hooks/evaluate-session.js +++ b/scripts/hooks/evaluate-session.js @@ -81,8 +81,8 @@ async function main() { process.exit(0); } - // Count user messages in session - const messageCount = countInFile(transcriptPath, /"type":"user"/g); + // Count user messages in session (allow optional whitespace around colon) + const messageCount = countInFile(transcriptPath, /"type"\s*:\s*"user"/g); // Skip short sessions if (messageCount < minSessionLength) { diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index 632c5f0..bb2133f 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -49,7 +49,7 @@ function extractSessionSummary(transcriptPath) { const text = typeof entry.content === 'string' ? entry.content : Array.isArray(entry.content) - ? entry.content.map(c => c.text || '').join(' ') + ? entry.content.map(c => (c && c.text) || '').join(' ') : ''; if (text.trim()) { userMessages.push(text.trim().slice(0, 200)); diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index aebe0d2..b830ebd 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -262,6 +262,155 @@ async function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + // evaluate-session.js: whitespace tolerance regression test + if (await asyncTest('counts user messages with whitespace in JSON (regression)', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + // Create transcript with whitespace around colons (pretty-printed style) + const lines = []; + for (let i = 0; i < 15; i++) { + lines.push('{ "type" : "user", "content": "message ' + i + '" }'); + } + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'evaluate-session.js'), stdinJson); + + assert.ok( + result.stderr.includes('15 messages'), + 'Should count user messages with whitespace in JSON, got: ' + result.stderr.trim() + ); + + cleanupTestDir(testDir); + })) passed++; else failed++; + + // session-end.js: content array with null elements regression test + if (await asyncTest('handles transcript with null content array elements (regression)', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + // Create transcript with null elements in content array + const lines = [ + '{"type":"user","content":[null,{"text":"hello"},null,{"text":"world"}]}', + '{"type":"user","content":"simple string message"}', + '{"type":"user","content":[{"text":"normal"},{"text":"array"}]}', + '{"type":"tool_use","tool_name":"Edit","tool_input":{"file_path":"/test.js"}}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + + // Should not crash (exit 0) + assert.strictEqual(result.code, 0, 'Should handle null content elements without crash'); + })) passed++; else failed++; + + // post-edit-console-warn.js tests + console.log('\npost-edit-console-warn.js:'); + + if (await asyncTest('warns about console.log in JS files', async () => { + const testDir = createTestDir(); + const testFile = path.join(testDir, 'test.js'); + fs.writeFileSync(testFile, 'const x = 1;\nconsole.log(x);\nreturn x;'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), stdinJson); + + assert.ok(result.stderr.includes('console.log'), 'Should warn about console.log'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('does not warn for non-JS files', async () => { + const testDir = createTestDir(); + const testFile = path.join(testDir, 'test.md'); + fs.writeFileSync(testFile, 'Use console.log for debugging'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), stdinJson); + + assert.ok(!result.stderr.includes('console.log'), 'Should not warn for non-JS files'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('does not warn for clean JS files', async () => { + const testDir = createTestDir(); + const testFile = path.join(testDir, 'clean.ts'); + fs.writeFileSync(testFile, 'const x = 1;\nreturn x;'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), stdinJson); + + assert.ok(!result.stderr.includes('WARNING'), 'Should not warn for clean files'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles missing file gracefully', async () => { + const stdinJson = JSON.stringify({ tool_input: { file_path: '/nonexistent/file.ts' } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), stdinJson); + + assert.strictEqual(result.code, 0, 'Should not crash on missing file'); + })) passed++; else failed++; + + if (await asyncTest('passes through original data on stdout', async () => { + const stdinJson = JSON.stringify({ tool_input: { file_path: '/test.py' } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-console-warn.js'), stdinJson); + + assert.ok(result.stdout.includes('tool_input'), 'Should pass through stdin data'); + })) passed++; else failed++; + + // post-edit-format.js tests + console.log('\npost-edit-format.js:'); + + if (await asyncTest('runs without error on empty stdin', async () => { + const result = await runScript(path.join(scriptsDir, 'post-edit-format.js')); + assert.strictEqual(result.code, 0, 'Should exit 0 on empty stdin'); + })) passed++; else failed++; + + if (await asyncTest('skips non-JS/TS files', async () => { + const stdinJson = JSON.stringify({ tool_input: { file_path: '/test.py' } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should exit 0 for non-JS files'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through stdin data'); + })) passed++; else failed++; + + if (await asyncTest('passes through data for invalid JSON', async () => { + const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), 'not json'); + assert.strictEqual(result.code, 0, 'Should exit 0 for invalid JSON'); + })) passed++; else failed++; + + // post-edit-typecheck.js tests + console.log('\npost-edit-typecheck.js:'); + + if (await asyncTest('runs without error on empty stdin', async () => { + const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js')); + assert.strictEqual(result.code, 0, 'Should exit 0 on empty stdin'); + })) passed++; else failed++; + + if (await asyncTest('skips non-TypeScript files', async () => { + const stdinJson = JSON.stringify({ tool_input: { file_path: '/test.js' } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should exit 0 for non-TS files'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through stdin data'); + })) passed++; else failed++; + + if (await asyncTest('handles nonexistent TS file gracefully', async () => { + const stdinJson = JSON.stringify({ tool_input: { file_path: '/nonexistent/file.ts' } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should exit 0 for missing file'); + })) passed++; else failed++; + + if (await asyncTest('handles TS file with no tsconfig gracefully', async () => { + const testDir = createTestDir(); + const testFile = path.join(testDir, 'test.ts'); + fs.writeFileSync(testFile, 'const x: number = 1;'); + + const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-typecheck.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should exit 0 when no tsconfig found'); + cleanupTestDir(testDir); + })) passed++; else failed++; + // hooks.json validation console.log('\nhooks.json Validation:');