mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: calendar-accurate date validation in parseSessionFilename, add 22 tests
- Fix parseSessionFilename to reject impossible dates (Feb 31, Apr 31, Feb 29 non-leap) using Date constructor month/day roundtrip check - Add 6 session-manager tests for calendar date validation edge cases - Add 3 session-manager tests for code blocks/special chars in getSessionStats - Add 10 package-manager tests for PM-specific command formats (getRunCommand and getExecCommand for pnpm, yarn, bun, npm) - Add 3 integration tests for session-end transcript parsing (mixed JSONL formats, malformed lines, nested user messages)
This commit is contained in:
@@ -405,6 +405,125 @@ async function runTests() {
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ==========================================
|
||||
// Session End Transcript Parsing Tests
|
||||
// ==========================================
|
||||
console.log('\nSession End Transcript Parsing:');
|
||||
|
||||
if (await asyncTest('session-end extracts summary from mixed JSONL formats', async () => {
|
||||
const testDir = createTestDir();
|
||||
const transcriptPath = path.join(testDir, 'mixed-transcript.jsonl');
|
||||
|
||||
// Create transcript with both direct tool_use and nested assistant message formats
|
||||
const lines = [
|
||||
JSON.stringify({ type: 'user', content: 'Fix the login bug' }),
|
||||
JSON.stringify({ type: 'tool_use', name: 'Read', input: { file_path: 'src/auth.ts' } }),
|
||||
JSON.stringify({ type: 'assistant', message: { content: [
|
||||
{ type: 'tool_use', name: 'Edit', input: { file_path: 'src/auth.ts' } }
|
||||
]}}),
|
||||
JSON.stringify({ type: 'user', content: 'Now add tests' }),
|
||||
JSON.stringify({ type: 'assistant', message: { content: [
|
||||
{ type: 'tool_use', name: 'Write', input: { file_path: 'tests/auth.test.ts' } },
|
||||
{ type: 'text', text: 'Here are the tests' }
|
||||
]}}),
|
||||
JSON.stringify({ type: 'user', content: 'Looks good, commit' })
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join('\n'));
|
||||
|
||||
try {
|
||||
const result = await runHookWithInput(
|
||||
path.join(scriptsDir, 'session-end.js'),
|
||||
{ transcript_path: transcriptPath },
|
||||
{ HOME: testDir, USERPROFILE: testDir }
|
||||
);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0');
|
||||
assert.ok(result.stderr.includes('[SessionEnd]'), 'Should have SessionEnd log');
|
||||
|
||||
// Verify a session file was created
|
||||
const sessionsDir = path.join(testDir, '.claude', 'sessions');
|
||||
if (fs.existsSync(sessionsDir)) {
|
||||
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.tmp'));
|
||||
assert.ok(files.length > 0, 'Should create a session file');
|
||||
|
||||
// Verify session content includes tasks from user messages
|
||||
const content = fs.readFileSync(path.join(sessionsDir, files[0]), 'utf8');
|
||||
assert.ok(content.includes('Fix the login bug'), 'Should include first user message');
|
||||
assert.ok(content.includes('auth.ts'), 'Should include modified files');
|
||||
}
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('session-end handles transcript with malformed lines gracefully', async () => {
|
||||
const testDir = createTestDir();
|
||||
const transcriptPath = path.join(testDir, 'malformed-transcript.jsonl');
|
||||
|
||||
const lines = [
|
||||
JSON.stringify({ type: 'user', content: 'Task 1' }),
|
||||
'{broken json here',
|
||||
JSON.stringify({ type: 'user', content: 'Task 2' }),
|
||||
'{"truncated":',
|
||||
JSON.stringify({ type: 'user', content: 'Task 3' })
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join('\n'));
|
||||
|
||||
try {
|
||||
const result = await runHookWithInput(
|
||||
path.join(scriptsDir, 'session-end.js'),
|
||||
{ transcript_path: transcriptPath },
|
||||
{ HOME: testDir, USERPROFILE: testDir }
|
||||
);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0 despite malformed lines');
|
||||
// Should still process the valid lines
|
||||
assert.ok(result.stderr.includes('[SessionEnd]'), 'Should have SessionEnd log');
|
||||
assert.ok(result.stderr.includes('unparseable'), 'Should warn about unparseable lines');
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('session-end creates session file with nested user messages', async () => {
|
||||
const testDir = createTestDir();
|
||||
const transcriptPath = path.join(testDir, 'nested-transcript.jsonl');
|
||||
|
||||
// Claude Code JSONL format uses nested message.content arrays
|
||||
const lines = [
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: [
|
||||
{ type: 'text', text: 'Refactor the utils module' }
|
||||
]}}),
|
||||
JSON.stringify({ type: 'assistant', message: { content: [
|
||||
{ type: 'tool_use', name: 'Read', input: { file_path: 'lib/utils.js' } }
|
||||
]}}),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'Approve the changes' }})
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, lines.join('\n'));
|
||||
|
||||
try {
|
||||
const result = await runHookWithInput(
|
||||
path.join(scriptsDir, 'session-end.js'),
|
||||
{ transcript_path: transcriptPath },
|
||||
{ HOME: testDir, USERPROFILE: testDir }
|
||||
);
|
||||
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0');
|
||||
|
||||
// Check session file was created
|
||||
const sessionsDir = path.join(testDir, '.claude', 'sessions');
|
||||
if (fs.existsSync(sessionsDir)) {
|
||||
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.tmp'));
|
||||
assert.ok(files.length > 0, 'Should create session file');
|
||||
const content = fs.readFileSync(path.join(sessionsDir, files[0]), 'utf8');
|
||||
assert.ok(content.includes('Refactor the utils module') || content.includes('Approve'),
|
||||
'Should extract user messages from nested format');
|
||||
}
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ==========================================
|
||||
// Error Handling Tests
|
||||
// ==========================================
|
||||
|
||||
@@ -738,6 +738,126 @@ function runTests() {
|
||||
assert.ok(pattern.includes('yarn build'), 'Should include yarn build');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// getRunCommand PM-specific format tests
|
||||
console.log('\ngetRunCommand (PM-specific formats):');
|
||||
|
||||
if (test('pnpm custom script: pnpm <script> (no run keyword)', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
||||
const cmd = pm.getRunCommand('lint');
|
||||
assert.strictEqual(cmd, 'pnpm lint', 'pnpm uses "pnpm <script>" format');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('yarn custom script: yarn <script>', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'yarn';
|
||||
const cmd = pm.getRunCommand('format');
|
||||
assert.strictEqual(cmd, 'yarn format', 'yarn uses "yarn <script>" format');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('bun custom script: bun run <script>', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'bun';
|
||||
const cmd = pm.getRunCommand('typecheck');
|
||||
assert.strictEqual(cmd, 'bun run typecheck', 'bun uses "bun run <script>" format');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('npm custom script: npm run <script>', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
||||
const cmd = pm.getRunCommand('lint');
|
||||
assert.strictEqual(cmd, 'npm run lint', 'npm uses "npm run <script>" format');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('pnpm install returns pnpm install', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
||||
assert.strictEqual(pm.getRunCommand('install'), 'pnpm install');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('yarn install returns yarn (no install keyword)', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'yarn';
|
||||
assert.strictEqual(pm.getRunCommand('install'), 'yarn');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('bun test returns bun test', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'bun';
|
||||
assert.strictEqual(pm.getRunCommand('test'), 'bun test');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// getExecCommand PM-specific format tests
|
||||
console.log('\ngetExecCommand (PM-specific formats):');
|
||||
|
||||
if (test('pnpm exec: pnpm dlx <binary>', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
||||
assert.strictEqual(pm.getExecCommand('prettier', '--write .'), 'pnpm dlx prettier --write .');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('yarn exec: yarn dlx <binary>', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'yarn';
|
||||
assert.strictEqual(pm.getExecCommand('eslint', '.'), 'yarn dlx eslint .');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('bun exec: bunx <binary>', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
process.env.CLAUDE_PACKAGE_MANAGER = 'bun';
|
||||
assert.strictEqual(pm.getExecCommand('tsc', '--noEmit'), 'bunx tsc --noEmit');
|
||||
} finally {
|
||||
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
||||
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('ignores unknown env var package manager', () => {
|
||||
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||
try {
|
||||
|
||||
@@ -614,6 +614,38 @@ src/main.ts
|
||||
assert.strictEqual(result.date, '2026-12-31');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects Feb 31 (calendar-inaccurate date)', () => {
|
||||
const result = sessionManager.parseSessionFilename('2026-02-31-abcd1234-session.tmp');
|
||||
assert.strictEqual(result, null, 'Feb 31 does not exist');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects Apr 31 (calendar-inaccurate date)', () => {
|
||||
const result = sessionManager.parseSessionFilename('2026-04-31-abcd1234-session.tmp');
|
||||
assert.strictEqual(result, null, 'Apr 31 does not exist');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects Feb 29 in non-leap year', () => {
|
||||
const result = sessionManager.parseSessionFilename('2025-02-29-abcd1234-session.tmp');
|
||||
assert.strictEqual(result, null, '2025 is not a leap year');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('accepts Feb 29 in leap year', () => {
|
||||
const result = sessionManager.parseSessionFilename('2024-02-29-abcd1234-session.tmp');
|
||||
assert.ok(result, '2024 is a leap year');
|
||||
assert.strictEqual(result.date, '2024-02-29');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('accepts Jun 30 (valid 30-day month)', () => {
|
||||
const result = sessionManager.parseSessionFilename('2026-06-30-abcd1234-session.tmp');
|
||||
assert.ok(result, 'June has 30 days');
|
||||
assert.strictEqual(result.date, '2026-06-30');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects Jun 31 (invalid 30-day month)', () => {
|
||||
const result = sessionManager.parseSessionFilename('2026-06-31-abcd1234-session.tmp');
|
||||
assert.strictEqual(result, null, 'June has only 30 days');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('datetime field is a Date object', () => {
|
||||
const result = sessionManager.parseSessionFilename('2026-06-15-abcdef12-session.tmp');
|
||||
assert.ok(result);
|
||||
@@ -722,6 +754,32 @@ src/main.ts
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// getSessionStats with code blocks and special characters
|
||||
console.log('\ngetSessionStats (code blocks & special chars):');
|
||||
|
||||
if (test('counts tasks with inline backticks correctly', () => {
|
||||
const content = '# Test\n\n### Completed\n- [x] Fixed `app.js` bug with `fs.readFile()`\n- [x] Ran `npm install` successfully\n\n### In Progress\n- [ ] Review `config.ts` changes\n';
|
||||
const stats = sessionManager.getSessionStats(content);
|
||||
assert.strictEqual(stats.completedItems, 2, 'Should count 2 completed items');
|
||||
assert.strictEqual(stats.inProgressItems, 1, 'Should count 1 in-progress item');
|
||||
assert.strictEqual(stats.totalItems, 3);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('handles special chars in notes section', () => {
|
||||
const content = '# Test\n\n### Notes for Next Session\nDon\'t forget: <important> & "quotes" & \'apostrophes\'\n';
|
||||
const stats = sessionManager.getSessionStats(content);
|
||||
assert.strictEqual(stats.hasNotes, true, 'Should detect notes section');
|
||||
const meta = sessionManager.parseSessionMetadata(content);
|
||||
assert.ok(meta.notes.includes('<important>'), 'Notes should preserve HTML-like content');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('counts items in multiline code-heavy session', () => {
|
||||
const content = '# Code Session\n\n### Completed\n- [x] Refactored `lib/utils.js`\n- [x] Updated `package.json` version\n- [x] Fixed `\\`` escaping bug\n\n### In Progress\n- [ ] Test `getSessionStats()` function\n- [ ] Review PR #42\n';
|
||||
const stats = sessionManager.getSessionStats(content);
|
||||
assert.strictEqual(stats.completedItems, 3);
|
||||
assert.strictEqual(stats.inProgressItems, 2);
|
||||
})) passed++; else failed++;
|
||||
|
||||
// getSessionStats with empty content
|
||||
if (test('getSessionStats handles empty string content', () => {
|
||||
const stats = sessionManager.getSessionStats('');
|
||||
|
||||
Reference in New Issue
Block a user