From b1b28f2f926978f05baa734efa4a937202ef180d Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 16:31:07 -0800 Subject: [PATCH] fix: capture stderr in typecheck hook, add 13 tests for session-end and utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - post-edit-typecheck.js: capture both stdout and stderr from tsc - hooks.test.js: 7 extractSessionSummary tests (JSONL parsing, array content, malformed lines, empty transcript, long message truncation, env var fallback) - utils.test.js: 6 tests (replaceInFile g-flag behavior, string replace, capture groups, writeFile overwrite, unicode content) Total test count: 294 → 307 --- scripts/hooks/post-edit-typecheck.js | 2 +- tests/hooks/hooks.test.js | 131 +++++++++++++++++++++++++++ tests/lib/utils.test.js | 80 ++++++++++++++++ 3 files changed, 212 insertions(+), 1 deletion(-) diff --git a/scripts/hooks/post-edit-typecheck.js b/scripts/hooks/post-edit-typecheck.js index a6913bc1..94e969d1 100644 --- a/scripts/hooks/post-edit-typecheck.js +++ b/scripts/hooks/post-edit-typecheck.js @@ -54,7 +54,7 @@ process.stdin.on('end', () => { }); } catch (err) { // tsc exits non-zero when there are errors — filter to edited file - const output = err.stdout || ''; + const output = (err.stdout || '') + (err.stderr || ''); const relevantLines = output .split('\n') .filter(line => line.includes(filePath) || line.includes(path.basename(filePath))) diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 81175c6d..0acff0a0 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -618,6 +618,137 @@ async function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; + // session-end.js extractSessionSummary tests + console.log('\nsession-end.js (extractSessionSummary):'); + + if (await asyncTest('extracts user messages from transcript', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const lines = [ + '{"type":"user","content":"Fix the login bug"}', + '{"type":"assistant","content":"I will fix it"}', + '{"type":"user","content":"Also add tests"}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles transcript with array content fields', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const lines = [ + '{"type":"user","content":[{"text":"Part 1"},{"text":"Part 2"}]}', + '{"type":"user","content":"Simple message"}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle array content without crash'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('extracts tool names and file paths from transcript', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const lines = [ + '{"type":"user","content":"Edit the file"}', + '{"type":"tool_use","tool_name":"Edit","tool_input":{"file_path":"/src/main.ts"}}', + '{"type":"tool_use","tool_name":"Read","tool_input":{"file_path":"/src/utils.ts"}}', + '{"type":"tool_use","tool_name":"Write","tool_input":{"file_path":"/src/new.ts"}}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0); + // Session file should contain summary with tools used + assert.ok( + result.stderr.includes('Created session file') || result.stderr.includes('Updated session file'), + 'Should create/update session file' + ); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles transcript with malformed JSON lines', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const lines = [ + '{"type":"user","content":"Valid message"}', + 'NOT VALID JSON', + '{"broken json', + '{"type":"user","content":"Another valid"}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should skip malformed lines gracefully'); + assert.ok( + result.stderr.includes('unparseable') || result.stderr.includes('Skipped'), + `Should report parse errors, got: ${result.stderr.substring(0, 200)}` + ); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('handles empty transcript (no user messages)', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + // Only tool_use entries, no user messages + const lines = [ + '{"type":"tool_use","tool_name":"Read","tool_input":{}}', + '{"type":"assistant","content":"done"}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle transcript with no user messages'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('truncates long user messages to 200 chars', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const longMsg = 'x'.repeat(500); + const lines = [ + `{"type":"user","content":"${longMsg}"}`, + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const stdinJson = JSON.stringify({ transcript_path: transcriptPath }); + const result = await runScript(path.join(scriptsDir, 'session-end.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should handle and truncate long messages'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (await asyncTest('uses CLAUDE_TRANSCRIPT_PATH env var as fallback', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + + const lines = [ + '{"type":"user","content":"Fallback test message"}', + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + // Send invalid JSON to stdin so it falls back to env var + const result = await runScript(path.join(scriptsDir, 'session-end.js'), 'not json', { + CLAUDE_TRANSCRIPT_PATH: transcriptPath + }); + assert.strictEqual(result.code, 0, 'Should use env var fallback'); + cleanupTestDir(testDir); + })) passed++; else failed++; + // hooks.json validation console.log('\nhooks.json Validation:'); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index 7b975ce8..9d1d6cea 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -505,6 +505,86 @@ function runTests() { assert.ok(dir.includes('learned')); })) passed++; else failed++; + // replaceInFile behavior tests + console.log('\nreplaceInFile (behavior):'); + + if (test('replaces first match when regex has no g flag', () => { + const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`); + try { + utils.writeFile(testFile, 'foo bar foo baz foo'); + utils.replaceInFile(testFile, /foo/, 'qux'); + const content = utils.readFile(testFile); + // Without g flag, only first 'foo' should be replaced + assert.strictEqual(content, 'qux bar foo baz foo'); + } finally { + fs.unlinkSync(testFile); + } + })) passed++; else failed++; + + if (test('replaces all matches when regex has g flag', () => { + const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`); + try { + utils.writeFile(testFile, 'foo bar foo baz foo'); + utils.replaceInFile(testFile, /foo/g, 'qux'); + const content = utils.readFile(testFile); + assert.strictEqual(content, 'qux bar qux baz qux'); + } finally { + fs.unlinkSync(testFile); + } + })) passed++; else failed++; + + if (test('replaces with string search (first occurrence)', () => { + const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`); + try { + utils.writeFile(testFile, 'hello world hello'); + utils.replaceInFile(testFile, 'hello', 'goodbye'); + const content = utils.readFile(testFile); + // String.replace with string search only replaces first + assert.strictEqual(content, 'goodbye world hello'); + } finally { + fs.unlinkSync(testFile); + } + })) passed++; else failed++; + + if (test('replaces with capture groups', () => { + const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`); + try { + utils.writeFile(testFile, '**Last Updated:** 10:30'); + utils.replaceInFile(testFile, /\*\*Last Updated:\*\*.*/, '**Last Updated:** 14:45'); + const content = utils.readFile(testFile); + assert.strictEqual(content, '**Last Updated:** 14:45'); + } finally { + fs.unlinkSync(testFile); + } + })) passed++; else failed++; + + // writeFile edge cases + console.log('\nwriteFile (edge cases):'); + + if (test('writeFile overwrites existing content', () => { + const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`); + try { + utils.writeFile(testFile, 'original'); + utils.writeFile(testFile, 'replaced'); + const content = utils.readFile(testFile); + assert.strictEqual(content, 'replaced'); + } finally { + fs.unlinkSync(testFile); + } + })) passed++; else failed++; + + if (test('writeFile handles unicode content', () => { + const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`); + try { + const unicode = '日本語テスト 🚀 émojis'; + utils.writeFile(testFile, unicode); + const content = utils.readFile(testFile); + assert.strictEqual(content, unicode); + } finally { + fs.unlinkSync(testFile); + } + })) passed++; else failed++; + // findFiles with regex special characters in pattern console.log('\nfindFiles (regex chars):');