import { describe, it, expect, beforeEach } from 'vitest';
import { resolve } from 'path';
import { PathResolver } from '../../src/utils/path-resolver.js';
import { searchMdxTool } from '../../src/tools/search-mdx.js';
const FIXTURES_DIR = resolve(process.cwd(), 'tests', 'fixtures');
describe('search_mdx tool', () => {
let pathResolver: PathResolver;
beforeEach(() => {
pathResolver = new PathResolver(FIXTURES_DIR);
});
describe('successful searches', () => {
it('should find matches in MDX file', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: 'Authentication' },
pathResolver
);
expect(result.isError).toBeFalsy();
expect(result.content).toHaveLength(1);
const parsed = JSON.parse(result.content[0].text);
expect(parsed).toHaveProperty('matches');
expect(parsed).toHaveProperty('totalMatches');
expect(parsed.matches.length).toBeGreaterThan(0);
expect(parsed.totalMatches).toBeGreaterThan(0);
});
it('should include line numbers in results', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: 'OAuth' },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
parsed.matches.forEach((match: any) => {
expect(match).toHaveProperty('lineNumber');
expect(typeof match.lineNumber).toBe('number');
expect(match.lineNumber).toBeGreaterThan(0);
});
});
it('should include context lines before and after match', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: 'API Keys', contextLines: 2 },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.matches.length).toBeGreaterThan(0);
const match = parsed.matches[0];
expect(match).toHaveProperty('context');
expect(match.context).toHaveProperty('before');
expect(match.context).toHaveProperty('after');
expect(Array.isArray(match.context.before)).toBe(true);
expect(Array.isArray(match.context.after)).toBe(true);
});
it('should respect contextLines parameter', async () => {
const result1 = await searchMdxTool(
{ path: 'search-test.mdx', query: 'GraphQL', contextLines: 1 },
pathResolver
);
const result2 = await searchMdxTool(
{ path: 'search-test.mdx', query: 'GraphQL', contextLines: 3 },
pathResolver
);
expect(result1.isError).toBeFalsy();
expect(result2.isError).toBeFalsy();
const parsed1 = JSON.parse(result1.content[0].text);
const parsed2 = JSON.parse(result2.content[0].text);
if (parsed1.matches.length > 0 && parsed2.matches.length > 0) {
const match1 = parsed1.matches[0];
const match2 = parsed2.matches[0];
expect(match1.context.before.length).toBeLessThanOrEqual(1);
expect(match2.context.before.length).toBeLessThanOrEqual(3);
}
});
it('should use default contextLines of 2 when not specified', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: 'Rate Limiting' },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
if (parsed.matches.length > 0) {
const match = parsed.matches[0];
expect(match.context.before.length).toBeLessThanOrEqual(2);
expect(match.context.after.length).toBeLessThanOrEqual(2);
}
});
it('should find multiple occurrences of the same term', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: 'API' },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.totalMatches).toBeGreaterThan(1);
expect(parsed.matches.length).toBe(parsed.totalMatches);
});
it('should return empty matches when query not found', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: 'NONEXISTENTTERM12345' },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.matches).toEqual([]);
expect(parsed.totalMatches).toBe(0);
});
});
describe('error handling', () => {
it('should handle non-existent files', async () => {
const result = await searchMdxTool(
{ path: 'nonexistent.mdx', query: 'test' },
pathResolver
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('File not found');
});
it('should handle unsafe paths', async () => {
const result = await searchMdxTool(
{ path: '../../etc/passwd', query: 'root' },
pathResolver
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('outside workspace root');
});
it('should handle absolute paths outside workspace', async () => {
const result = await searchMdxTool(
{ path: '/etc/passwd', query: 'root' },
pathResolver
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('outside workspace root');
});
});
describe('search accuracy', () => {
it('should find exact case-sensitive matches', async () => {
const result = await searchMdxTool(
{ path: 'basic-with-frontmatter.mdx', query: 'Installation' },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
parsed.matches.forEach((match: any) => {
expect(match.content).toContain('Installation');
});
});
it('should include the matched line content', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: '404' },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
if (parsed.matches.length > 0) {
const match = parsed.matches[0];
expect(match).toHaveProperty('content');
expect(match.content).toContain('404');
}
});
it('should search across different file content types', async () => {
const result1 = await searchMdxTool(
{ path: 'basic-with-frontmatter.mdx', query: 'MDX' },
pathResolver
);
const result2 = await searchMdxTool(
{ path: 'with-jsx-components.mdx', query: 'Alert' },
pathResolver
);
expect(result1.isError).toBeFalsy();
expect(result2.isError).toBeFalsy();
const parsed1 = JSON.parse(result1.content[0].text);
const parsed2 = JSON.parse(result2.content[0].text);
expect(parsed1.totalMatches).toBeGreaterThan(0);
expect(parsed2.totalMatches).toBeGreaterThan(0);
});
});
describe('context handling', () => {
it('should handle context at beginning of file', async () => {
const result = await searchMdxTool(
{ path: 'no-frontmatter.mdx', query: 'Simple MDX Document', contextLines: 5 },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
if (parsed.matches.length > 0) {
const match = parsed.matches[0];
// Should have no or minimal context before if it's the first line
expect(match.context.before.length).toBeLessThan(3);
}
});
it('should return string arrays for context', async () => {
const result = await searchMdxTool(
{ path: 'search-test.mdx', query: 'REST', contextLines: 2 },
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
if (parsed.matches.length > 0) {
const match = parsed.matches[0];
match.context.before.forEach((line: any) => {
expect(typeof line).toBe('string');
});
match.context.after.forEach((line: any) => {
expect(typeof line).toBe('string');
});
}
});
});
});