Skip to main content
Glama
hybrid-search-unit.test.ts34.3 kB
/** * Unit tests for hybrid search functionality * * Tests the HybridSearchManager and DatabaseManager classes in isolation, * covering database operations, search functionality, and error handling. */ import { test, describe, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { HybridSearchManager } from '../../src/database/search-manager.js'; import { DatabaseManager, serializeMetadataValue, deserializeMetadataValue } from '../../src/database/schema.js'; import { createTestWorkspace, cleanupTestWorkspace, createTestNotesWithMetadata, type TestContext } from './helpers/test-utils.js'; describe('Hybrid Search Unit Tests', () => { let context: TestContext; beforeEach(async () => { context = await createTestWorkspace('hybrid-search'); }); afterEach(async () => { await cleanupTestWorkspace(context); }); describe('DatabaseManager', () => { test('should connect to database and initialize schema', async () => { const dbManager = new DatabaseManager(context.tempDir); const connection = await dbManager.connect(); assert(connection, 'Should establish database connection'); assert(typeof connection.run === 'function', 'Should have run method'); assert(typeof connection.get === 'function', 'Should have get method'); assert(typeof connection.all === 'function', 'Should have all method'); assert(typeof connection.close === 'function', 'Should have close method'); // Verify tables were created const tables = await connection.all(` SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' `); const tableNames = tables.map((t: any) => t.name); assert(tableNames.includes('notes'), 'Should create notes table'); assert(tableNames.includes('note_metadata'), 'Should create note_metadata table'); assert(tableNames.includes('notes_fts'), 'Should create FTS table'); await connection.close(); }); test('should handle database rebuild', async () => { const dbManager = new DatabaseManager(context.tempDir); const connection = await dbManager.connect(); // Insert test data await connection.run( ` INSERT INTO notes (id, title, content, type, filename, path, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ 'test-1', 'Test Note', 'Test content', 'general', 'test.md', '/test.md', '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z' ] ); // Verify data exists const beforeRebuild = await connection.get('SELECT COUNT(*) as count FROM notes'); assert.equal( (beforeRebuild as any).count, 1, 'Should have test data before rebuild' ); // Rebuild database await dbManager.rebuild(); // Verify data was cleared const afterRebuild = await connection.get('SELECT COUNT(*) as count FROM notes'); assert.equal((afterRebuild as any).count, 0, 'Should clear data after rebuild'); await connection.close(); }); test('should handle connection errors gracefully', async () => { // Create a temporary file first, then try to use it as a directory path // This should fail reliably on all platforms since you can't create a directory // where a file already exists const tempFilePath = path.join(context.tempDir, 'blocking-file.txt'); await fs.writeFile(tempFilePath, 'test content'); // Now try to create a database manager using the file as if it were a directory const dbManager = new DatabaseManager(tempFilePath); try { await dbManager.connect(); assert.fail('Should throw error when trying to use file as directory'); } catch (error) { assert(error instanceof Error, 'Should throw Error instance'); // The error can come from fs.mkdir() or SQLite connection // Check for various error patterns that indicate connection/path issues assert( error.message.includes('Failed to connect') || error.message.includes('ENOTDIR') || error.message.includes('EEXIST') || error.message.includes('ENOENT') || error.message.includes('not a directory') || error.message.includes('file exists') || error.message.includes('no such file or directory') || error.message.includes('mkdir') || error.message.includes('cannot create') || error.message.includes('permission denied') || error.message.includes('access denied'), `Should provide meaningful error message. Got: "${error.message}"` ); } }); }); describe('HybridSearchManager Initialization', () => { test('should initialize with workspace path', () => { const hybridSearch = new HybridSearchManager(context.tempDir); assert(hybridSearch, 'Should create HybridSearchManager instance'); }); test('should handle database statistics', async () => { const hybridSearch = new HybridSearchManager(context.tempDir); const stats = await hybridSearch.getStats(); assert(typeof stats.noteCount === 'number', 'Should return note count'); assert(typeof stats.metadataCount === 'number', 'Should return metadata count'); assert(typeof stats.dbSize !== 'undefined', 'Should return database size'); assert.equal(stats.noteCount, 0, 'Should start with zero notes'); await hybridSearch.close(); }); test('should handle index rebuilding', async () => { const hybridSearch = new HybridSearchManager(context.tempDir); // Create test files await createTestNotesWithMetadata(context); let progressCalls = 0; await hybridSearch.rebuildIndex((processed, total) => { progressCalls++; assert(typeof processed === 'number', 'Should provide processed count'); assert(typeof total === 'number', 'Should provide total count'); assert(processed <= total, 'Processed should not exceed total'); }); assert(progressCalls > 0, 'Should call progress callback'); // Verify notes were indexed const stats = await hybridSearch.getStats(); assert(stats.noteCount > 0, 'Should have indexed notes'); await hybridSearch.close(); }); }); describe('Simple Search (searchNotes)', () => { let hybridSearch: HybridSearchManager; beforeEach(async () => { hybridSearch = new HybridSearchManager(context.tempDir); await createTestNotesWithMetadata(context); await hybridSearch.rebuildIndex(); }); afterEach(async () => { await hybridSearch.close(); }); test('should find notes by text content', async () => { const results = await hybridSearch.searchNotes('Atomic Habits'); assert(results.length > 0, 'Should find matching notes'); assert(results[0].title.includes('Atomic Habits'), 'Should match note title'); assert(typeof results[0].score === 'number', 'Should include search score'); }); test('should filter by note type', async () => { const results = await hybridSearch.searchNotes('', 'book-review'); assert(results.length > 0, 'Should find notes of specified type'); results.forEach(result => { assert( result.metadata?.type === 'book-review', 'Should only return book-review notes' ); }); }); test('should respect search limits', async () => { const results = await hybridSearch.searchNotes('', null, 1); assert(results.length <= 1, 'Should respect limit parameter'); }); test('should support regex search', async () => { try { const results = await hybridSearch.searchNotes('Atomic.*Habits', null, 10, true); if (results.length > 0) { assert( results[0].title.includes('Atomic Habits'), 'Should match regex pattern' ); } } catch (error) { // SQLite may not have REGEXP function enabled by default // This is acceptable for the test environment assert( error instanceof Error && (error.message.includes('no such function: REGEXP') || error.message.includes('SQLITE_ERROR')), 'Should handle missing REGEXP function gracefully' ); } }); test('should handle empty queries', async () => { const results = await hybridSearch.searchNotes(''); assert(results.length > 0, 'Should return all notes for empty query'); }); test('should handle non-existent terms', async () => { const results = await hybridSearch.searchNotes('nonexistentterm12345'); assert.equal(results.length, 0, 'Should return empty array for non-existent terms'); }); test('should generate content snippets', async () => { const results = await hybridSearch.searchNotes('habit formation'); if (results.length > 0) { assert(typeof results[0].snippet === 'string', 'Should include content snippet'); assert(results[0].snippet!.length > 0, 'Snippet should not be empty'); } }); }); describe('Advanced Search (searchNotesAdvanced)', () => { let hybridSearch: HybridSearchManager; beforeEach(async () => { hybridSearch = new HybridSearchManager(context.tempDir); await createTestNotesWithMetadata(context); await hybridSearch.rebuildIndex(); }); afterEach(async () => { await hybridSearch.close(); }); test('should filter by note type', async () => { const results = await hybridSearch.searchNotesAdvanced({ type: 'book-review' }); assert(results.results.length > 0, 'Should find book-review notes'); results.results.forEach(result => { assert( result.metadata?.type === 'book-review', 'Should only return book-review notes' ); }); }); test('should filter by metadata with equality operator', async () => { const results = await hybridSearch.searchNotesAdvanced({ metadata_filters: [{ key: 'status', value: 'completed' }] }); assert(results.results.length > 0, 'Should find notes with completed status'); results.results.forEach(result => { assert( result.metadata?.status === 'completed', 'Should only return completed notes' ); }); }); test('should filter by metadata with comparison operators', async () => { const results = await hybridSearch.searchNotesAdvanced({ metadata_filters: [{ key: 'rating', operator: '>=', value: '4' }] }); assert(results.results.length > 0, 'Should find highly rated notes'); results.results.forEach(result => { if (result.metadata?.rating !== undefined) { assert( Number(result.metadata.rating) >= 4, 'Should only return highly rated notes' ); } }); }); test('should filter by multiple metadata conditions', async () => { const results = await hybridSearch.searchNotesAdvanced({ metadata_filters: [ { key: 'type', value: 'book-review' }, { key: 'status', value: 'completed' } ] }); results.results.forEach(result => { assert(result.metadata?.type === 'book-review', 'Should match type filter'); assert(result.metadata?.status === 'completed', 'Should match status filter'); }); }); test('should filter by date ranges', async () => { const results = await hybridSearch.searchNotesAdvanced({ updated_within: '30d' }); assert(results.results.length >= 0, 'Should handle date filtering'); // All test notes should be recent if (results.results.length > 0) { results.results.forEach(result => { const updatedDate = new Date(result.lastUpdated); const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); assert( updatedDate > thirtyDaysAgo, 'Should only return recently updated notes' ); }); } }); test('should combine content search with filters', async () => { const results = await hybridSearch.searchNotesAdvanced({ content_contains: 'habits', metadata_filters: [{ key: 'type', value: 'book-review' }] }); results.results.forEach(result => { assert(result.metadata?.type === 'book-review', 'Should match type filter'); assert( result.title.toLowerCase().includes('habits') || (result.snippet && result.snippet.toLowerCase().includes('habits')), 'Should contain search term' ); }); }); test('should support sorting', async () => { const results = await hybridSearch.searchNotesAdvanced({ sort: [{ field: 'title', order: 'asc' }] }); if (results.results.length > 1) { for (let i = 1; i < results.results.length; i++) { assert( results.results[i - 1].title <= results.results[i].title, 'Should sort by title ascending' ); } } }); test('should handle LIKE operator for partial matches', async () => { const results = await hybridSearch.searchNotesAdvanced({ metadata_filters: [{ key: 'author', operator: 'LIKE', value: '%James%' }] }); results.results.forEach(result => { if (result.metadata?.author) { assert( result.metadata.author.toString().includes('James'), 'Should match partial author name' ); } }); }); test('should handle IN operator for multiple values', async () => { const results = await hybridSearch.searchNotesAdvanced({ metadata_filters: [ { key: 'status', operator: 'IN', value: '["completed", "in-progress"]' } ] }); results.results.forEach(result => { if (result.metadata?.status) { assert( ['completed', 'in-progress'].includes(result.metadata.status.toString()), 'Should match one of the specified values' ); } }); }); }); describe('SQL Search (searchNotesSQL)', () => { let hybridSearch: HybridSearchManager; beforeEach(async () => { hybridSearch = new HybridSearchManager(context.tempDir); await createTestNotesWithMetadata(context); await hybridSearch.rebuildIndex(); }); afterEach(async () => { await hybridSearch.close(); }); test('should execute basic SELECT queries', async () => { const results = await hybridSearch.searchNotesSQL({ query: 'SELECT * FROM notes WHERE type = ?', params: ['book-review'] }); assert(results.results.length > 0, 'Should return matching notes'); results.results.forEach(result => { assert(result.metadata?.type === 'book-review', 'Should match WHERE condition'); }); }); test('should execute JOIN queries with metadata', async () => { const results = await hybridSearch.searchNotesSQL({ query: ` SELECT n.*, m.value as rating FROM notes n JOIN note_metadata m ON n.id = m.note_id WHERE m.key = 'rating' AND CAST(m.value AS INTEGER) >= 4 ` }); results.results.forEach(result => { if (result.metadata?.rating !== undefined) { assert(Number(result.metadata.rating) >= 4, 'Should match JOIN condition'); } }); }); test('should execute aggregation queries', async () => { const results = await hybridSearch.searchNotesSQL({ query: ` SELECT type, COUNT(*) as count FROM notes GROUP BY type ORDER BY count DESC ` }); assert(results.results.length > 0, 'Should return aggregated results'); // Check if we have aggregation results if (results.results.length > 0) { const result = results.results[0]; // The result should be an aggregated row, not a note assert( 'count' in result || 'note_count' in result || typeof result.count === 'number', 'Should include aggregated count' ); } }); test('should prevent dangerous SQL operations', async () => { const dangerousQueries = [ 'DROP TABLE notes', 'DELETE FROM notes', 'INSERT INTO notes VALUES (1, "hack", "content", "type", "file", "path", "2024-01-01", "2024-01-01")', 'UPDATE notes SET title = "hacked"', 'ALTER TABLE notes ADD COLUMN hacked TEXT', 'CREATE TABLE hacked (id TEXT)' ]; for (const query of dangerousQueries) { try { await hybridSearch.searchNotesSQL({ query }); assert.fail(`Should reject dangerous query: ${query}`); } catch (error) { assert(error instanceof Error, 'Should throw error for dangerous queries'); assert( error.message.includes('Only SELECT queries are allowed') || error.message.includes('Query contains prohibited keywords'), `Should provide security error message for: ${query}` ); } } }); test('should handle parameterized queries safely', async () => { const results = await hybridSearch.searchNotesSQL({ query: 'SELECT * FROM notes WHERE title LIKE ? AND type = ?', params: ['%Atomic%', 'book-review'] }); results.results.forEach(result => { assert(result.title.includes('Atomic'), 'Should match title parameter'); assert(result.metadata?.type === 'book-review', 'Should match type parameter'); }); }); test('should respect query limits', async () => { const results = await hybridSearch.searchNotesSQL({ query: 'SELECT * FROM notes', limit: 1 }); assert(results.results.length <= 1, 'Should respect limit parameter'); }); test('should handle query timeouts', async () => { // This test is difficult to implement reliably across different systems // but we can at least verify the timeout parameter is accepted const results = await hybridSearch.searchNotesSQL({ query: 'SELECT * FROM notes', timeout: 1000 }); assert( typeof results.query_time_ms === 'number', 'Should track query execution time' ); }); test('should handle SQL syntax errors gracefully', async () => { try { await hybridSearch.searchNotesSQL({ query: 'SELECT * FROM nonexistent_table WHERE invalid syntax' }); assert.fail('Should throw error for invalid SQL'); } catch (error) { assert(error instanceof Error, 'Should throw Error for invalid SQL'); assert(error.message.length > 0, 'Should provide error message'); } }); }); describe('Index Management', () => { let hybridSearch: HybridSearchManager; beforeEach(async () => { hybridSearch = new HybridSearchManager(context.tempDir); await createTestNotesWithMetadata(context); await hybridSearch.rebuildIndex(); }); afterEach(async () => { await hybridSearch.close(); }); test('should upsert notes with metadata', async () => { // Create a test file to avoid file system errors const testFilePath = context.workspace.getNotePath('general', 'test-upsert.md'); await fs.writeFile(testFilePath, 'Test content', 'utf8'); const metadata = { tags: ['test', 'unit'], priority: 5, completed: false, created: '2024-01-01T00:00:00Z' }; await hybridSearch.upsertNote( 'test-note-1', 'Test Note', 'Test content', 'general', 'test-upsert.md', testFilePath, metadata ); // Verify note was indexed const results = await hybridSearch.searchNotes('Test Note'); assert(results.length > 0, 'Should find upserted note'); assert.equal(results[0].title, 'Test Note', 'Should match note title'); assert.deepEqual( results[0].tags, ['test', 'unit'], 'Should preserve metadata tags' ); }); test('should update existing notes', async () => { // Create a test file to avoid file system errors const testFilePath = context.workspace.getNotePath('general', 'test-update.md'); await fs.writeFile(testFilePath, 'Original content', 'utf8'); await hybridSearch.upsertNote( 'test-note-1', 'Original Title', 'Original content', 'general', 'test-update.md', testFilePath, { status: 'draft' } ); // Update the note await fs.writeFile(testFilePath, 'Updated content', 'utf8'); await hybridSearch.upsertNote( 'test-note-1', 'Updated Title', 'Updated content', 'general', 'test-update.md', testFilePath, { status: 'published' } ); // Verify update const results = await hybridSearch.searchNotes('Updated Title'); assert(results.length > 0, 'Should find updated note'); assert.equal(results[0].title, 'Updated Title', 'Should have updated title'); assert.equal( results[0].metadata?.status, 'published', 'Should have updated metadata' ); // Verify old title is not found const oldResults = await hybridSearch.searchNotes('Original Title'); assert(oldResults.length === 0, 'Should not find old title'); }); test('should remove notes from index', async () => { // Create a test file to avoid file system errors const testFilePath = context.workspace.getNotePath('general', 'test-remove.md'); await fs.writeFile(testFilePath, 'This note will be removed', 'utf8'); await hybridSearch.upsertNote( 'test-note-to-remove', 'Note to Remove', 'This note will be removed', 'general', 'test-remove.md', testFilePath, {} ); // Verify note exists let results = await hybridSearch.searchNotes('Note to Remove'); assert(results.length > 0, 'Should find note before removal'); // Remove note await hybridSearch.removeNote('test-note-to-remove'); // Verify note is removed results = await hybridSearch.searchNotes('Note to Remove'); assert(results.length === 0, 'Should not find note after removal'); }); test('should handle file system scanning', async () => { // Create test files await createTestNotesWithMetadata(context); // Rebuild from file system await hybridSearch.rebuildFromFileSystem(); // Verify notes were indexed const stats = await hybridSearch.getStats(); assert(stats.noteCount > 0, 'Should have indexed notes from file system'); const results = await hybridSearch.searchNotes('Atomic Habits'); assert(results.length > 0, 'Should find notes from file system'); }); }); describe('Metadata Serialization', () => { test('should serialize and deserialize string values', () => { const { value, type } = serializeMetadataValue('test string'); assert.equal(value, 'test string', 'Should preserve string value'); assert.equal(type, 'string', 'Should identify as string type'); const deserialized = deserializeMetadataValue(value, type); assert.equal(deserialized, 'test string', 'Should deserialize correctly'); }); test('should serialize and deserialize number values', () => { const { value, type } = serializeMetadataValue(42); assert.equal(value, '42', 'Should convert number to string'); assert.equal(type, 'number', 'Should identify as number type'); const deserialized = deserializeMetadataValue(value, type); assert.equal(deserialized, 42, 'Should deserialize to number'); }); test('should serialize and deserialize boolean values', () => { const { value: trueValue, type: trueType } = serializeMetadataValue(true); assert.equal(trueValue, 'true', 'Should convert true to string'); assert.equal(trueType, 'boolean', 'Should identify as boolean type'); const { value: falseValue, type: falseType } = serializeMetadataValue(false); assert.equal(falseValue, 'false', 'Should convert false to string'); assert.equal(falseType, 'boolean', 'Should identify as boolean type'); assert.equal( deserializeMetadataValue(trueValue, trueType), true, 'Should deserialize true' ); assert.equal( deserializeMetadataValue(falseValue, falseType), false, 'Should deserialize false' ); }); test('should serialize and deserialize array values', () => { const testArray = ['tag1', 'tag2', 'tag3']; const { value, type } = serializeMetadataValue(testArray); assert.equal(type, 'array', 'Should identify as array type'); const deserialized = deserializeMetadataValue(value, type); assert(Array.isArray(deserialized), 'Should deserialize to array'); assert.deepEqual(deserialized, testArray, 'Should preserve array contents'); }); test('should serialize and deserialize date values', () => { const testDate = '2024-01-01T00:00:00Z'; const { value, type } = serializeMetadataValue(testDate); assert.equal(value, testDate, 'Should preserve date string'); assert.equal(type, 'date', 'Should identify as date type'); const deserialized = deserializeMetadataValue(value, type); assert.equal(deserialized, testDate, 'Should deserialize date correctly'); }); test('should handle null and undefined values', () => { const { value: nullValue, type: nullType } = serializeMetadataValue(null); assert.equal(nullValue, '', 'Should convert null to empty string'); assert.equal(nullType, 'string', 'Should default to string type'); const { value: undefinedValue, type: undefinedType } = serializeMetadataValue(undefined); assert.equal(undefinedValue, '', 'Should convert undefined to empty string'); assert.equal(undefinedType, 'string', 'Should default to string type'); }); test('should handle malformed array deserialization', () => { const deserialized = deserializeMetadataValue('invalid json', 'array'); assert(Array.isArray(deserialized), 'Should return empty array for invalid JSON'); assert.equal(deserialized.length, 0, 'Should return empty array'); }); }); describe('Error Handling', () => { let hybridSearch: HybridSearchManager; beforeEach(async () => { hybridSearch = new HybridSearchManager(context.tempDir); await createTestNotesWithMetadata(context); await hybridSearch.rebuildIndex(); }); afterEach(async () => { await hybridSearch.close(); }); test('should handle database connection failures gracefully', async () => { // Close the database to simulate connection failure await hybridSearch.close(); try { await hybridSearch.searchNotes('test'); assert.fail('Should throw error when database is closed'); } catch (error) { assert(error instanceof Error, 'Should throw Error instance'); } }); test('should handle invalid regex patterns', async () => { try { await hybridSearch.searchNotes('[invalid regex', null, 10, true); assert.fail('Should throw error for invalid regex'); } catch (error) { assert(error instanceof Error, 'Should throw Error for invalid regex'); } }); test('should handle malformed metadata gracefully', async () => { // Create a test file to avoid file system errors const testFilePath = context.workspace.getNotePath( 'general', 'test-bad-metadata.md' ); await fs.writeFile(testFilePath, 'Content', 'utf8'); const circularObj = {} as any; circularObj.self = circularObj; // Create circular reference const badMetadata = { circular: circularObj, normal: 'value' }; // Should handle gracefully without crashing await hybridSearch.upsertNote( 'bad-metadata-note', 'Note with Bad Metadata', 'Content', 'general', 'test-bad-metadata.md', testFilePath, badMetadata ); const results = await hybridSearch.searchNotes('Bad Metadata'); assert(results.length >= 0, 'Should handle malformed metadata without crashing'); }); test('should handle very large queries gracefully', async () => { const veryLongQuery = 'a'.repeat(10000); const results = await hybridSearch.searchNotes(veryLongQuery); assert(Array.isArray(results), 'Should handle very long queries'); }); test('should handle concurrent operations safely', async () => { // Create multiple concurrent operations const promises: Promise<void>[] = []; for (let i = 0; i < 10; i++) { const filePath = context.workspace.getNotePath('general', `concurrent-${i}.md`); await fs.writeFile(filePath, `Content ${i}`, 'utf8'); promises.push( hybridSearch.upsertNote( `concurrent-note-${i}`, `Concurrent Note ${i}`, `Content ${i}`, 'general', `concurrent-${i}.md`, filePath, { index: i } ) ); } // Wait for all operations to complete await Promise.all(promises); // Verify all notes were indexed const results = await hybridSearch.searchNotes('Concurrent Note'); assert(results.length === 10, 'Should handle concurrent operations correctly'); }); }); describe('Performance', () => { let hybridSearch: HybridSearchManager; beforeEach(async () => { hybridSearch = new HybridSearchManager(context.tempDir); await createTestNotesWithMetadata(context); await hybridSearch.rebuildIndex(); }); afterEach(async () => { await hybridSearch.close(); }); test('should handle search operations efficiently', async () => { // Ensure directories exist await fs.mkdir(context.workspace.getNoteTypePath('projects'), { recursive: true }); // Create a reasonable number of test notes const notePromises: Promise<void>[] = []; for (let i = 0; i < 50; i++) { const noteType = i % 3 === 0 ? 'projects' : 'general'; const filePath = context.workspace.getNotePath(noteType, `perf-${i}.md`); const content = `This is test content for performance testing. Note number ${i} contains searchable keywords.`; await fs.writeFile(filePath, content, 'utf8'); notePromises.push( hybridSearch.upsertNote( `perf-note-${i}`, `Performance Test Note ${i}`, content, noteType, `perf-${i}.md`, filePath, { index: i, category: i % 5 === 0 ? 'important' : 'normal', tags: [`tag${i % 10}`, 'performance', 'test'] } ) ); } await Promise.all(notePromises); // Measure search performance const startTime = Date.now(); const results = await hybridSearch.searchNotes('Performance Test'); const searchTime = Date.now() - startTime; assert(results.length > 0, 'Should find performance test notes'); assert(searchTime < 1000, 'Should complete search within reasonable time'); }); test('should handle complex queries efficiently', async () => { // Ensure projects directory exists await fs.mkdir(context.workspace.getNoteTypePath('projects'), { recursive: true }); // Add some test data first const testFilePath = context.workspace.getNotePath('projects', 'complex-query.md'); await fs.writeFile(testFilePath, 'Content for complex query testing', 'utf8'); await hybridSearch.upsertNote( 'complex-query-note', 'Complex Query Test', 'Content for complex query testing', 'projects', 'complex-query.md', testFilePath, { priority: 5, status: 'active', tags: ['complex', 'query', 'test'] } ); const startTime = Date.now(); const results = await hybridSearch.searchNotesAdvanced({ content_contains: 'complex', metadata_filters: [ { key: 'priority', operator: '>=', value: '3' }, { key: 'status', value: 'active' } ], sort: [{ field: 'updated', order: 'desc' }] }); const queryTime = Date.now() - startTime; assert(typeof results.query_time_ms === 'number', 'Should track query time'); assert(queryTime < 1000, 'Should complete complex query within reasonable time'); }); }); describe('Database Close and Cleanup', () => { test('should close database connections properly', async () => { const hybridSearch = new HybridSearchManager(context.tempDir); // Initialize by performing a search await hybridSearch.searchNotes('test'); // Close should not throw await hybridSearch.close(); // Subsequent operations should fail try { await hybridSearch.searchNotes('test'); assert.fail('Should throw error after close'); } catch (error) { assert(error instanceof Error, 'Should throw error after close'); } }); test('should handle multiple close calls gracefully', async () => { const hybridSearch = new HybridSearchManager(context.tempDir); // Initialize await hybridSearch.searchNotes('test'); // Multiple closes should not throw await hybridSearch.close(); await hybridSearch.close(); await hybridSearch.close(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/disnet/flint-note'

If you have feedback or need assistance with the MCP directory API, please join our Discord server