feat: Add build and publish scripts for claude-mem

- Implemented build.js to bundle TypeScript source into a minified executable using Bun.
- Created publish.js to handle version bumping, building, and publishing to npm with user prompts.
- Added tests for database schema and hook functions in database-schema.test.ts.
- Introduced integration tests for hooks database in hooks-database-integration.test.ts.
- Developed end-to-end tests for SDK prompts and parser in sdk-prompts-parser.test.ts.
- Created session lifecycle tests to simulate complete Claude Code session in session-lifecycle.test.ts.
This commit is contained in:
Alex Newman
2025-10-15 20:23:32 -04:00
parent 58a9554bb3
commit 01b477da26
8 changed files with 0 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env bun
/**
* Test script for Phase 1 implementation
* Tests database schema and hook functions
*/
import { DatabaseManager, migrations } from './src/services/sqlite/index.js';
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
import path from 'path';
import fs from 'fs';
async function testDatabaseSchema() {
console.log('🧪 Testing Database Schema...\n');
// Initialize database with migrations
const manager = DatabaseManager.getInstance();
for (const migration of migrations) {
manager.registerMigration(migration);
}
const db = await manager.initialize();
console.log('✅ Database initialized');
// Check that migration 004 was applied
const version = manager.getCurrentVersion();
console.log(`✅ Current schema version: ${version}`);
if (version < 4) {
console.error('❌ Migration 004 was not applied!');
process.exit(1);
}
// Verify tables exist
const tables = [
'sdk_sessions',
'observation_queue',
'observations',
'session_summaries'
];
for (const table of tables) {
const query = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`);
const result = query.get(table);
if (!result) {
console.error(`❌ Table ${table} does not exist!`);
process.exit(1);
}
console.log(`✅ Table ${table} exists`);
}
console.log('\n✅ All schema tests passed!\n');
// Don't close yet - keep connection for other tests
}
async function testHooksDatabase() {
console.log('🧪 Testing Hooks Database...\n');
const hooksDb = new HooksDatabase();
// Clean up any existing test data first
try {
const manager = DatabaseManager.getInstance();
const db = manager.getConnection();
db.run('DELETE FROM session_summaries WHERE project = ?', ['test-project']);
db.run('DELETE FROM observations WHERE project = ?', ['test-project']);
db.run('DELETE FROM observation_queue WHERE sdk_session_id LIKE ?', ['test-sdk-session-id%']);
db.run('DELETE FROM sdk_sessions WHERE project = ? OR claude_session_id = ?', ['test-project', 'test-claude-session-1']);
} catch (error) {
// Ignore cleanup errors
}
// Test creating an SDK session
const sessionId = hooksDb.createSDKSession(
'test-claude-session-1',
'test-project',
'Test user prompt'
);
console.log(`✅ Created SDK session with ID: ${sessionId}`);
// Test finding active session
const found = hooksDb.findActiveSDKSession('test-claude-session-1');
if (!found || found.id !== sessionId) {
console.error('❌ Could not find created session!');
process.exit(1);
}
console.log(`✅ Found active session: ${found.project}`);
// Test updating SDK session ID
hooksDb.updateSDKSessionId(sessionId, 'test-sdk-session-id');
const updated = hooksDb.findActiveSDKSession('test-claude-session-1');
if (!updated || updated.sdk_session_id !== 'test-sdk-session-id') {
console.error('❌ SDK session ID was not updated!');
process.exit(1);
}
console.log(`✅ Updated SDK session ID: ${updated.sdk_session_id}`);
// Test queuing observation
hooksDb.queueObservation(
'test-sdk-session-id',
'Read',
'{"file_path": "test.ts"}',
'{"content": "test content"}'
);
console.log('✅ Queued observation');
// Test getting pending observations
const pending = hooksDb.getPendingObservations('test-sdk-session-id', 10);
if (pending.length !== 1) {
console.error('❌ Expected 1 pending observation!');
process.exit(1);
}
console.log(`✅ Found ${pending.length} pending observation(s)`);
// Test marking observation as processed
hooksDb.markObservationProcessed(pending[0].id);
const stillPending = hooksDb.getPendingObservations('test-sdk-session-id', 10);
if (stillPending.length !== 0) {
console.error('❌ Observation was not marked as processed!');
process.exit(1);
}
console.log('✅ Marked observation as processed');
// Test storing observation
hooksDb.storeObservation(
'test-sdk-session-id',
'test-project',
'feature',
'Implemented test feature'
);
console.log('✅ Stored observation');
// Test storing summary
hooksDb.storeSummary(
'test-sdk-session-id',
'test-project',
{
request: 'Test request',
completed: 'Test completed',
learned: 'Test learned',
next_steps: 'Test next steps',
files_edited: '["test.ts"]'
}
);
console.log('✅ Stored summary');
// Test getting recent summaries
const summaries = hooksDb.getRecentSummaries('test-project', 10);
if (summaries.length !== 1) {
console.error('❌ Expected 1 summary!');
process.exit(1);
}
console.log(`✅ Found ${summaries.length} summary(ies)`);
console.log(` Request: ${summaries[0].request}`);
// Test marking session as completed
hooksDb.markSessionCompleted(sessionId);
const completed = hooksDb.findActiveSDKSession('test-claude-session-1');
if (completed) {
console.error('❌ Session should not be active after completion!');
process.exit(1);
}
console.log('✅ Marked session as completed');
hooksDb.close();
console.log('\n✅ All hooks database tests passed!\n');
}
async function cleanup() {
console.log('🧹 Cleaning up test data...\n');
try {
const manager = DatabaseManager.getInstance();
const db = manager.getConnection();
// Clean up test data
db.run('DELETE FROM session_summaries WHERE project = ?', ['test-project']);
db.run('DELETE FROM observations WHERE project = ?', ['test-project']);
db.run('DELETE FROM observation_queue WHERE sdk_session_id = ?', ['test-sdk-session-id']);
db.run('DELETE FROM sdk_sessions WHERE project = ?', ['test-project']);
console.log('✅ Test data cleaned up\n');
manager.close();
} catch (error: any) {
// Database might already be closed, that's okay
console.log('✅ Test data cleanup skipped (database already closed)\n');
}
}
// Run tests
(async () => {
try {
await testDatabaseSchema();
await testHooksDatabase();
await cleanup();
console.log('🎉 Phase 1 implementation tests passed!\n');
process.exit(0);
} catch (error: any) {
console.error(`\n❌ Test failed: ${error.message}`);
console.error(error.stack);
process.exit(1);
}
})();
+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/env bun
/**
* Phase 3 Integration Tests
* Tests the complete hook lifecycle and end-to-end integration
*
* Note: These tests verify database integration rather than calling hooks directly
* since hooks call process.exit() which would terminate the test process
*/
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
import { DatabaseManager } from './src/services/sqlite/Database.js';
import { migrations } from './src/services/sqlite/migrations.js';
import fs from 'fs';
import path from 'path';
// Test database path
const TEST_DB_DIR = '/tmp/claude-mem-phase3-test';
const TEST_DB_PATH = path.join(TEST_DB_DIR, 'claude-mem.db');
describe('Phase 3: Hook Database Integration', () => {
beforeAll(async () => {
// Create test directory
fs.mkdirSync(TEST_DB_DIR, { recursive: true });
// Set test environment
process.env.CLAUDE_MEM_DATA_DIR = TEST_DB_DIR;
// Initialize database with migrations
const dbManager = DatabaseManager.getInstance();
migrations.forEach(m => dbManager.registerMigration(m));
await dbManager.initialize();
dbManager.close();
});
afterAll(() => {
// Clean up test database and all files
if (fs.existsSync(TEST_DB_DIR)) {
const files = fs.readdirSync(TEST_DB_DIR);
files.forEach(file => {
fs.unlinkSync(path.join(TEST_DB_DIR, file));
});
fs.rmdirSync(TEST_DB_DIR);
}
});
describe('HooksDatabase - Session Management', () => {
it('should create and find SDK sessions', () => {
const db = new HooksDatabase();
const sessionId = db.createSDKSession(
'test-claude-session-1',
'my-project',
'Implement authentication'
);
expect(sessionId).toBeGreaterThan(0);
const found = db.findActiveSDKSession('test-claude-session-1');
expect(found).not.toBeNull();
expect(found!.project).toBe('my-project');
expect(found!.id).toBe(sessionId);
db.close();
});
it('should update SDK session ID', () => {
const db = new HooksDatabase();
const sessionId = db.createSDKSession(
'test-claude-session-2',
'my-project',
'Test prompt'
);
db.updateSDKSessionId(sessionId, 'sdk-session-abc');
const found = db.findActiveSDKSession('test-claude-session-2');
expect(found!.sdk_session_id).toBe('sdk-session-abc');
db.close();
});
it('should mark session as completed', () => {
const db = new HooksDatabase();
const sessionId = db.createSDKSession(
'test-claude-session-3',
'my-project',
'Test prompt'
);
db.markSessionCompleted(sessionId);
const found = db.findActiveSDKSession('test-claude-session-3');
expect(found).toBeNull(); // Should not find active session
db.close();
});
});
describe('HooksDatabase - Observation Queue', () => {
it('should queue and retrieve observations', () => {
const db = new HooksDatabase();
// Create session first (FK constraint requirement)
const sessionId = db.createSDKSession('claude-queue-1', 'test-project', 'Test');
db.updateSDKSessionId(sessionId, 'sdk-queue-test-1');
db.queueObservation(
'sdk-queue-test-1',
'Read',
JSON.stringify({ file_path: 'src/app.ts' }),
JSON.stringify({ content: 'test content' })
);
const pending = db.getPendingObservations('sdk-queue-test-1', 10);
expect(pending).toHaveLength(1);
expect(pending[0].tool_name).toBe('Read');
db.close();
});
it('should mark observations as processed', () => {
const db = new HooksDatabase();
// Create session first (FK constraint requirement)
const sessionId = db.createSDKSession('claude-queue-2', 'test-project', 'Test');
db.updateSDKSessionId(sessionId, 'sdk-queue-test-2');
db.queueObservation(
'sdk-queue-test-2',
'Edit',
JSON.stringify({ file_path: 'src/app.ts' }),
JSON.stringify({ success: true })
);
const pending = db.getPendingObservations('sdk-queue-test-2', 10);
expect(pending).toHaveLength(1);
db.markObservationProcessed(pending[0].id);
const stillPending = db.getPendingObservations('sdk-queue-test-2', 10);
expect(stillPending).toHaveLength(0);
db.close();
});
it('should queue FINALIZE messages', () => {
const db = new HooksDatabase();
// Create session first (FK constraint requirement)
const sessionId = db.createSDKSession('claude-finalize', 'test-project', 'Test');
db.updateSDKSessionId(sessionId, 'sdk-finalize-test');
db.queueObservation('sdk-finalize-test', 'FINALIZE', '{}', '{}');
const pending = db.getPendingObservations('sdk-finalize-test', 10);
expect(pending).toHaveLength(1);
expect(pending[0].tool_name).toBe('FINALIZE');
db.close();
});
});
describe('HooksDatabase - Observations Storage', () => {
it('should store observations from SDK', () => {
const db = new HooksDatabase();
// Create session first (FK constraint requirement)
const sessionId = db.createSDKSession('claude-obs-1', 'my-project', 'Test');
db.updateSDKSessionId(sessionId, 'sdk-obs-test-1');
db.storeObservation(
'sdk-obs-test-1',
'my-project',
'feature',
'Implemented JWT authentication'
);
const dbInstance = (db as any).db;
const query = dbInstance.query('SELECT * FROM observations WHERE sdk_session_id = ?');
const observations = query.all('sdk-obs-test-1');
expect(observations).toHaveLength(1);
expect(observations[0].type).toBe('feature');
expect(observations[0].text).toBe('Implemented JWT authentication');
db.close();
});
});
describe('HooksDatabase - Summaries', () => {
it('should store and retrieve summaries', () => {
const db = new HooksDatabase();
// Create session first (FK constraint requirement)
const sessionId = db.createSDKSession('claude-summary-1', 'my-project', 'Test');
db.updateSDKSessionId(sessionId, 'sdk-summary-test-1');
db.storeSummary('sdk-summary-test-1', 'my-project', {
request: 'Implement authentication',
investigated: 'Existing patterns',
learned: 'No JWT support',
completed: 'Implemented JWT',
next_steps: 'Add tests',
files_read: JSON.stringify(['src/auth.ts']),
files_edited: JSON.stringify(['src/auth.ts']),
notes: 'Used bcrypt'
});
const summaries = db.getRecentSummaries('my-project', 10);
expect(summaries.length).toBeGreaterThan(0);
const summary = summaries.find(s => s.request === 'Implement authentication');
expect(summary).not.toBeUndefined();
expect(summary!.completed).toBe('Implemented JWT');
db.close();
});
it('should return recent summaries only for specific project', () => {
const db = new HooksDatabase();
// Create sessions first (FK constraint requirement)
const session1Id = db.createSDKSession('claude-proj-1', 'project-1', 'Test');
db.updateSDKSessionId(session1Id, 'sdk-proj1');
const session2Id = db.createSDKSession('claude-proj-2', 'project-2', 'Test');
db.updateSDKSessionId(session2Id, 'sdk-proj2');
db.storeSummary('sdk-proj1', 'project-1', {
request: 'Feature for project 1',
completed: 'Done'
});
db.storeSummary('sdk-proj2', 'project-2', {
request: 'Feature for project 2',
completed: 'Done'
});
const proj1Summaries = db.getRecentSummaries('project-1', 10);
const proj2Summaries = db.getRecentSummaries('project-2', 10);
expect(proj1Summaries.every(s => s.request?.includes('project 1'))).toBe(true);
expect(proj2Summaries.every(s => s.request?.includes('project 2'))).toBe(true);
db.close();
});
});
});
console.log('Running Phase 3 Integration Tests...');
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env bun
/**
* Phase 2 End-to-End Tests
* Tests SDK prompts, parser, and integration with HooksDatabase
*/
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from './src/sdk/prompts.js';
import { parseObservations, parseSummary } from './src/sdk/parser.js';
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
import { DatabaseManager } from './src/services/sqlite/Database.js';
import { migrations } from './src/services/sqlite/migrations.js';
import fs from 'fs';
import path from 'path';
// Test database path
const TEST_DB_DIR = '/tmp/claude-mem-test';
const TEST_DB_PATH = path.join(TEST_DB_DIR, 'claude-mem.db');
describe('SDK Prompts', () => {
it('should build init prompt with all required sections', () => {
const prompt = buildInitPrompt('test-project', 'session-123', 'Implement JWT auth');
expect(prompt).toContain('test-project');
expect(prompt).toContain('session-123');
expect(prompt).toContain('Implement JWT auth');
expect(prompt).toContain('SESSION CONTEXT');
expect(prompt).toContain('YOUR ROLE');
expect(prompt).toContain('WHAT TO CAPTURE');
expect(prompt).toContain('HOW TO STORE OBSERVATIONS');
expect(prompt).toContain('<observation>');
expect(prompt).toContain('<type>');
expect(prompt).toContain('<text>');
});
it('should build observation prompt with tool details', () => {
const obs = {
id: 1,
tool_name: 'Edit',
tool_input: JSON.stringify({ file: 'src/auth.ts' }),
tool_output: JSON.stringify({ success: true }),
created_at_epoch: Date.now()
};
const prompt = buildObservationPrompt(obs);
expect(prompt).toContain('TOOL OBSERVATION');
expect(prompt).toContain('Edit');
expect(prompt).toContain('src/auth.ts');
expect(prompt).toContain('ANALYSIS TASK');
});
it('should build finalize prompt with session context', () => {
const session = {
id: 1,
sdk_session_id: 'sdk-123',
project: 'test-project',
user_prompt: 'Implement JWT auth'
};
const prompt = buildFinalizePrompt(session);
expect(prompt).toContain('SESSION ENDING');
expect(prompt).toContain('FINAL TASK');
expect(prompt).toContain('<summary>');
expect(prompt).toContain('<request>');
expect(prompt).toContain('<files_read>');
});
});
describe('XML Parser', () => {
describe('parseObservations', () => {
it('should parse single observation', () => {
const text = `
<observation>
<type>feature</type>
<text>Implemented JWT token refresh flow</text>
</observation>
`;
const observations = parseObservations(text);
expect(observations).toHaveLength(1);
expect(observations[0].type).toBe('feature');
expect(observations[0].text).toBe('Implemented JWT token refresh flow');
});
it('should parse multiple observations', () => {
const text = `
<observation>
<type>feature</type>
<text>Implemented JWT token refresh flow</text>
</observation>
<observation>
<type>bugfix</type>
<text>Fixed race condition in auth middleware</text>
</observation>
`;
const observations = parseObservations(text);
expect(observations).toHaveLength(2);
expect(observations[0].type).toBe('feature');
expect(observations[1].type).toBe('bugfix');
});
it('should skip observations with invalid types', () => {
const text = `
<observation>
<type>invalid-type</type>
<text>This should be skipped</text>
</observation>
<observation>
<type>feature</type>
<text>This should be kept</text>
</observation>
`;
const observations = parseObservations(text);
expect(observations).toHaveLength(1);
expect(observations[0].type).toBe('feature');
});
it('should handle observations with surrounding text', () => {
const text = `
I analyzed the code and found something interesting:
<observation>
<type>discovery</type>
<text>API rate limit is 100 requests per minute</text>
</observation>
This is an important finding.
`;
const observations = parseObservations(text);
expect(observations).toHaveLength(1);
expect(observations[0].type).toBe('discovery');
});
});
describe('parseSummary', () => {
it('should parse complete summary with all fields', () => {
const text = `
<summary>
<request>Implement JWT authentication system</request>
<investigated>Existing auth middleware, session management</investigated>
<learned>Current system uses session cookies; no JWT support</learned>
<completed>Implemented JWT token + refresh flow with 7-day expiry</completed>
<next_steps>Add token revocation API endpoint; write integration tests</next_steps>
<files_read>
<file>src/auth.ts</file>
<file>src/middleware/session.ts</file>
</files_read>
<files_edited>
<file>src/auth.ts</file>
<file>src/middleware/auth.ts</file>
</files_edited>
<notes>Token secret stored in .env</notes>
</summary>
`;
const summary = parseSummary(text);
expect(summary).not.toBeNull();
expect(summary!.request).toBe('Implement JWT authentication system');
expect(summary!.investigated).toBe('Existing auth middleware, session management');
expect(summary!.learned).toBe('Current system uses session cookies; no JWT support');
expect(summary!.completed).toBe('Implemented JWT token + refresh flow with 7-day expiry');
expect(summary!.next_steps).toBe('Add token revocation API endpoint; write integration tests');
expect(summary!.files_read).toEqual(['src/auth.ts', 'src/middleware/session.ts']);
expect(summary!.files_edited).toEqual(['src/auth.ts', 'src/middleware/auth.ts']);
expect(summary!.notes).toBe('Token secret stored in .env');
});
it('should handle empty file arrays', () => {
const text = `
<summary>
<request>Research API documentation</request>
<investigated>API endpoints and authentication methods</investigated>
<learned>API uses OAuth 2.0</learned>
<completed>Documented authentication flow</completed>
<next_steps>Implement OAuth client</next_steps>
<files_read></files_read>
<files_edited></files_edited>
<notes>Documentation is incomplete</notes>
</summary>
`;
const summary = parseSummary(text);
expect(summary).not.toBeNull();
expect(summary!.files_read).toEqual([]);
expect(summary!.files_edited).toEqual([]);
});
it('should return null if required fields are missing', () => {
const text = `
<summary>
<request>Implement JWT authentication system</request>
<investigated>Existing auth middleware</investigated>
</summary>
`;
const summary = parseSummary(text);
expect(summary).toBeNull();
});
it('should return null if no summary block found', () => {
const text = 'This is just regular text without a summary.';
const summary = parseSummary(text);
expect(summary).toBeNull();
});
});
});
describe('HooksDatabase Integration', () => {
let db: HooksDatabase;
beforeAll(async () => {
// Create test directory
fs.mkdirSync(TEST_DB_DIR, { recursive: true });
// Set test environment
process.env.CLAUDE_MEM_DATA_DIR = TEST_DB_DIR;
// Initialize database with migrations
const dbManager = DatabaseManager.getInstance();
migrations.forEach(m => dbManager.registerMigration(m));
await dbManager.initialize();
dbManager.close();
});
afterAll(() => {
// Clean up test database and all files
if (fs.existsSync(TEST_DB_DIR)) {
const files = fs.readdirSync(TEST_DB_DIR);
files.forEach(file => {
fs.unlinkSync(path.join(TEST_DB_DIR, file));
});
fs.rmdirSync(TEST_DB_DIR);
}
});
it('should store and retrieve observations', () => {
db = new HooksDatabase();
// Create session
const sessionId = db.createSDKSession('claude-123', 'test-project', 'Test prompt');
db.updateSDKSessionId(sessionId, 'sdk-123');
// Store observation
db.storeObservation('sdk-123', 'test-project', 'feature', 'Implemented JWT auth');
// Verify storage
const dbInstance = (db as any).db;
const query = dbInstance.query('SELECT * FROM observations WHERE sdk_session_id = ?');
const observations = query.all('sdk-123');
expect(observations).toHaveLength(1);
expect(observations[0].type).toBe('feature');
expect(observations[0].text).toBe('Implemented JWT auth');
expect(observations[0].project).toBe('test-project');
db.close();
});
it('should store and retrieve summaries', () => {
db = new HooksDatabase();
// Create session
const sessionId = db.createSDKSession('claude-456', 'test-project', 'Test prompt');
db.updateSDKSessionId(sessionId, 'sdk-456');
// Store summary
const summaryData = {
request: 'Implement feature',
investigated: 'Existing code',
learned: 'Found patterns',
completed: 'Implemented feature',
next_steps: 'Add tests',
files_read: JSON.stringify(['src/app.ts']),
files_edited: JSON.stringify(['src/app.ts']),
notes: 'Used TypeScript'
};
db.storeSummary('sdk-456', 'test-project', summaryData);
// Verify storage
const summaries = db.getRecentSummaries('test-project', 10);
expect(summaries).toHaveLength(1);
expect(summaries[0].request).toBe('Implement feature');
expect(summaries[0].completed).toBe('Implemented feature');
db.close();
});
it('should queue and process observations', () => {
db = new HooksDatabase();
// Create session
const sessionId = db.createSDKSession('claude-789', 'test-project', 'Test prompt');
db.updateSDKSessionId(sessionId, 'sdk-789');
// Queue observation
db.queueObservation(
'sdk-789',
'Edit',
JSON.stringify({ file: 'src/auth.ts' }),
JSON.stringify({ success: true })
);
// Get pending observations
const pending = db.getPendingObservations('sdk-789', 10);
expect(pending).toHaveLength(1);
expect(pending[0].tool_name).toBe('Edit');
// Mark as processed
db.markObservationProcessed(pending[0].id);
// Verify no pending observations
const pendingAfter = db.getPendingObservations('sdk-789', 10);
expect(pendingAfter).toHaveLength(0);
db.close();
});
});
console.log('Running Phase 2 Tests...');
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env bun
/**
* Phase 3 End-to-End Lifecycle Test
* Simulates a complete Claude Code session lifecycle through database operations
*
* This test verifies that all hook database operations work together correctly
* to support a full session from initialization to summary generation
*/
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { HooksDatabase } from './src/services/sqlite/HooksDatabase.js';
import { DatabaseManager } from './src/services/sqlite/Database.js';
import { migrations } from './src/services/sqlite/migrations.js';
import fs from 'fs';
import path from 'path';
// Test database path
const TEST_DB_DIR = '/tmp/claude-mem-e2e-test';
describe('Phase 3: End-to-End Lifecycle', () => {
beforeAll(async () => {
// Clean up any existing test directory
if (fs.existsSync(TEST_DB_DIR)) {
const files = fs.readdirSync(TEST_DB_DIR);
files.forEach(file => {
fs.unlinkSync(path.join(TEST_DB_DIR, file));
});
fs.rmdirSync(TEST_DB_DIR);
}
// Create test directory
fs.mkdirSync(TEST_DB_DIR, { recursive: true });
// Set test environment
process.env.CLAUDE_MEM_DATA_DIR = TEST_DB_DIR;
// Initialize database with migrations
const dbManager = DatabaseManager.getInstance();
migrations.forEach(m => dbManager.registerMigration(m));
await dbManager.initialize();
dbManager.close();
});
afterAll(() => {
// Clean up test database and all files
if (fs.existsSync(TEST_DB_DIR)) {
const files = fs.readdirSync(TEST_DB_DIR);
files.forEach(file => {
fs.unlinkSync(path.join(TEST_DB_DIR, file));
});
fs.rmdirSync(TEST_DB_DIR);
}
});
it('should complete full session lifecycle', () => {
const claudeSessionId = 'e2e-session-1';
const project = 'my-app';
const userPrompt = 'Implement user authentication with JWT';
// Step 1: Create SDK session (simulates newHook)
console.log('\n=== Step 1: Initialize Session ===');
let db = new HooksDatabase();
const sessionId = db.createSDKSession(claudeSessionId, project, userPrompt);
expect(sessionId).toBeGreaterThan(0);
const session = db.findActiveSDKSession(claudeSessionId);
expect(session).not.toBeNull();
expect(session!.project).toBe(project);
// Simulate SDK worker capturing session ID
db.updateSDKSessionId(sessionId, 'sdk-e2e-1');
db.close();
// Step 2: Queue multiple observations (simulates saveHook)
console.log('\n=== Step 2: Queue Observations ===');
db = new HooksDatabase();
const observations = [
{ tool: 'Read', input: { file_path: 'src/auth.ts' }, output: { content: 'export function login() {}' } },
{ tool: 'Edit', input: { file_path: 'src/auth.ts' }, output: { success: true } },
{ tool: 'Write', input: { file_path: 'src/middleware/auth.ts' }, output: { success: true } },
{ tool: 'Bash', input: { command: 'npm install jsonwebtoken' }, output: { stdout: 'added 1 package' } },
{ tool: 'Read', input: { file_path: 'package.json' }, output: { content: '{"dependencies": {}}' } }
];
for (const obs of observations) {
db.queueObservation(
'sdk-e2e-1',
obs.tool,
JSON.stringify(obs.input),
JSON.stringify(obs.output)
);
}
const pending = db.getPendingObservations('sdk-e2e-1', 100);
expect(pending.length).toBe(observations.length);
db.close();
// Step 3: Process observations (simulates SDK worker)
console.log('\n=== Step 3: Process Observations ===');
db = new HooksDatabase();
for (const obs of pending) {
// Simulate SDK extracting meaningful observations
if (obs.tool_name === 'Edit' || obs.tool_name === 'Write') {
db.storeObservation(
'sdk-e2e-1',
project,
'feature',
`Modified ${JSON.parse(obs.tool_input).file_path}`
);
}
db.markObservationProcessed(obs.id);
}
const stillPending = db.getPendingObservations('sdk-e2e-1', 100);
expect(stillPending.length).toBe(0);
db.close();
// Step 4: Queue FINALIZE message (simulates summaryHook)
console.log('\n=== Step 4: Queue FINALIZE ===');
db = new HooksDatabase();
db.queueObservation('sdk-e2e-1', 'FINALIZE', '{}', '{}');
const finalizeMsg = db.getPendingObservations('sdk-e2e-1', 100);
expect(finalizeMsg.length).toBe(1);
expect(finalizeMsg[0].tool_name).toBe('FINALIZE');
db.close();
// Step 5: Generate summary (simulates SDK worker finalization)
console.log('\n=== Step 5: Generate Summary ===');
db = new HooksDatabase();
db.storeSummary('sdk-e2e-1', project, {
request: 'Implement user authentication with JWT',
investigated: 'Existing auth.ts file and authentication patterns',
learned: 'Current system had basic login function without JWT support',
completed: 'Implemented JWT-based authentication with login function and auth middleware',
next_steps: 'Add token refresh mechanism and write unit tests',
files_read: JSON.stringify(['src/auth.ts', 'package.json']),
files_edited: JSON.stringify(['src/auth.ts', 'src/middleware/auth.ts']),
notes: 'Installed jsonwebtoken package for JWT support'
});
db.markSessionCompleted(sessionId);
db.close();
// Verify summary stored
db = new HooksDatabase();
const summaries = db.getRecentSummaries(project, 10);
expect(summaries.length).toBe(1);
expect(summaries[0].request).toBe('Implement user authentication with JWT');
expect(summaries[0].completed).toContain('JWT-based authentication');
db.close();
// Step 6: Retrieve context for next session (simulates contextHook)
console.log('\n=== Step 6: Retrieve Context ===');
db = new HooksDatabase();
const contextSummaries = db.getRecentSummaries(project, 5);
expect(contextSummaries.length).toBeGreaterThan(0);
expect(contextSummaries[0].request).toBe('Implement user authentication with JWT');
expect(contextSummaries[0].files_edited).toContain('src/auth.ts');
// Verify session is no longer active
const completedSession = db.findActiveSDKSession(claudeSessionId);
expect(completedSession).toBeNull();
db.close();
console.log('\n✅ End-to-end lifecycle test passed!');
});
it('should handle performance requirements (< 50ms per operation)', () => {
const db = new HooksDatabase();
// Create session
const sessionId = db.createSDKSession('perf-test', 'perf-project', 'Test');
db.updateSDKSessionId(sessionId, 'sdk-perf-1');
// Test queue observation performance
const iterations = 20;
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
db.queueObservation(
'sdk-perf-1',
'Read',
JSON.stringify({ file_path: `test-${i}.ts` }),
JSON.stringify({ content: 'test' })
);
const duration = performance.now() - start;
times.push(duration);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const maxTime = Math.max(...times);
console.log(`\nPerformance Results:`);
console.log(` Average time: ${avgTime.toFixed(2)}ms`);
console.log(` Max time: ${maxTime.toFixed(2)}ms`);
// Should be well under 50ms requirement
expect(avgTime).toBeLessThan(50);
expect(maxTime).toBeLessThan(100);
db.close();
});
it('should handle interrupted sessions gracefully', () => {
const db = new HooksDatabase();
// Create session
const sessionId = db.createSDKSession(
'interrupt-test',
'interrupt-project',
'Test interruption'
);
db.updateSDKSessionId(sessionId, 'sdk-interrupt-1');
// Queue some observations
for (let i = 0; i < 5; i++) {
db.queueObservation(
'sdk-interrupt-1',
'Read',
JSON.stringify({ file_path: `file-${i}.ts` }),
JSON.stringify({ content: `content ${i}` })
);
}
// Simulate user interruption (no FINALIZE message)
// Observations should remain in queue
const pending = db.getPendingObservations('sdk-interrupt-1', 100);
expect(pending.length).toBe(5);
// Session should still be active
const stillActive = db.findActiveSDKSession('interrupt-test');
expect(stillActive).not.toBeNull();
db.close();
console.log('\n✅ Interrupted session test passed!');
});
it('should support multiple concurrent projects', () => {
const db = new HooksDatabase();
// Create sessions for different projects
const proj1Id = db.createSDKSession('session-proj1', 'project-1', 'Feature A');
const proj2Id = db.createSDKSession('session-proj2', 'project-2', 'Feature B');
db.updateSDKSessionId(proj1Id, 'sdk-proj1');
db.updateSDKSessionId(proj2Id, 'sdk-proj2');
// Store summaries for each project
db.storeSummary('sdk-proj1', 'project-1', {
request: 'Feature A for project 1',
completed: 'Implemented feature A'
});
db.storeSummary('sdk-proj2', 'project-2', {
request: 'Feature B for project 2',
completed: 'Implemented feature B'
});
// Retrieve summaries - should be project-specific
const proj1Summaries = db.getRecentSummaries('project-1', 10);
const proj2Summaries = db.getRecentSummaries('project-2', 10);
expect(proj1Summaries.length).toBeGreaterThan(0);
expect(proj2Summaries.length).toBeGreaterThan(0);
expect(proj1Summaries[0].request).toContain('project 1');
expect(proj2Summaries[0].request).toContain('project 2');
db.close();
console.log('\n✅ Multiple projects test passed!');
});
});
console.log('Running Phase 3 End-to-End Tests...');