Skip to main content
Glama

ABSD DevOps MCP Server

by anthonybir
search-streaming.test.ts10.8 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { SearchSessionManager } from '../../src/tools/filesystem/search-manager.js'; import { startSearchTool, getMoreSearchResultsTool, stopSearchTool, listSearchesTool, } from '../../src/tools/filesystem/search-streaming.js'; import { SecurityValidator } from '../../src/security/validator.js'; import type { Config } from '../../src/types/config.js'; import { tmpdir } from 'node:os'; import { mkdirSync, writeFileSync, rmSync, realpathSync } from 'node:fs'; import { join } from 'node:path'; import { EventEmitter } from 'node:events'; // Mock child process class MockChildProcess extends EventEmitter { stdout = new EventEmitter(); stderr = new EventEmitter(); killed = false; constructor(private args: string[]) { super(); // Handle --version check immediately for ripgrep availability if (args.includes('--version')) { process.nextTick(() => { this.emit('exit', 0); }); } } kill() { this.killed = true; this.emit('exit', 0); } } // Mock spawn vi.mock('node:child_process', () => ({ spawn: vi.fn((cmd: string, args: string[]) => new MockChildProcess(args)), })); describe('search_streaming tools', () => { let testDir: string; let validator: SecurityValidator; let mockLogger: any; let config: Config; let searchManager: SearchSessionManager; beforeEach(() => { // Create temp test directory testDir = join(tmpdir(), `absd-mcp-search-test-${Date.now()}`); mkdirSync(testDir, { recursive: true }); // Create test files writeFileSync(join(testDir, 'test1.txt'), 'Hello World\nFoo Bar\n'); writeFileSync(join(testDir, 'test2.txt'), 'Another file\nWith content\n'); writeFileSync(join(testDir, 'test.js'), 'console.log("test");\n'); // Mock logger mockLogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; config = { // Use resolved path for config to handle macOS symlinks allowedDirectories: [realpathSync(testDir)], blockedCommands: [], fileReadLineLimit: 1000, fileWriteLineLimit: 50, sessionTimeout: 30000, logLevel: 'error', urlDenylist: [], urlTimeout: 10000, }; validator = new SecurityValidator(config, mockLogger); searchManager = new SearchSessionManager(mockLogger); }); afterEach(() => { // Cleanup searchManager.dispose(); try { rmSync(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } vi.clearAllMocks(); }); describe('start_search', () => { it('should start a file search and return session ID', async () => { const result = await startSearchTool( { path: testDir, pattern: 'test', searchType: 'files', }, validator, mockLogger, searchManager ); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Search started:'); expect(result.content[0].text).toMatch(/[0-9a-f-]{36}/); // UUID pattern }); it('should start a content search', async () => { const result = await startSearchTool( { path: testDir, pattern: 'Hello', searchType: 'content', }, validator, mockLogger, searchManager ); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Search started:'); }); it('should validate path before starting search', async () => { const result = await startSearchTool( { path: '/invalid/path/../../../etc/passwd', pattern: 'test', searchType: 'files', }, validator, mockLogger, searchManager ); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Error'); }); it('should apply default values', async () => { const result = await startSearchTool( { path: testDir, pattern: 'test', // searchType defaults to 'files' // ignoreCase defaults to true }, validator, mockLogger, searchManager ); expect(result.content[0].text).toContain('Search started:'); }); }); describe('get_more_search_results', () => { it('should retrieve search results', async () => { // Start a search first const startResult = await startSearchTool( { path: testDir, pattern: 'test', searchType: 'files', }, validator, mockLogger, searchManager ); const sessionId = startResult.content[0].text.match(/([0-9a-f-]{36})/)?.[1]; expect(sessionId).toBeDefined(); // Get results const result = await getMoreSearchResultsTool( { sessionId: sessionId!, offset: 0, length: 10, }, mockLogger, searchManager ); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Search Results'); expect(result.content[0].text).toContain('Status:'); }); it('should handle invalid session ID', async () => { const result = await getMoreSearchResultsTool( { sessionId: 'invalid-session-id', offset: 0, length: 10, }, mockLogger, searchManager ); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Error'); }); it('should support negative offset (tail behavior)', async () => { const startResult = await startSearchTool( { path: testDir, pattern: 'test', searchType: 'files', }, validator, mockLogger, searchManager ); const sessionId = startResult.content[0].text.match(/([0-9a-f-]{36})/)?.[1]!; const result = await getMoreSearchResultsTool( { sessionId, offset: -5, length: 10, }, mockLogger, searchManager ); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Search Results'); }); }); describe('stop_search', () => { it('should stop a running search', async () => { const startResult = await startSearchTool( { path: testDir, pattern: 'test', searchType: 'files', }, validator, mockLogger, searchManager ); const sessionId = startResult.content[0].text.match(/([0-9a-f-]{36})/)?.[1]!; const result = await stopSearchTool( { sessionId, }, mockLogger, searchManager ); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('stopped'); }); it('should handle invalid session ID', async () => { const result = await stopSearchTool( { sessionId: 'invalid-id', }, mockLogger, searchManager ); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Error'); }); }); describe('list_searches', () => { it('should list all active searches', async () => { // Start multiple searches await startSearchTool( { path: testDir, pattern: 'test1', searchType: 'files', }, validator, mockLogger, searchManager ); await startSearchTool( { path: testDir, pattern: 'test2', searchType: 'content', }, validator, mockLogger, searchManager ); const result = await listSearchesTool(mockLogger, searchManager); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Active Search Sessions'); expect(result.content[0].text).toContain('test1'); expect(result.content[0].text).toContain('test2'); }); it('should handle no active searches', async () => { const result = await listSearchesTool(mockLogger, searchManager); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('No active search sessions'); }); }); describe('SearchSessionManager', () => { it('should enforce max concurrent sessions', async () => { const maxSessions = 5; // Per plan: keep resource usage predictable // Start max sessions const promises: Promise<any>[] = []; for (let i = 0; i < maxSessions; i++) { promises.push( startSearchTool( { path: testDir, pattern: `test${i}`, searchType: 'files', }, validator, mockLogger, searchManager ) ); } await Promise.all(promises); // Try to start one more const result = await startSearchTool( { path: testDir, pattern: 'overflow', searchType: 'files', }, validator, mockLogger, searchManager ); expect(result.content[0].text).toContain('Error'); expect(result.content[0].text).toContain('Maximum concurrent searches'); }); it('should clean up on dispose', async () => { const startResult = await startSearchTool( { path: testDir, pattern: 'test', searchType: 'files', }, validator, mockLogger, searchManager ); const sessionId = startResult.content[0].text.match(/([0-9a-f-]{36})/)?.[1]!; // Dispose should kill all sessions searchManager.dispose(); // Trying to get results should fail const result = await getMoreSearchResultsTool( { sessionId, offset: 0, length: 10, }, mockLogger, searchManager ); expect(result.content[0].text).toContain('Error'); }); it('should clear cleanup timer on dispose', () => { // Access private cleanupInterval via type casting const manager = searchManager as any; // Verify timer exists before dispose expect(manager.cleanupInterval).toBeDefined(); // Dispose should clear the timer searchManager.dispose(); // Verify timer is cleared expect(manager.cleanupInterval).toBeUndefined(); }); }); });

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/anthonybir/ABSD_MCP'

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