@skyramp/mcp 0.0.44 → 0.0.45
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 +15 -0
- 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/DriftAnalysisService.js +924 -0
- package/build/services/ModularizationService.js +16 -1
- package/build/services/TestDiscoveryService.js +237 -0
- package/build/services/TestExecutionService.js +311 -0
- package/build/services/TestGenerationService.js +16 -2
- package/build/services/TestHealthService.js +653 -0
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +5 -3
- package/build/tools/code-refactor/modularizationTool.js +8 -2
- package/build/tools/executeSkyrampTestTool.js +12 -122
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
- package/build/tools/generate-tests/generateFuzzRestTool.js +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
- package/build/tools/generate-tests/generateSmokeRestTool.js +1 -1
- package/build/tools/generate-tests/generateUIRestTool.js +1 -1
- package/build/tools/test-maintenance/actionsTool.js +202 -0
- package/build/tools/test-maintenance/analyzeTestDriftTool.js +188 -0
- package/build/tools/test-maintenance/calculateHealthScoresTool.js +248 -0
- package/build/tools/test-maintenance/discoverTestsTool.js +135 -0
- package/build/tools/test-maintenance/executeBatchTestsTool.js +188 -0
- package/build/tools/test-maintenance/stateCleanupTool.js +145 -0
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +16 -1
- package/build/tools/test-recommendation/recommendTestsTool.js +6 -2
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- 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 +238 -0
- package/build/utils/utils.test.js +25 -9
- package/package.json +6 -3
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
export class TestHealthService {
|
|
4
|
+
/**
|
|
5
|
+
* Generate comprehensive health report for tests
|
|
6
|
+
*/
|
|
7
|
+
async generateHealthReport(tests, driftResults) {
|
|
8
|
+
logger.info(`Generating health report for ${tests.length} tests`);
|
|
9
|
+
const healthAnalyses = [];
|
|
10
|
+
const recommendations = [];
|
|
11
|
+
// Analyze each test
|
|
12
|
+
for (const test of tests) {
|
|
13
|
+
const driftData = driftResults?.find((d) => d.testFile === test.testFile);
|
|
14
|
+
const analysis = await this.analyzeTestHealth(test.testFile, test.execution, driftData, test.apiSchema);
|
|
15
|
+
healthAnalyses.push(analysis);
|
|
16
|
+
recommendations.push(analysis.recommendation);
|
|
17
|
+
}
|
|
18
|
+
// Calculate summary statistics
|
|
19
|
+
const summary = {
|
|
20
|
+
totalTests: tests.length,
|
|
21
|
+
healthy: healthAnalyses.filter((a) => a.healthScore.status === "healthy")
|
|
22
|
+
.length,
|
|
23
|
+
atRisk: healthAnalyses.filter((a) => a.healthScore.status === "at_risk")
|
|
24
|
+
.length,
|
|
25
|
+
broken: healthAnalyses.filter((a) => a.healthScore.status === "broken")
|
|
26
|
+
.length,
|
|
27
|
+
unknown: healthAnalyses.filter((a) => a.healthScore.status === "unknown")
|
|
28
|
+
.length,
|
|
29
|
+
averageHealthScore: healthAnalyses.length > 0
|
|
30
|
+
? Math.round(healthAnalyses.reduce((sum, a) => sum + a.healthScore.overall, 0) / healthAnalyses.length)
|
|
31
|
+
: 0,
|
|
32
|
+
};
|
|
33
|
+
// Analyze coverage if API schemas are available
|
|
34
|
+
let coverage;
|
|
35
|
+
const apiSchemas = tests
|
|
36
|
+
.map((t) => t.apiSchema)
|
|
37
|
+
.filter((s) => s);
|
|
38
|
+
if (apiSchemas.length > 0) {
|
|
39
|
+
try {
|
|
40
|
+
coverage = await this.analyzeCoverage(tests, apiSchemas[0]);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger.error(`Failed to analyze coverage: ${error.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
summary,
|
|
48
|
+
tests: healthAnalyses,
|
|
49
|
+
recommendations: this.prioritizeRecommendations(recommendations),
|
|
50
|
+
coverage,
|
|
51
|
+
generatedAt: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Analyze health of a single test
|
|
56
|
+
*/
|
|
57
|
+
async analyzeTestHealth(testFile, execution, drift, apiSchema) {
|
|
58
|
+
// Calculate execution score
|
|
59
|
+
const executionScore = execution
|
|
60
|
+
? this.calculateExecutionScore(execution)
|
|
61
|
+
: undefined;
|
|
62
|
+
// Calculate health score
|
|
63
|
+
const healthScore = this.calculateHealthScore(executionScore?.score, drift?.driftScore);
|
|
64
|
+
// Identify issues
|
|
65
|
+
const issues = this.identifyIssues(execution, drift);
|
|
66
|
+
// Extract API endpoint info from test
|
|
67
|
+
const apiEndpoint = apiSchema
|
|
68
|
+
? await this.extractEndpointFromTest(testFile, apiSchema)
|
|
69
|
+
: undefined;
|
|
70
|
+
// Generate recommendation
|
|
71
|
+
const recommendation = this.generateRecommendation(testFile, healthScore, drift?.driftScore, execution, issues, apiEndpoint);
|
|
72
|
+
return {
|
|
73
|
+
testFile,
|
|
74
|
+
healthScore,
|
|
75
|
+
issues,
|
|
76
|
+
recommendation,
|
|
77
|
+
executionData: execution
|
|
78
|
+
? {
|
|
79
|
+
passed: execution.passed,
|
|
80
|
+
duration: execution.duration,
|
|
81
|
+
errors: execution.errors,
|
|
82
|
+
warnings: execution.warnings,
|
|
83
|
+
}
|
|
84
|
+
: undefined,
|
|
85
|
+
driftData: drift
|
|
86
|
+
? {
|
|
87
|
+
driftScore: drift.driftScore,
|
|
88
|
+
changes: drift.changes?.length || 0,
|
|
89
|
+
affectedFiles: drift.affectedFiles?.files?.length || 0,
|
|
90
|
+
}
|
|
91
|
+
: undefined,
|
|
92
|
+
apiEndpoint,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Calculate execution score (0-100)
|
|
97
|
+
*/
|
|
98
|
+
calculateExecutionScore(execution) {
|
|
99
|
+
let score;
|
|
100
|
+
let status;
|
|
101
|
+
if (execution.crashed) {
|
|
102
|
+
score = 0;
|
|
103
|
+
status = "crashed";
|
|
104
|
+
}
|
|
105
|
+
else if (execution.passed) {
|
|
106
|
+
if (execution.warnings.length > 0) {
|
|
107
|
+
// Passed with warnings: 50-100 based on warning count
|
|
108
|
+
score = Math.max(50, 100 - execution.warnings.length * 10);
|
|
109
|
+
status = "passed_with_warnings";
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
score = 100;
|
|
113
|
+
status = "passed";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (execution.errors.length > 0) {
|
|
117
|
+
// Failed with errors: 30-50 based on error count
|
|
118
|
+
score = Math.max(30, 50 - execution.errors.length * 5);
|
|
119
|
+
status = "failed";
|
|
120
|
+
}
|
|
121
|
+
else if (execution.duration > 280000) {
|
|
122
|
+
// Near timeout (5 min = 300000ms)
|
|
123
|
+
score = 20;
|
|
124
|
+
status = "timeout";
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
score = 30;
|
|
128
|
+
status = "failed";
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
score,
|
|
132
|
+
status,
|
|
133
|
+
hasWarnings: execution.warnings.length > 0,
|
|
134
|
+
hasErrors: execution.errors.length > 0,
|
|
135
|
+
crashed: execution.crashed,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Calculate overall health score
|
|
140
|
+
*
|
|
141
|
+
* Health status is primarily drift-based with optional execution refinement:
|
|
142
|
+
* - Healthy: drift < 20 (and execution >= 80 if available)
|
|
143
|
+
* - At Risk: drift 20-40 (and execution >= 60 if available)
|
|
144
|
+
* - Broken: drift >= 40 (or execution < 60 if available)
|
|
145
|
+
*/
|
|
146
|
+
calculateHealthScore(executionScore, driftScore) {
|
|
147
|
+
let overall;
|
|
148
|
+
let calculationMethod;
|
|
149
|
+
if (executionScore !== undefined && driftScore !== undefined) {
|
|
150
|
+
// Combined: 60% execution, 40% drift (inverted)
|
|
151
|
+
overall = Math.round(0.6 * executionScore + 0.4 * (100 - driftScore));
|
|
152
|
+
calculationMethod = "combined";
|
|
153
|
+
}
|
|
154
|
+
else if (executionScore !== undefined) {
|
|
155
|
+
overall = executionScore;
|
|
156
|
+
calculationMethod = "execution_only";
|
|
157
|
+
}
|
|
158
|
+
else if (driftScore !== undefined) {
|
|
159
|
+
overall = 100 - driftScore;
|
|
160
|
+
calculationMethod = "drift_only";
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
overall = 0;
|
|
164
|
+
calculationMethod = "drift_only";
|
|
165
|
+
}
|
|
166
|
+
// Determine health status (primarily drift-based, execution is optional refinement)
|
|
167
|
+
// User requirements interpretation:
|
|
168
|
+
// - Healthy: drift < 20 AND (no execution OR execution >= 80)
|
|
169
|
+
// - At Risk: drift 20-40 AND (no execution OR execution >= 60)
|
|
170
|
+
// - Broken: drift >= 40 OR execution < 60
|
|
171
|
+
let status;
|
|
172
|
+
const drift = driftScore !== undefined ? driftScore : -1; // -1 means no drift data
|
|
173
|
+
const exec = executionScore !== undefined ? executionScore : -1; // -1 means no execution data
|
|
174
|
+
if (drift === -1 && exec === -1) {
|
|
175
|
+
// No data available
|
|
176
|
+
status = "unknown";
|
|
177
|
+
}
|
|
178
|
+
else if (drift !== -1) {
|
|
179
|
+
// Drift-based status (primary)
|
|
180
|
+
if (drift >= 40) {
|
|
181
|
+
// High drift (>= 40): Broken regardless of execution
|
|
182
|
+
status = "broken";
|
|
183
|
+
}
|
|
184
|
+
else if (drift >= 20 && drift < 40) {
|
|
185
|
+
// Medium drift (20-40): At risk, unless execution is very poor
|
|
186
|
+
status = exec !== -1 && exec < 60 ? "broken" : "at_risk";
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Low drift (< 20): Healthy, but execution can downgrade to at_risk
|
|
190
|
+
// Since code hasn't changed much, even failing tests are "at_risk" not "broken"
|
|
191
|
+
if (exec !== -1 && exec < 80) {
|
|
192
|
+
status = "at_risk"; // Any execution issues with low drift → at_risk
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
status = "healthy";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (exec !== -1) {
|
|
200
|
+
// Only execution data available (no drift)
|
|
201
|
+
if (exec >= 80) {
|
|
202
|
+
status = "healthy";
|
|
203
|
+
}
|
|
204
|
+
else if (exec >= 60) {
|
|
205
|
+
status = "at_risk";
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
status = "broken";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
status = "unknown";
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
overall,
|
|
216
|
+
executionScore,
|
|
217
|
+
driftScore,
|
|
218
|
+
status,
|
|
219
|
+
calculationMethod,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Identify specific issues with a test
|
|
224
|
+
*/
|
|
225
|
+
identifyIssues(execution, drift) {
|
|
226
|
+
const issues = [];
|
|
227
|
+
// Check execution issues
|
|
228
|
+
if (execution) {
|
|
229
|
+
if (execution.crashed) {
|
|
230
|
+
issues.push({
|
|
231
|
+
type: "crash",
|
|
232
|
+
severity: "critical",
|
|
233
|
+
description: "Test crashed during execution",
|
|
234
|
+
details: execution.errors.join("; "),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else if (execution.errors.length > 0) {
|
|
238
|
+
issues.push({
|
|
239
|
+
type: "test_failures",
|
|
240
|
+
severity: "high",
|
|
241
|
+
description: `Test failed with ${execution.errors.length} error(s)`,
|
|
242
|
+
details: execution.errors.slice(0, 3).join("; "),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (execution.duration > 280000) {
|
|
246
|
+
issues.push({
|
|
247
|
+
type: "timeout",
|
|
248
|
+
severity: "medium",
|
|
249
|
+
description: "Test approaching timeout threshold",
|
|
250
|
+
details: `Duration: ${(execution.duration / 1000).toFixed(1)}s`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Check drift issues
|
|
255
|
+
if (drift &&
|
|
256
|
+
drift.changes &&
|
|
257
|
+
Array.isArray(drift.changes) &&
|
|
258
|
+
drift.changes.length > 0) {
|
|
259
|
+
const hasCodeChanges = drift.changes.some((c) => ["code_change", "function_changed", "class_changed"].includes(c.type));
|
|
260
|
+
if (hasCodeChanges) {
|
|
261
|
+
issues.push({
|
|
262
|
+
type: "code_changes",
|
|
263
|
+
severity: "medium",
|
|
264
|
+
description: "Code changes detected in dependencies",
|
|
265
|
+
details: `${drift.affectedFiles?.files.length || 0} file(s) changed`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const endpointsRemoved = drift.changes.filter((c) => c.type === "endpoint_removed");
|
|
269
|
+
if (endpointsRemoved.length > 0) {
|
|
270
|
+
issues.push({
|
|
271
|
+
type: "endpoints_removed",
|
|
272
|
+
severity: "high",
|
|
273
|
+
description: `${endpointsRemoved.length} API endpoint(s) removed`,
|
|
274
|
+
details: endpointsRemoved.map((c) => c.description).join("; "),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const schemaChanges = drift.changes.filter((c) => ["endpoint_modified", "authentication_changed"].includes(c.type));
|
|
278
|
+
if (schemaChanges.length > 0) {
|
|
279
|
+
issues.push({
|
|
280
|
+
type: "schema_changes",
|
|
281
|
+
severity: "high",
|
|
282
|
+
description: "API schema changes detected",
|
|
283
|
+
details: schemaChanges.map((c) => c.description).join("; "),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const authChanged = drift.changes.some((c) => c.type === "authentication_changed");
|
|
287
|
+
if (authChanged) {
|
|
288
|
+
issues.push({
|
|
289
|
+
type: "authentication_changed",
|
|
290
|
+
severity: "critical",
|
|
291
|
+
description: "Authentication mechanism changed",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return issues;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Generate recommendation for a test
|
|
299
|
+
*
|
|
300
|
+
* Recommendation logic (primarily drift-based):
|
|
301
|
+
* 1. IF drift > 70: REGENERATE (HIGH priority)
|
|
302
|
+
* 2. ELSE IF endpoint missing: DELETE (HIGH priority)
|
|
303
|
+
* 3. ELSE IF 30 < drift <= 70: UPDATE (MEDIUM priority)
|
|
304
|
+
* 4. ELSE IF drift > 10: VERIFY (LOW priority)
|
|
305
|
+
* 5. ELSE: VERIFY (LOW priority, healthy test)
|
|
306
|
+
*
|
|
307
|
+
* Execution failures enhance rationale but don't change primary action
|
|
308
|
+
*/
|
|
309
|
+
generateRecommendation(testFile, healthScore, driftScore, execution, issues, apiEndpoint) {
|
|
310
|
+
const drift = driftScore !== undefined ? driftScore : -1; // -1 means no drift data
|
|
311
|
+
let action;
|
|
312
|
+
let priority;
|
|
313
|
+
let rationale;
|
|
314
|
+
let estimatedWork = "SMALL";
|
|
315
|
+
// Handle missing drift data first
|
|
316
|
+
if (drift === -1) {
|
|
317
|
+
// No drift data available - base recommendation on health status and execution
|
|
318
|
+
if (healthScore.status === "unknown") {
|
|
319
|
+
action = "VERIFY";
|
|
320
|
+
priority = "MEDIUM";
|
|
321
|
+
rationale = "Unable to analyze drift - manual verification recommended";
|
|
322
|
+
estimatedWork = "SMALL";
|
|
323
|
+
// If execution data shows failure, escalate
|
|
324
|
+
if (execution && !execution.passed) {
|
|
325
|
+
priority = "HIGH";
|
|
326
|
+
rationale = "Drift analysis unavailable and test is failing - investigate immediately";
|
|
327
|
+
estimatedWork = "MEDIUM";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (execution && !execution.passed) {
|
|
331
|
+
// No drift data but test is failing
|
|
332
|
+
action = "UPDATE";
|
|
333
|
+
priority = "HIGH";
|
|
334
|
+
rationale = "Test is failing but drift analysis unavailable - review test logic and dependencies";
|
|
335
|
+
estimatedWork = "MEDIUM";
|
|
336
|
+
}
|
|
337
|
+
else if (execution && execution.passed) {
|
|
338
|
+
// No drift data but test is passing
|
|
339
|
+
action = "VERIFY";
|
|
340
|
+
priority = "LOW";
|
|
341
|
+
rationale = "Drift analysis unavailable but test is passing - periodic verification recommended";
|
|
342
|
+
estimatedWork = "SMALL";
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
// No drift data, no execution data
|
|
346
|
+
action = "VERIFY";
|
|
347
|
+
priority = "MEDIUM";
|
|
348
|
+
rationale = "No drift or execution data available - analysis needed";
|
|
349
|
+
estimatedWork = "SMALL";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (drift > 70) {
|
|
353
|
+
// High drift -> REGENERATE
|
|
354
|
+
action = "REGENERATE";
|
|
355
|
+
priority = "HIGH";
|
|
356
|
+
rationale =
|
|
357
|
+
"High drift detected - significant code changes since test creation";
|
|
358
|
+
estimatedWork = "MEDIUM";
|
|
359
|
+
// Enhance rationale with test failures if present
|
|
360
|
+
if (execution && !execution.passed) {
|
|
361
|
+
rationale += ". Test is also failing";
|
|
362
|
+
}
|
|
363
|
+
// Add specific issues
|
|
364
|
+
if (issues && issues.length > 0) {
|
|
365
|
+
const criticalIssues = issues.filter((i) => ["critical", "high"].includes(i.severity));
|
|
366
|
+
if (criticalIssues.length > 0) {
|
|
367
|
+
rationale += `. Critical issues: ${criticalIssues
|
|
368
|
+
.map((i) => i.description)
|
|
369
|
+
.join(", ")}`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (apiEndpoint?.exists === false) {
|
|
374
|
+
// Endpoint removed from schema -> DELETE
|
|
375
|
+
action = "DELETE";
|
|
376
|
+
priority = "HIGH";
|
|
377
|
+
rationale = "Endpoint no longer exists in API schema";
|
|
378
|
+
estimatedWork = "SMALL";
|
|
379
|
+
}
|
|
380
|
+
else if (drift > 30 && drift <= 70) {
|
|
381
|
+
// Moderate drift -> UPDATE
|
|
382
|
+
action = "UPDATE";
|
|
383
|
+
priority = "MEDIUM";
|
|
384
|
+
rationale =
|
|
385
|
+
"Moderate drift detected - related code changes may affect test";
|
|
386
|
+
estimatedWork = "SMALL";
|
|
387
|
+
// Enhance with schema changes
|
|
388
|
+
const schemaChanges = issues?.filter((i) => [
|
|
389
|
+
"schema_changes",
|
|
390
|
+
"endpoints_removed",
|
|
391
|
+
"authentication_changed",
|
|
392
|
+
].includes(i.type));
|
|
393
|
+
if (schemaChanges && schemaChanges.length > 0) {
|
|
394
|
+
rationale += `. Schema changes: ${schemaChanges
|
|
395
|
+
.map((i) => i.description)
|
|
396
|
+
.join(", ")}`;
|
|
397
|
+
estimatedWork = "MEDIUM";
|
|
398
|
+
}
|
|
399
|
+
// Note execution failures
|
|
400
|
+
if (execution && !execution.passed) {
|
|
401
|
+
rationale += ". Test is also failing - requires immediate attention";
|
|
402
|
+
priority = "HIGH"; // Escalate priority for failing tests
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (drift > 10) {
|
|
406
|
+
// Low drift -> VERIFY
|
|
407
|
+
action = "VERIFY";
|
|
408
|
+
priority = "LOW";
|
|
409
|
+
rationale = "Minor changes detected - test likely still valid";
|
|
410
|
+
estimatedWork = "SMALL";
|
|
411
|
+
// Note execution status for context
|
|
412
|
+
if (execution) {
|
|
413
|
+
if (!execution.passed) {
|
|
414
|
+
rationale += ". However, test is failing - review needed";
|
|
415
|
+
priority = "MEDIUM"; // Escalate for failing tests
|
|
416
|
+
}
|
|
417
|
+
else if (execution.warnings.length > 0) {
|
|
418
|
+
rationale += ". Test passed with warnings";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
// Minimal/no drift -> VERIFY
|
|
424
|
+
action = "VERIFY";
|
|
425
|
+
priority = "LOW";
|
|
426
|
+
rationale = "Test appears healthy - periodic verification recommended";
|
|
427
|
+
estimatedWork = "SMALL";
|
|
428
|
+
// Handle edge case: low drift but test is failing
|
|
429
|
+
if (execution && !execution.passed) {
|
|
430
|
+
action = "UPDATE";
|
|
431
|
+
priority = "HIGH";
|
|
432
|
+
rationale =
|
|
433
|
+
"Test is failing despite low drift - may indicate environmental issues or test flakiness";
|
|
434
|
+
estimatedWork = "MEDIUM";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Determine endpoint status
|
|
438
|
+
let endpointStatus;
|
|
439
|
+
if (apiEndpoint === undefined) {
|
|
440
|
+
endpointStatus = undefined;
|
|
441
|
+
}
|
|
442
|
+
else if (apiEndpoint.exists) {
|
|
443
|
+
endpointStatus = "exists";
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
endpointStatus = "missing";
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
testFile,
|
|
450
|
+
action,
|
|
451
|
+
priority,
|
|
452
|
+
rationale,
|
|
453
|
+
estimatedWork,
|
|
454
|
+
issues,
|
|
455
|
+
details: {
|
|
456
|
+
driftScore: drift,
|
|
457
|
+
executionPassed: execution?.passed,
|
|
458
|
+
endpointStatus,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Parse OpenAPI schema and extract endpoints
|
|
464
|
+
*/
|
|
465
|
+
async parseApiSchema(schemaPath) {
|
|
466
|
+
let schema;
|
|
467
|
+
try {
|
|
468
|
+
if (schemaPath.startsWith("http://") ||
|
|
469
|
+
schemaPath.startsWith("https://")) {
|
|
470
|
+
// Fetch from URL
|
|
471
|
+
const response = await fetch(schemaPath);
|
|
472
|
+
if (!response.ok) {
|
|
473
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
474
|
+
}
|
|
475
|
+
schema = await response.json();
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
// Read from file
|
|
479
|
+
const content = fs.readFileSync(schemaPath, "utf-8");
|
|
480
|
+
schema = JSON.parse(content);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
logger.error(`Failed to parse API schema: ${error.message}`);
|
|
485
|
+
throw new Error(`Could not parse API schema at ${schemaPath}`);
|
|
486
|
+
}
|
|
487
|
+
const endpoints = [];
|
|
488
|
+
const paths = schema.paths || {};
|
|
489
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
490
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
491
|
+
if (["get", "post", "put", "delete", "patch"].includes(method)) {
|
|
492
|
+
const op = operation;
|
|
493
|
+
const authRequired = this.checkAuthRequired(op, schema);
|
|
494
|
+
endpoints.push({
|
|
495
|
+
path: pathStr,
|
|
496
|
+
method: method.toUpperCase(),
|
|
497
|
+
operationId: op.operationId,
|
|
498
|
+
authRequired,
|
|
499
|
+
parameters: op.parameters || [],
|
|
500
|
+
requestBody: op.requestBody,
|
|
501
|
+
responses: op.responses,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return endpoints;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Check if endpoint requires authentication
|
|
510
|
+
*/
|
|
511
|
+
checkAuthRequired(operation, schema) {
|
|
512
|
+
// Check security at operation level
|
|
513
|
+
if (operation.security && operation.security.length > 0) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
// Check security at schema level
|
|
517
|
+
if (schema.security && schema.security.length > 0) {
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Analyze test coverage
|
|
524
|
+
*/
|
|
525
|
+
async analyzeCoverage(tests, apiSchema) {
|
|
526
|
+
const endpoints = await this.parseApiSchema(apiSchema);
|
|
527
|
+
const coverage = [];
|
|
528
|
+
for (const endpoint of endpoints) {
|
|
529
|
+
const endpointKey = `${endpoint.method} ${endpoint.path}`;
|
|
530
|
+
const coveredBy = tests.filter((t) => this.testCoversEndpoint(t.testFile, endpoint));
|
|
531
|
+
coverage.push({
|
|
532
|
+
endpoint: endpointKey,
|
|
533
|
+
method: endpoint.method,
|
|
534
|
+
covered: coveredBy.length > 0,
|
|
535
|
+
testFiles: coveredBy.map((t) => t.testFile),
|
|
536
|
+
endpointInfo: endpoint,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const coveredEndpoints = coverage.filter((c) => c.covered).length;
|
|
540
|
+
const uncoveredEndpoints = endpoints.filter((ep) => !coverage.find((c) => c.endpoint === `${ep.method} ${ep.path}`)
|
|
541
|
+
?.covered);
|
|
542
|
+
return {
|
|
543
|
+
totalEndpoints: endpoints.length,
|
|
544
|
+
coveredEndpoints,
|
|
545
|
+
coveragePercentage: endpoints.length > 0
|
|
546
|
+
? Math.round((coveredEndpoints / endpoints.length) * 100)
|
|
547
|
+
: 0,
|
|
548
|
+
uncoveredEndpoints,
|
|
549
|
+
coverage,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Check if test covers endpoint
|
|
554
|
+
*/
|
|
555
|
+
testCoversEndpoint(testFile, endpoint) {
|
|
556
|
+
try {
|
|
557
|
+
const content = fs.readFileSync(testFile, "utf-8");
|
|
558
|
+
// Convert OpenAPI-style path to regex, e.g. /users/{id} -> /users/[^/]+
|
|
559
|
+
// Replace path parameters with regex pattern, escaping the path parts but not the regex itself
|
|
560
|
+
const pathRegexString = endpoint.path
|
|
561
|
+
.split(/(\{[^}]+\})/) // Split on path parameters, keeping them in the result
|
|
562
|
+
.map((part, index) => {
|
|
563
|
+
if (part.match(/^\{[^}]+\}$/)) {
|
|
564
|
+
// This is a path parameter like {id}, replace with regex pattern
|
|
565
|
+
return "[^/]+";
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
// This is a literal path part, escape special regex chars
|
|
569
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
.join('');
|
|
573
|
+
// pathRegexString is already escaped, so we can use it directly
|
|
574
|
+
const pathRegex = new RegExp(pathRegexString);
|
|
575
|
+
const method = endpoint.method.toUpperCase();
|
|
576
|
+
const methodLower = endpoint.method.toLowerCase();
|
|
577
|
+
// Check if path matches
|
|
578
|
+
if (!pathRegex.test(content)) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
// More precise method detection patterns
|
|
582
|
+
const methodPatterns = [
|
|
583
|
+
// Python: method="PUT" or method='PUT'
|
|
584
|
+
new RegExp(`method\\s*=\\s*["']${method}["']`, "i"),
|
|
585
|
+
// JavaScript/TypeScript: method: 'PUT' or .put( or .PUT(
|
|
586
|
+
new RegExp(`method\\s*:\\s*["']${method}["']`, "i"),
|
|
587
|
+
new RegExp(`\\.${methodLower}\\s*\\(`, "i"),
|
|
588
|
+
// HTTP request patterns: PUT /path
|
|
589
|
+
new RegExp(`${method}\\s+${pathRegexString}`, "i"),
|
|
590
|
+
// Axios/fetch: { method: 'PUT' }
|
|
591
|
+
new RegExp(`["']method["']\\s*:\\s*["']${method}["']`, "i"),
|
|
592
|
+
// RestAssured/Java: .put()
|
|
593
|
+
new RegExp(`\\.${methodLower}\\(`, "i"),
|
|
594
|
+
// Go: http.MethodPut or "PUT"
|
|
595
|
+
new RegExp(`http\\.Method${method.charAt(0) + methodLower.slice(1)}`, "i"),
|
|
596
|
+
new RegExp(`["']${method}["']`, "i"),
|
|
597
|
+
];
|
|
598
|
+
// Check if any pattern matches
|
|
599
|
+
return methodPatterns.some((pattern) => pattern.test(content));
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Extract endpoint information from test file
|
|
607
|
+
*/
|
|
608
|
+
async extractEndpointFromTest(testFile, apiSchema) {
|
|
609
|
+
try {
|
|
610
|
+
const content = fs.readFileSync(testFile, "utf-8");
|
|
611
|
+
const endpoints = await this.parseApiSchema(apiSchema);
|
|
612
|
+
// Find matching endpoint
|
|
613
|
+
for (const endpoint of endpoints) {
|
|
614
|
+
if (this.testCoversEndpoint(testFile, endpoint)) {
|
|
615
|
+
return {
|
|
616
|
+
path: endpoint.path,
|
|
617
|
+
method: endpoint.method,
|
|
618
|
+
exists: true,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return { path: "unknown", method: "unknown", exists: false };
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Prioritize and sort recommendations
|
|
630
|
+
*/
|
|
631
|
+
prioritizeRecommendations(recommendations) {
|
|
632
|
+
const priorityOrder = {
|
|
633
|
+
CRITICAL: 0,
|
|
634
|
+
HIGH: 1,
|
|
635
|
+
MEDIUM: 2,
|
|
636
|
+
LOW: 3,
|
|
637
|
+
};
|
|
638
|
+
return recommendations.sort((a, b) => {
|
|
639
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
640
|
+
if (priorityDiff !== 0)
|
|
641
|
+
return priorityDiff;
|
|
642
|
+
// Within same priority, sort by action
|
|
643
|
+
const actionOrder = {
|
|
644
|
+
REGENERATE: 0,
|
|
645
|
+
DELETE: 1,
|
|
646
|
+
UPDATE: 2,
|
|
647
|
+
ADD: 3,
|
|
648
|
+
VERIFY: 4,
|
|
649
|
+
};
|
|
650
|
+
return actionOrder[a.action] - actionOrder[b.action];
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
@@ -12,7 +12,7 @@ Logging into Skyramp provides access to additional platform features and service
|
|
|
12
12
|
.string()
|
|
13
13
|
.describe("The prompt user provided to login to Skyramp"),
|
|
14
14
|
},
|
|
15
|
-
|
|
15
|
+
_meta: {
|
|
16
16
|
keywords: ["login", "authenticate", "skyramp login"],
|
|
17
17
|
},
|
|
18
18
|
}, async (params) => {
|
|
@@ -11,7 +11,7 @@ Logout from Skyramp platform to end your authenticated session.`,
|
|
|
11
11
|
.string()
|
|
12
12
|
.describe("The prompt user provided to logout from Skyramp"),
|
|
13
13
|
},
|
|
14
|
-
|
|
14
|
+
_meta: {
|
|
15
15
|
keywords: ["logout", "sign out", "skyramp logout"],
|
|
16
16
|
},
|
|
17
17
|
}, async (params) => {
|
|
@@ -37,16 +37,18 @@ export function registerCodeReuseTool(server) {
|
|
|
37
37
|
3. **ONLY IF** reusable helpers are found in OTHER test files → move to utils and update imports
|
|
38
38
|
4. **IF NO** existing utils or reusable helpers are found in test files then reuse is complete, without creating any files or changes to the test files
|
|
39
39
|
5. Verify no duplicate code remains and tests work as expected
|
|
40
|
-
6. **MANDATORY**: After code reuse is complete, proceed to modularization by calling skyramp_modularization tool
|
|
40
|
+
6. **MANDATORY**: After code reuse is complete, proceed to modularization by calling skyramp_modularization tool ONLY for UI, E2E, INTEGRATION or LOAD tests generated from traces.
|
|
41
41
|
|
|
42
42
|
**WHEN TO SKIP (DO NOTHING):**
|
|
43
43
|
- Other test files only have repetitive patterns, no helper functions
|
|
44
44
|
- Other test files are essentially identical to current file
|
|
45
45
|
- No actual function definitions found in other test files
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
**MANDATORY**: ONLY ALLOW CODE REUSE IF THE IS TRACE BASED FLAG IS SET TO TRUE ELSE DO NOT ALLOW CODE REUSE AND LEAVE THE TEST FILE AS IS.
|
|
48
|
+
**CRITICAL**: NON TRACE BASED TESTS ARE ALREADY MODULARIZED AND DO NOT NEED CODE REUSE.
|
|
47
49
|
The tool will provide step-by-step instructions that MUST be followed exactly.`,
|
|
48
50
|
inputSchema: codeReuseSchema.shape,
|
|
49
|
-
|
|
51
|
+
_meta: {
|
|
50
52
|
keywords: [
|
|
51
53
|
"code reuse",
|
|
52
54
|
"duplicate code",
|