UNPKG

@skyramp/mcp

Version:

Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution

238 lines (237 loc) 8.98 kB
import * as fs from "fs"; import * as path from "path"; import { simpleGit } from "simple-git"; import { logger } from "../utils/logger.js"; import fg from "fast-glob"; export class TestDiscoveryService { EXCLUDED_DIRS = [ "node_modules", "venv", ".venv", "build", "dist", ".git", "__pycache__", ".pytest_cache", "coverage", ".next", "out", "target", ]; SKYRAMP_MARKER = "Generated by Skyramp"; // Supported test file extensions SUPPORTED_EXTENSIONS = [".py", ".js", ".ts", ".java"]; // Concurrency control for parallel operations MAX_CONCURRENT_OPERATIONS = 10; // Cache git client and repo status per repository gitClientCache = new Map(); isGitRepoCache = new Map(); /** * Discover all Skyramp tests in a repository * Uses fast-glob for cross-platform file scanning */ async discoverTests(repositoryPath) { logger.info(`Starting test discovery in: ${repositoryPath}`); if (!fs.existsSync(repositoryPath)) { throw new Error(`Repository path does not exist: ${repositoryPath}`); } const stats = fs.statSync(repositoryPath); if (!stats.isDirectory()) { throw new Error(`Path is not a directory: ${repositoryPath}`); } // Initialize git client cache for this repository await this.initializeGitClient(repositoryPath); // Use cross-platform file search to find files containing Skyramp marker const testFiles = this.findSkyrampTestsWithGrep(repositoryPath); logger.info(`Found ${testFiles.length} Skyramp test files`); // Process files in parallel with concurrency control const skyrampTests = await this.processFilesInBatches(testFiles, repositoryPath); logger.info(`Discovered ${skyrampTests.length} Skyramp tests`); // Clean up cache to free memory this.gitClientCache.clear(); this.isGitRepoCache.clear(); return { tests: skyrampTests, }; } /** * Initialize git client and check if repository is a git repo */ async initializeGitClient(repositoryPath) { try { const git = simpleGit(repositoryPath); this.gitClientCache.set(repositoryPath, git); const isRepo = await git.checkIsRepo(); this.isGitRepoCache.set(repositoryPath, isRepo); if (isRepo) { logger.debug(`Git repository detected at: ${repositoryPath}`); } else { logger.debug(`Not a git repository: ${repositoryPath}`); } } catch (error) { logger.debug(`Could not initialize git client: ${error.message}`); this.isGitRepoCache.set(repositoryPath, false); } } /** * Process test files in parallel batches with concurrency control */ async processFilesInBatches(testFiles, repositoryPath) { const results = []; // Process files in batches to control concurrency for (let i = 0; i < testFiles.length; i += this.MAX_CONCURRENT_OPERATIONS) { const batch = testFiles.slice(i, i + this.MAX_CONCURRENT_OPERATIONS); const batchResults = await Promise.all(batch.map(async (testFile) => { try { return await this.extractTestMetadata(testFile, repositoryPath); } catch (error) { logger.error(`Error processing test file ${testFile}: ${error}`); return null; } })); // Filter out null results and add to final results results.push(...batchResults.filter((test) => test !== null)); } return results; } /** * Find files containing Skyramp marker using cross-platform Node.js file search */ findSkyrampTestsWithGrep(repositoryPath) { try { // Build glob patterns for supported extensions const globPatterns = this.SUPPORTED_EXTENSIONS.map((ext) => `**/*${ext}`); // Build ignore patterns for excluded directories const ignorePatterns = this.EXCLUDED_DIRS.map((dir) => `**/${dir}/**`); // Use fast-glob to find all matching files const allFiles = fg.sync(globPatterns, { cwd: repositoryPath, ignore: ignorePatterns, absolute: true, caseSensitiveMatch: false, }); logger.debug(`Found ${allFiles.length} candidate files to search`); // Read files and check for Skyramp marker const matchingFiles = []; const marker = this.SKYRAMP_MARKER; // Process files in batches to avoid memory issues const batchSize = 100; for (let i = 0; i < allFiles.length; i += batchSize) { const batch = allFiles.slice(i, i + batchSize); const batchResults = batch.filter((file) => { try { // Read file and check for marker const content = fs.readFileSync(file, "utf-8"); return content.includes(marker); } catch (error) { // Skip files that can't be read (permissions, etc.) logger.debug(`Skipping file ${file}: ${error}`); return false; } }); matchingFiles.push(...batchResults); } logger.debug(`Found ${matchingFiles.length} files with Skyramp marker`); return matchingFiles; } catch (error) { logger.error(`File search failed: ${error.message}`); logger.info("Falling back to directory scanning method"); return []; } } /** * Extract metadata from a test file * File is already confirmed to contain Skyramp marker by file search */ async extractTestMetadata(testFile, repositoryPath) { let content; try { // Read full file (file search already confirmed it has Skyramp marker) content = fs.readFileSync(testFile, "utf-8"); } catch (error) { logger.debug(`Could not read file ${testFile}: ${error}`); return null; } const language = this.detectLanguage(testFile); const testType = this.detectTestType(content, testFile); const apiSchema = this.extractApiSchema(content); const framework = this.extractFramework(content); return { testFile, testType, language, framework, apiSchema, }; } /** * Detect programming language from file extension */ detectLanguage(testFile) { const ext = path.extname(testFile); const languageMap = { ".py": "python", ".js": "javascript", ".ts": "typescript", ".java": "java", }; return languageMap[ext] || "unknown"; } /** * Detect test type from file content and name * Checks Skyramp command line, filename, and content patterns */ detectTestType(content, testFile) { const testTypes = [ "smoke", "integration", "load", "contract", "fuzz", "e2e", "ui", ]; // First, try to extract from Skyramp command line // Pattern: skyramp generate smoke rest ... const commandMatch = content.match(/skyramp generate (\w+)/i); if (commandMatch && commandMatch[1]) { const type = commandMatch[1].toLowerCase(); if (testTypes.includes(type)) { return type; } } return ""; } /** * Extract API schema path from test content * Looks for Skyramp command line first, then fallback to other patterns */ extractApiSchema(content) { // First, try to extract from Skyramp command line // Pattern: --api-schema http://localhost:8000/openapi.json const apiSchemaMatch = content.match(/--api-schema\s+([^\s\\]+)/); if (apiSchemaMatch && apiSchemaMatch[1]) { return apiSchemaMatch[1]; } return ""; } /** * Extract Framework from test content * Looks for Skyramp command line first, then fallback to other patterns */ extractFramework(content) { // First, try to extract from Skyramp command line // Pattern: --framework pytest const frameworkMatch = content.match(/--framework\s+([^\s\\]+)/); if (frameworkMatch && frameworkMatch[1]) { return frameworkMatch[1]; } return ""; } }