fix: capture stderr in typecheck hook, add 13 tests for session-end and utils

- 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
This commit is contained in:
Affaan Mustafa
2026-02-12 16:31:07 -08:00
parent e9f0f1334f
commit b1b28f2f92
3 changed files with 212 additions and 1 deletions

View File

@@ -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)))

View File

@@ -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:');

View File

@@ -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):');