/**
* @file loda_index.test.js
* @description Unit tests for LODA-MCP-COMP-03: loda_index
* @covers Document structure caching with TTL and LRU eviction
*/
const { LodaIndex } = require('../loda/loda_index');
describe('LODA-MCP-COMP-03: loda_index', () => {
// Mock parse function
const mockParseFunc = (path) => ({
sections: [
{ id: 'section-0', header: 'Test', startLine: 1, endLine: 10 }
],
totalLines: 10,
fileName: path.split('/').pop()
});
describe('getStructure()', () => {
test('UT-COMP03-001: first call invokes parse function', () => {
const index = new LodaIndex();
let parseCount = 0;
const countingParse = (path) => {
parseCount++;
return mockParseFunc(path);
};
index.getStructure('/test/doc.md', countingParse);
expect(parseCount).toBe(1);
});
test('UT-COMP03-002: second call uses cache', () => {
const index = new LodaIndex();
let parseCount = 0;
const countingParse = (path) => {
parseCount++;
return mockParseFunc(path);
};
index.getStructure('/test/doc.md', countingParse);
index.getStructure('/test/doc.md', countingParse);
expect(parseCount).toBe(1); // Still 1, not 2
});
test('UT-COMP03-003: different paths use separate cache entries', () => {
const index = new LodaIndex();
let parseCount = 0;
const countingParse = (path) => {
parseCount++;
return mockParseFunc(path);
};
index.getStructure('/test/doc1.md', countingParse);
index.getStructure('/test/doc2.md', countingParse);
expect(parseCount).toBe(2);
});
test('UT-COMP03-004: cache respects maxAge (TTL)', async () => {
const index = new LodaIndex({ maxAge: 50 }); // 50ms TTL
let parseCount = 0;
const countingParse = (path) => {
parseCount++;
return mockParseFunc(path);
};
index.getStructure('/test/doc.md', countingParse);
expect(parseCount).toBe(1);
// Wait for TTL to expire (add buffer for timing variance)
await new Promise(resolve => setTimeout(resolve, 100));
index.getStructure('/test/doc.md', countingParse);
expect(parseCount).toBe(2); // Should re-parse after TTL
});
test('UT-COMP03-005: cache respects maxSize (LRU eviction)', () => {
const index = new LodaIndex({ maxSize: 2 });
let parseCount = 0;
const countingParse = (path) => {
parseCount++;
return mockParseFunc(path);
};
// Fill cache
index.getStructure('/test/doc1.md', countingParse); // 1
index.getStructure('/test/doc2.md', countingParse); // 2
index.getStructure('/test/doc3.md', countingParse); // 3, evicts doc1
parseCount = 0; // Reset counter
// doc2 and doc3 should be cached
index.getStructure('/test/doc2.md', countingParse);
index.getStructure('/test/doc3.md', countingParse);
expect(parseCount).toBe(0); // No new parses
// doc1 was evicted, should re-parse
index.getStructure('/test/doc1.md', countingParse);
expect(parseCount).toBe(1);
});
});
describe('invalidate()', () => {
test('UT-COMP03-006: invalidate removes specific entry', () => {
const index = new LodaIndex();
let parseCount = 0;
const countingParse = (path) => {
parseCount++;
return mockParseFunc(path);
};
index.getStructure('/test/doc.md', countingParse);
expect(parseCount).toBe(1);
index.invalidate('/test/doc.md');
index.getStructure('/test/doc.md', countingParse);
expect(parseCount).toBe(2); // Re-parsed after invalidation
});
test('UT-COMP03-007: invalidate non-existent path is safe', () => {
const index = new LodaIndex();
// Should not throw
expect(() => index.invalidate('/nonexistent/doc.md')).not.toThrow();
});
});
describe('getStats()', () => {
test('UT-COMP03-008: stats reflect cache state', () => {
const index = new LodaIndex({ maxSize: 100 });
index.getStructure('/test/doc1.md', mockParseFunc);
index.getStructure('/test/doc2.md', mockParseFunc);
const stats = index.getStats();
expect(stats.size).toBe(2);
expect(stats.maxSize).toBe(100);
});
});
});