Files
repomix-mirror/tests/mcp/tools/fileSystemReadDirectoryTool.test.ts
Kazuki Yamada f67731056a test: Round-3 PR review feedback
- validateFileSafety: pin the negative path of `if (config.security.enableSecurityCheck)`
  — every other test enabled the check, so a regression that always runs
  the security check would have passed silently.
- unifiedWorker:
  - Add a positive workerData=securityCheck + ambiguous-task case so the
    pair (override + this) distinguishes "inference always wins" from
    "inference wins only when it yields a value".
  - Stop pretending the handler-cache test verifies caching. Both branches
    of `if (cached) return cached;` end with the same Map.set, and Node's
    own module cache makes the dynamic import effectively free, so the
    cache is unobservable from outside without exposing internals.
    Renamed to "repeated calls" with a comment explaining the limitation.
- fileSystemReadDirectoryTool: translate the pre-existing Japanese comment
  to English per CLAUDE.md.
- TokenCounter: extract `LoadEncodingFn` type alias instead of the
  unusual `typeof loadEncoding`, so a signature drift between the local
  function and the deps field would surface at the type level.
2026-04-26 22:47:21 +09:00

130 lines
4.9 KiB
TypeScript

import fs from 'node:fs/promises';
import path from 'node:path';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { registerFileSystemReadDirectoryTool } from '../../../src/mcp/tools/fileSystemReadDirectoryTool.js';
vi.mock('node:fs/promises');
vi.mock('node:path');
describe('FileSystemReadDirectoryTool', () => {
const mockServer = {
registerTool: vi.fn().mockReturnThis(),
} as unknown as McpServer;
let toolHandler: (args: { path: string }) => Promise<CallToolResult>;
beforeEach(() => {
vi.resetAllMocks();
registerFileSystemReadDirectoryTool(mockServer);
toolHandler = (mockServer.registerTool as ReturnType<typeof vi.fn>).mock.calls[0][2];
// Default mock for path.isAbsolute
vi.mocked(path.isAbsolute).mockImplementation((p: string) => p.startsWith('/'));
});
test('should register tool with correct parameters', () => {
expect(mockServer.registerTool).toHaveBeenCalledWith(
'file_system_read_directory',
expect.any(Object), // tool spec
expect.any(Function),
);
});
test('should handle relative path error', async () => {
const testPath = 'relative/path';
vi.mocked(path.isAbsolute).mockReturnValue(false);
const result = await toolHandler({ path: testPath });
expect(result).toEqual({
isError: true,
content: [
{
type: 'text',
text: JSON.stringify({ errorMessage: `Error: Path must be absolute. Received: ${testPath}` }, null, 2),
},
],
});
});
test('should handle non-existent directory', async () => {
const testPath = '/non/existent/dir';
vi.mocked(path.isAbsolute).mockReturnValue(true);
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT'));
const result = await toolHandler({ path: testPath });
expect(result).toEqual({
isError: true,
content: [
{
type: 'text',
text: JSON.stringify({ errorMessage: `Error: Directory not found at path: ${testPath}` }, null, 2),
},
],
});
});
test('should error when path points to a file', async () => {
const testPath = '/some/file.txt';
vi.mocked(path.isAbsolute).mockReturnValue(true);
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => false } as Awaited<ReturnType<typeof fs.stat>>);
const result = await toolHandler({ path: testPath });
expect(result.isError).toBe(true);
const text = (result.content[0] as { type: 'text'; text: string }).text;
expect(JSON.parse(text).errorMessage).toContain('not a directory');
});
test('should list directory contents with [FILE]/[DIR] prefixes', async () => {
const testPath = '/some/dir';
vi.mocked(path.isAbsolute).mockReturnValue(true);
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as Awaited<ReturnType<typeof fs.stat>>);
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'a.ts', isFile: () => true, isDirectory: () => false },
{ name: 'subdir', isFile: () => false, isDirectory: () => true },
{ name: 'b.md', isFile: () => true, isDirectory: () => false },
] as unknown as Awaited<ReturnType<typeof fs.readdir>>);
const result = await toolHandler({ path: testPath });
expect(result.isError).toBeUndefined();
const text = (result.content[0] as { type: 'text'; text: string }).text;
const parsed = JSON.parse(text);
expect(parsed.contents).toEqual(['[FILE] a.ts', '[DIR] subdir', '[FILE] b.md']);
expect(parsed.fileCount).toBe(2);
expect(parsed.directoryCount).toBe(1);
expect(parsed.totalItems).toBe(3);
});
test('should report empty directory placeholder', async () => {
const testPath = '/empty/dir';
vi.mocked(path.isAbsolute).mockReturnValue(true);
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as Awaited<ReturnType<typeof fs.stat>>);
vi.mocked(fs.readdir).mockResolvedValue([] as unknown as Awaited<ReturnType<typeof fs.readdir>>);
const result = await toolHandler({ path: testPath });
const text = (result.content[0] as { type: 'text'; text: string }).text;
const parsed = JSON.parse(text);
expect(parsed.contents).toEqual(['(empty directory)']);
expect(parsed.totalItems).toBe(0);
});
test('should report readdir failures via the outer catch', async () => {
const testPath = '/some/dir';
vi.mocked(path.isAbsolute).mockReturnValue(true);
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as Awaited<ReturnType<typeof fs.stat>>);
vi.mocked(fs.readdir).mockRejectedValue(new Error('EACCES'));
const result = await toolHandler({ path: testPath });
expect(result.isError).toBe(true);
const text = (result.content[0] as { type: 'text'; text: string }).text;
expect(JSON.parse(text).errorMessage).toContain('EACCES');
});
});