mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 03:13:29 +08:00
test: add 33 edge case tests for session-manager, session-aliases, and hooks
- session-manager: CRLF handling, empty sections, multi-heading title, context extraction, notes/context detection, MB file size, uppercase ID rejection - session-aliases: missing timestamps sort, title search, createdAt preservation, whitespace-only path rejection, empty string title behavior - hooks: session-start isolated HOME, template vs real session injection, learned skills count, check-console-log passthrough Total test count: 261 → 294
This commit is contained in:
@@ -101,6 +101,123 @@ async function runTests() {
|
|||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// session-start.js edge cases
|
||||||
|
console.log('\nsession-start.js (edge cases):');
|
||||||
|
|
||||||
|
if (await asyncTest('exits 0 even with isolated empty HOME', async () => {
|
||||||
|
const isoHome = path.join(os.tmpdir(), `ecc-iso-start-${Date.now()}`);
|
||||||
|
fs.mkdirSync(path.join(isoHome, '.claude', 'sessions'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });
|
||||||
|
try {
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
|
||||||
|
HOME: isoHome
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0, `Exit code should be 0, got ${result.code}`);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('reports package manager detection', async () => {
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'session-start.js'));
|
||||||
|
assert.ok(
|
||||||
|
result.stderr.includes('Package manager') || result.stderr.includes('[SessionStart]'),
|
||||||
|
'Should report package manager info'
|
||||||
|
);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('skips template session content', async () => {
|
||||||
|
const isoHome = path.join(os.tmpdir(), `ecc-tpl-start-${Date.now()}`);
|
||||||
|
const sessionsDir = path.join(isoHome, '.claude', 'sessions');
|
||||||
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });
|
||||||
|
|
||||||
|
// Create a session file with template placeholder
|
||||||
|
const sessionFile = path.join(sessionsDir, '2026-02-11-abcd1234-session.tmp');
|
||||||
|
fs.writeFileSync(sessionFile, '## Current State\n\n[Session context goes here]\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
|
||||||
|
HOME: isoHome
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
// stdout should NOT contain the template content
|
||||||
|
assert.ok(
|
||||||
|
!result.stdout.includes('Previous session summary'),
|
||||||
|
'Should not inject template session content'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('injects real session content', async () => {
|
||||||
|
const isoHome = path.join(os.tmpdir(), `ecc-real-start-${Date.now()}`);
|
||||||
|
const sessionsDir = path.join(isoHome, '.claude', 'sessions');
|
||||||
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });
|
||||||
|
|
||||||
|
// Create a real session file
|
||||||
|
const sessionFile = path.join(sessionsDir, '2026-02-11-efgh5678-session.tmp');
|
||||||
|
fs.writeFileSync(sessionFile, '# Real Session\n\nI worked on authentication refactor.\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
|
||||||
|
HOME: isoHome
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
assert.ok(
|
||||||
|
result.stdout.includes('Previous session summary'),
|
||||||
|
'Should inject real session content'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
result.stdout.includes('authentication refactor'),
|
||||||
|
'Should include session content text'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('reports learned skills count', async () => {
|
||||||
|
const isoHome = path.join(os.tmpdir(), `ecc-skills-start-${Date.now()}`);
|
||||||
|
const learnedDir = path.join(isoHome, '.claude', 'skills', 'learned');
|
||||||
|
fs.mkdirSync(learnedDir, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(isoHome, '.claude', 'sessions'), { recursive: true });
|
||||||
|
|
||||||
|
// Create learned skill files
|
||||||
|
fs.writeFileSync(path.join(learnedDir, 'testing-patterns.md'), '# Testing');
|
||||||
|
fs.writeFileSync(path.join(learnedDir, 'debugging.md'), '# Debugging');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
|
||||||
|
HOME: isoHome
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
assert.ok(
|
||||||
|
result.stderr.includes('2 learned skill(s)'),
|
||||||
|
`Should report 2 learned skills, stderr: ${result.stderr}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// check-console-log.js tests
|
||||||
|
console.log('\ncheck-console-log.js:');
|
||||||
|
|
||||||
|
if (await asyncTest('passes through stdin data to stdout', async () => {
|
||||||
|
const stdinData = JSON.stringify({ tool_name: 'Write', tool_input: {} });
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'check-console-log.js'), stdinData);
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
assert.ok(result.stdout.includes('tool_name'), 'Should pass through stdin data');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('exits 0 with empty stdin', async () => {
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'check-console-log.js'), '');
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// session-end.js tests
|
// session-end.js tests
|
||||||
console.log('\nsession-end.js:');
|
console.log('\nsession-end.js:');
|
||||||
|
|
||||||
|
|||||||
@@ -376,6 +376,98 @@ function runTests() {
|
|||||||
assert.ok(result.error);
|
assert.ok(result.error);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// listAliases edge cases
|
||||||
|
console.log('\nlistAliases (edge cases):');
|
||||||
|
|
||||||
|
if (test('handles entries with missing timestamps gracefully', () => {
|
||||||
|
resetAliases();
|
||||||
|
const data = aliases.loadAliases();
|
||||||
|
// Entry with neither updatedAt nor createdAt
|
||||||
|
data.aliases['no-dates'] = {
|
||||||
|
sessionPath: '/path/no-dates',
|
||||||
|
title: 'No Dates'
|
||||||
|
};
|
||||||
|
data.aliases['has-dates'] = {
|
||||||
|
sessionPath: '/path/has-dates',
|
||||||
|
createdAt: '2026-03-01T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-03-01T00:00:00.000Z',
|
||||||
|
title: 'Has Dates'
|
||||||
|
};
|
||||||
|
aliases.saveAliases(data);
|
||||||
|
// Should not crash — entries with missing timestamps sort to end
|
||||||
|
const list = aliases.listAliases();
|
||||||
|
assert.strictEqual(list.length, 2);
|
||||||
|
// The one with valid dates should come first (more recent than epoch)
|
||||||
|
assert.strictEqual(list[0].name, 'has-dates');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('search matches title in addition to name', () => {
|
||||||
|
resetAliases();
|
||||||
|
aliases.setAlias('project-x', '/path', 'Database Migration Feature');
|
||||||
|
aliases.setAlias('project-y', '/path2', 'Auth Refactor');
|
||||||
|
const list = aliases.listAliases({ search: 'migration' });
|
||||||
|
assert.strictEqual(list.length, 1);
|
||||||
|
assert.strictEqual(list[0].name, 'project-x');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('limit of 0 returns empty array', () => {
|
||||||
|
resetAliases();
|
||||||
|
aliases.setAlias('test', '/path');
|
||||||
|
const list = aliases.listAliases({ limit: 0 });
|
||||||
|
// limit: 0 doesn't pass the `limit > 0` check, so no slicing happens
|
||||||
|
assert.ok(list.length >= 1, 'limit=0 should not apply (falsy)');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('search with no matches returns empty array', () => {
|
||||||
|
resetAliases();
|
||||||
|
aliases.setAlias('alpha', '/path1');
|
||||||
|
aliases.setAlias('beta', '/path2');
|
||||||
|
const list = aliases.listAliases({ search: 'zzzznonexistent' });
|
||||||
|
assert.strictEqual(list.length, 0);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// setAlias edge cases
|
||||||
|
console.log('\nsetAlias (edge cases):');
|
||||||
|
|
||||||
|
if (test('rejects non-string session path types', () => {
|
||||||
|
resetAliases();
|
||||||
|
const result = aliases.setAlias('valid-name', 42);
|
||||||
|
assert.strictEqual(result.success, false);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects whitespace-only session path', () => {
|
||||||
|
resetAliases();
|
||||||
|
const result = aliases.setAlias('valid-name', ' ');
|
||||||
|
assert.strictEqual(result.success, false);
|
||||||
|
assert.ok(result.error.includes('empty'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('preserves createdAt on update', () => {
|
||||||
|
resetAliases();
|
||||||
|
aliases.setAlias('preserve-date', '/path/v1', 'V1');
|
||||||
|
const first = aliases.loadAliases().aliases['preserve-date'];
|
||||||
|
const firstCreated = first.createdAt;
|
||||||
|
|
||||||
|
// Update same alias
|
||||||
|
aliases.setAlias('preserve-date', '/path/v2', 'V2');
|
||||||
|
const second = aliases.loadAliases().aliases['preserve-date'];
|
||||||
|
|
||||||
|
assert.strictEqual(second.createdAt, firstCreated, 'createdAt should be preserved');
|
||||||
|
assert.notStrictEqual(second.sessionPath, '/path/v1', 'sessionPath should be updated');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// updateAliasTitle edge case
|
||||||
|
console.log('\nupdateAliasTitle (edge cases):');
|
||||||
|
|
||||||
|
if (test('empty string title becomes null', () => {
|
||||||
|
resetAliases();
|
||||||
|
aliases.setAlias('title-test', '/path', 'Original Title');
|
||||||
|
const result = aliases.updateAliasTitle('title-test', '');
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
const resolved = aliases.resolveAlias('title-test');
|
||||||
|
assert.strictEqual(resolved.title, null, 'Empty string title should become null');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// saveAliases atomic write tests
|
// saveAliases atomic write tests
|
||||||
console.log('\nsaveAliases (atomic write):');
|
console.log('\nsaveAliases (atomic write):');
|
||||||
|
|
||||||
|
|||||||
@@ -462,6 +462,122 @@ src/main.ts
|
|||||||
assert.ok(result, 'Should find old-format session by filename');
|
assert.ok(result, 'Should find old-format session by filename');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// parseSessionMetadata edge cases
|
||||||
|
console.log('\nparseSessionMetadata (edge cases):');
|
||||||
|
|
||||||
|
if (test('handles CRLF line endings', () => {
|
||||||
|
const content = '# CRLF Session\r\n\r\n**Date:** 2026-03-01\r\n**Started:** 09:00\r\n\r\n### Completed\r\n- [x] Task A\r\n- [x] Task B\r\n';
|
||||||
|
const meta = sessionManager.parseSessionMetadata(content);
|
||||||
|
assert.strictEqual(meta.title, 'CRLF Session');
|
||||||
|
assert.strictEqual(meta.date, '2026-03-01');
|
||||||
|
assert.strictEqual(meta.started, '09:00');
|
||||||
|
assert.strictEqual(meta.completed.length, 2);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('takes first h1 heading as title', () => {
|
||||||
|
const content = '# First Title\n\nSome text\n\n# Second Title\n';
|
||||||
|
const meta = sessionManager.parseSessionMetadata(content);
|
||||||
|
assert.strictEqual(meta.title, 'First Title');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('handles empty sections (Completed with no items)', () => {
|
||||||
|
const content = '# Session\n\n### Completed\n\n### In Progress\n\n';
|
||||||
|
const meta = sessionManager.parseSessionMetadata(content);
|
||||||
|
assert.deepStrictEqual(meta.completed, []);
|
||||||
|
assert.deepStrictEqual(meta.inProgress, []);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('handles content with only title and notes', () => {
|
||||||
|
const content = '# Just Notes\n\n### Notes for Next Session\nRemember to test\n';
|
||||||
|
const meta = sessionManager.parseSessionMetadata(content);
|
||||||
|
assert.strictEqual(meta.title, 'Just Notes');
|
||||||
|
assert.strictEqual(meta.notes, 'Remember to test');
|
||||||
|
assert.deepStrictEqual(meta.completed, []);
|
||||||
|
assert.deepStrictEqual(meta.inProgress, []);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('extracts context with backtick fenced block', () => {
|
||||||
|
const content = '# Session\n\n### Context to Load\n```\nsrc/index.ts\nlib/utils.js\n```\n';
|
||||||
|
const meta = sessionManager.parseSessionMetadata(content);
|
||||||
|
assert.strictEqual(meta.context, 'src/index.ts\nlib/utils.js');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('trims whitespace from title', () => {
|
||||||
|
const content = '# Spaces Around Title \n';
|
||||||
|
const meta = sessionManager.parseSessionMetadata(content);
|
||||||
|
assert.strictEqual(meta.title, 'Spaces Around Title');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// getSessionStats edge cases
|
||||||
|
console.log('\ngetSessionStats (edge cases):');
|
||||||
|
|
||||||
|
if (test('detects notes and context presence', () => {
|
||||||
|
const content = '# Stats Test\n\n### Notes for Next Session\nSome notes\n\n### Context to Load\n```\nfile.ts\n```\n';
|
||||||
|
const stats = sessionManager.getSessionStats(content);
|
||||||
|
assert.strictEqual(stats.hasNotes, true);
|
||||||
|
assert.strictEqual(stats.hasContext, true);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('detects absence of notes and context', () => {
|
||||||
|
const content = '# Simple Session\n\nJust some content\n';
|
||||||
|
const stats = sessionManager.getSessionStats(content);
|
||||||
|
assert.strictEqual(stats.hasNotes, false);
|
||||||
|
assert.strictEqual(stats.hasContext, false);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('treats Unix absolute path ending with .tmp as file path', () => {
|
||||||
|
// Content that starts with / and ends with .tmp should be treated as a path
|
||||||
|
// This tests the looksLikePath heuristic
|
||||||
|
const fakeContent = '/some/path/session.tmp';
|
||||||
|
// Since the file doesn't exist, getSessionContent returns null,
|
||||||
|
// parseSessionMetadata(null) returns defaults
|
||||||
|
const stats = sessionManager.getSessionStats(fakeContent);
|
||||||
|
assert.strictEqual(stats.totalItems, 0);
|
||||||
|
assert.strictEqual(stats.lineCount, 0);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// getSessionSize edge case
|
||||||
|
console.log('\ngetSessionSize (edge cases):');
|
||||||
|
|
||||||
|
if (test('returns MB for large file', () => {
|
||||||
|
const dir = createTempSessionDir();
|
||||||
|
try {
|
||||||
|
const sessionPath = path.join(dir, 'large.tmp');
|
||||||
|
// Create a file > 1MB
|
||||||
|
fs.writeFileSync(sessionPath, 'x'.repeat(1024 * 1024 + 100));
|
||||||
|
const size = sessionManager.getSessionSize(sessionPath);
|
||||||
|
assert.ok(size.includes('MB'), `Expected MB, got: ${size}`);
|
||||||
|
} finally {
|
||||||
|
cleanup(dir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// appendSessionContent edge case
|
||||||
|
if (test('appendSessionContent returns false for invalid path', () => {
|
||||||
|
const result = sessionManager.appendSessionContent('/nonexistent/deep/path/session.tmp', 'content');
|
||||||
|
assert.strictEqual(result, false);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// parseSessionFilename edge cases
|
||||||
|
console.log('\nparseSessionFilename (additional edge cases):');
|
||||||
|
|
||||||
|
if (test('rejects uppercase letters in short ID', () => {
|
||||||
|
const result = sessionManager.parseSessionFilename('2026-02-01-ABCD1234-session.tmp');
|
||||||
|
assert.strictEqual(result, null, 'Uppercase letters should be rejected');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects filenames with extra segments', () => {
|
||||||
|
const result = sessionManager.parseSessionFilename('2026-02-01-abc12345-extra-session.tmp');
|
||||||
|
assert.strictEqual(result, null, 'Extra segments should be rejected');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('datetime field is a Date object', () => {
|
||||||
|
const result = sessionManager.parseSessionFilename('2026-06-15-abcdef12-session.tmp');
|
||||||
|
assert.ok(result);
|
||||||
|
assert.ok(result.datetime instanceof Date, 'datetime should be a Date');
|
||||||
|
assert.ok(!isNaN(result.datetime.getTime()), 'datetime should be valid');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
process.env.HOME = origHome;
|
process.env.HOME = origHome;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user