Skip to main content
Glama
wildcard-search-service.test.ts17.1 kB
/** * WildcardSearchService Test Suite * Target: Achieve 75% coverage for infrastructure foundation * * Coverage Scope: * - Basic wildcard search execution * - Graph context inclusion/exclusion * - Memory type filtering * - Result mapping and processing * - Neo4j integer conversion * - Error handling for malformed data */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { WildcardSearchService } from '../../../../src/infrastructure/services/search/wildcard-search-service'; import { Session } from 'neo4j-driver'; import neo4j from 'neo4j-driver'; // Mock Neo4j driver vi.mock('neo4j-driver', async (importOriginal) => { const actual = await importOriginal() as any; return { ...actual, int: vi.fn((value: number) => ({ toNumber: () => value, value })) }; }); describe('WildcardSearchService', () => { let service: WildcardSearchService; let mockSession: Session; beforeEach(() => { mockSession = { run: vi.fn() } as any; service = new WildcardSearchService(mockSession); }); describe('Basic Wildcard Search', () => { it('should execute basic wildcard search without graph context', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{"key": "value"}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return [ { id: 'obs-1', content: 'Test observation', createdAt: '2024-01-01T01:00:00Z' } ]; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, false); expect(mockSession.run).toHaveBeenCalledWith( expect.stringContaining('MATCH (m:Memory)'), expect.objectContaining({ limit: expect.objectContaining({ low: 10, high: 0 }) }) ); expect(results).toHaveLength(1); expect(results[0]).toMatchObject({ id: 'test-memory-1', name: 'Test Memory', type: 'note', score: 1.0, metadata: { key: 'value' }, observations: [ { id: 'obs-1', content: 'Test observation', createdAt: '2024-01-01T01:00:00Z' } ] }); }); it('should execute wildcard search with memory type filtering', async () => { const mockResult = { records: [] }; (mockSession.run as any).mockResolvedValue(mockResult); await service.search(10, false, ['note', 'task']); expect(mockSession.run).toHaveBeenCalledWith( expect.stringContaining('WHERE m.memoryType IN $memoryTypes'), expect.objectContaining({ memoryTypes: ['note', 'task'], limit: expect.anything() }) ); }); it('should handle empty results gracefully', async () => { const mockResult = { records: [] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, false); expect(results).toEqual([]); }); }); describe('Graph Context Search', () => { it('should execute wildcard search with graph context', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return []; case 'ancestors': return [ { id: 'ancestor-1', name: 'Parent Memory', type: 'parent', relation: 'INFLUENCES', distance: { toNumber: () => 1 }, strength: 0.8, source: 'agent', createdAt: '2024-01-01T00:00:00Z' } ]; case 'descendants': return [ { id: 'descendant-1', name: 'Child Memory', type: 'child', relation: 'DEPENDS_ON', distance: { toNumber: () => 1 }, strength: 0.7, source: 'user', createdAt: '2024-01-02T00:00:00Z' } ]; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, true); expect(mockSession.run).toHaveBeenCalledWith( expect.stringContaining('OPTIONAL MATCH path1 = (ancestor:Memory)'), expect.anything() ); expect(results[0].related).toBeDefined(); expect(results[0].related?.ancestors).toHaveLength(1); expect(results[0].related?.descendants).toHaveLength(1); expect(results[0].related?.ancestors?.[0]).toMatchObject({ id: 'ancestor-1', name: 'Parent Memory', relation: 'INFLUENCES', distance: 1 }); }); it('should handle graph context with empty relations', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return []; case 'ancestors': return []; case 'descendants': return []; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, true); expect(results[0].related).toBeUndefined(); }); }); describe('Data Processing', () => { it('should process observations correctly', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return [ { id: 'obs-1', content: 'Valid observation', createdAt: '2024-01-01T01:00:00Z' }, { id: 'obs-2', content: '', createdAt: '2024-01-01T02:00:00Z' }, // Empty content - should be filtered { id: 'obs-3', content: 'Another valid observation' }, // Missing createdAt - should use default null, // Null observation - should be filtered { content: 'No ID observation', createdAt: '2024-01-01T04:00:00Z' } ]; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, false); expect(results[0].observations).toHaveLength(3); expect(results[0].observations[0]).toMatchObject({ id: 'obs-1', content: 'Valid observation', createdAt: '2024-01-01T01:00:00Z' }); expect(results[0].observations[1]).toMatchObject({ content: 'Another valid observation', createdAt: expect.any(String) // Should have default timestamp }); expect(results[0].observations[2]).toMatchObject({ content: 'No ID observation', createdAt: '2024-01-01T04:00:00Z' }); }); it('should handle non-array observations gracefully', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return null; // Non-array observation default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, false); expect(results[0].observations).toEqual([]); }); it('should parse metadata correctly', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{"status": "active", "priority": 1}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return []; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, false); expect(results[0].metadata).toEqual({ status: 'active', priority: 1 }); }); it('should handle invalid metadata gracefully', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return 'invalid-json{'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return []; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, false); expect(results[0].metadata).toEqual({}); }); it('should handle null metadata gracefully', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return null; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return []; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, false); expect(results[0].metadata).toEqual({}); }); }); describe('Neo4j Integer Conversion', () => { it('should convert Neo4j integers for distance fields', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return []; case 'ancestors': return [ { id: 'ancestor-1', name: 'Parent Memory', type: 'parent', relation: 'INFLUENCES', distance: { toNumber: () => 2 }, // Neo4j Integer object strength: 0.8 } ]; case 'descendants': return []; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, true); expect(results[0].related?.ancestors?.[0].distance).toBe(2); }); it('should handle items without Neo4j integer distance', async () => { const mockResult = { records: [ { get: vi.fn((key: string) => { switch (key) { case 'id': return 'test-memory-1'; case 'name': return 'Test Memory'; case 'type': return 'note'; case 'metadata': return '{}'; case 'createdAt': return '2024-01-01T00:00:00Z'; case 'modifiedAt': return '2024-01-02T00:00:00Z'; case 'lastAccessed': return '2024-01-03T00:00:00Z'; case 'observations': return []; case 'ancestors': return [ { id: 'ancestor-1', name: 'Parent Memory', type: 'parent', relation: 'INFLUENCES', distance: 1, // Already a regular number strength: 0.8 } ]; case 'descendants': return []; default: return null; } }) } ] }; (mockSession.run as any).mockResolvedValue(mockResult); const results = await service.search(10, true); expect(results[0].related?.ancestors?.[0].distance).toBe(1); }); }); describe('Query Building', () => { it('should build basic query without WHERE clause for no memory types', async () => { const mockResult = { records: [] }; (mockSession.run as any).mockResolvedValue(mockResult); await service.search(10, false); const [cypherQuery] = (mockSession.run as any).mock.calls[0]; expect(cypherQuery).toContain('MATCH (m:Memory)'); expect(cypherQuery).not.toContain('WHERE m.memoryType IN $memoryTypes'); }); it('should build query with WHERE clause for memory types', async () => { const mockResult = { records: [] }; (mockSession.run as any).mockResolvedValue(mockResult); await service.search(10, false, ['note']); const [cypherQuery] = (mockSession.run as any).mock.calls[0]; expect(cypherQuery).toContain('WHERE m.memoryType IN $memoryTypes'); }); it('should use graph context query when includeGraphContext is true', async () => { const mockResult = { records: [] }; (mockSession.run as any).mockResolvedValue(mockResult); await service.search(10, true); const [cypherQuery] = (mockSession.run as any).mock.calls[0]; expect(cypherQuery).toContain('OPTIONAL MATCH path1 = (ancestor:Memory)'); expect(cypherQuery).toContain('OPTIONAL MATCH path2 = (m)-[rel2:RELATES_TO*1..2]->(descendant:Memory)'); }); it('should pass correct parameters to Neo4j session', async () => { const mockResult = { records: [] }; (mockSession.run as any).mockResolvedValue(mockResult); await service.search(25, false, ['note', 'task']); const [, params] = (mockSession.run as any).mock.calls[0]; expect(params).toEqual({ memoryTypes: ['note', 'task'], limit: expect.objectContaining({ low: 25, high: 0 }) }); }); }); });

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/sylweriusz/mcp-neo4j-memory-server'

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