0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
import { EventEmitter } from 'events';
|
|
import type { ChildProcess } from 'child_process';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { mockProcessEnv } from '../../util/utils';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
spawn: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('child_process', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('child_process')>();
|
|
return {
|
|
...actual,
|
|
spawn: mocks.spawn,
|
|
};
|
|
});
|
|
|
|
import {
|
|
startFilesystemMcpServer,
|
|
waitForFilesystemMcpServerReady,
|
|
} from '../../../src/codeScan/mcp/filesystem';
|
|
|
|
class FakeChildProcess extends EventEmitter {
|
|
exitCode: number | null = null;
|
|
killed = false;
|
|
kill = vi.fn();
|
|
pid = 1234;
|
|
signalCode: NodeJS.Signals | null = null;
|
|
stderr = new EventEmitter();
|
|
}
|
|
|
|
function createFakeProcess(): ChildProcess & { stderr: EventEmitter } {
|
|
return new FakeChildProcess() as unknown as ChildProcess & { stderr: EventEmitter };
|
|
}
|
|
|
|
describe('filesystem MCP server management', () => {
|
|
const originalEnv = { ...process.env };
|
|
let restoreEnv: () => void;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.resetAllMocks();
|
|
restoreEnv = mockProcessEnv(originalEnv, { clear: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreEnv();
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('strips npm before config when spawning the filesystem MCP server', () => {
|
|
const restoreNpmConfig = mockProcessEnv({
|
|
NPM_CONFIG_BEFORE: '2026-03-29T00:00:00.000Z',
|
|
npm_config_before: '2026-03-29T00:00:00.000Z',
|
|
});
|
|
try {
|
|
mocks.spawn.mockReturnValue(createFakeProcess());
|
|
|
|
startFilesystemMcpServer(process.cwd());
|
|
|
|
const spawnOptions = mocks.spawn.mock.calls[0]?.[2];
|
|
expect(spawnOptions?.env?.NPM_CONFIG_BEFORE).toBeUndefined();
|
|
expect(spawnOptions?.env?.npm_config_before).toBeUndefined();
|
|
} finally {
|
|
restoreNpmConfig();
|
|
}
|
|
});
|
|
|
|
it('resolves when the filesystem MCP server prints its ready marker', async () => {
|
|
const mcpProcess = createFakeProcess();
|
|
const ready = waitForFilesystemMcpServerReady(mcpProcess);
|
|
|
|
mcpProcess.stderr.emit('data', Buffer.from('Secure MCP Filesystem Server running on stdio\n'));
|
|
|
|
await expect(ready).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('resolves when the ready marker is split across stderr chunks', async () => {
|
|
const mcpProcess = createFakeProcess();
|
|
const ready = waitForFilesystemMcpServerReady(mcpProcess);
|
|
|
|
mcpProcess.stderr.emit('data', Buffer.from('Secure MCP Filesystem Server '));
|
|
mcpProcess.stderr.emit('data', Buffer.from('running on stdio\n'));
|
|
|
|
await expect(ready).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('rejects when the filesystem MCP server exits before it is ready', async () => {
|
|
const mcpProcess = createFakeProcess();
|
|
const ready = waitForFilesystemMcpServerReady(mcpProcess);
|
|
|
|
mcpProcess.emit('exit', 1, null);
|
|
|
|
await expect(ready).rejects.toThrow('Filesystem MCP server exited before ready: code 1');
|
|
});
|
|
|
|
it('rejects immediately when the process has already exited', async () => {
|
|
const mcpProcess = createFakeProcess();
|
|
Object.defineProperty(mcpProcess, 'exitCode', { value: 1 });
|
|
|
|
await expect(waitForFilesystemMcpServerReady(mcpProcess)).rejects.toThrow(
|
|
'Filesystem MCP server exited before ready: code 1',
|
|
);
|
|
});
|
|
|
|
it('rejects immediately when the process was already killed', async () => {
|
|
const mcpProcess = createFakeProcess();
|
|
Object.defineProperty(mcpProcess, 'killed', { value: true });
|
|
|
|
await expect(waitForFilesystemMcpServerReady(mcpProcess)).rejects.toThrow(
|
|
'Filesystem MCP server exited before ready: unknown reason',
|
|
);
|
|
});
|
|
|
|
it('rejects when stderr is unavailable', async () => {
|
|
const mcpProcess = createFakeProcess();
|
|
Object.defineProperty(mcpProcess, 'stderr', { value: null });
|
|
|
|
await expect(waitForFilesystemMcpServerReady(mcpProcess)).rejects.toThrow(
|
|
'Filesystem MCP server stderr pipe unavailable',
|
|
);
|
|
});
|
|
|
|
it('rejects when the filesystem MCP server readiness times out', async () => {
|
|
const mcpProcess = createFakeProcess();
|
|
const ready = waitForFilesystemMcpServerReady(mcpProcess, 1000);
|
|
const expectation = expect(ready).rejects.toThrow(
|
|
'Timed out waiting for filesystem MCP server to be ready after 1000ms',
|
|
);
|
|
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
await expectation;
|
|
});
|
|
});
|