UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

325 lines (268 loc) 11.5 kB
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { SQLiteManager } from '../../../storage/sqlite-manager.js'; import { setupRAGRetrievalTools } from '../tools.js'; import { RequestContext } from '../../../core/types.js'; import { randomUUID } from 'crypto'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; describe('RAG Retrieval Tools', () => { let db: SQLiteManager; let tools: any; let context: RequestContext; let tempDir: string; let testFile: string; beforeEach(async () => { // Create temporary database tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rag-test-')); const dbPath = path.join(tempDir, 'test.db'); db = new SQLiteManager(dbPath); await db.initialize(); // Add a small delay to ensure database is fully ready await new Promise(resolve => setTimeout(resolve, 50)); // Create a test project record to satisfy foreign key constraints const projectResult = await db.run(` INSERT OR REPLACE INTO projects (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?) `, ['test-project', 'Test Project', 'Test project for RAG tests', Date.now(), Date.now()]); // Verify project was created if (!projectResult.success) { throw new Error('Failed to create test project: ' + projectResult.error); } // Setup context context = { requestId: randomUUID(), userId: 'test-user', projectId: 'test-project', db: db, startTime: Date.now() }; // Load tools const registration = await setupRAGRetrievalTools(); tools = registration.tools.reduce((acc: any, tool: any) => { acc[tool.name] = tool; return acc; }, {}); // Create test markdown file testFile = path.join(tempDir, 'test-doc.md'); await fs.writeFile(testFile, '# Test Document\n\nThis is a test document for RAG indexing.\n\n## Section 1\n\nSome content here.\n\n## Section 2\n\nMore content here.'); }); afterEach(async () => { await db.close(); await fs.rm(tempDir, { recursive: true, force: true }); }); describe('rag_index_document', () => { it('should index a single document successfully', async () => { const result = await tools.rag_index_document.execute({ path: path.relative(process.cwd(), testFile) }, context); expect(result.success).toBe(true); expect(result.data.document).toBeDefined(); expect(result.data.document.path).toBeDefined(); expect(result.data.document.chunkCount).toBeGreaterThan(0); expect(result.data.message).toContain('Successfully indexed document'); }); it('should handle non-existent file', async () => { const result = await tools.rag_index_document.execute({ path: 'non-existent-file.md' }, context); expect(result.success).toBe(false); expect(result.error?.code).toBe('FILE_NOT_FOUND'); expect(result.error?.message).toContain('Document not found'); }); it('should update existing document', async () => { // Index once const firstResult = await tools.rag_index_document.execute({ path: path.relative(process.cwd(), testFile) }, context); expect(firstResult.success).toBe(true); // Update file content await fs.writeFile(testFile, '# Updated Document\n\nThis content has been updated.'); // Index again const secondResult = await tools.rag_index_document.execute({ path: path.relative(process.cwd(), testFile) }, context); expect(secondResult.success).toBe(true); expect(secondResult.data.document.id).toBeDefined(); }); }); describe('rag_search', () => { beforeEach(async () => { // Add a small delay to ensure database is ready await new Promise(resolve => setTimeout(resolve, 10)); // Index test document before search tests const indexResult = await tools.rag_index_document.execute({ path: path.relative(process.cwd(), testFile) }, context); // Ensure indexing succeeded before running search tests expect(indexResult.success).toBe(true); }); it('should search indexed documents', async () => { const result = await tools.rag_search.execute({ query: 'test document', limit: 5 }, context); expect(result.success).toBe(true); expect(result.data.results).toBeDefined(); expect(Array.isArray(result.data.results)).toBe(true); expect(result.data.query).toBe('test document'); expect(result.data.executionTime).toBeGreaterThanOrEqual(0); }); it('should handle empty query gracefully', async () => { const result = await tools.rag_search.execute({ query: 'nonexistent content that should not match' }, context); expect(result.success).toBe(true); expect(result.data.results).toBeDefined(); expect(Array.isArray(result.data.results)).toBe(true); }); it('should respect limit parameter', async () => { const result = await tools.rag_search.execute({ query: 'content', limit: 2 }, context); expect(result.success).toBe(true); expect(result.data.results.length).toBeLessThanOrEqual(2); }); it('should log search history', async () => { await tools.rag_search.execute({ query: 'test search history', limit: 5 }, context); // Check that search was logged const historyResult = await db.query( 'SELECT * FROM rag_search_history WHERE project_id = ? ORDER BY created_at DESC LIMIT 1', [context.projectId] ); expect(historyResult.success).toBe(true); expect(historyResult.data?.length).toBe(1); expect(historyResult.data?.[0].query).toBe('test search history'); expect(historyResult.data?.[0].limit_count).toBe(5); }); }); describe('rag_get_stats', () => { it('should return empty stats for new index', async () => { const result = await tools.rag_get_stats.execute({}, context); expect(result.success).toBe(true); expect(result.data.stats).toBeDefined(); expect(result.data.stats.totalDocuments).toBe(0); expect(result.data.stats.totalChunks).toBe(0); expect(result.data.stats.totalCollections).toBe(0); expect(result.data.stats.lastIndexed).toBe('Never'); }); it('should return correct stats after indexing', async () => { // Index a document first await tools.rag_index_document.execute({ path: path.relative(process.cwd(), testFile) }, context); const result = await tools.rag_get_stats.execute({}, context); expect(result.success).toBe(true); expect(result.data.stats).toBeDefined(); expect(result.data.stats.totalDocuments).toBe(1); expect(result.data.stats.totalChunks).toBeGreaterThan(0); expect(result.data.stats.lastIndexed).not.toBe('Never'); }); }); describe('rag_clear_index', () => { beforeEach(async () => { // Index test document before clear tests await tools.rag_index_document.execute({ path: path.relative(process.cwd(), testFile) }, context); }); it('should clear all indexed documents', async () => { // Verify there are documents let statsResult = await tools.rag_get_stats.execute({}, context); expect(statsResult.data.stats.totalDocuments).toBe(1); // Clear index const result = await tools.rag_clear_index.execute({}, context); expect(result.success).toBe(true); expect(result.data.message).toContain('Cleared all indexed documents'); // Verify documents are cleared statsResult = await tools.rag_get_stats.execute({}, context); expect(statsResult.data.stats.totalDocuments).toBe(0); }); }); describe('rag_index_collection', () => { it('should create and index default docs collection', async () => { const result = await tools.rag_index_collection.execute({ collection: 'docs' }, context); expect(result.success).toBe(true); expect(result.data.collection).toBeDefined(); expect(result.data.collection.name).toBe('docs'); expect(result.data.message).toContain('Indexed collection'); }); it('should create and index default readme collection', async () => { const result = await tools.rag_index_collection.execute({ collection: 'readme' }, context); expect(result.success).toBe(true); expect(result.data.collection).toBeDefined(); expect(result.data.collection.name).toBe('readme'); }); it('should handle non-existent collection', async () => { const result = await tools.rag_index_collection.execute({ collection: 'non-existent-collection' }, context); expect(result.success).toBe(false); expect(result.error?.code).toBe('COLLECTION_NOT_FOUND'); expect(result.error?.message).toContain('Collection not found'); }); }); describe('rag_index_directory', () => { let testDir: string; beforeEach(async () => { testDir = path.join(tempDir, 'test-docs'); await fs.mkdir(testDir, { recursive: true }); // Create multiple test files await fs.writeFile(path.join(testDir, 'doc1.md'), '# Document 1\n\nContent of document 1.'); await fs.writeFile(path.join(testDir, 'doc2.md'), '# Document 2\n\nContent of document 2.'); await fs.writeFile(path.join(testDir, 'not-markdown.txt'), 'This should be ignored.'); }); it('should index all markdown files in directory', async () => { const result = await tools.rag_index_directory.execute({ path: path.relative(process.cwd(), testDir) }, context); expect(result.success).toBe(true); expect(result.data.summary).toBeDefined(); expect(result.data.summary.totalFiles).toBe(2); // Only .md files expect(result.data.summary.indexed).toBe(2); expect(result.data.summary.failed).toBe(0); }); it('should handle non-existent directory', async () => { const result = await tools.rag_index_directory.execute({ path: 'non-existent-directory' }, context); expect(result.success).toBe(false); expect(result.error?.code).toBe('DIRECTORY_NOT_FOUND'); }); it('should handle file instead of directory', async () => { const result = await tools.rag_index_directory.execute({ path: path.relative(process.cwd(), testFile) }, context); expect(result.success).toBe(false); expect(result.error?.code).toBe('NOT_A_DIRECTORY'); }); }); describe('Input validation', () => { it('should validate required parameters', async () => { // Test missing query for search expect(() => { tools.rag_search.inputSchema; }).not.toThrow(); // Test missing path for index document expect(() => { tools.rag_index_document.inputSchema; }).not.toThrow(); }); it('should validate parameter types and constraints', async () => { const searchSchema = tools.rag_search.inputSchema; expect(searchSchema.properties.query.type).toBe('string'); expect(searchSchema.properties.limit.minimum).toBe(1); expect(searchSchema.properties.limit.maximum).toBe(100); expect(searchSchema.properties.threshold.minimum).toBe(0); expect(searchSchema.properties.threshold.maximum).toBe(1); }); }); });