chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user