/**
* @file loda_search_handler.test.js
* @description Unit tests for LODA-MCP-COMP-02: loda_search_handler
* @covers Complete search flow orchestration
*/
const path = require('path');
const fs = require('fs');
const { LodaSearchHandler } = require('../loda/loda_search_handler');
describe('LODA-MCP-COMP-02: loda_search_handler', () => {
// Test document path
const testDocPath = path.join(__dirname, 'fixtures', 'test_document.md');
const fixturesDir = path.join(__dirname, 'fixtures');
// Mock parse function matching server's parseDocumentStructure
const mockParseFunc = (docPath) => {
const content = fs.readFileSync(docPath, 'utf8');
const lines = content.split('\n');
const sections = [];
let currentSectionId = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const headerText = headingMatch[2];
let endLine = lines.length - 1;
for (let j = i + 1; j < lines.length; j++) {
const nextHeadingMatch = lines[j].match(/^(#{1,6})\s+/);
if (nextHeadingMatch && nextHeadingMatch[1].length <= level) {
endLine = j - 1;
break;
}
}
sections.push({
id: `section-${currentSectionId++}`,
header: headerText,
level,
startLine: i + 1,
endLine: endLine + 1,
lineCount: endLine - i + 1
});
}
}
return {
sections,
totalLines: lines.length,
fileName: path.basename(docPath)
};
};
// Setup: Create test fixtures
beforeAll(() => {
if (!fs.existsSync(fixturesDir)) {
fs.mkdirSync(fixturesDir, { recursive: true });
}
const testContent = `# Introduction
Welcome to the test document.
## Authentication
This section covers authentication and login processes.
Users can authenticate using OAuth or API keys.
## Database
Database configuration and setup instructions.
Supports PostgreSQL and SQLite.
## API Reference
Complete API documentation.
Endpoints for user management and data access.
`;
fs.writeFileSync(testDocPath, testContent);
});
// Cleanup
afterAll(() => {
if (fs.existsSync(testDocPath)) {
fs.unlinkSync(testDocPath);
}
if (fs.existsSync(fixturesDir)) {
fs.rmdirSync(fixturesDir);
}
});
describe('search()', () => {
test('UT-COMP02-001: returns relevant sections for query', () => {
const handler = new LodaSearchHandler(mockParseFunc);
const result = handler.search(testDocPath, 'authentication');
expect(result.sections.length).toBeGreaterThan(0);
expect(result.sections[0].header.toLowerCase()).toContain('authentication');
});
test('UT-COMP02-002: respects maxSections limit', () => {
const handler = new LodaSearchHandler(mockParseFunc);
const result = handler.search(testDocPath, 'test', { maxSections: 2 });
expect(result.sections.length).toBeLessThanOrEqual(2);
});
test('UT-COMP02-003: respects contextBudget', () => {
const handler = new LodaSearchHandler(mockParseFunc);
const result = handler.search(testDocPath, 'test', { contextBudget: 50 });
expect(result.metadata).toBeDefined();
expect(result.metadata.budgetStatus).toBeDefined();
// Budget may cause truncation or exceed status
expect(['SAFE', 'WARNING', 'EXCEEDED']).toContain(result.metadata.budgetStatus);
});
test('UT-COMP02-004: returns empty for no matches', () => {
const handler = new LodaSearchHandler(mockParseFunc);
const result = handler.search(testDocPath, 'xyznonexistent123');
expect(result.sections.length).toBe(0);
});
test('UT-COMP02-005: includes metadata in response', () => {
const handler = new LodaSearchHandler(mockParseFunc);
const result = handler.search(testDocPath, 'authentication');
expect(result).toHaveProperty('query');
expect(result).toHaveProperty('documentPath');
expect(result).toHaveProperty('metadata');
expect(result.metadata).toHaveProperty('totalSections');
expect(result.query).toBe('authentication');
});
test('UT-COMP02-006: sections include required fields', () => {
const handler = new LodaSearchHandler(mockParseFunc);
const result = handler.search(testDocPath, 'database');
// Search for 'database' should find the Database section
expect(result.sections.length).toBeGreaterThan(0);
const dbSection = result.sections.find(s =>
s.header.toLowerCase().includes('database')
);
expect(dbSection).toBeDefined();
// Response sections include id, header, level, score, lineRange, tokenEstimate
expect(dbSection).toHaveProperty('id');
expect(dbSection).toHaveProperty('header');
expect(dbSection).toHaveProperty('score');
expect(dbSection.score).toBeGreaterThan(0);
});
});
describe('caching behavior', () => {
test('UT-COMP02-007: second search uses cached index', () => {
let parseCount = 0;
const countingParse = (docPath) => {
parseCount++;
return mockParseFunc(docPath);
};
const handler = new LodaSearchHandler(countingParse);
handler.search(testDocPath, 'auth');
handler.search(testDocPath, 'database');
// Parse should only be called once (cached)
expect(parseCount).toBe(1);
});
});
describe('bloom filter behavior', () => {
test('UT-COMP02-008: bloom filter eliminates non-matching sections', () => {
const handler = new LodaSearchHandler(mockParseFunc, { useBloomFilter: true });
const result = handler.search(testDocPath, 'postgresql');
// Should primarily return Database section
if (result.sections.length > 0) {
const hasDatabase = result.sections.some(s =>
s.header.toLowerCase().includes('database')
);
expect(hasDatabase).toBe(true);
}
});
});
});