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
233 lines
9.7 KiB
TypeScript
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);
|
|
});
|
|
});
|