Files
repomix-mirror/tests/mcp/tools/grepRepomixOutputTool.test.ts
Kazuki Yamada 2fac8d85ee perf(core): Optimize chunk merging and avoid redundant string split in grep tool
- Replace string += with array accumulation + join('\n') in mergeAdjacentChunks
  to avoid O(k²) copying when merging adjacent tree-sitter code chunks
- Extract searchInLines from searchInContent in grepRepomixOutputTool so
  performGrepSearch splits content once and reuses the lines array for both
  search and formatting, avoiding a redundant O(n) split on large files
2026-03-28 15:12:29 +09:00

740 lines
30 KiB
TypeScript

import fs from 'node:fs/promises';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
createRegexPattern,
formatSearchResults,
performGrepSearch,
registerGrepRepomixOutputTool,
searchInContent,
} from '../../../src/mcp/tools/grepRepomixOutputTool.js';
import * as mcpToolRuntime from '../../../src/mcp/tools/mcpToolRuntime.js';
vi.mock('node:fs/promises');
vi.mock('../../../src/shared/logger.js');
vi.mock('../../../src/mcp/tools/mcpToolRuntime.js', async () => {
const actual = await vi.importActual('../../../src/mcp/tools/mcpToolRuntime.js');
return {
...actual,
getOutputFilePath: vi.fn(),
};
});
describe('grepRepomixOutputTool', () => {
describe('createRegexPattern', () => {
it('should create a case-sensitive regex by default', () => {
const regex = createRegexPattern('test', false);
expect(regex.flags).toBe('g');
expect(regex.source).toBe('test');
});
it('should create a case-insensitive regex when specified', () => {
const regex = createRegexPattern('test', true);
expect(regex.flags).toBe('gi');
expect(regex.source).toBe('test');
});
it('should throw error for invalid regex patterns', () => {
expect(() => createRegexPattern('[invalid', false)).toThrow('Invalid regular expression pattern');
});
it('should use dependency injection for RegExp', () => {
// Create a mock that works as a constructor using regular function syntax
const mockRegExp = vi.fn().mockImplementation(function (this: unknown, pattern: string, flags: string) {
return new RegExp(pattern, flags);
});
createRegexPattern('test', false, { RegExp: mockRegExp as unknown as RegExpConstructor });
expect(mockRegExp).toHaveBeenCalledWith('test', 'g');
});
});
describe('searchInContent', () => {
it('should find matches in content', () => {
const content = 'line 1\npattern match\nline 3\nanother pattern\nline 5';
const options = { pattern: 'pattern', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: 'pattern match',
matchedText: 'pattern',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: 'another pattern',
matchedText: 'pattern',
});
});
it('should handle case-insensitive search', () => {
const content = 'Line 1\nPATTERN match\nline 3';
const options = { pattern: 'pattern', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: true };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(1);
expect(matches[0]).toEqual({
lineNumber: 2,
line: 'PATTERN match',
matchedText: 'PATTERN',
});
});
it('should return empty array when no matches found', () => {
const content = 'line 1\nline 2\nline 3';
const options = { pattern: 'notfound', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(0);
});
it('should use dependency injection for regex creation', () => {
const mockCreateRegexPattern = vi.fn().mockReturnValue(/test/g);
const content = 'test content';
const options = { pattern: 'test', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
searchInContent(content, options, { createRegexPattern: mockCreateRegexPattern });
expect(mockCreateRegexPattern).toHaveBeenCalledWith('test', false);
});
it('should handle Japanese text search', () => {
const content = '最初の行\n日本語のパターン検索\n3行目\n別の日本語パターン\n最後の行';
const options = { pattern: '日本語', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: '日本語のパターン検索',
matchedText: '日本語',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: '別の日本語パターン',
matchedText: '日本語',
});
});
it('should handle Chinese text search', () => {
const content = '第一行\n中文搜索模式\n第三行\n另一个中文模式\n最后一行';
const options = { pattern: '中文', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: '中文搜索模式',
matchedText: '中文',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: '另一个中文模式',
matchedText: '中文',
});
});
it('should handle Korean text search', () => {
const content = '첫 번째 줄\n한국어 패턴 검색\n세 번째 줄\n다른 한국어 패턴\n마지막 줄';
const options = { pattern: '한국어', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: '한국어 패턴 검색',
matchedText: '한국어',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: '다른 한국어 패턴',
matchedText: '한국어',
});
});
it('should handle emoji search', () => {
const content = 'line 1\n🎉 celebration emoji\nline 3\nanother 🎉 here\nline 5';
const options = { pattern: '🎉', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: '🎉 celebration emoji',
matchedText: '🎉',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: 'another 🎉 here',
matchedText: '🎉',
});
});
it('should handle mixed multilingual content', () => {
const content = 'English line\n日本語とEnglishの混在\n中文和English混合\n🚀 emoji with text\nNormal line';
const options = { pattern: 'English', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(3);
expect(matches[0]).toEqual({
lineNumber: 1,
line: 'English line',
matchedText: 'English',
});
expect(matches[1]).toEqual({
lineNumber: 2,
line: '日本語とEnglishの混在',
matchedText: 'English',
});
expect(matches[2]).toEqual({
lineNumber: 3,
line: '中文和English混合',
matchedText: 'English',
});
});
it('should handle special characters and symbols', () => {
const content = 'line 1\n$special #symbols @test\nline 3\n&more $special chars\nline 5';
const options = { pattern: '\\$special', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: '$special #symbols @test',
matchedText: '$special',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: '&more $special chars',
matchedText: '$special',
});
});
it('should handle regex patterns with Unicode', () => {
const content = 'file1.js\nファイル1.ts\nfile2.py\nファイル2.jsx\ntest.md';
const options = {
pattern: 'ファイル\\d+\\.(ts|jsx)',
contextLines: 0,
beforeLines: 0,
afterLines: 0,
ignoreCase: false,
};
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: 'ファイル1.ts',
matchedText: 'ファイル1.ts',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: 'ファイル2.jsx',
matchedText: 'ファイル2.jsx',
});
});
it('should handle case-insensitive search with multibyte characters', () => {
const content = '日本語テスト\nNIPPON語test\n中文测试\nTEST中文';
const options = { pattern: 'test', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: true };
const matches = searchInContent(content, options);
expect(matches).toHaveLength(2);
expect(matches[0]).toEqual({
lineNumber: 2,
line: 'NIPPON語test',
matchedText: 'test',
});
expect(matches[1]).toEqual({
lineNumber: 4,
line: 'TEST中文',
matchedText: 'TEST',
});
});
it('should handle complex Unicode regex patterns', () => {
const content = 'user@example.com\nユーザー@例.jp\ntest@テスト.org\n管理者@サンプル.co.jp\nnormal text';
const options = {
pattern: '.+@.+\\.(com|jp|org)',
contextLines: 0,
beforeLines: 0,
afterLines: 0,
ignoreCase: false,
};
const matches = searchInContent(content, options);
expect(matches).toHaveLength(4);
expect(matches[0].line).toBe('user@example.com');
expect(matches[1].line).toBe('ユーザー@例.jp');
expect(matches[2].line).toBe('test@テスト.org');
expect(matches[3].line).toBe('管理者@サンプル.co.jp');
});
});
describe('formatSearchResults', () => {
const lines = ['line 1', 'pattern match', 'line 3', 'another pattern', 'line 5'];
const matches = [
{ lineNumber: 2, line: 'pattern match', matchedText: 'pattern' },
{ lineNumber: 4, line: 'another pattern', matchedText: 'pattern' },
];
it('should format results without context lines', () => {
const result = formatSearchResults(lines, matches, 0, 0);
expect(result).toEqual(['2:pattern match', '--', '4:another pattern']);
});
it('should format results with equal before and after context lines', () => {
const result = formatSearchResults(lines, matches, 1, 1);
expect(result).toEqual(['1-line 1', '2:pattern match', '3-line 3', '--', '4:another pattern', '5-line 5']);
});
it('should format results with different before and after context lines', () => {
const result = formatSearchResults(lines, matches, 1, 0);
expect(result).toEqual(['1-line 1', '2:pattern match', '--', '3-line 3', '4:another pattern']);
});
it('should format results with only after context lines', () => {
const result = formatSearchResults(lines, matches, 0, 1);
expect(result).toEqual(['2:pattern match', '3-line 3', '--', '4:another pattern', '5-line 5']);
});
it('should format results with more before than after context lines', () => {
const extendedLines = ['line 0', 'line 1', 'pattern match', 'line 3', 'another pattern', 'line 5', 'line 6'];
const extendedMatches = [
{ lineNumber: 3, line: 'pattern match', matchedText: 'pattern' },
{ lineNumber: 5, line: 'another pattern', matchedText: 'pattern' },
];
const result = formatSearchResults(extendedLines, extendedMatches, 2, 1);
expect(result).toEqual([
'1-line 0',
'2-line 1',
'3:pattern match',
'4-line 3',
'--',
'5:another pattern',
'6-line 5',
]);
});
it('should return empty array for no matches', () => {
const result = formatSearchResults(lines, [], 0, 0);
expect(result).toEqual([]);
});
it('should handle overlapping context correctly', () => {
const closeMatches = [
{ lineNumber: 2, line: 'pattern match', matchedText: 'pattern' },
{ lineNumber: 3, line: 'line 3', matchedText: 'line' },
];
const result = formatSearchResults(lines, closeMatches, 1, 1);
// Should not duplicate lines and should merge overlapping contexts
expect(result).toEqual(['1-line 1', '2:pattern match', '3-line 3', '4-another pattern']);
});
});
describe('performGrepSearch', () => {
it('should perform complete grep search and return formatted results', () => {
const content = 'line 1\npattern match\nline 3\nanother pattern\nline 5';
const options = { pattern: 'pattern', contextLines: 1, beforeLines: 1, afterLines: 1, ignoreCase: false };
const result = performGrepSearch(content, options);
expect(result.matches).toHaveLength(2);
expect(result.formattedOutput).toContain('2:pattern match');
expect(result.formattedOutput).toContain('4:another pattern');
expect(result.formattedOutput).toContain('1-line 1');
expect(result.formattedOutput).toContain('3-line 3');
});
it('should use dependency injection for search functions', () => {
const mockSearchInLines = vi.fn().mockReturnValue([]);
const mockFormatSearchResults = vi.fn().mockReturnValue(['formatted']);
const content = 'test content';
const options = { pattern: 'test', contextLines: 0, beforeLines: 0, afterLines: 0, ignoreCase: false };
const result = performGrepSearch(content, options, {
searchInLines: mockSearchInLines,
formatSearchResults: mockFormatSearchResults,
});
expect(mockSearchInLines).toHaveBeenCalledWith(['test content'], options);
expect(mockFormatSearchResults).toHaveBeenCalledWith(['test content'], [], 0, 0);
expect(result.formattedOutput).toEqual(['formatted']);
});
it('should handle different before and after context lines', () => {
const content = 'line 1\nline 2\npattern match\nline 4\nline 5';
const options = { pattern: 'pattern', contextLines: 0, beforeLines: 2, afterLines: 1, ignoreCase: false };
const result = performGrepSearch(content, options);
expect(result.matches).toHaveLength(1);
expect(result.formattedOutput).toContain('1-line 1');
expect(result.formattedOutput).toContain('2-line 2');
expect(result.formattedOutput).toContain('3:pattern match');
expect(result.formattedOutput).toContain('4-line 4');
expect(result.formattedOutput).not.toContain('5-line 5');
});
});
describe('registerGrepRepomixOutputTool integration tests', () => {
const mockMcpServer = {
registerTool: vi.fn(),
} as const;
type ToolHandlerType = (args: {
outputId: string;
pattern: string;
contextLines?: number;
beforeLines?: number;
afterLines?: number;
ignoreCase?: boolean;
}) => Promise<{
isError?: boolean;
content: Array<{ type: string; text: string }>;
}>;
let toolHandler: ToolHandlerType;
beforeEach(() => {
vi.resetAllMocks();
registerGrepRepomixOutputTool(mockMcpServer as unknown as McpServer);
toolHandler = mockMcpServer.registerTool.mock.calls[0][2];
});
it('should register the tool with correct parameters', () => {
expect(mockMcpServer.registerTool).toHaveBeenCalledWith(
'grep_repomix_output',
expect.any(Object), // tool spec
expect.any(Function),
);
});
it('should find matches and return them with line numbers', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\npattern match\nline 3\nanother pattern\nline 5');
const result = await toolHandler({ outputId: 'test-id', pattern: 'pattern' });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
expect(parsedResult.formattedOutput).toContain('2:pattern match');
expect(parsedResult.formattedOutput).toContain('4:another pattern');
});
it('should handle separate before and after context lines', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\nline 2\npattern match\nline 4\nline 5\nline 6');
const result = await toolHandler({
outputId: 'test-id',
pattern: 'pattern',
beforeLines: 2,
afterLines: 1,
});
const parsedResult = JSON.parse(result.content[0].text);
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('1-line 1');
expect(formattedOutputString).toContain('2-line 2');
expect(formattedOutputString).toContain('3:pattern match');
expect(formattedOutputString).toContain('4-line 4');
expect(formattedOutputString).not.toContain('5-line 5');
});
it('should prioritize beforeLines and afterLines over contextLines', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\nline 2\npattern match\nline 4\nline 5');
const result = await toolHandler({
outputId: 'test-id',
pattern: 'pattern',
contextLines: 2,
beforeLines: 1,
afterLines: 0,
});
const parsedResult = JSON.parse(result.content[0].text);
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('2-line 2');
expect(formattedOutputString).toContain('3:pattern match');
expect(formattedOutputString).not.toContain('1-line 1');
expect(formattedOutputString).not.toContain('4-line 4');
});
it('should use contextLines when beforeLines and afterLines are not specified', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\nline 2\npattern match\nline 4\nline 5');
const result = await toolHandler({ outputId: 'test-id', pattern: 'pattern', contextLines: 1 });
const parsedResult = JSON.parse(result.content[0].text);
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('2-line 2');
expect(formattedOutputString).toContain('3:pattern match');
expect(formattedOutputString).toContain('4-line 4');
});
it('should handle case insensitive search', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('Line 1\nPATTERN match\nline 3');
const result = await toolHandler({ outputId: 'test-id', pattern: 'pattern', ignoreCase: true });
const parsedResult = JSON.parse(result.content[0].text);
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('2:PATTERN match');
});
it('should return no matches message when pattern not found', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\nline 2\nline 3');
const result = await toolHandler({ outputId: 'test-id', pattern: 'notfound' });
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('No matches found');
});
it('should handle invalid regex patterns', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('some content');
const result = await toolHandler({ outputId: 'test-id', pattern: '[invalid' });
expect(result.isError).toBe(true);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Invalid regular expression pattern');
});
it('should handle file not found error', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue(undefined);
const result = await toolHandler({ outputId: 'test-id', pattern: 'test' });
expect(result.isError).toBe(true);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Output file with ID test-id not found');
});
it('should handle file access error', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockRejectedValue(new Error('File not accessible'));
const result = await toolHandler({ outputId: 'test-id', pattern: 'test' });
expect(result.isError).toBe(true);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Output file does not exist');
});
// Multilingual and Unicode content integration tests
it('should handle Japanese text in file content', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('最初の行\n日本語のパターン\n3行目\n別の日本語\n最後の行');
const result = await toolHandler({ outputId: 'test-id', pattern: '日本語' });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
expect(parsedResult.formattedOutput).toContain('2:日本語のパターン');
expect(parsedResult.formattedOutput).toContain('4:別の日本語');
});
it('should handle Chinese text in file content', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('第一行\n中文搜索\n第三行\n更多中文\n最后一行');
const result = await toolHandler({ outputId: 'test-id', pattern: '中文' });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
expect(parsedResult.formattedOutput).toContain('2:中文搜索');
expect(parsedResult.formattedOutput).toContain('4:更多中文');
});
it('should handle Korean text in file content', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('첫 번째 줄\n한국어 검색\n세 번째 줄\n다른 한국어\n마지막 줄');
const result = await toolHandler({ outputId: 'test-id', pattern: '한국어' });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
expect(parsedResult.formattedOutput).toContain('2:한국어 검색');
expect(parsedResult.formattedOutput).toContain('4:다른 한국어');
});
it('should handle emoji content in file', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\n🎉 celebration\nline 3\n🚀 rocket emoji\nline 5');
const result = await toolHandler({ outputId: 'test-id', pattern: '🎉|🚀' });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
expect(parsedResult.formattedOutput).toContain('2:🎉 celebration');
expect(parsedResult.formattedOutput).toContain('4:🚀 rocket emoji');
});
it('should handle mixed multilingual content in file', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue(
'English line\n日本語とEnglish混在\n中文和English混合\n🌟 mixed content\nनमस्ते English',
);
const result = await toolHandler({ outputId: 'test-id', pattern: 'English', contextLines: 1 });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 4 match(es)');
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('1:English line');
expect(formattedOutputString).toContain('2-日本語とEnglish混在');
expect(formattedOutputString).toContain('3-中文和English混合');
expect(formattedOutputString).toContain('5:नमस्ते English');
});
it('should handle complex Unicode regex patterns in file content', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue(
'user@example.com\nユーザー@例.jp\ntest@テスト.org\n管理者@サンプル.co.jp\nnormal text',
);
const result = await toolHandler({ outputId: 'test-id', pattern: '.+@.+\\.(com|jp|org)' });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 4 match(es)');
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('1:user@example.com');
expect(formattedOutputString).toContain('2:ユーザー@例.jp');
expect(formattedOutputString).toContain('3:test@テスト.org');
expect(formattedOutputString).toContain('4:管理者@サンプル.co.jp');
});
it('should handle special characters with escaping in file content', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue(
'normal line\n$special chars #symbols\nline 3\n&more $special items\nend line',
);
const result = await toolHandler({ outputId: 'test-id', pattern: '\\$special', contextLines: 1 });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('2:$special chars #symbols');
expect(formattedOutputString).toContain('4:&more $special items');
});
it('should handle case-insensitive search with multibyte characters in file', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('日本語テスト\nNIPPON語test\n中文测试\nTEST中文\nnormal');
const result = await toolHandler({ outputId: 'test-id', pattern: 'test', ignoreCase: true });
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
const formattedOutputString = parsedResult.formattedOutput.join('\n');
expect(formattedOutputString).toContain('2:NIPPON語test');
expect(formattedOutputString).toContain('4:TEST中文');
});
it('should handle string parameters by coercing them to numbers', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\npattern match\nline 3\nanother pattern\nline 5');
// Simulate Cursor AI sending strings instead of numbers
const result = await toolHandler({
outputId: 'test-id',
pattern: 'pattern',
contextLines: '1' as unknown as number,
beforeLines: '2' as unknown as number,
afterLines: '1' as unknown as number,
});
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
expect(parsedResult.formattedOutput.length).toBeGreaterThan(0);
});
it('should handle mixed string and number parameters', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('line 1\npattern match\nline 3\nanother pattern\nline 5');
// Test with some parameters as strings and others as numbers
const result = await toolHandler({
outputId: 'test-id',
pattern: 'pattern',
contextLines: '1' as unknown as number,
beforeLines: 2,
afterLines: '0' as unknown as number,
});
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.description).toContain('Found 2 match(es)');
});
});
});