fix: context hook updates and cleanup (#150)
* fix(context-hook): update savings message to reference mem-search skill Changed "Use claude-mem search" to "Use the mem-search skill" for clarity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: delete outdated docs, experiments, and test results Removed: - docs/context/ (moved to private/) - experiment/ (obsolete) - test-results/ (stale) - tests/ (outdated) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(user-message-hook): update support link to Discord community --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,182 +0,0 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
/**
|
||||
* CWD Propagation Tests
|
||||
*
|
||||
* These tests verify that the working directory (cwd) context flows correctly
|
||||
* from hook input through the worker service to the SDK agent prompts.
|
||||
*/
|
||||
|
||||
describe('CWD Propagation Tests', () => {
|
||||
test('save-hook should extract cwd from input', () => {
|
||||
// Test that PostToolUseInput interface includes cwd
|
||||
const mockInput = {
|
||||
session_id: 'test-session',
|
||||
cwd: '/home/user/project',
|
||||
tool_name: 'ReadTool',
|
||||
tool_input: { path: 'README.md' },
|
||||
tool_response: { content: 'test' }
|
||||
};
|
||||
|
||||
// Verify the shape matches PostToolUseInput
|
||||
assert.strictEqual(typeof mockInput.cwd, 'string');
|
||||
assert.strictEqual(mockInput.cwd, '/home/user/project');
|
||||
});
|
||||
|
||||
test('ObservationData should include cwd field', () => {
|
||||
// Import the type to ensure it compiles with cwd
|
||||
type ObservationData = {
|
||||
tool_name: string;
|
||||
tool_input: any;
|
||||
tool_response: any;
|
||||
prompt_number: number;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
const mockData: ObservationData = {
|
||||
tool_name: 'ReadTool',
|
||||
tool_input: { path: 'test.ts' },
|
||||
tool_response: { content: 'test' },
|
||||
prompt_number: 1,
|
||||
cwd: '/test/project'
|
||||
};
|
||||
|
||||
assert.strictEqual(mockData.cwd, '/test/project');
|
||||
});
|
||||
|
||||
test('PendingMessage should include cwd field', () => {
|
||||
// Import the type to ensure it compiles with cwd
|
||||
type PendingMessage = {
|
||||
type: 'observation' | 'summarize';
|
||||
tool_name?: string;
|
||||
tool_input?: any;
|
||||
tool_response?: any;
|
||||
prompt_number?: number;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
const mockMessage: PendingMessage = {
|
||||
type: 'observation',
|
||||
tool_name: 'ReadTool',
|
||||
tool_input: { path: 'test.ts' },
|
||||
tool_response: { content: 'test' },
|
||||
prompt_number: 1,
|
||||
cwd: '/test/workspace'
|
||||
};
|
||||
|
||||
assert.strictEqual(mockMessage.cwd, '/test/workspace');
|
||||
});
|
||||
|
||||
test('buildObservationPrompt should include tool_cwd when present', () => {
|
||||
// Mock implementation of what buildObservationPrompt does
|
||||
const mockObservation = {
|
||||
id: 1,
|
||||
tool_name: 'ReadTool',
|
||||
tool_input: JSON.stringify({ path: 'test.ts' }),
|
||||
tool_output: JSON.stringify({ content: 'test' }),
|
||||
created_at_epoch: Date.now(),
|
||||
cwd: '/home/user/my-project'
|
||||
};
|
||||
|
||||
// Simulate the prompt generation
|
||||
const promptSegment = mockObservation.cwd
|
||||
? `\n <tool_cwd>${mockObservation.cwd}</tool_cwd>`
|
||||
: '';
|
||||
|
||||
// Verify cwd is included in the prompt
|
||||
assert.ok(promptSegment.includes('<tool_cwd>'));
|
||||
assert.ok(promptSegment.includes('/home/user/my-project'));
|
||||
});
|
||||
|
||||
test('buildObservationPrompt should handle missing cwd gracefully', () => {
|
||||
// Mock observation without cwd
|
||||
const mockObservation = {
|
||||
id: 1,
|
||||
tool_name: 'ReadTool',
|
||||
tool_input: JSON.stringify({ path: 'test.ts' }),
|
||||
tool_output: JSON.stringify({ content: 'test' }),
|
||||
created_at_epoch: Date.now()
|
||||
};
|
||||
|
||||
// Simulate the prompt generation (no cwd)
|
||||
const promptSegment = mockObservation.cwd
|
||||
? `\n <tool_cwd>${mockObservation.cwd}</tool_cwd>`
|
||||
: '';
|
||||
|
||||
// Verify no tool_cwd element when cwd is undefined
|
||||
assert.strictEqual(promptSegment, '');
|
||||
});
|
||||
|
||||
test('worker API body should include cwd field', () => {
|
||||
// Mock worker API request body
|
||||
const requestBody = {
|
||||
tool_name: 'ReadTool',
|
||||
tool_input: JSON.stringify({ path: 'test.ts' }),
|
||||
tool_response: JSON.stringify({ content: 'test' }),
|
||||
prompt_number: 1,
|
||||
cwd: '/workspace/project'
|
||||
};
|
||||
|
||||
// Verify all expected fields are present
|
||||
assert.strictEqual(requestBody.tool_name, 'ReadTool');
|
||||
assert.strictEqual(requestBody.prompt_number, 1);
|
||||
assert.strictEqual(requestBody.cwd, '/workspace/project');
|
||||
});
|
||||
|
||||
test('buildInitPrompt should mention spatial awareness', () => {
|
||||
// Mock the init prompt check
|
||||
const initPromptSnippet = `SPATIAL AWARENESS: Tool executions include the working directory (tool_cwd) to help you understand:
|
||||
- Which repository/project is being worked on
|
||||
- Where files are located relative to the project root
|
||||
- How to match requested paths to actual execution paths`;
|
||||
|
||||
// Verify the prompt explains spatial awareness
|
||||
assert.ok(initPromptSnippet.includes('SPATIAL AWARENESS'));
|
||||
assert.ok(initPromptSnippet.includes('tool_cwd'));
|
||||
assert.ok(initPromptSnippet.includes('working directory'));
|
||||
});
|
||||
|
||||
test('cwd should flow from hook to worker to SDK agent', () => {
|
||||
// End-to-end flow test (conceptual)
|
||||
const hookInput = {
|
||||
session_id: 'test-123',
|
||||
cwd: '/home/developer/awesome-project',
|
||||
tool_name: 'ReadTool',
|
||||
tool_input: { path: 'src/index.ts' },
|
||||
tool_response: { content: 'export default...' }
|
||||
};
|
||||
|
||||
// Step 1: Hook extracts cwd
|
||||
const extractedCwd = hookInput.cwd;
|
||||
assert.strictEqual(extractedCwd, '/home/developer/awesome-project');
|
||||
|
||||
// Step 2: Worker receives cwd in observation data
|
||||
const observationData = {
|
||||
tool_name: hookInput.tool_name,
|
||||
tool_input: hookInput.tool_input,
|
||||
tool_response: hookInput.tool_response,
|
||||
prompt_number: 1,
|
||||
cwd: extractedCwd
|
||||
};
|
||||
assert.strictEqual(observationData.cwd, extractedCwd);
|
||||
|
||||
// Step 3: SDK agent includes cwd in observation prompt
|
||||
const sdkObservation = {
|
||||
id: 0,
|
||||
tool_name: observationData.tool_name,
|
||||
tool_input: JSON.stringify(observationData.tool_input),
|
||||
tool_output: JSON.stringify(observationData.tool_response),
|
||||
created_at_epoch: Date.now(),
|
||||
cwd: observationData.cwd
|
||||
};
|
||||
assert.strictEqual(sdkObservation.cwd, extractedCwd);
|
||||
|
||||
// Step 4: Prompt includes tool_cwd element
|
||||
const promptSnippet = sdkObservation.cwd
|
||||
? `<tool_cwd>${sdkObservation.cwd}</tool_cwd>`
|
||||
: '';
|
||||
assert.ok(promptSnippet.includes('<tool_cwd>'));
|
||||
assert.ok(promptSnippet.includes(extractedCwd));
|
||||
});
|
||||
});
|
||||
@@ -1,332 +0,0 @@
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import Database from 'better-sqlite3';
|
||||
import { SessionSearch } from '../src/services/sqlite/SessionSearch';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const TEST_DB_DIR = '/tmp/claude-mem-test';
|
||||
const TEST_DB_PATH = path.join(TEST_DB_DIR, 'test.db');
|
||||
|
||||
describe('SessionSearch FTS5 Injection Tests', () => {
|
||||
let search: SessionSearch;
|
||||
let db: Database.Database;
|
||||
|
||||
// Setup test database before each test
|
||||
function setupTestDB() {
|
||||
// Clean up any existing test database
|
||||
if (fs.existsSync(TEST_DB_DIR)) {
|
||||
fs.rmSync(TEST_DB_DIR, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(TEST_DB_DIR, { recursive: true });
|
||||
|
||||
// Create database with required schema
|
||||
db = new Database(TEST_DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// Create minimal schema needed for search tests
|
||||
// Note: Using claude_session_id to match SessionSearch expectations
|
||||
db.exec(`
|
||||
CREATE TABLE sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
started_at_epoch INTEGER DEFAULT ((unixepoch() * 1000))
|
||||
);
|
||||
|
||||
CREATE TABLE observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER DEFAULT 1,
|
||||
type TEXT NOT NULL,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
narrative TEXT,
|
||||
text TEXT,
|
||||
facts TEXT,
|
||||
concepts TEXT,
|
||||
files_read TEXT,
|
||||
files_modified TEXT,
|
||||
project TEXT,
|
||||
created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
|
||||
FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
|
||||
);
|
||||
|
||||
CREATE TABLE session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER DEFAULT 1,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
notes TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
project TEXT,
|
||||
created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
|
||||
FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
|
||||
);
|
||||
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER DEFAULT 1,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
|
||||
FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
|
||||
);
|
||||
|
||||
-- Create FTS5 tables manually
|
||||
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
narrative,
|
||||
text,
|
||||
facts,
|
||||
concepts,
|
||||
content='observations',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE session_summaries_fts USING fts5(
|
||||
request,
|
||||
investigated,
|
||||
learned,
|
||||
completed,
|
||||
next_steps,
|
||||
notes,
|
||||
content='session_summaries',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
|
||||
prompt_text,
|
||||
content='user_prompts',
|
||||
content_rowid='id'
|
||||
);
|
||||
|
||||
-- Create triggers for observations
|
||||
CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER observations_au AFTER UPDATE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
-- Create triggers for session_summaries
|
||||
CREATE TRIGGER session_summaries_ai AFTER INSERT ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER session_summaries_ad AFTER DELETE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER session_summaries_au AFTER UPDATE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
|
||||
-- Create triggers for user_prompts
|
||||
CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
|
||||
INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
|
||||
VALUES('delete', old.id, old.prompt_text);
|
||||
INSERT INTO user_prompts_fts(rowid, prompt_text)
|
||||
VALUES (new.id, new.prompt_text);
|
||||
END;
|
||||
`);
|
||||
|
||||
db.close();
|
||||
|
||||
// Create SessionSearch instance
|
||||
return new SessionSearch(TEST_DB_PATH);
|
||||
}
|
||||
|
||||
function teardownTestDB() {
|
||||
if (search) {
|
||||
search.close();
|
||||
search = null;
|
||||
}
|
||||
if (fs.existsSync(TEST_DB_DIR)) {
|
||||
fs.rmSync(TEST_DB_DIR, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('should escape double quotes in search queries', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-1', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-1', 1, 'feature', 'Test observation', 'A test "quoted" narrative', 'Some text', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test query with double quotes - should not cause injection
|
||||
const maliciousQuery = 'test" OR 1=1 --';
|
||||
|
||||
// This should not throw an error and should search safely
|
||||
const results = search.searchObservations(maliciousQuery);
|
||||
|
||||
// With proper escaping, this should return 0 results (no match for the literal string)
|
||||
// Without escaping, it could match everything due to OR 1=1
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should handle FTS5 special operators safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-2', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-2', 1, 'feature', 'Security feature', 'Implements security', 'Authentication system', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test queries with FTS5 operators that should be escaped
|
||||
const testQueries = [
|
||||
'AND OR NOT', // Boolean operators
|
||||
'(parentheses)', // Grouping
|
||||
'asterisk*', // Wildcard
|
||||
'column:value', // Column filter attempt
|
||||
];
|
||||
|
||||
testQueries.forEach(query => {
|
||||
// Should not throw an error
|
||||
const results = search.searchObservations(query);
|
||||
assert.strictEqual(Array.isArray(results), true, `Should return array for query: ${query}`);
|
||||
});
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should find exact phrase matches when properly escaped', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-3', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-3', 1, 'feature', 'Hello world', 'This is a hello world example', 'Hello world program', '[]', '[]', '[]', '[]', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-3', 2, 'feature', 'Goodbye moon', 'This is something else', 'Different content', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Search for exact phrase
|
||||
const results = search.searchObservations('hello world');
|
||||
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
assert.ok(results.length > 0, 'Should find at least one result');
|
||||
assert.ok(
|
||||
results.some(r => r.title?.toLowerCase().includes('hello') || r.narrative?.toLowerCase().includes('hello')),
|
||||
'Should find observation with "hello"'
|
||||
);
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should handle empty and special character queries safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-4', 'test-project');
|
||||
INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
|
||||
VALUES ('test-session-4', 1, 'feature', 'Test', 'Test observation', 'Test content', '[]', '[]', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test edge cases
|
||||
const edgeCases = [
|
||||
'""', // Empty quoted string
|
||||
' ', // Whitespace only
|
||||
'!!!', // Special characters
|
||||
'@#$%', // More special characters
|
||||
];
|
||||
|
||||
edgeCases.forEach(query => {
|
||||
// Should not throw an error
|
||||
const results = search.searchObservations(query);
|
||||
assert.strictEqual(Array.isArray(results), true, `Should return array for edge case: "${query}"`);
|
||||
});
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should search session summaries safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-5', 'test-project');
|
||||
INSERT INTO session_summaries (claude_session_id, prompt_number, request, investigated, learned, completed, next_steps, notes, files_read, files_edited, project)
|
||||
VALUES ('test-session-5', 1, 'Implement feature', 'Looked into options', 'Learned new approach', 'Completed task', 'Next: testing', 'Notes here', '[]', '[]', 'test-project');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test with potential injection
|
||||
const maliciousQuery = 'feature" OR type:*';
|
||||
const results = search.searchSessions(maliciousQuery);
|
||||
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
|
||||
test('should search user prompts safely', () => {
|
||||
search = setupTestDB();
|
||||
|
||||
// Insert test data
|
||||
const db = new Database(TEST_DB_PATH);
|
||||
db.exec(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-6', 'test-project');
|
||||
INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text)
|
||||
VALUES ('test-session-6', 1, 'Please implement authentication');
|
||||
`);
|
||||
db.close();
|
||||
|
||||
// Test with potential injection
|
||||
const maliciousQuery = 'authentication" AND request:*';
|
||||
const results = search.searchUserPrompts(maliciousQuery);
|
||||
|
||||
assert.strictEqual(Array.isArray(results), true, 'Should return an array');
|
||||
|
||||
teardownTestDB();
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Test script to verify process cleanup
|
||||
# This script tests that uvx/python processes are properly cleaned up
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Process Cleanup Test ==="
|
||||
echo ""
|
||||
|
||||
# Function to count uvx/python processes
|
||||
count_processes() {
|
||||
local count=$(ps aux | grep -E "(uvx|python.*chroma)" | grep -v grep | wc -l)
|
||||
echo "$count"
|
||||
}
|
||||
|
||||
# Initial count
|
||||
echo "1. Initial process count:"
|
||||
initial=$(count_processes)
|
||||
echo " uvx/python/chroma processes: $initial"
|
||||
echo ""
|
||||
|
||||
# Start a node process that creates ChromaSync
|
||||
echo "2. Starting test process that creates ChromaSync..."
|
||||
cat > /tmp/test-chroma-cleanup.mjs << 'EOF'
|
||||
import { ChromaSync } from './src/services/sync/ChromaSync.js';
|
||||
|
||||
const sync = new ChromaSync('test-project');
|
||||
|
||||
console.log('[TEST] ChromaSync created, connecting...');
|
||||
|
||||
// Try to connect (this spawns uvx process)
|
||||
try {
|
||||
await sync.ensureBackfilled();
|
||||
console.log('[TEST] Backfill started');
|
||||
} catch (error) {
|
||||
console.log('[TEST] Backfill failed (expected if no data):', error.message);
|
||||
}
|
||||
|
||||
// Wait a bit for process to start
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const countBefore = parseInt(process.env.COUNT_BEFORE || '0');
|
||||
const countAfter = process.argv[2];
|
||||
|
||||
console.log('[TEST] Process count before:', countBefore);
|
||||
|
||||
// Close the sync (should terminate uvx process)
|
||||
console.log('[TEST] Closing ChromaSync...');
|
||||
await sync.close();
|
||||
|
||||
// Wait for process to terminate
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
console.log('[TEST] ChromaSync closed, process should be terminated');
|
||||
process.exit(0);
|
||||
EOF
|
||||
|
||||
# Run test
|
||||
COUNT_BEFORE=$initial node /tmp/test-chroma-cleanup.mjs 2>&1 &
|
||||
TEST_PID=$!
|
||||
|
||||
# Wait for process to spawn
|
||||
sleep 3
|
||||
|
||||
# Count during execution
|
||||
during=$(count_processes)
|
||||
echo " During execution: $during processes"
|
||||
echo ""
|
||||
|
||||
# Wait for test to complete
|
||||
wait $TEST_PID 2>/dev/null || true
|
||||
|
||||
# Wait a bit for cleanup
|
||||
sleep 2
|
||||
|
||||
# Final count
|
||||
echo "3. Final process count:"
|
||||
final=$(count_processes)
|
||||
echo " uvx/python/chroma processes: $final"
|
||||
echo ""
|
||||
|
||||
# Check if we leaked processes
|
||||
leaked=$((final - initial))
|
||||
if [ $leaked -gt 0 ]; then
|
||||
echo "❌ FAIL: Leaked $leaked process(es)"
|
||||
echo ""
|
||||
echo "Current processes:"
|
||||
ps aux | grep -E "(uvx|python.*chroma)" | grep -v grep
|
||||
exit 1
|
||||
else
|
||||
echo "✅ PASS: No process leaks detected"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/test-chroma-cleanup.mjs
|
||||
Reference in New Issue
Block a user