chore: import upstream snapshot with attribution
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) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
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) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
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) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
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) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mockProcessEnv } from '../../util/utils';
|
||||
|
||||
const { mockRandomUUID } = vi.hoisted(() => ({
|
||||
mockRandomUUID: vi.fn(() => 'secure-mcp-session-id'),
|
||||
}));
|
||||
|
||||
vi.mock('node:crypto', () => ({
|
||||
randomUUID: mockRandomUUID,
|
||||
}));
|
||||
|
||||
const expressMocks = vi.hoisted(() => {
|
||||
const close = vi.fn((callback: (error?: Error) => void) => callback());
|
||||
const listen = vi.fn((_port: number, callback: () => void) => {
|
||||
callback();
|
||||
return { close };
|
||||
});
|
||||
const app = {
|
||||
use: vi.fn(),
|
||||
post: vi.fn(),
|
||||
get: vi.fn(),
|
||||
listen,
|
||||
};
|
||||
const json = vi.fn();
|
||||
const express = Object.assign(
|
||||
vi.fn(() => app),
|
||||
{ json },
|
||||
);
|
||||
return { app, close, express, json, listen };
|
||||
});
|
||||
|
||||
vi.mock('express', () => ({
|
||||
default: expressMocks.express,
|
||||
}));
|
||||
|
||||
// Mock dependencies before importing the module
|
||||
vi.mock('../../../src/logger', () => ({
|
||||
default: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
transports: [],
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/telemetry', () => ({
|
||||
default: {
|
||||
record: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock all tool registrations
|
||||
vi.mock('../../../src/commands/mcp/tools/listEvaluations', () => ({
|
||||
registerListEvaluationsTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/getEvaluationDetails', () => ({
|
||||
registerGetEvaluationDetailsTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/validatePromptfooConfig', () => ({
|
||||
registerValidatePromptfooConfigTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/testProvider', () => ({
|
||||
registerTestProviderTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/runAssertion', () => ({
|
||||
registerRunAssertionTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/runEvaluation', () => ({
|
||||
registerRunEvaluationTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/shareEvaluation', () => ({
|
||||
registerShareEvaluationTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/generateDataset', () => ({
|
||||
registerGenerateDatasetTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/generateTestCases', () => ({
|
||||
registerGenerateTestCasesTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/compareProviders', () => ({
|
||||
registerCompareProvidersTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/redteamRun', () => ({
|
||||
registerRedteamRunTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/tools/redteamGenerate', () => ({
|
||||
registerRedteamGenerateTool: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/mcp/resources', () => ({
|
||||
registerResources: vi.fn(),
|
||||
}));
|
||||
|
||||
// Store the McpServer constructor calls using vi.hoisted
|
||||
const mcpServerMocks = vi.hoisted(() => {
|
||||
const mcpServerCalls: Array<{ name: string; version: string; description?: string }> = [];
|
||||
|
||||
// Create a mock class that can be instantiated with `new`
|
||||
const mockMcpServerImplementation = function MockMcpServer(
|
||||
this: {
|
||||
connect: ReturnType<typeof vi.fn>;
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
tool: ReturnType<typeof vi.fn>;
|
||||
resource: ReturnType<typeof vi.fn>;
|
||||
},
|
||||
config: { name: string; version: string; description?: string },
|
||||
) {
|
||||
mcpServerCalls.push(config);
|
||||
this.connect = vi.fn();
|
||||
this.close = vi.fn().mockResolvedValue(undefined);
|
||||
this.tool = vi.fn();
|
||||
this.resource = vi.fn();
|
||||
};
|
||||
const MockMcpServer = vi.fn(mockMcpServerImplementation);
|
||||
|
||||
return { mcpServerCalls, MockMcpServer, mockMcpServerImplementation };
|
||||
});
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
|
||||
McpServer: mcpServerMocks.MockMcpServer,
|
||||
}));
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
||||
StdioServerTransport: vi.fn().mockImplementation(() => ({})),
|
||||
}));
|
||||
|
||||
const streamableHttpMocks = vi.hoisted(() => ({
|
||||
constructor: vi.fn().mockImplementation(function MockStreamableHTTPServerTransport() {
|
||||
return {
|
||||
handleRequest: vi.fn(),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@modelcontextprotocol/sdk/server/streamableHttp.js', () => ({
|
||||
StreamableHTTPServerTransport: streamableHttpMocks.constructor,
|
||||
}));
|
||||
|
||||
const { mcpServerCalls } = mcpServerMocks;
|
||||
|
||||
describe('MCP Server', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mcpServerMocks.MockMcpServer.mockReset().mockImplementation(
|
||||
mcpServerMocks.mockMcpServerImplementation,
|
||||
);
|
||||
streamableHttpMocks.constructor.mockClear();
|
||||
mockRandomUUID.mockClear();
|
||||
mcpServerCalls.length = 0;
|
||||
});
|
||||
|
||||
describe('createMcpServer', () => {
|
||||
it('should create server with correct name and version', async () => {
|
||||
const { createMcpServer } = await import('../../../src/commands/mcp/server');
|
||||
await createMcpServer();
|
||||
|
||||
expect(mcpServerCalls.length).toBe(1);
|
||||
expect(mcpServerCalls[0].name).toBe('Promptfoo MCP');
|
||||
expect(mcpServerCalls[0].version).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('should create server with a description field', async () => {
|
||||
const { createMcpServer } = await import('../../../src/commands/mcp/server');
|
||||
await createMcpServer();
|
||||
|
||||
expect(mcpServerCalls.length).toBe(1);
|
||||
expect(mcpServerCalls[0]).toHaveProperty('description');
|
||||
expect(typeof mcpServerCalls[0].description).toBe('string');
|
||||
});
|
||||
|
||||
it('should have a meaningful description that describes server capabilities', async () => {
|
||||
const { createMcpServer } = await import('../../../src/commands/mcp/server');
|
||||
await createMcpServer();
|
||||
|
||||
expect(mcpServerCalls.length).toBe(1);
|
||||
const description = mcpServerCalls[0].description;
|
||||
|
||||
// Verify the description contains key information about the server
|
||||
expect(description).toBeDefined();
|
||||
expect(description!.length).toBeGreaterThan(50); // Should be descriptive
|
||||
expect(description).toContain('MCP server');
|
||||
expect(description).toContain('evaluation');
|
||||
});
|
||||
|
||||
it('should include security testing capabilities in description', async () => {
|
||||
const { createMcpServer } = await import('../../../src/commands/mcp/server');
|
||||
await createMcpServer();
|
||||
|
||||
expect(mcpServerCalls.length).toBe(1);
|
||||
const description = mcpServerCalls[0].description;
|
||||
|
||||
// The description should mention security/red teaming capabilities
|
||||
expect(description).toMatch(/red team|security/i);
|
||||
});
|
||||
|
||||
it('should register all expected tools', async () => {
|
||||
const { createMcpServer } = await import('../../../src/commands/mcp/server');
|
||||
const { registerListEvaluationsTool } = await import(
|
||||
'../../../src/commands/mcp/tools/listEvaluations'
|
||||
);
|
||||
const { registerGetEvaluationDetailsTool } = await import(
|
||||
'../../../src/commands/mcp/tools/getEvaluationDetails'
|
||||
);
|
||||
const { registerValidatePromptfooConfigTool } = await import(
|
||||
'../../../src/commands/mcp/tools/validatePromptfooConfig'
|
||||
);
|
||||
const { registerTestProviderTool } = await import(
|
||||
'../../../src/commands/mcp/tools/testProvider'
|
||||
);
|
||||
const { registerRunAssertionTool } = await import(
|
||||
'../../../src/commands/mcp/tools/runAssertion'
|
||||
);
|
||||
const { registerRunEvaluationTool } = await import(
|
||||
'../../../src/commands/mcp/tools/runEvaluation'
|
||||
);
|
||||
const { registerShareEvaluationTool } = await import(
|
||||
'../../../src/commands/mcp/tools/shareEvaluation'
|
||||
);
|
||||
const { registerGenerateDatasetTool } = await import(
|
||||
'../../../src/commands/mcp/tools/generateDataset'
|
||||
);
|
||||
const { registerGenerateTestCasesTool } = await import(
|
||||
'../../../src/commands/mcp/tools/generateTestCases'
|
||||
);
|
||||
const { registerCompareProvidersTool } = await import(
|
||||
'../../../src/commands/mcp/tools/compareProviders'
|
||||
);
|
||||
const { registerRedteamRunTool } = await import('../../../src/commands/mcp/tools/redteamRun');
|
||||
const { registerRedteamGenerateTool } = await import(
|
||||
'../../../src/commands/mcp/tools/redteamGenerate'
|
||||
);
|
||||
const { registerResources } = await import('../../../src/commands/mcp/resources');
|
||||
|
||||
await createMcpServer();
|
||||
|
||||
// Core evaluation tools
|
||||
expect(registerListEvaluationsTool).toHaveBeenCalled();
|
||||
expect(registerGetEvaluationDetailsTool).toHaveBeenCalled();
|
||||
expect(registerValidatePromptfooConfigTool).toHaveBeenCalled();
|
||||
expect(registerTestProviderTool).toHaveBeenCalled();
|
||||
expect(registerRunAssertionTool).toHaveBeenCalled();
|
||||
expect(registerRunEvaluationTool).toHaveBeenCalled();
|
||||
expect(registerShareEvaluationTool).toHaveBeenCalled();
|
||||
|
||||
// Generation tools
|
||||
expect(registerGenerateDatasetTool).toHaveBeenCalled();
|
||||
expect(registerGenerateTestCasesTool).toHaveBeenCalled();
|
||||
expect(registerCompareProvidersTool).toHaveBeenCalled();
|
||||
|
||||
// Redteam tools
|
||||
expect(registerRedteamRunTool).toHaveBeenCalled();
|
||||
expect(registerRedteamGenerateTool).toHaveBeenCalled();
|
||||
|
||||
// Resources
|
||||
expect(registerResources).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should track telemetry for MCP server creation', async () => {
|
||||
const { createMcpServer } = await import('../../../src/commands/mcp/server');
|
||||
const telemetry = await import('../../../src/telemetry');
|
||||
|
||||
await createMcpServer();
|
||||
|
||||
expect(telemetry.default.record).toHaveBeenCalledWith('feature_used', {
|
||||
feature: 'mcp_server',
|
||||
transport: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('startHttpMcpServer', () => {
|
||||
it('creates cryptographically random session identifiers', async () => {
|
||||
const restoreEnv = mockProcessEnv({ MCP_TRANSPORT: undefined });
|
||||
let shutdown: (() => void) | undefined;
|
||||
vi.spyOn(process, 'once').mockImplementation(((event: string, listener: () => void) => {
|
||||
if (event === 'SIGINT') {
|
||||
shutdown = listener;
|
||||
}
|
||||
return process;
|
||||
}) as typeof process.once);
|
||||
|
||||
let serverPromise: Promise<void> | undefined;
|
||||
try {
|
||||
const { startHttpMcpServer } = await import('../../../src/commands/mcp/server');
|
||||
serverPromise = startHttpMcpServer(3100);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(streamableHttpMocks.constructor).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
const transportOptions = streamableHttpMocks.constructor.mock.calls[0][0] as {
|
||||
sessionIdGenerator: () => string;
|
||||
};
|
||||
expect(transportOptions.sessionIdGenerator()).toBe('secure-mcp-session-id');
|
||||
expect(mockRandomUUID).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
if (shutdown) {
|
||||
shutdown();
|
||||
await serverPromise;
|
||||
}
|
||||
restoreEnv();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user