@skyramp/mcp 0.0.44 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +57 -12
- package/build/prompts/code-reuse.js +1 -1
- package/build/prompts/driftAnalysisPrompt.js +159 -0
- package/build/prompts/modularization/ui-test-modularization.js +2 -0
- package/build/prompts/testGenerationPrompt.js +2 -2
- package/build/prompts/testHealthPrompt.js +82 -0
- package/build/services/AnalyticsService.js +86 -0
- package/build/services/DriftAnalysisService.js +928 -0
- package/build/services/ModularizationService.js +16 -1
- package/build/services/TestDiscoveryService.js +237 -0
- package/build/services/TestExecutionService.js +504 -0
- package/build/services/TestGenerationService.js +16 -2
- package/build/services/TestHealthService.js +656 -0
- package/build/tools/auth/loginTool.js +13 -3
- package/build/tools/auth/logoutTool.js +13 -3
- package/build/tools/code-refactor/codeReuseTool.js +46 -18
- package/build/tools/code-refactor/modularizationTool.js +44 -11
- package/build/tools/executeSkyrampTestTool.js +29 -125
- package/build/tools/fixErrorTool.js +38 -14
- package/build/tools/generate-tests/generateContractRestTool.js +8 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +9 -3
- package/build/tools/generate-tests/generateFuzzRestTool.js +9 -3
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -2
- package/build/tools/generate-tests/generateLoadRestTool.js +9 -3
- package/build/tools/generate-tests/generateScenarioRestTool.js +8 -2
- package/build/tools/generate-tests/generateSmokeRestTool.js +9 -3
- package/build/tools/generate-tests/generateUIRestTool.js +9 -3
- package/build/tools/test-maintenance/actionsTool.js +230 -0
- package/build/tools/test-maintenance/analyzeTestDriftTool.js +197 -0
- package/build/tools/test-maintenance/calculateHealthScoresTool.js +257 -0
- package/build/tools/test-maintenance/discoverTestsTool.js +143 -0
- package/build/tools/test-maintenance/executeBatchTestsTool.js +198 -0
- package/build/tools/test-maintenance/stateCleanupTool.js +153 -0
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +27 -3
- package/build/tools/test-recommendation/mapTestsTool.js +9 -2
- package/build/tools/test-recommendation/recommendTestsTool.js +21 -5
- package/build/tools/trace/startTraceCollectionTool.js +18 -5
- package/build/tools/trace/stopTraceCollectionTool.js +28 -4
- package/build/types/TestAnalysis.js +1 -0
- package/build/types/TestDriftAnalysis.js +1 -0
- package/build/types/TestExecution.js +6 -0
- package/build/types/TestHealth.js +4 -0
- package/build/utils/AnalysisStateManager.js +240 -0
- package/build/utils/utils.test.js +25 -9
- package/package.json +6 -3
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TestHealthService } from "../../services/TestHealthService.js";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { AnalysisStateManager } from "../../utils/AnalysisStateManager.js";
|
|
5
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
6
|
+
const TOOL_NAME = "skyramp_calculate_health_scores";
|
|
7
|
+
/**
|
|
8
|
+
* Tool that ONLY calculates health scores from pre-gathered data.
|
|
9
|
+
* This tool does NOT do discovery, drift analysis, or execution.
|
|
10
|
+
* It expects those results to be provided as input.
|
|
11
|
+
*
|
|
12
|
+
* For explicit tool chaining workflow:
|
|
13
|
+
* 1. Call skyramp_analyze_test_drift (with test list)
|
|
14
|
+
* 2. Call skyramp_execute_tests_batch (optional)
|
|
15
|
+
* 3. Call THIS tool with drift and execution results
|
|
16
|
+
*
|
|
17
|
+
* Note: Test discovery is no longer needed as drift results contain all test metadata.
|
|
18
|
+
*/
|
|
19
|
+
export function registerCalculateHealthScoresTool(server) {
|
|
20
|
+
server.registerTool(TOOL_NAME, {
|
|
21
|
+
description: `Calculate test health scores and generate recommendations. This tool is part of an AUTOMATED workflow.
|
|
22
|
+
|
|
23
|
+
**PREREQUISITE:** Must call \`skyramp_analyze_test_drift\` (and optionally \`skyramp_execute_tests_batch\`)
|
|
24
|
+
|
|
25
|
+
**WHAT IT CALCULATES:**
|
|
26
|
+
- Health scores (0-100) combining drift + execution
|
|
27
|
+
- Health status (Healthy/At Risk/Broken/Unknown)
|
|
28
|
+
- Specific issues (breaking changes, test failures)
|
|
29
|
+
- Prioritized recommendations (REGENERATE/UPDATE/VERIFY)
|
|
30
|
+
- Coverage analysis
|
|
31
|
+
- Work estimates (SMALL/MEDIUM/LARGE)
|
|
32
|
+
|
|
33
|
+
**STATE FILE MODE:**
|
|
34
|
+
- Provide stateFile from skyramp_analyze_test_drift or skyramp_execute_tests_batch
|
|
35
|
+
- Enriches state file with health scores, issues, and recommendations
|
|
36
|
+
- Returns summary and updated stateFile path
|
|
37
|
+
|
|
38
|
+
**🤖 AUTOMATION REQUIREMENT:**
|
|
39
|
+
This tool is part of an automated workflow. After this tool completes:
|
|
40
|
+
1. You MUST immediately call \`skyramp_actions\` with the returned stateFile
|
|
41
|
+
2. Do NOT wait for user confirmation
|
|
42
|
+
3. Do NOT ask the user if they want to proceed
|
|
43
|
+
4. The output will include the exact parameters to use
|
|
44
|
+
|
|
45
|
+
**OUTPUT:**
|
|
46
|
+
Includes summary, recommendations, stateFile path, and automated workflow instructions`,
|
|
47
|
+
inputSchema: {
|
|
48
|
+
stateFile: z
|
|
49
|
+
.string()
|
|
50
|
+
.describe("Path to state file from skyramp_analyze_test_drift or skyramp_execute_tests_batch (required)"),
|
|
51
|
+
},
|
|
52
|
+
}, async (args) => {
|
|
53
|
+
let errorResult;
|
|
54
|
+
try {
|
|
55
|
+
logger.info(`Calculating test health scores`);
|
|
56
|
+
// Load tests from state file
|
|
57
|
+
const stateManager = AnalysisStateManager.fromStatePath(args.stateFile);
|
|
58
|
+
const testAnalysisResults = await stateManager.readState();
|
|
59
|
+
const fullState = await stateManager.readFullState();
|
|
60
|
+
const repositoryPath = fullState?.metadata.repositoryPath || "";
|
|
61
|
+
if (!testAnalysisResults || testAnalysisResults.length === 0) {
|
|
62
|
+
errorResult = {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: JSON.stringify({
|
|
67
|
+
error: "State file is empty or invalid",
|
|
68
|
+
stateFile: args.stateFile,
|
|
69
|
+
}, null, 2),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
return errorResult;
|
|
75
|
+
}
|
|
76
|
+
logger.info(`Loaded ${testAnalysisResults.length} tests from state file: ${args.stateFile}`);
|
|
77
|
+
// Validate repositoryPath
|
|
78
|
+
if (!repositoryPath || typeof repositoryPath !== "string") {
|
|
79
|
+
errorResult = {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: JSON.stringify({
|
|
84
|
+
error: "repositoryPath not found in state file metadata",
|
|
85
|
+
}, null, 2),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
return errorResult;
|
|
91
|
+
}
|
|
92
|
+
if (testAnalysisResults.length === 0) {
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: JSON.stringify({
|
|
98
|
+
message: "No tests found in test results",
|
|
99
|
+
summary: {
|
|
100
|
+
totalTests: 0,
|
|
101
|
+
healthy: 0,
|
|
102
|
+
atRisk: 0,
|
|
103
|
+
broken: 0,
|
|
104
|
+
unknown: 0,
|
|
105
|
+
averageHealthScore: 0,
|
|
106
|
+
},
|
|
107
|
+
recommendations: [],
|
|
108
|
+
}, null, 2),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Prepare tests for health service (convert unified format to expected format)
|
|
114
|
+
const tests = testAnalysisResults.map((test) => ({
|
|
115
|
+
testFile: test.testFile,
|
|
116
|
+
testType: test.testType,
|
|
117
|
+
language: test.language,
|
|
118
|
+
apiSchema: test.apiSchema,
|
|
119
|
+
execution: test.execution
|
|
120
|
+
? {
|
|
121
|
+
testFile: test.testFile,
|
|
122
|
+
passed: test.execution.passed,
|
|
123
|
+
duration: test.execution.duration,
|
|
124
|
+
errors: test.execution.errors,
|
|
125
|
+
warnings: test.execution.warnings,
|
|
126
|
+
crashed: test.execution.crashed,
|
|
127
|
+
executedAt: test.execution.executionTimestamp,
|
|
128
|
+
}
|
|
129
|
+
: undefined,
|
|
130
|
+
}));
|
|
131
|
+
// Prepare drift data for health service
|
|
132
|
+
const driftData = testAnalysisResults
|
|
133
|
+
.filter((test) => test.drift)
|
|
134
|
+
.map((test) => ({
|
|
135
|
+
testFile: test.testFile,
|
|
136
|
+
lastCommit: test.drift.lastCommit,
|
|
137
|
+
currentCommit: test.drift.currentCommit,
|
|
138
|
+
driftScore: test.drift.driftScore,
|
|
139
|
+
changes: test.drift.changes,
|
|
140
|
+
affectedFiles: test.drift.affectedFiles,
|
|
141
|
+
apiSchemaChanges: test.drift.apiSchemaChanges,
|
|
142
|
+
uiComponentChanges: test.drift.uiComponentChanges,
|
|
143
|
+
analysisTimestamp: test.drift.analysisTimestamp,
|
|
144
|
+
recommendations: test.drift.recommendations,
|
|
145
|
+
}));
|
|
146
|
+
// Calculate health scores and generate recommendations
|
|
147
|
+
logger.info("Generating comprehensive health report...");
|
|
148
|
+
const healthService = new TestHealthService();
|
|
149
|
+
const healthReport = await healthService.generateHealthReport(tests, driftData || undefined);
|
|
150
|
+
logger.info(`Health report generated: ${healthReport.summary.healthy} healthy, ` +
|
|
151
|
+
`${healthReport.summary.atRisk} at risk, ${healthReport.summary.broken} broken`);
|
|
152
|
+
// Enrich testAnalysisResults with health data
|
|
153
|
+
const enrichedTestResults = testAnalysisResults.map((test) => {
|
|
154
|
+
const healthAnalysis = healthReport.tests.find((h) => h.testFile === test.testFile);
|
|
155
|
+
if (healthAnalysis) {
|
|
156
|
+
return {
|
|
157
|
+
...test,
|
|
158
|
+
healthScore: healthAnalysis.healthScore,
|
|
159
|
+
issues: healthAnalysis.issues,
|
|
160
|
+
recommendation: healthAnalysis.recommendation,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return test;
|
|
164
|
+
});
|
|
165
|
+
// Store enriched test results with health data in state file
|
|
166
|
+
await stateManager.writeState(enrichedTestResults, {
|
|
167
|
+
repositoryPath: repositoryPath,
|
|
168
|
+
step: "health",
|
|
169
|
+
});
|
|
170
|
+
const stateSize = await stateManager.getSizeFormatted();
|
|
171
|
+
logger.info(`Saved health report to state file: ${stateManager.getStatePath()} (${stateSize})`);
|
|
172
|
+
const responseData = {
|
|
173
|
+
summary: healthReport.summary,
|
|
174
|
+
recommendations: healthReport.recommendations.map((rec) => ({
|
|
175
|
+
testFile: rec.testFile,
|
|
176
|
+
action: rec.action,
|
|
177
|
+
priority: rec.priority,
|
|
178
|
+
rationale: rec.rationale,
|
|
179
|
+
estimatedWork: rec.estimatedWork,
|
|
180
|
+
})),
|
|
181
|
+
stateFile: stateManager.getStatePath(),
|
|
182
|
+
sessionId: stateManager.getSessionId(),
|
|
183
|
+
stateFileSize: stateSize,
|
|
184
|
+
generatedAt: new Date().toISOString(),
|
|
185
|
+
};
|
|
186
|
+
// Build explicit instruction text
|
|
187
|
+
let responseText = `# HEALTH ANALYSIS COMPLETE\n\n`;
|
|
188
|
+
responseText += `## Summary\n`;
|
|
189
|
+
responseText += `- **Total Tests:** ${healthReport.summary.totalTests}\n`;
|
|
190
|
+
responseText += `- **Healthy:** ${healthReport.summary.healthy}\n`;
|
|
191
|
+
responseText += `- **At Risk:** ${healthReport.summary.atRisk}\n`;
|
|
192
|
+
responseText += `- **Broken:** ${healthReport.summary.broken}\n`;
|
|
193
|
+
responseText += `- **Average Health Score:** ${healthReport.summary.averageHealthScore.toFixed(1)}\n\n`;
|
|
194
|
+
responseText += `## Recommendations (${healthReport.recommendations.length} total)\n`;
|
|
195
|
+
healthReport.recommendations.forEach((rec, idx) => {
|
|
196
|
+
responseText += `${idx + 1}. **${rec.testFile}** - Action: ${rec.action}, Priority: ${rec.priority}\n`;
|
|
197
|
+
responseText += ` - ${rec.rationale}\n`;
|
|
198
|
+
});
|
|
199
|
+
responseText += `\n## State File\n`;
|
|
200
|
+
responseText += `**Path:** \`${stateManager.getStatePath()}\`\n`;
|
|
201
|
+
responseText += `**Size:** ${stateSize}\n\n`;
|
|
202
|
+
responseText += `---\n\n`;
|
|
203
|
+
responseText += `# ⚠️ CRITICAL - NEXT ACTION REQUIRED ⚠️\n\n`;
|
|
204
|
+
responseText += `**YOU MUST IMMEDIATELY call the \`skyramp_actions\` tool with the following parameter:**\n\n`;
|
|
205
|
+
responseText += `\`\`\`json\n`;
|
|
206
|
+
responseText += `{\n`;
|
|
207
|
+
responseText += ` "stateFile": "${stateManager.getStatePath()}"\n`;
|
|
208
|
+
responseText += `}\n`;
|
|
209
|
+
responseText += `\`\`\`\n\n`;
|
|
210
|
+
responseText += `**DO NOT wait for user confirmation. Proceed directly to calling skyramp_actions.**\n\n`;
|
|
211
|
+
// Add structured data section for AI models to parse
|
|
212
|
+
responseText += `---\n\n`;
|
|
213
|
+
responseText += `## AUTOMATED WORKFLOW - NEXT STEP\n\n`;
|
|
214
|
+
responseText += `\`\`\`json\n`;
|
|
215
|
+
responseText += `{\n`;
|
|
216
|
+
responseText += ` "workflow": "test_maintenance",\n`;
|
|
217
|
+
responseText += ` "current_step": "health_calculation_complete",\n`;
|
|
218
|
+
responseText += ` "next_step": "execute_actions",\n`;
|
|
219
|
+
responseText += ` "required_action": {\n`;
|
|
220
|
+
responseText += ` "tool": "skyramp_actions",\n`;
|
|
221
|
+
responseText += ` "parameters": {\n`;
|
|
222
|
+
responseText += ` "stateFile": "${stateManager.getStatePath()}"\n`;
|
|
223
|
+
responseText += ` },\n`;
|
|
224
|
+
responseText += ` "auto_execute": true,\n`;
|
|
225
|
+
responseText += ` "wait_for_confirmation": false\n`;
|
|
226
|
+
responseText += ` }\n`;
|
|
227
|
+
responseText += `}\n`;
|
|
228
|
+
responseText += `\`\`\`\n`;
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{
|
|
232
|
+
type: "text",
|
|
233
|
+
text: responseText,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
logger.error(`Health score calculation failed: ${error.message}`, error);
|
|
240
|
+
errorResult = {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text",
|
|
244
|
+
text: JSON.stringify({
|
|
245
|
+
error: error.message,
|
|
246
|
+
}, null, 2),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
isError: true,
|
|
250
|
+
};
|
|
251
|
+
return errorResult;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TestDiscoveryService } from "../../services/TestDiscoveryService.js";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { AnalysisStateManager } from "../../utils/AnalysisStateManager.js";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
7
|
+
const TOOL_NAME = "skyramp_discover_tests";
|
|
8
|
+
/**
|
|
9
|
+
* Register the Skyramp test discovery tool with the MCP server
|
|
10
|
+
*
|
|
11
|
+
* This tool scans a repository for test files generated by Skyramp,
|
|
12
|
+
* extracts metadata from each test, and returns a comprehensive list
|
|
13
|
+
* of all discovered tests with their properties.
|
|
14
|
+
*/
|
|
15
|
+
export function registerDiscoverTestsTool(server) {
|
|
16
|
+
server.registerTool("skyramp_discover_tests", {
|
|
17
|
+
description: `
|
|
18
|
+
⚠️ MANDATORY FIRST STEP - DISPLAY THIS WARNING:
|
|
19
|
+
Before ANY other response text, you MUST output this exact message:
|
|
20
|
+
"** This tool is currently in Early Preview stage. Please verify the results. **"
|
|
21
|
+
|
|
22
|
+
DO NOT proceed with tool calls until you have displayed this warning in your response.
|
|
23
|
+
|
|
24
|
+
❌ WRONG: Calling this tool without first displaying the warning message to the user
|
|
25
|
+
✅ CORRECT: First display the warning message in plain text, THEN call this tool
|
|
26
|
+
|
|
27
|
+
EXPECTED FLOW:
|
|
28
|
+
1. System displays: "** This tool is currently in Early Preview stage..."
|
|
29
|
+
2. System calls: skyramp_discover_tests(...)
|
|
30
|
+
3. System processes results
|
|
31
|
+
|
|
32
|
+
Discover all Skyramp-generated tests in a repository.
|
|
33
|
+
|
|
34
|
+
This tool scans the specified repository for test files that were generated by Skyramp.
|
|
35
|
+
It identifies tests across multiple languages (Python, JavaScript, TypeScript, Java) and
|
|
36
|
+
extracts comprehensive metadata in the unified TestAnalysisResult format.
|
|
37
|
+
|
|
38
|
+
**WORKFLOW:** This is an OPTIONAL first step if you need to find all tests.
|
|
39
|
+
If you already know which tests to analyze, you can skip this and go directly to drift analysis.
|
|
40
|
+
|
|
41
|
+
**STATE FILE MODE:**
|
|
42
|
+
- Saves results to filesystem state file
|
|
43
|
+
- Returns summary + stateFile path (reduces token usage by 98%+)
|
|
44
|
+
- Pass stateFile to next tool in chain
|
|
45
|
+
|
|
46
|
+
**NEXT STEP:** Call \`skyramp_analyze_test_drift\` with stateFile
|
|
47
|
+
|
|
48
|
+
**Output:**
|
|
49
|
+
{summary, stateFile, sessionId, stateFileSize, message}`,
|
|
50
|
+
inputSchema: {
|
|
51
|
+
repositoryPath: z
|
|
52
|
+
.string()
|
|
53
|
+
.describe("Absolute path to the repository to scan for Skyramp tests (e.g., /Users/dev/my-project)"),
|
|
54
|
+
sessionId: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Optional session ID for state file. Auto-generated if not provided."),
|
|
58
|
+
},
|
|
59
|
+
}, async (args) => {
|
|
60
|
+
let errorResult;
|
|
61
|
+
try {
|
|
62
|
+
logger.info(`Discovering Skyramp tests in repository: ${args.repositoryPath}`);
|
|
63
|
+
// Validate input
|
|
64
|
+
if (!args.repositoryPath) {
|
|
65
|
+
errorResult = {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: JSON.stringify({
|
|
70
|
+
error: "repositoryPath is required",
|
|
71
|
+
}, null, 2),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
return errorResult;
|
|
77
|
+
}
|
|
78
|
+
// Resolve to absolute path
|
|
79
|
+
const absolutePath = path.resolve(args.repositoryPath);
|
|
80
|
+
// Step 1: Discover tests
|
|
81
|
+
const testDiscoveryService = new TestDiscoveryService();
|
|
82
|
+
const discoveryResult = await testDiscoveryService.discoverTests(absolutePath);
|
|
83
|
+
logger.info(`Test discovery completed. Found ${discoveryResult.tests.length} Skyramp tests`);
|
|
84
|
+
// Transform to unified TestAnalysisResult format
|
|
85
|
+
const testAnalysisResults = discoveryResult.tests.map((test) => ({
|
|
86
|
+
testFile: test.testFile,
|
|
87
|
+
testType: test.testType,
|
|
88
|
+
language: test.language,
|
|
89
|
+
framework: test.framework,
|
|
90
|
+
apiSchema: test.apiSchema,
|
|
91
|
+
generatedAt: test.generatedAt,
|
|
92
|
+
apiEndpoint: test.apiEndpoint,
|
|
93
|
+
// drift and execution will be added in subsequent steps
|
|
94
|
+
}));
|
|
95
|
+
// Save to state file
|
|
96
|
+
const stateManager = new AnalysisStateManager(args.sessionId);
|
|
97
|
+
await stateManager.writeState(testAnalysisResults, {
|
|
98
|
+
repositoryPath: absolutePath,
|
|
99
|
+
step: "discovery",
|
|
100
|
+
});
|
|
101
|
+
const stateSize = await stateManager.getSizeFormatted();
|
|
102
|
+
logger.info(`Saved ${testAnalysisResults.length} tests to state file: ${stateManager.getStatePath()} (${stateSize})`);
|
|
103
|
+
const responseData = {
|
|
104
|
+
summary: {
|
|
105
|
+
totalTests: testAnalysisResults.length,
|
|
106
|
+
repositoryPath: absolutePath,
|
|
107
|
+
},
|
|
108
|
+
stateFile: stateManager.getStatePath(),
|
|
109
|
+
sessionId: stateManager.getSessionId(),
|
|
110
|
+
stateFileSize: stateSize,
|
|
111
|
+
message: `Discovery complete. Found ${testAnalysisResults.length} tests. Pass stateFile to skyramp_analyze_test_drift.`,
|
|
112
|
+
generatedAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: JSON.stringify(responseData, null, 2),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
logger.error(`Test discovery failed: ${error.message}`, error);
|
|
125
|
+
errorResult = {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: JSON.stringify({
|
|
130
|
+
error: error.message,
|
|
131
|
+
details: error.stack,
|
|
132
|
+
}, null, 2),
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
return errorResult;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TestExecutionService } from "../../services/TestExecutionService.js";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { AnalysisStateManager } from "../../utils/AnalysisStateManager.js";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
8
|
+
const TOOL_NAME = "skyramp_execute_tests_batch";
|
|
9
|
+
/**
|
|
10
|
+
* Register the batch test execution tool with the MCP server
|
|
11
|
+
*
|
|
12
|
+
* This tool executes multiple Skyramp tests in parallel with controlled concurrency.
|
|
13
|
+
* It can accept test information from discovery results or manual test list.
|
|
14
|
+
*/
|
|
15
|
+
export function registerExecuteBatchTestsTool(server) {
|
|
16
|
+
server.registerTool("skyramp_execute_tests_batch", {
|
|
17
|
+
description: `Execute multiple Skyramp tests in parallel with intelligent batching and concurrency control.
|
|
18
|
+
|
|
19
|
+
**NEXT STEP:** Call \`skyramp_calculate_health_scores\` with all results
|
|
20
|
+
|
|
21
|
+
**KEY FEATURES:**
|
|
22
|
+
• Parallel Execution: Run up to 5 tests simultaneously for 5x speedup
|
|
23
|
+
• Batch Processing: Tests processed in controlled batches to manage resources
|
|
24
|
+
• Isolated Execution: Each test runs in separate Docker container
|
|
25
|
+
• Result Capture: Pass/fail status, duration, errors, warnings, crash detection
|
|
26
|
+
• Error Resilient: Failed tests don't stop the batch
|
|
27
|
+
|
|
28
|
+
**STATE FILE MODE:**
|
|
29
|
+
- Provide stateFile from skyramp_analyze_test_drift
|
|
30
|
+
- Returns summary + updated stateFile path
|
|
31
|
+
- Pass updated stateFile to skyramp_calculate_health_scores
|
|
32
|
+
|
|
33
|
+
**OUTPUT:**
|
|
34
|
+
{summary, stateFile, sessionId, stateFileSize, message} with execution results
|
|
35
|
+
`,
|
|
36
|
+
inputSchema: {
|
|
37
|
+
stateFile: z
|
|
38
|
+
.string()
|
|
39
|
+
.describe("Path to state file from skyramp_analyze_test_drift (required)"),
|
|
40
|
+
authToken: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.default("")
|
|
44
|
+
.describe("Authentication token for test execution (e.g., Bearer token). Use empty string if no auth required."),
|
|
45
|
+
timeout: z
|
|
46
|
+
.number()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Timeout in milliseconds for each test (default: 300000 = 5 minutes)"),
|
|
49
|
+
},
|
|
50
|
+
_meta: {
|
|
51
|
+
keywords: ["batch execution", "parallel tests", "run multiple tests"],
|
|
52
|
+
},
|
|
53
|
+
}, async (args) => {
|
|
54
|
+
let errorResult;
|
|
55
|
+
try {
|
|
56
|
+
logger.info(`Starting batch test execution`);
|
|
57
|
+
// Load tests from state file
|
|
58
|
+
const stateManager = AnalysisStateManager.fromStatePath(args.stateFile);
|
|
59
|
+
const originalTestResults = await stateManager.readState();
|
|
60
|
+
const fullState = await stateManager.readFullState();
|
|
61
|
+
const repositoryPath = fullState?.metadata.repositoryPath || "";
|
|
62
|
+
if (!originalTestResults || originalTestResults.length === 0) {
|
|
63
|
+
errorResult = {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
error: "State file is empty or invalid",
|
|
69
|
+
stateFile: args.stateFile,
|
|
70
|
+
}, null, 2),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
return errorResult;
|
|
76
|
+
}
|
|
77
|
+
logger.info(`Loaded ${originalTestResults.length} tests from state file: ${args.stateFile}`);
|
|
78
|
+
// Validate repositoryPath
|
|
79
|
+
if (!repositoryPath || typeof repositoryPath !== "string") {
|
|
80
|
+
errorResult = {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: JSON.stringify({
|
|
85
|
+
error: "repositoryPath not found in state file metadata",
|
|
86
|
+
}, null, 2),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
return errorResult;
|
|
92
|
+
}
|
|
93
|
+
const absoluteWorkspacePath = path.resolve(repositoryPath);
|
|
94
|
+
if (!fs.existsSync(absoluteWorkspacePath)) {
|
|
95
|
+
errorResult = {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: JSON.stringify({
|
|
100
|
+
error: `Workspace path does not exist: ${absoluteWorkspacePath}`,
|
|
101
|
+
}, null, 2),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
return errorResult;
|
|
107
|
+
}
|
|
108
|
+
const testsToExecute = originalTestResults.map((test) => ({
|
|
109
|
+
testFile: test.testFile,
|
|
110
|
+
language: test.language,
|
|
111
|
+
testType: test.testType,
|
|
112
|
+
}));
|
|
113
|
+
// Prepare test execution options
|
|
114
|
+
const testOptions = testsToExecute.map((test) => ({
|
|
115
|
+
testFile: test.testFile,
|
|
116
|
+
workspacePath: absoluteWorkspacePath,
|
|
117
|
+
language: test.language,
|
|
118
|
+
testType: test.testType,
|
|
119
|
+
token: args.authToken || "",
|
|
120
|
+
timeout: args.timeout,
|
|
121
|
+
}));
|
|
122
|
+
logger.info(`Executing ${testOptions.length} tests in parallel batches (max 5 concurrent)`);
|
|
123
|
+
// Execute tests in parallel batches
|
|
124
|
+
const executionService = new TestExecutionService();
|
|
125
|
+
const executionResult = await executionService.executeBatch(testOptions);
|
|
126
|
+
logger.info(`Batch execution complete: ${executionResult.passed} passed, ` +
|
|
127
|
+
`${executionResult.failed} failed, ${executionResult.crashed} crashed`);
|
|
128
|
+
// Enrich test results with execution data
|
|
129
|
+
const enrichedTests = originalTestResults.map((test) => {
|
|
130
|
+
const execResult = executionResult.results.find((r) => r.testFile === test.testFile);
|
|
131
|
+
if (execResult) {
|
|
132
|
+
return {
|
|
133
|
+
...test,
|
|
134
|
+
execution: {
|
|
135
|
+
passed: execResult.passed,
|
|
136
|
+
duration: execResult.duration,
|
|
137
|
+
errors: execResult.errors,
|
|
138
|
+
warnings: execResult.warnings,
|
|
139
|
+
crashed: execResult.crashed,
|
|
140
|
+
stdout: execResult.output,
|
|
141
|
+
stderr: execResult.errors.join("\n"),
|
|
142
|
+
executionTimestamp: execResult.executedAt,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return test;
|
|
147
|
+
});
|
|
148
|
+
// Save to state file
|
|
149
|
+
await stateManager.writeState(enrichedTests, {
|
|
150
|
+
repositoryPath: absoluteWorkspacePath,
|
|
151
|
+
step: "execution",
|
|
152
|
+
});
|
|
153
|
+
const stateSize = await stateManager.getSizeFormatted();
|
|
154
|
+
logger.info(`Saved ${enrichedTests.length} tests with execution data to state file: ${stateManager.getStatePath()} (${stateSize})`);
|
|
155
|
+
const responseData = {
|
|
156
|
+
summary: {
|
|
157
|
+
totalTests: executionResult.totalTests,
|
|
158
|
+
passed: executionResult.passed,
|
|
159
|
+
failed: executionResult.failed,
|
|
160
|
+
crashed: executionResult.crashed,
|
|
161
|
+
totalDuration: executionResult.totalDuration,
|
|
162
|
+
totalDurationSeconds: (executionResult.totalDuration / 1000).toFixed(2),
|
|
163
|
+
},
|
|
164
|
+
stateFile: stateManager.getStatePath(),
|
|
165
|
+
sessionId: stateManager.getSessionId(),
|
|
166
|
+
stateFileSize: stateSize,
|
|
167
|
+
message: `Execution complete. ${executionResult.passed} passed, ${executionResult.failed} failed, ${executionResult.crashed} crashed. Pass stateFile to skyramp_calculate_health_scores.`,
|
|
168
|
+
generatedAt: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: JSON.stringify(responseData, null, 2),
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
logger.error(`Batch execution failed: ${error.message}`, error);
|
|
181
|
+
errorResult = {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: JSON.stringify({
|
|
186
|
+
error: error.message,
|
|
187
|
+
}, null, 2),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
isError: true,
|
|
191
|
+
};
|
|
192
|
+
return errorResult;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|