Skip to main content
Glama
NorthSeacoder

Frontend Test Generation & Code Review MCP Server

test-generation-workflow.ts12.9 kB
/** * TestGenerationWorkflowTool - 一键式测试生成工作流 * * 整合完整的测试生成流程: * 1. 获取 diff 和项目配置 * 2. 分析测试矩阵 * 3. 生成测试用例 * 4. 写入测试文件 * 5. 运行测试 * 6. (可选)自动修复失败的测试 */ import { z } from 'zod'; import { BaseTool } from '../core/base-tool.js'; import type { ToolMetadata } from '../core/base-tool.js'; import { FetchDiffFromRepoTool } from './fetch-diff-from-repo.js'; import { AnalyzeTestMatrixWorkerTool } from './analyze-test-matrix-worker.js'; import { GenerateTestsWorkerTool } from './generate-tests-worker.js'; import { WriteTestFileTool } from './write-test-file.js'; import { RunTestsTool } from './run-tests.js'; import { FixFailingTestsTool } from './fix-failing-tests.js'; import { getAppContext } from '../core/app-context.js'; import { logger } from '../utils/logger.js'; import type { TestMatrix } from '../schemas/test-matrix.js'; import type { TestCase } from '../schemas/test-plan.js'; import type { TestFix } from '../agents/test-fix-agent.js'; import type { ProjectConfig } from '../orchestrator/project-detector.js'; const ArgsSchema = z.object({ repoUrl: z.string().describe('Git 仓库 URL 或本地路径'), branch: z.string().describe('要分析的分支'), baselineBranch: z.string().optional().describe('对比基准分支(默认 origin/HEAD)'), scenarios: z.array(z.string()).optional().describe('要生成的测试场景列表'), autoFix: z.boolean().optional().default(false).describe('是否自动修复失败的测试'), maxFixAttempts: z.number().optional().default(3).describe('最大修复尝试次数'), maxTests: z.number().optional().describe('最大测试用例数量'), workDir: z.string().optional().describe('可选:指定工作目录'), }); type WorkflowArgs = z.infer<typeof ArgsSchema>; interface TestRunResult { success: boolean; framework: string; summary: { total: number; passed: number; failed: number; skipped: number; duration: number; }; stdout: string; stderr: string; exitCode: number; } interface WorkflowOutput { workspaceId: string; projectConfig: ProjectConfig; matrix: TestMatrix; tests: TestCase[]; filesWritten: string[]; testResults: TestRunResult; fixes?: TestFix[]; steps: { step: string; duration: number; success: boolean; error?: string; }[]; totalDuration: number; } export class TestGenerationWorkflowTool extends BaseTool<WorkflowArgs, WorkflowOutput> { getMetadata(): ToolMetadata { return { name: 'test-generation-workflow', description: `一键式测试生成工作流。 此工具整合了完整的测试生成流程,从获取代码变更到生成测试、运行测试,可选自动修复失败测试。 **流程步骤**: 1. 获取 diff 和项目配置(fetch-diff-from-repo) 2. 分析测试矩阵(analyze-test-matrix-worker) 3. 生成测试用例(generate-tests-worker) 4. 写入测试文件(write-test-file) 5. 运行测试(run-tests) 6. 自动修复失败测试(fix-failing-tests,可选) **参数**: - repoUrl: Git 仓库 URL 或本地路径 - branch: 要分析的分支 - baselineBranch: 对比基准分支(可选) - scenarios: 要生成的测试场景(可选,如 ['happy-path', 'edge-case']) - autoFix: 是否自动修复失败的测试(默认 false) - maxFixAttempts: 最大修复尝试次数(默认 3) - maxTests: 最大测试用例数量(可选) - workDir: 指定工作目录(可选) **返回**: - workspaceId: 工作区 ID - projectConfig: 项目配置 - matrix: 测试矩阵 - tests: 生成的测试用例 - filesWritten: 写入的测试文件列表 - testResults: 测试运行结果 - fixes: 应用的修复(如果启用了自动修复) - steps: 各步骤执行信息 - totalDuration: 总耗时(毫秒)`, inputSchema: {}, }; } getZodSchema() { return ArgsSchema; } async executeImpl(args: WorkflowArgs): Promise<WorkflowOutput> { const startTime = Date.now(); const steps: WorkflowOutput['steps'] = []; const recordStep = (step: string, startedAt: number, success: boolean, error?: string) => { steps.push({ step, duration: Date.now() - startedAt, success, error, }); }; logger.info('[TestGenerationWorkflow] Starting workflow', { repoUrl: args.repoUrl, branch: args.branch, autoFix: args.autoFix, }); const context = getAppContext(); const { openai, embedding, state, contextStore, workspaceManager } = context; if (!openai || !embedding || !state || !contextStore) { throw new Error('Required services not initialized'); } if (!workspaceManager) { throw new Error('WorkspaceManager not initialized'); } let workspaceId = ''; let projectConfig: ProjectConfig | undefined; let matrix: TestMatrix | undefined; let tests: TestCase[] = []; let filesWritten: string[] = []; let testResults: TestRunResult | undefined; let fixes: TestFix[] | undefined; let diff = ''; // Step 1: Fetch diff & project config const step1Start = Date.now(); try { const fetchDiffTool = new FetchDiffFromRepoTool(); const fetchResult = await fetchDiffTool.execute({ repoUrl: args.repoUrl, branch: args.branch, baselineBranch: args.baselineBranch, workDir: args.workDir, }); if (!fetchResult.success || !fetchResult.data) { throw new Error(fetchResult.error || 'Failed to fetch diff'); } workspaceId = fetchResult.data.workspaceId; projectConfig = fetchResult.data.projectConfig; diff = fetchResult.data.diff; recordStep('fetch-diff', step1Start, true); logger.info('[TestGenerationWorkflow] Step 1 completed', { workspaceId, diffLength: diff.length, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); recordStep('fetch-diff', step1Start, false, message); throw new Error(`Step [fetch-diff] failed: ${message}`); } if (!workspaceId || !projectConfig) { throw new Error('Workspace or project config is not available after fetch step'); } const workspace = workspaceManager.getWorkspace(workspaceId); if (!workspace) { throw new Error(`Workspace not found: ${workspaceId}`); } const projectRoot = workspace.workDir || projectConfig.projectRoot; // Step 2: Analyze test matrix const step2Start = Date.now(); try { const analyzeTool = new AnalyzeTestMatrixWorkerTool(openai); const analyzeResult = await analyzeTool.execute({ workspaceId, diff, projectConfig: projectConfig as any, }); if (!analyzeResult.success || !analyzeResult.data) { throw new Error(analyzeResult.error || 'Failed to analyze test matrix'); } matrix = analyzeResult.data; recordStep('analyze-matrix', step2Start, true); logger.info('[TestGenerationWorkflow] Step 2 completed', { featuresCount: matrix.features?.length || 0, scenariosCount: matrix.scenarios?.length || 0, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); recordStep('analyze-matrix', step2Start, false, message); throw new Error(`Step [analyze-matrix] failed: ${message}`); } if (!matrix) { throw new Error('Test matrix not generated'); } // Step 3: Generate tests const step3Start = Date.now(); try { const generateTool = new GenerateTestsWorkerTool(openai, embedding, state, contextStore); const generateResult = await generateTool.execute({ workspaceId, diff, matrix, projectConfig: projectConfig as any, scenarios: args.scenarios, maxTests: args.maxTests, }); if (!generateResult.success || !generateResult.data) { throw new Error(generateResult.error || 'Failed to generate tests'); } tests = generateResult.data; recordStep('generate-tests', step3Start, true); logger.info('[TestGenerationWorkflow] Step 3 completed', { testsCount: tests.length, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); recordStep('generate-tests', step3Start, false, message); throw new Error(`Step [generate-tests] failed: ${message}`); } // Step 4: Write test files (skip if没有测试生成) if (tests.length > 0) { const step4Start = Date.now(); try { const writeTool = new WriteTestFileTool(); const writeResult = await writeTool.execute({ tests, projectRoot, overwrite: true, }); if (!writeResult.success || !writeResult.data) { throw new Error(writeResult.error || 'Failed to write test files'); } filesWritten = writeResult.data.filesWritten; recordStep('write-files', step4Start, true); logger.info('[TestGenerationWorkflow] Step 4 completed', { filesWritten: filesWritten.length, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); recordStep('write-files', step4Start, false, message); throw new Error(`Step [write-files] failed: ${message}`); } } else { logger.warn('[TestGenerationWorkflow] No tests generated, skipping write & run steps'); recordStep('write-files', Date.now(), true); } // Step 5: Run tests(当写入成功或原本就有测试文件时) if (tests.length > 0) { const step5Start = Date.now(); try { const runTestsTool = new RunTestsTool(); const runResult = await runTestsTool.execute({ projectRoot, workspaceId, framework: (projectConfig.testFramework as 'vitest' | 'jest') || 'vitest', testFiles: filesWritten, }); if (!runResult.success || !runResult.data) { throw new Error(runResult.error || 'Failed to run tests'); } testResults = runResult.data; recordStep('run-tests', step5Start, true); logger.info('[TestGenerationWorkflow] Step 5 completed', { total: testResults.summary.total, passed: testResults.summary.passed, failed: testResults.summary.failed, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); recordStep('run-tests', step5Start, false, message); throw new Error(`Step [run-tests] failed: ${message}`); } } else { // 若没有生成测试,则构造一个默认的测试结果 testResults = { success: true, framework: (projectConfig.testFramework as 'vitest' | 'jest') || 'vitest', summary: { total: 0, passed: 0, failed: 0, skipped: 0, duration: 0, }, stdout: 'No tests were generated, so no test run was executed.', stderr: '', exitCode: 0, }; recordStep('run-tests', Date.now(), true); } // Step 6: Auto fix failing tests (optional) if (args.autoFix && testResults && testResults.summary.failed > 0) { const step6Start = Date.now(); try { const fixTool = new FixFailingTestsTool(); const fixResult = await fixTool.execute({ workspaceId, projectRoot, testResults, maxAttempts: args.maxFixAttempts, }); if (fixResult.success && fixResult.data) { fixes = fixResult.data.fixes; if (fixResult.data.retriedResults) { testResults = fixResult.data.retriedResults; } } recordStep('fix-tests', step6Start, fixResult.success, fixResult.error ?? undefined); logger.info('[TestGenerationWorkflow] Step 6 completed', { fixesApplied: fixes?.length || 0, finalFailed: testResults.summary.failed, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); recordStep('fix-tests', step6Start, false, message); logger.warn('[TestGenerationWorkflow] Step 6 failed', { error: message }); } } const totalDuration = Date.now() - startTime; logger.info('[TestGenerationWorkflow] Workflow completed successfully', { totalDuration, testsGenerated: tests.length, testsPassed: testResults?.summary.passed || 0, testsFailed: testResults?.summary.failed || 0, }); return { workspaceId, projectConfig, matrix, tests, filesWritten, testResults, fixes, steps, totalDuration, }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NorthSeacoder/fe-testgen-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server