Files
yamadashy--repomix/tests/mcp/tools/readRepomixOutputTool.test.ts
wehub-resource-sync 719032b19f
Update Schema / Update configuration json schema (push) Has been cancelled
Memory Benchmark / Memory Test (Full Analysis) (push) Has been cancelled
CI Quality / Lint GitHub Actions (actionlint) (push) Has been cancelled
CI Quality / Lint GitHub Actions (zizmor) (push) Has been cancelled
CI Quality / Check typos (push) Has been cancelled
CI / Test coverage (push) Has been cancelled
CI / Test (22.x, windows-latest) (push) Has been cancelled
CI / Test (24.x, macos-latest) (push) Has been cancelled
CI / Test (24.x, ubuntu-latest) (push) Has been cancelled
CI / Test (24.x, windows-latest) (push) Has been cancelled
CI / Test (26.x, macos-latest) (push) Has been cancelled
CI / Test (26.x, ubuntu-latest) (push) Has been cancelled
CI / Test (26.x, windows-latest) (push) Has been cancelled
CI / Test with Bun (latest, macos-latest) (push) Has been cancelled
CI / Test with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Test with Bun (latest, windows-latest) (push) Has been cancelled
CI / Build and run (22.x, macos-latest) (push) Has been cancelled
CI / Build and run (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (22.x, windows-latest) (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CI Browser Extension / Lint Browser Extension (push) Has been cancelled
CI Browser Extension / Test Browser Extension (push) Has been cancelled
Memory Benchmark / Memory Test (push) Has been cancelled
CI Website / Lint Website Client (push) Has been cancelled
CI Website / Lint Website Server (push) Has been cancelled
CI Website / Test Website Server (push) Has been cancelled
CI Website / Bundle Website Server (push) Has been cancelled
CI / Lint Biome (push) Has been cancelled
CI / Lint oxlint (push) Has been cancelled
CI / Lint TypeScript (push) Has been cancelled
CI / Lint Secretlint (push) Has been cancelled
CI / Test (22.x, macos-latest) (push) Has been cancelled
CI / Test (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, macos-latest) (push) Has been cancelled
CI / Build and run (24.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, windows-latest) (push) Has been cancelled
CI / Build and run (26.x, macos-latest) (push) Has been cancelled
CI / Build and run (26.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (26.x, windows-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, macos-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Docker / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker / build (linux/arm/v7, ubuntu-24.04-arm) (push) Has been cancelled
Docker / build (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker / merge (push) Has been cancelled
Pack repository with Repomix / pack-repo (push) Has been cancelled
Performance Benchmark History / Benchmark (macos-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (ubuntu-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (windows-latest) (push) Has been cancelled
Performance Benchmark History / Store Results (push) Has been cancelled
Test Repomix Action / Test Node.js 22 (push) Has been cancelled
Test Repomix Action / Test Node.js 24 (push) Has been cancelled
Test Repomix Action / Test Node.js 26 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:37 +08:00

233 lines
9.7 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 { runSecretLint } from '../../../src/core/security/workers/securityCheckWorker.js';
import * as mcpToolRuntime from '../../../src/mcp/tools/mcpToolRuntime.js';
import { registerReadRepomixOutputTool } from '../../../src/mcp/tools/readRepomixOutputTool.js';
vi.mock('node:fs/promises');
vi.mock('../../../src/mcp/tools/mcpToolRuntime.js', async () => {
const actual = await vi.importActual('../../../src/mcp/tools/mcpToolRuntime.js');
return {
...actual,
getOutputFilePath: vi.fn(),
requiresSecretScan: vi.fn(),
};
});
vi.mock('../../../src/core/security/workers/securityCheckWorker.js', () => ({
createSecretLintConfig: vi.fn().mockReturnValue({}),
runSecretLint: vi.fn().mockResolvedValue(null),
}));
vi.mock('../../../src/shared/logger.js', () => ({
logger: {
trace: vi.fn(),
error: vi.fn(),
},
}));
describe('readRepomixOutputTool', () => {
const mockMcpServer = {
registerTool: vi.fn(),
} as const;
type ToolHandlerType = (args: { outputId: string; startLine?: number; endLine?: number }) => Promise<{
isError?: boolean;
content: Array<{ type: string; text: string }>;
}>;
let toolHandler: ToolHandlerType;
beforeEach(() => {
vi.resetAllMocks();
registerReadRepomixOutputTool(mockMcpServer as unknown as McpServer);
toolHandler = mockMcpServer.registerTool.mock.calls[0][2];
});
it('should register the tool with correct parameters', () => {
expect(mockMcpServer.registerTool).toHaveBeenCalledWith(
'read_repomix_output',
expect.any(Object), // tool spec
expect.any(Function),
);
});
it('should return an error if output file ID is not found', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue(undefined);
const result = await toolHandler({ outputId: 'non-existent-id' });
expect(mcpToolRuntime.getOutputFilePath).toHaveBeenCalledWith('non-existent-id');
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Error: Output file with ID non-existent-id not found');
});
it('should return an error if the file does not exist', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockRejectedValue(new Error('File not found'));
const result = await toolHandler({ outputId: 'test-id' });
expect(mcpToolRuntime.getOutputFilePath).toHaveBeenCalledWith('test-id');
expect(fs.access).toHaveBeenCalledWith('/path/to/file.xml');
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Error: Output file does not exist at path: /path/to/file.xml');
});
it('should successfully read the file content', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/file.xml');
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('File content here');
const result = await toolHandler({ outputId: 'test-id' });
expect(mcpToolRuntime.getOutputFilePath).toHaveBeenCalledWith('test-id');
expect(fs.access).toHaveBeenCalledWith('/path/to/file.xml');
expect(fs.readFile).toHaveBeenCalledWith('/path/to/file.xml', 'utf8');
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
// The structured content is handled internally by the MCP framework
});
it('should block attach-sourced content that fails the secret scan', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/attached.json');
vi.mocked(mcpToolRuntime.requiresSecretScan).mockReturnValue(true);
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('api_key = "leaked-secret"');
// Secret scan flags the content.
vi.mocked(runSecretLint).mockResolvedValue({ filePath: '/path/to/attached.json', messages: [] } as never);
const result = await toolHandler({ outputId: 'attached-id' });
expect(runSecretLint).toHaveBeenCalled();
expect(result.isError).toBe(true);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Security check failed');
});
it('should not secret-scan outputs that are not attach-sourced', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockReturnValue('/path/to/packed.xml');
vi.mocked(mcpToolRuntime.requiresSecretScan).mockReturnValue(false);
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('File content here');
const result = await toolHandler({ outputId: 'packed-id' });
expect(runSecretLint).not.toHaveBeenCalled();
expect(result.isError).toBeUndefined();
});
it('should handle unexpected errors during execution', async () => {
vi.mocked(mcpToolRuntime.getOutputFilePath).mockImplementation(() => {
throw new Error('Unexpected error');
});
const result = await toolHandler({ outputId: 'test-id' });
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Unexpected error');
});
it('should read specific line range when startLine and endLine are provided', 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\nLine 4\nLine 5');
const result = await toolHandler({ outputId: 'test-id', startLine: 2, endLine: 4 });
expect(fs.readFile).toHaveBeenCalledWith('/path/to/file.xml', 'utf8');
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
// The structured content is handled internally by the MCP framework
});
it('should read from startLine to end when only startLine is provided', 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\nLine 4\nLine 5');
const result = await toolHandler({ outputId: 'test-id', startLine: 3 });
expect(result.content).toHaveLength(1);
// The structured content is handled internally by the MCP framework
});
it('should read from beginning to endLine when only endLine is provided', 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\nLine 4\nLine 5');
const result = await toolHandler({ outputId: 'test-id', endLine: 2 });
expect(result.content).toHaveLength(1);
// The structured content is handled internally by the MCP framework
});
it('should return an error if startLine exceeds total 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\nLine 3');
const result = await toolHandler({ outputId: 'test-id', startLine: 10 });
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Error: Start line 10 exceeds total lines (3)');
});
it('should return an error if startLine is greater than endLine', 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\nLine 4\nLine 5');
const result = await toolHandler({ outputId: 'test-id', startLine: 4, endLine: 2 });
expect(result.isError).toBe(true);
expect(result.content).toHaveLength(1);
const parsedResult = JSON.parse(result.content[0].text);
expect(parsedResult.errorMessage).toContain('Error: Start line (4) cannot be greater than end line (2)');
});
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\nLine 2\nLine 3\nLine 4\nLine 5');
// Simulate Cursor AI sending strings instead of numbers
const result = await toolHandler({
outputId: 'test-id',
startLine: '2' as unknown as number,
endLine: '4' as unknown as number,
});
expect(fs.readFile).toHaveBeenCalledWith('/path/to/file.xml', 'utf8');
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
// The structured content is handled internally by the MCP framework
});
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\nLine 2\nLine 3\nLine 4\nLine 5');
// Test with startLine as string and endLine as number
const result = await toolHandler({
outputId: 'test-id',
startLine: '2' as unknown as number,
endLine: 4,
});
expect(result.isError).toBeUndefined();
expect(result.content).toHaveLength(1);
});
});