Skip to main content
Glama
NorthSeacoder

Frontend Test Generation & Code Review MCP Server

generate-gen-test-rule.ts6.83 kB
/** * GenerateGenTestRuleTool - 生成 gen-test MCP 规则草稿 * * 在当前工作区内生成 `.cursor/rules/test-strategy.md`,结合可探测信息自动填充, * 其余字段提供默认值与 TODO 注释,提醒用户自行校正。 */ import { z } from 'zod'; import { readFile, writeFile, mkdir } from 'fs/promises'; import { dirname, resolve } from 'path'; import { existsSync } from 'fs'; import { fileURLToPath } from 'url'; import { BaseTool } from '../core/base-tool.js'; import type { ToolMetadata } from '../core/base-tool.js'; import { getAppContext } from '../core/app-context.js'; import { logger } from '../utils/logger.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ArgsSchema = z.object({ workspaceId: z.string().describe('工作区 ID'), outputPath: z .string() .optional() .default('.cursor/rules/test-strategy.md') .describe('输出路径(相对于项目根目录)'), }); type GenerateGenTestRuleArgs = z.infer<typeof ArgsSchema>; interface GenerateGenTestRuleOutput { filePath: string; content: string; note: string; } export class GenerateGenTestRuleTool extends BaseTool<GenerateGenTestRuleArgs, GenerateGenTestRuleOutput> { getMetadata(): ToolMetadata { return { name: 'generate-gen-test-rule', description: `生成 gen-test MCP 规则草稿文件(默认 .cursor/rules/test-strategy.md),在 Cursor 中可通过「帮我配置下 gen-test mcp」快捷触发。 此工具会自动写入可探测的信息(仓库/分支、monorepo 类型、测试框架、测试模式等),其余字段提供默认值与 TODO 注释,提示用户手动调整。 **参数**: - workspaceId: 工作区 ID(从 fetch-diff-from-repo 获取) - outputPath: 输出路径(默认 .cursor/rules/test-strategy.md) **返回**: - filePath: 生成的文件路径 - content: 文件内容 - note: 提醒用户检查并补充 TODO 片段`, inputSchema: {}, }; } getZodSchema() { return ArgsSchema; } async executeImpl(args: GenerateGenTestRuleArgs): Promise<GenerateGenTestRuleOutput> { const { workspaceId, outputPath } = args; const context = getAppContext(); const { workspaceManager, projectDetector } = context; logger.info('[GenerateGenTestRule] Generating gen-test rules', { workspaceId, outputPath }); if (!workspaceManager) { throw new Error('WorkspaceManager not initialized'); } if (!projectDetector) { throw new Error('ProjectDetector not initialized'); } const workspace = workspaceManager.getWorkspace(workspaceId); if (!workspace) { throw new Error(`Workspace not found: ${workspaceId}`); } const projectConfig = await projectDetector.detectProject( workspace.workDir, workspace.packageRoot, [] ); logger.info('[GenerateGenTestRule] Project config retrieved', { isMonorepo: projectConfig.isMonorepo, testFramework: projectConfig.testFramework, packageRoot: projectConfig.packageRoot || workspace.packageRoot, }); const templatePath = resolve(__dirname, '../../docs/gen-test-rule-template.md'); let template: string; try { template = await readFile(templatePath, 'utf-8'); } catch (error) { logger.error('[GenerateGenTestRule] Failed to read template', { error }); throw new Error(`Failed to read gen-test rule template: ${templatePath}`); } const content = this.populateTemplate(template, { workspace, projectConfig, }); const absoluteOutputPath = resolve(workspace.workDir, outputPath); const outputDir = dirname(absoluteOutputPath); try { if (!existsSync(outputDir)) { await mkdir(outputDir, { recursive: true }); } await writeFile(absoluteOutputPath, content, 'utf-8'); logger.info('[GenerateGenTestRule] Rule file generated', { filePath: absoluteOutputPath, workspaceId, }); return { filePath: outputPath, content, note: '规则为自动生成草稿,请按照文件中的 TODO 与注释手动校正后再使用。', }; } catch (error) { logger.error('[GenerateGenTestRule] Failed to write file', { error }); throw new Error(`Failed to write gen-test rule file: ${absoluteOutputPath}`); } } /** * 填充模板变量 */ private populateTemplate( template: string, data: { workspace: any; projectConfig: any; } ): string { const { workspace, projectConfig } = data; const replacements: Record<string, string> = { '{{PROJECT_NAME}}': this.extractProjectName(workspace.repoUrl), '{{GENERATED_AT}}': new Date().toISOString(), '{{REPO_URL}}': workspace.repoUrl || 'N/A', '{{BRANCH}}': workspace.sourceBranch || workspace.branch || 'N/A', '{{BASELINE_BRANCH}}': workspace.baselineBranch || 'main', '{{PROJECT_TYPE}}': projectConfig.isMonorepo ? 'Monorepo' : '单仓库', '{{MONOREPO_TYPE}}': projectConfig.monorepoType || 'N/A', '{{PACKAGE_ROOT}}': projectConfig.packageRoot || workspace.packageRoot || projectConfig.projectRoot || '(填写主要子项目根目录)', '{{AFFECTED_SUBPROJECTS}}': this.formatList(projectConfig.affectedSubProjects || workspace.affectedSubProjects), '{{TESTABLE_SUBPROJECTS}}': this.formatList(projectConfig.testableSubProjects || workspace.testableSubProjects), '{{TEST_FRAMEWORK}}': projectConfig.testFramework || 'vitest', '{{TEST_PATTERN}}': projectConfig.testPattern || '**/*.{test,spec}.{ts,tsx,js,jsx}', '{{HAS_EXISTING_TESTS}}': projectConfig.hasExistingTests ? '是' : '否', '{{MAX_TESTS}}': process.env.MAX_TESTS || '10', '{{MAX_FIX_ATTEMPTS}}': process.env.FIX_MAX_ATTEMPTS || '3', '{{FIX_CONFIDENCE_THRESHOLD}}': process.env.FIX_CONFIDENCE_THRESHOLD || '0.5', }; let result = template; for (const [key, value] of Object.entries(replacements)) { result = result.replace(new RegExp(key, 'g'), value); } return result; } /** * 从仓库 URL 提取项目名称 */ private extractProjectName(repoUrl: string): string { if (!repoUrl) { return 'Unknown Project'; } if (!repoUrl.startsWith('http') && !repoUrl.startsWith('git@')) { const parts = repoUrl.split('/'); return parts[parts.length - 1] || 'Local Project'; } const match = repoUrl.match(/\/([^\/]+?)(\.git)?$/); if (match && match[1]) { return match[1]; } return 'Unknown Project'; } /** * 格式化列表 */ private formatList(items?: string[]): string { if (!items || items.length === 0) { return ' - 无'; } return items.map((item) => ` - ${item}`).join('\n'); } }

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