@skyramp/mcp
Version:
Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution
238 lines (237 loc) • 8.98 kB
JavaScript
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 "";
}
}