feat: Implement Claude-mem MCP Search Server with session and observation search capabilities
- Added search functionality for observations and sessions using full-text search. - Implemented formatting functions for search results with citations. - Created multiple tools for searching by various criteria including concept, file, type, and advanced search. - Integrated structured filters and pagination options for search queries. - Established error handling for search operations and server initialization.
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"claude-mem-search": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
|
||||||
"timeout": 180000
|
"timeout": 180000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
|
||||||
"timeout": 60000
|
"timeout": 60000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
|
||||||
"timeout": 180000
|
"timeout": 180000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
|
||||||
"timeout": 60000
|
"timeout": 60000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
|
||||||
"timeout": 60000
|
"timeout": 60000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Executable
+137
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+32
-2
@@ -25,8 +25,13 @@ const WORKER_SERVICE = {
|
|||||||
source: 'src/services/worker-service.ts'
|
source: 'src/services/worker-service.ts'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SEARCH_SERVER = {
|
||||||
|
name: 'search-server',
|
||||||
|
source: 'src/servers/search-server.ts'
|
||||||
|
};
|
||||||
|
|
||||||
async function buildHooks() {
|
async function buildHooks() {
|
||||||
console.log('🔨 Building claude-mem hooks and worker service...\n');
|
console.log('🔨 Building claude-mem hooks, worker service, and search server...\n');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read version from package.json
|
// Read version from package.json
|
||||||
@@ -103,9 +108,34 @@ async function buildHooks() {
|
|||||||
console.log(`✓ ${hook.name} built (${sizeInKB} KB)`);
|
console.log(`✓ ${hook.name} built (${sizeInKB} KB)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n✅ All hooks and worker service built successfully!');
|
// Build search server
|
||||||
|
console.log(`\n🔧 Building search server...`);
|
||||||
|
await build({
|
||||||
|
entryPoints: [SEARCH_SERVER.source],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: 'node18',
|
||||||
|
format: 'esm',
|
||||||
|
outfile: `${hooksDir}/${SEARCH_SERVER.name}.js`,
|
||||||
|
minify: true,
|
||||||
|
external: ['better-sqlite3'],
|
||||||
|
define: {
|
||||||
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||||
|
},
|
||||||
|
banner: {
|
||||||
|
js: '#!/usr/bin/env node'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make search server executable
|
||||||
|
fs.chmodSync(`${hooksDir}/${SEARCH_SERVER.name}.js`, 0o755);
|
||||||
|
const searchStats = fs.statSync(`${hooksDir}/${SEARCH_SERVER.name}.js`);
|
||||||
|
console.log(`✓ search-server built (${(searchStats.size / 1024).toFixed(2)} KB)`);
|
||||||
|
|
||||||
|
console.log('\n✅ All hooks, worker service, and search server built successfully!');
|
||||||
console.log(` Hooks: ${hooksDir}/`);
|
console.log(` Hooks: ${hooksDir}/`);
|
||||||
console.log(` Worker: ${distDir}/worker-service.cjs`);
|
console.log(` Worker: ${distDir}/worker-service.cjs`);
|
||||||
|
console.log(` Search: ${hooksDir}/search-server.js`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('\n❌ Build failed:', error.message);
|
console.error('\n❌ Build failed:', error.message);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Transcript Replay Tool
|
* Transcript Replay Tool
|
||||||
*
|
*
|
||||||
@@ -197,7 +197,7 @@ async function replayTranscript(transcriptPath: string, projectName: string = 'c
|
|||||||
|
|
||||||
// Spawn worker exactly as production hooks do
|
// Spawn worker exactly as production hooks do
|
||||||
const workerPath = join(process.cwd(), 'scripts/hooks/worker.js');
|
const workerPath = join(process.cwd(), 'scripts/hooks/worker.js');
|
||||||
const worker = spawn('bun', [workerPath, String(sessionId)], {
|
const worker = spawn('node', [workerPath, String(sessionId)], {
|
||||||
detached: false, // Keep attached to see errors
|
detached: false, // Keep attached to see errors
|
||||||
stdio: ['ignore', 'pipe', 'pipe'] // Pipe output to see what's happening
|
stdio: ['ignore', 'pipe', 'pipe'] // Pipe output to see what's happening
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Worker Entry Point
|
* Worker Entry Point
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* SDK Worker Process
|
* SDK Worker Process
|
||||||
* Background server that processes tool observations via Unix socket
|
* Background server that processes tool observations via Unix socket
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude-mem MCP Search Server
|
||||||
|
* Exposes SessionSearch capabilities as MCP tools with search_result formatting
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { SessionSearch } from '../services/sqlite/SessionSearch.js';
|
||||||
|
import { ObservationSearchResult, SessionSummarySearchResult } from '../services/sqlite/types.js';
|
||||||
|
|
||||||
|
// Initialize search instance
|
||||||
|
let search: SessionSearch;
|
||||||
|
try {
|
||||||
|
search = new SessionSearch();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[search-server] Failed to initialize search:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format observation as search_result with citations
|
||||||
|
*/
|
||||||
|
function formatObservationResult(obs: ObservationSearchResult, index: number) {
|
||||||
|
const source = `claude-mem://observation/${obs.id}`;
|
||||||
|
const title = obs.title || `Observation #${obs.id}`;
|
||||||
|
|
||||||
|
// Build content from available fields
|
||||||
|
const contentParts: string[] = [];
|
||||||
|
|
||||||
|
if (obs.subtitle) {
|
||||||
|
contentParts.push(`**${obs.subtitle}**`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obs.narrative) {
|
||||||
|
contentParts.push(obs.narrative);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obs.text) {
|
||||||
|
contentParts.push(obs.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add metadata
|
||||||
|
const metadata: string[] = [];
|
||||||
|
metadata.push(`Type: ${obs.type}`);
|
||||||
|
|
||||||
|
if (obs.facts) {
|
||||||
|
try {
|
||||||
|
const facts = JSON.parse(obs.facts);
|
||||||
|
if (facts.length > 0) {
|
||||||
|
metadata.push(`Facts: ${facts.join('; ')}`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obs.concepts) {
|
||||||
|
try {
|
||||||
|
const concepts = JSON.parse(obs.concepts);
|
||||||
|
if (concepts.length > 0) {
|
||||||
|
metadata.push(`Concepts: ${concepts.join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obs.files_read || obs.files_modified) {
|
||||||
|
const files: string[] = [];
|
||||||
|
if (obs.files_read) {
|
||||||
|
try {
|
||||||
|
files.push(...JSON.parse(obs.files_read));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (obs.files_modified) {
|
||||||
|
try {
|
||||||
|
files.push(...JSON.parse(obs.files_modified));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (files.length > 0) {
|
||||||
|
metadata.push(`Files: ${[...new Set(files)].join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.length > 0) {
|
||||||
|
contentParts.push(`\n---\n${metadata.join(' | ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = contentParts.join('\n\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'search_result' as const,
|
||||||
|
source,
|
||||||
|
title,
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: content || 'No content available'
|
||||||
|
}],
|
||||||
|
citations: { enabled: true }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format session summary as search_result with citations
|
||||||
|
*/
|
||||||
|
function formatSessionResult(session: SessionSummarySearchResult, index: number) {
|
||||||
|
const source = `claude-mem://session/${session.sdk_session_id}`;
|
||||||
|
const title = session.request || `Session ${session.sdk_session_id.substring(0, 8)}`;
|
||||||
|
|
||||||
|
// Build content from available fields
|
||||||
|
const contentParts: string[] = [];
|
||||||
|
|
||||||
|
if (session.completed) {
|
||||||
|
contentParts.push(`**Completed:** ${session.completed}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.learned) {
|
||||||
|
contentParts.push(`**Learned:** ${session.learned}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.investigated) {
|
||||||
|
contentParts.push(`**Investigated:** ${session.investigated}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.next_steps) {
|
||||||
|
contentParts.push(`**Next Steps:** ${session.next_steps}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.notes) {
|
||||||
|
contentParts.push(`**Notes:** ${session.notes}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add metadata
|
||||||
|
const metadata: string[] = [];
|
||||||
|
|
||||||
|
if (session.files_read || session.files_edited) {
|
||||||
|
const files: string[] = [];
|
||||||
|
if (session.files_read) {
|
||||||
|
try {
|
||||||
|
files.push(...JSON.parse(session.files_read));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (session.files_edited) {
|
||||||
|
try {
|
||||||
|
files.push(...JSON.parse(session.files_edited));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (files.length > 0) {
|
||||||
|
metadata.push(`Files: ${[...new Set(files)].join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(session.created_at_epoch).toLocaleDateString();
|
||||||
|
metadata.push(`Date: ${date}`);
|
||||||
|
|
||||||
|
if (metadata.length > 0) {
|
||||||
|
contentParts.push(`\n---\n${metadata.join(' | ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = contentParts.join('\n\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'search_result' as const,
|
||||||
|
source,
|
||||||
|
title,
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: content || 'No content available'
|
||||||
|
}],
|
||||||
|
citations: { enabled: true }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common filter schema
|
||||||
|
*/
|
||||||
|
const filterSchema = z.object({
|
||||||
|
project: z.string().optional().describe('Filter by project name'),
|
||||||
|
type: z.union([
|
||||||
|
z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']),
|
||||||
|
z.array(z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']))
|
||||||
|
]).optional().describe('Filter by observation type'),
|
||||||
|
concepts: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by concept tags'),
|
||||||
|
files: z.union([z.string(), z.array(z.string())]).optional().describe('Filter by file paths (partial match)'),
|
||||||
|
dateRange: z.object({
|
||||||
|
start: z.union([z.string(), z.number()]).optional().describe('Start date (ISO string or epoch)'),
|
||||||
|
end: z.union([z.string(), z.number()]).optional().describe('End date (ISO string or epoch)')
|
||||||
|
}).optional().describe('Filter by date range'),
|
||||||
|
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
|
||||||
|
offset: z.number().min(0).default(0).describe('Number of results to skip'),
|
||||||
|
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('relevance').describe('Sort order')
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start the MCP server
|
||||||
|
*/
|
||||||
|
const server = createSdkMcpServer({
|
||||||
|
name: 'claude-mem-search',
|
||||||
|
version: '1.0.0',
|
||||||
|
tools: [
|
||||||
|
// Tool 1: Search observations
|
||||||
|
tool(
|
||||||
|
'search_observations',
|
||||||
|
'Search observations using full-text search across titles, narratives, facts, and concepts',
|
||||||
|
{
|
||||||
|
query: z.string().describe('Search query for FTS5 full-text search'),
|
||||||
|
...filterSchema.shape
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
const { query, ...options } = args;
|
||||||
|
const results = search.searchObservations(query, options);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `No observations found matching "${query}"`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: results.map((obs, i) => formatObservationResult(obs, i))
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Search failed: ${error.message}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tool 2: Search sessions
|
||||||
|
tool(
|
||||||
|
'search_sessions',
|
||||||
|
'Search session summaries using full-text search across requests, completions, learnings, and notes',
|
||||||
|
{
|
||||||
|
query: z.string().describe('Search query for FTS5 full-text search'),
|
||||||
|
project: z.string().optional().describe('Filter by project name'),
|
||||||
|
dateRange: z.object({
|
||||||
|
start: z.union([z.string(), z.number()]).optional(),
|
||||||
|
end: z.union([z.string(), z.number()]).optional()
|
||||||
|
}).optional().describe('Filter by date range'),
|
||||||
|
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
|
||||||
|
offset: z.number().min(0).default(0).describe('Number of results to skip'),
|
||||||
|
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('relevance').describe('Sort order')
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
const { query, ...options } = args;
|
||||||
|
const results = search.searchSessions(query, options);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `No sessions found matching "${query}"`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: results.map((session, i) => formatSessionResult(session, i))
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Search failed: ${error.message}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tool 3: Find by concept
|
||||||
|
tool(
|
||||||
|
'find_by_concept',
|
||||||
|
'Find observations tagged with a specific concept',
|
||||||
|
{
|
||||||
|
concept: z.string().describe('Concept tag to search for'),
|
||||||
|
project: z.string().optional().describe('Filter by project name'),
|
||||||
|
dateRange: z.object({
|
||||||
|
start: z.union([z.string(), z.number()]).optional(),
|
||||||
|
end: z.union([z.string(), z.number()]).optional()
|
||||||
|
}).optional().describe('Filter by date range')
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
const { concept, ...filters } = args;
|
||||||
|
const results = search.findByConcept(concept, filters);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `No observations found with concept "${concept}"`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: results.map((obs, i) => formatObservationResult(obs, i))
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Search failed: ${error.message}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tool 4: Find by file
|
||||||
|
tool(
|
||||||
|
'find_by_file',
|
||||||
|
'Find observations and sessions that reference a specific file path',
|
||||||
|
{
|
||||||
|
filePath: z.string().describe('File path to search for (supports partial matching)'),
|
||||||
|
project: z.string().optional().describe('Filter by project name'),
|
||||||
|
dateRange: z.object({
|
||||||
|
start: z.union([z.string(), z.number()]).optional(),
|
||||||
|
end: z.union([z.string(), z.number()]).optional()
|
||||||
|
}).optional().describe('Filter by date range')
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
const { filePath, ...filters } = args;
|
||||||
|
const results = search.findByFile(filePath, filters);
|
||||||
|
|
||||||
|
const totalResults = results.observations.length + results.sessions.length;
|
||||||
|
|
||||||
|
if (totalResults === 0) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `No results found for file "${filePath}"`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: any[] = [];
|
||||||
|
|
||||||
|
// Add observations
|
||||||
|
results.observations.forEach((obs, i) => {
|
||||||
|
content.push(formatObservationResult(obs, i));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add sessions
|
||||||
|
results.sessions.forEach((session, i) => {
|
||||||
|
content.push(formatSessionResult(session, i + results.observations.length));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { content };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Search failed: ${error.message}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tool 5: Find by type
|
||||||
|
tool(
|
||||||
|
'find_by_type',
|
||||||
|
'Find observations of a specific type (decision, bugfix, feature, refactor, discovery, change)',
|
||||||
|
{
|
||||||
|
type: z.union([
|
||||||
|
z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']),
|
||||||
|
z.array(z.enum(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']))
|
||||||
|
]).describe('Observation type(s) to filter by'),
|
||||||
|
project: z.string().optional().describe('Filter by project name'),
|
||||||
|
dateRange: z.object({
|
||||||
|
start: z.union([z.string(), z.number()]).optional(),
|
||||||
|
end: z.union([z.string(), z.number()]).optional()
|
||||||
|
}).optional().describe('Filter by date range')
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
const { type, ...filters } = args;
|
||||||
|
const results = search.findByType(type, filters);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
const typeStr = Array.isArray(type) ? type.join(', ') : type;
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `No observations found with type "${typeStr}"`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: results.map((obs, i) => formatObservationResult(obs, i))
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Search failed: ${error.message}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tool 6: Advanced search
|
||||||
|
tool(
|
||||||
|
'advanced_search',
|
||||||
|
'Advanced search combining full-text search with structured filters across both observations and sessions',
|
||||||
|
{
|
||||||
|
textQuery: z.string().optional().describe('Optional text query for FTS5 search'),
|
||||||
|
searchSessions: z.boolean().default(true).describe('Include session summaries in results'),
|
||||||
|
...filterSchema.shape
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
const results = search.advancedSearch(args);
|
||||||
|
|
||||||
|
const totalResults = results.observations.length + results.sessions.length;
|
||||||
|
|
||||||
|
if (totalResults === 0) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: 'No results found matching the search criteria'
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: any[] = [];
|
||||||
|
|
||||||
|
// Add observations
|
||||||
|
results.observations.forEach((obs, i) => {
|
||||||
|
content.push(formatObservationResult(obs, i));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add sessions
|
||||||
|
results.sessions.forEach((session, i) => {
|
||||||
|
content.push(formatSessionResult(session, i + results.observations.length));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { content };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Search failed: ${error.message}`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
console.error('[search-server] Starting claude-mem search server...');
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
import net from 'net';
|
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
|
||||||
const socketPath = '/Users/alexnewman/.claude-mem/test-bun.sock';
|
|
||||||
|
|
||||||
const server = net.createServer(() => {});
|
|
||||||
|
|
||||||
server.listen(socketPath, () => {
|
|
||||||
console.log('Server listening');
|
|
||||||
console.log('existsSync says:', existsSync(socketPath));
|
|
||||||
console.log('Checking with ls...');
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', (err) => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Creating session..."
|
|
||||||
echo '{"session_id":"test-socket-789","cwd":"/Users/alexnewman/Scripts/claude-mem","prompt":"testing"}' | bun src/bin/cli.ts new
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
SESSION_ID=$(sqlite3 ~/.claude-mem/claude-mem.db "SELECT id FROM sdk_sessions ORDER BY id DESC LIMIT 1;")
|
|
||||||
echo "Session ID: $SESSION_ID"
|
|
||||||
|
|
||||||
echo "Starting worker..."
|
|
||||||
bun src/sdk/worker.ts $SESSION_ID 2>&1 &
|
|
||||||
WORKER_PID=$!
|
|
||||||
echo "Worker PID: $WORKER_PID"
|
|
||||||
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
if ps -p $WORKER_PID > /dev/null 2>&1; then
|
|
||||||
echo "✅ Worker is RUNNING!"
|
|
||||||
if [ -e ~/.claude-mem/worker-$SESSION_ID.sock ]; then
|
|
||||||
echo "✅ Socket file exists!"
|
|
||||||
ls -la ~/.claude-mem/worker-$SESSION_ID.sock
|
|
||||||
else
|
|
||||||
echo "❌ Socket file NOT found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try to send a message
|
|
||||||
echo "Sending test observation..."
|
|
||||||
echo '{"type":"observation","tool_name":"TestTool","tool_input":"{}","tool_output":"{}"}' | nc -U ~/.claude-mem/worker-$SESSION_ID.sock
|
|
||||||
echo "Message sent!"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# Send finalize
|
|
||||||
echo "Sending finalize..."
|
|
||||||
echo '{"type":"finalize"}' | nc -U ~/.claude-mem/worker-$SESSION_ID.sock
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
if ps -p $WORKER_PID > /dev/null 2>&1; then
|
|
||||||
echo "⚠️ Worker still running after finalize"
|
|
||||||
kill $WORKER_PID
|
|
||||||
else
|
|
||||||
echo "✅ Worker exited cleanly after finalize"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "❌ Worker exited prematurely"
|
|
||||||
fi
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
#!/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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
#!/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...');
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
#!/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...');
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
#!/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...');
|
|
||||||
Reference in New Issue
Block a user