Files
wehub-resource-sync 0d3cb498a3
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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

322 lines
10 KiB
TypeScript

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();
}
});
});
});