Skip to main content
Glama
search-engine.test.ts17.2 kB
/** * Tests for the search engine module */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { SearchEngine } from '../search/search-engine.js'; import { QueryBuilder } from '../search/query-builder.js'; import { formatResults, formatForAI } from '../search/result-formatter.js'; import type { BaseDocument } from '../schema/base-schema.js'; describe('SearchEngine', () => { let engine: SearchEngine; beforeEach(async () => { engine = new SearchEngine(); await engine.initialize(); }); afterEach(async () => { await engine.shutdown(); }); describe('initialization', () => { it('initializes successfully', () => { expect(engine.isInitialized()).toBe(true); }); it('creates Orama database', () => { expect(engine.getDb()).not.toBeNull(); }); }); describe('insert', () => { it('inserts a single document', async () => { const doc: BaseDocument = { id: 'test:1', documentType: 'type', name: 'TestType', description: 'A test type.', searchTokens: 'Test Type', library: 'test-lib', category: 'class', properties: '[]', typeDefinition: '', nestedTypes: '', typeKind: 'class', parameters: '', returnType: '', returnTypeDefinition: '', signature: '', className: '', filePath: '', keywords: '', }; await engine.insert(doc); const result = await engine.search({ query: 'TestType' }); expect(result.results.length).toBe(1); expect(result.results[0].name).toBe('TestType'); }); }); describe('insertBatch', () => { it('inserts multiple documents', async () => { const docs: BaseDocument[] = [ { id: 'test:1', documentType: 'type', name: 'TypeOne', description: 'First type.', searchTokens: 'Type One', library: 'test-lib', category: 'class', properties: '[]', typeDefinition: '', nestedTypes: '', typeKind: 'class', parameters: '', returnType: '', returnTypeDefinition: '', signature: '', className: '', filePath: '', keywords: '', }, { id: 'test:2', documentType: 'type', name: 'TypeTwo', description: 'Second type.', searchTokens: 'Type Two', library: 'test-lib', category: 'interface', properties: '[]', typeDefinition: '', nestedTypes: '', typeKind: 'interface', parameters: '', returnType: '', returnTypeDefinition: '', signature: '', className: '', filePath: '', keywords: '', }, ]; await engine.insertBatch(docs); const result = await engine.search({ query: 'Type' }); expect(result.results.length).toBe(2); }); it('handles empty array', async () => { await expect(engine.insertBatch([])).resolves.not.toThrow(); }); }); describe('search', () => { beforeEach(async () => { const docs: BaseDocument[] = [ { id: 'method:CoreV1Api.listNamespacedPod', documentType: 'method', name: 'listNamespacedPod', description: 'List pods in a namespace.', searchTokens: 'list Namespaced Pod', library: '@kubernetes/client-node', category: 'list', properties: '', typeDefinition: '', nestedTypes: '', typeKind: '', parameters: '[]', returnType: 'Promise<V1PodList>', returnTypeDefinition: '', signature: 'listNamespacedPod(namespace: string): Promise<V1PodList>', className: 'CoreV1Api', filePath: '', keywords: '', }, { id: 'method:CoreV1Api.createNamespacedPod', documentType: 'method', name: 'createNamespacedPod', description: 'Create a pod in a namespace.', searchTokens: 'create Namespaced Pod', library: '@kubernetes/client-node', category: 'create', properties: '', typeDefinition: '', nestedTypes: '', typeKind: '', parameters: '[]', returnType: 'Promise<V1Pod>', returnTypeDefinition: '', signature: 'createNamespacedPod(namespace: string, body: V1Pod): Promise<V1Pod>', className: 'CoreV1Api', filePath: '', keywords: '', }, { id: 'type:V1Pod', documentType: 'type', name: 'V1Pod', description: 'Pod resource.', searchTokens: 'V1 Pod', library: '@kubernetes/client-node', category: 'class', properties: '[]', typeDefinition: '', nestedTypes: '', typeKind: 'class', parameters: '', returnType: '', returnTypeDefinition: '', signature: '', className: '', filePath: '', keywords: '', }, { id: 'method:PrometheusDriver.instantQuery', documentType: 'method', name: 'instantQuery', description: 'Execute an instant query.', searchTokens: 'instant Query', library: 'prometheus-query', category: 'query', properties: '', typeDefinition: '', nestedTypes: '', typeKind: '', parameters: '[]', returnType: 'Promise<QueryResult>', returnTypeDefinition: '', signature: 'instantQuery(query: string): Promise<QueryResult>', className: 'PrometheusDriver', filePath: '', keywords: '', }, ]; await engine.insertBatch(docs); }); it('finds documents by query term', async () => { const result = await engine.search({ query: 'Pod' }); expect(result.results.length).toBeGreaterThan(0); expect(result.results.some((r) => r.name.includes('Pod'))).toBe(true); }); it('filters by documentType', async () => { const result = await engine.search({ query: 'Pod', documentType: 'method', }); expect(result.results.length).toBeGreaterThan(0); expect(result.results.every((r) => r.documentType === 'method')).toBe(true); }); it('filters by category', async () => { const result = await engine.search({ query: 'Pod', category: 'list', }); expect(result.results.length).toBe(1); expect(result.results[0].name).toBe('listNamespacedPod'); }); it('filters by library', async () => { const result = await engine.search({ query: 'query', library: 'prometheus-query', }); expect(result.results.length).toBe(1); expect(result.results[0].library).toBe('prometheus-query'); }); it('excludes categories', async () => { const result = await engine.search({ query: 'Pod', exclude: { categories: ['create'] }, }); expect(result.results.every((r) => r.category !== 'create')).toBe(true); }); it('excludes libraries', async () => { const result = await engine.search({ exclude: { libraries: ['prometheus-query'] }, }); expect(result.results.every((r) => r.library !== 'prometheus-query')).toBe(true); }); it('respects limit parameter', async () => { const result = await engine.search({ query: 'Pod', limit: 1 }); expect(result.results.length).toBe(1); }); it('respects offset parameter', async () => { const firstPage = await engine.search({ query: 'Pod', limit: 1, offset: 0 }); const secondPage = await engine.search({ query: 'Pod', limit: 1, offset: 1 }); if (firstPage.totalMatches > 1) { expect(firstPage.results[0].id).not.toBe(secondPage.results[0]?.id); } }); it('returns totalMatches count', async () => { const result = await engine.search({ query: 'Pod', limit: 1 }); expect(result.totalMatches).toBeGreaterThanOrEqual(1); }); it('returns search time', async () => { const result = await engine.search({ query: 'Pod' }); expect(result.searchTime).toBeGreaterThanOrEqual(0); }); it('returns facets', async () => { const result = await engine.search({ query: 'Pod' }); expect(result.facets).toBeDefined(); expect(result.facets.documentType).toBeDefined(); expect(result.facets.library).toBeDefined(); expect(result.facets.category).toBeDefined(); }); }); describe('remove', () => { it('removes a document by ID', async () => { const doc: BaseDocument = { id: 'test:removable', documentType: 'type', name: 'RemovableType', description: 'A removable type.', searchTokens: 'Removable Type', library: 'test-lib', category: 'class', properties: '[]', typeDefinition: '', nestedTypes: '', typeKind: 'class', parameters: '', returnType: '', returnTypeDefinition: '', signature: '', className: '', filePath: '', keywords: '', }; await engine.insert(doc); // Verify it was inserted let result = await engine.search({ query: 'Removable' }); expect(result.results.length).toBe(1); // Remove it await engine.remove('test:removable'); // Verify it was removed result = await engine.search({ query: 'Removable' }); expect(result.results.length).toBe(0); }); }); }); describe('QueryBuilder', () => { it('builds basic query', () => { const options = QueryBuilder.create().query('Pod').build(); expect(options.query).toBe('Pod'); }); it('sets document type filter', () => { const options = QueryBuilder.create() .query('Pod') .documentType('method') .build(); expect(options.documentType).toBe('method'); }); it('sets category filter', () => { const options = QueryBuilder.create().query('Pod').category('list').build(); expect(options.category).toBe('list'); }); it('sets library filter', () => { const options = QueryBuilder.create() .query('Pod') .library('@kubernetes/client-node') .build(); expect(options.library).toBe('@kubernetes/client-node'); }); it('sets exclude categories', () => { const options = QueryBuilder.create() .query('Pod') .excludeCategories(['delete', 'create']) .build(); expect(options.exclude?.categories).toEqual(['delete', 'create']); }); it('sets exclude libraries', () => { const options = QueryBuilder.create() .query('Pod') .excludeLibraries(['prometheus-query']) .build(); expect(options.exclude?.libraries).toEqual(['prometheus-query']); }); it('sets limit', () => { const options = QueryBuilder.create().query('Pod').limit(5).build(); expect(options.limit).toBe(5); }); it('sets offset', () => { const options = QueryBuilder.create().query('Pod').offset(10).build(); expect(options.offset).toBe(10); }); it('sets page number', () => { const options = QueryBuilder.create().query('Pod').page(3, 10).build(); expect(options.limit).toBe(10); expect(options.offset).toBe(20); // (3-1) * 10 }); it('sets boost weights', () => { const options = QueryBuilder.create() .query('Pod') .boost({ name: 5, description: 1 }) .build(); expect(options.boost).toEqual({ name: 5, description: 1 }); }); it('chains multiple options', () => { const options = QueryBuilder.create() .query('Pod') .documentType('method') .category('list') .library('@kubernetes/client-node') .excludeCategories(['delete']) .limit(10) .offset(5) .build(); expect(options.query).toBe('Pod'); expect(options.documentType).toBe('method'); expect(options.category).toBe('list'); expect(options.library).toBe('@kubernetes/client-node'); expect(options.exclude?.categories).toEqual(['delete']); expect(options.limit).toBe(10); expect(options.offset).toBe(5); }); }); describe('Result Formatter', () => { const mockSearchResult = { results: [ { id: 'method:CoreV1Api.listNamespacedPod', documentType: 'method' as const, name: 'listNamespacedPod', description: 'List pods in a namespace.', searchTokens: 'list Namespaced Pod', library: '@kubernetes/client-node', category: 'list', properties: '', typeDefinition: '', nestedTypes: '', typeKind: '', parameters: JSON.stringify([ { name: 'namespace', type: 'string', optional: false }, ]), returnType: 'Promise<V1PodList>', returnTypeDefinition: '', signature: 'listNamespacedPod(namespace: string): Promise<V1PodList>', className: 'CoreV1Api', filePath: '', keywords: '', }, { id: 'type:V1Pod', documentType: 'type' as const, name: 'V1Pod', description: 'Pod resource.', searchTokens: 'V1 Pod', library: '@kubernetes/client-node', category: 'class', properties: JSON.stringify([ { name: 'metadata', type: 'V1ObjectMeta', optional: true }, { name: 'spec', type: 'V1PodSpec', optional: true }, ]), typeDefinition: 'class V1Pod { ... }', nestedTypes: 'V1ObjectMeta,V1PodSpec', typeKind: 'class', parameters: '', returnType: '', returnTypeDefinition: '', signature: '', className: '', filePath: '', keywords: '', }, ] as BaseDocument[], totalMatches: 2, facets: { documentType: { method: 1, type: 1 }, library: { '@kubernetes/client-node': 2 }, category: { list: 1, class: 1 }, }, searchTime: 5.2, }; describe('formatResults', () => { it('formats search results to structured output', () => { const formatted = formatResults(mockSearchResult); expect(formatted.summary).toContain('Found 2 result'); expect(formatted.items.length).toBe(2); expect(formatted.totalMatches).toBe(2); expect(formatted.searchTime).toBe(5.2); }); it('includes method-specific fields', () => { const formatted = formatResults(mockSearchResult); const methodItem = formatted.items.find((i) => i.type === 'method'); expect(methodItem).toBeDefined(); expect(methodItem?.parameters).toBeDefined(); expect(methodItem?.returnType).toBe('Promise<V1PodList>'); expect(methodItem?.signature).toBeDefined(); expect(methodItem?.className).toBe('CoreV1Api'); }); it('includes type-specific fields', () => { const formatted = formatResults(mockSearchResult); const typeItem = formatted.items.find((i) => i.type === 'type'); expect(typeItem).toBeDefined(); expect(typeItem?.properties).toBeDefined(); expect(typeItem?.properties?.length).toBe(2); expect(typeItem?.nestedTypes).toContain('V1ObjectMeta'); expect(typeItem?.typeKind).toBe('class'); }); it('limits properties when maxProperties option is set', () => { const formatted = formatResults(mockSearchResult, { maxProperties: 1 }); const typeItem = formatted.items.find((i) => i.type === 'type'); expect(typeItem?.properties?.length).toBe(1); }); it('includes pagination info', () => { const formatted = formatResults(mockSearchResult); expect(formatted.pagination).toBeDefined(); expect(formatted.pagination.limit).toBe(2); expect(formatted.pagination.hasMore).toBe(false); }); it('sets hasMore when more results exist', () => { const resultWithMore = { ...mockSearchResult, totalMatches: 10, }; const formatted = formatResults(resultWithMore); expect(formatted.pagination.hasMore).toBe(true); }); }); describe('formatForAI', () => { it('formats results as markdown string', () => { const output = formatForAI(mockSearchResult); expect(typeof output).toBe('string'); expect(output).toContain('Found 2 result'); expect(output).toContain('## listNamespacedPod'); expect(output).toContain('## V1Pod'); }); it('includes method parameters in output', () => { const output = formatForAI(mockSearchResult); expect(output).toContain('Parameters:'); expect(output).toContain('namespace'); }); it('includes type properties in output', () => { const output = formatForAI(mockSearchResult); expect(output).toContain('Properties:'); expect(output).toContain('metadata'); }); it('includes return type for methods', () => { const output = formatForAI(mockSearchResult); expect(output).toContain('Returns: Promise<V1PodList>'); }); }); });

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/harche/ProDisco'

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