@skyramp/mcp 0.0.45 → 0.0.47
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 +66 -29
- package/build/prompts/test-recommendation/repository-analysis-prompt.js +7 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +7 -1
- package/build/prompts/testGenerationPrompt.js +1 -0
- package/build/services/AnalyticsService.js +78 -0
- package/build/services/DriftAnalysisService.js +6 -2
- package/build/services/TestExecutionService.js +322 -11
- package/build/services/TestHealthService.js +8 -5
- package/build/tools/auth/loginTool.js +12 -2
- package/build/tools/auth/logoutTool.js +12 -2
- package/build/tools/code-refactor/codeReuseTool.js +41 -15
- package/build/tools/code-refactor/modularizationTool.js +36 -9
- package/build/tools/executeSkyrampTestTool.js +45 -5
- package/build/tools/fixErrorTool.js +37 -13
- package/build/tools/generate-tests/generateContractRestTool.js +11 -3
- package/build/tools/generate-tests/generateE2ERestTool.js +8 -2
- package/build/tools/generate-tests/generateFuzzRestTool.js +11 -3
- package/build/tools/generate-tests/generateIntegrationRestTool.js +11 -3
- package/build/tools/generate-tests/generateLoadRestTool.js +11 -3
- package/build/tools/generate-tests/generateScenarioRestTool.js +10 -2
- package/build/tools/generate-tests/generateSmokeRestTool.js +11 -3
- package/build/tools/generate-tests/generateUIRestTool.js +10 -3
- package/build/tools/test-maintenance/actionsTool.js +175 -147
- package/build/tools/test-maintenance/analyzeTestDriftTool.js +14 -5
- package/build/tools/test-maintenance/calculateHealthScoresTool.js +13 -4
- package/build/tools/test-maintenance/discoverTestsTool.js +10 -2
- package/build/tools/test-maintenance/executeBatchTestsTool.js +14 -4
- package/build/tools/test-maintenance/stateCleanupTool.js +11 -3
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +18 -4
- package/build/tools/test-recommendation/mapTestsTool.js +21 -5
- package/build/tools/test-recommendation/recommendTestsTool.js +17 -3
- package/build/tools/trace/startTraceCollectionTool.js +17 -4
- package/build/tools/trace/stopTraceCollectionTool.js +27 -3
- package/build/types/TestTypes.js +17 -3
- package/build/utils/AnalysisStateManager.js +3 -1
- package/package.json +2 -2
|
@@ -6,20 +6,176 @@ import { stripVTControlCharacters } from "util";
|
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
7
|
const DEFAULT_TIMEOUT = 300000; // 5 minutes
|
|
8
8
|
const MAX_CONCURRENT_EXECUTIONS = 5;
|
|
9
|
-
const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.2.
|
|
9
|
+
const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.2.41";
|
|
10
10
|
const DOCKER_PLATFORM = "linux/amd64";
|
|
11
|
+
const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress updates during execution
|
|
11
12
|
// Files and directories to exclude when mounting workspace to Docker container
|
|
12
13
|
export const EXCLUDED_MOUNT_ITEMS = [
|
|
13
14
|
"package-lock.json",
|
|
14
15
|
"package.json",
|
|
15
16
|
"node_modules",
|
|
16
17
|
];
|
|
18
|
+
/**
|
|
19
|
+
* Find the start index of a comment in a line, ignoring comment delimiters inside strings
|
|
20
|
+
* Returns -1 if no comment is found outside of strings
|
|
21
|
+
*/
|
|
22
|
+
function findCommentStart(line) {
|
|
23
|
+
let inSingleQuote = false;
|
|
24
|
+
let inDoubleQuote = false;
|
|
25
|
+
let escaped = false;
|
|
26
|
+
for (let i = 0; i < line.length; i++) {
|
|
27
|
+
const char = line[i];
|
|
28
|
+
const nextChar = i + 1 < line.length ? line[i + 1] : "";
|
|
29
|
+
// Handle escape sequences
|
|
30
|
+
if (escaped) {
|
|
31
|
+
escaped = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (char === "\\") {
|
|
35
|
+
escaped = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// Track string boundaries
|
|
39
|
+
if (char === "'" && !inDoubleQuote) {
|
|
40
|
+
inSingleQuote = !inSingleQuote;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === '"' && !inSingleQuote) {
|
|
44
|
+
inDoubleQuote = !inDoubleQuote;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// Check for comments only if not in a string
|
|
48
|
+
if (!inSingleQuote && !inDoubleQuote) {
|
|
49
|
+
if (char === "/" && nextChar === "/") {
|
|
50
|
+
return i;
|
|
51
|
+
}
|
|
52
|
+
if (char === "#") {
|
|
53
|
+
return i;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return -1; // No comment found
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Filter out comments from code lines
|
|
61
|
+
* Returns array of non-comment lines with inline comments removed
|
|
62
|
+
*/
|
|
63
|
+
function filterComments(lines) {
|
|
64
|
+
const nonCommentLines = [];
|
|
65
|
+
let inMultiLineComment = false;
|
|
66
|
+
let inPythonMultiLineComment = false;
|
|
67
|
+
for (let line of lines) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
// Check for Python multi-line string comments (""" or ''')
|
|
70
|
+
// Count occurrences to handle single-line strings like '''comment'''
|
|
71
|
+
const tripleDoubleCount = (trimmed.match(/"""/g) || []).length;
|
|
72
|
+
const tripleSingleCount = (trimmed.match(/'''/g) || []).length;
|
|
73
|
+
if (tripleDoubleCount > 0) {
|
|
74
|
+
// Odd number: opening or closing a multi-line comment (toggle state)
|
|
75
|
+
// Even number: complete string on same line (e.g., """comment""" - don't toggle)
|
|
76
|
+
if (tripleDoubleCount % 2 === 1) {
|
|
77
|
+
inPythonMultiLineComment = !inPythonMultiLineComment;
|
|
78
|
+
}
|
|
79
|
+
continue; // Skip any line with triple quotes
|
|
80
|
+
}
|
|
81
|
+
if (tripleSingleCount > 0) {
|
|
82
|
+
if (tripleSingleCount % 2 === 1) {
|
|
83
|
+
inPythonMultiLineComment = !inPythonMultiLineComment;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (inPythonMultiLineComment) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Check for multi-line comment start/end (/* */)
|
|
91
|
+
if (trimmed.includes("/*")) {
|
|
92
|
+
if (trimmed.includes("*/")) {
|
|
93
|
+
// Single-line multi-line comment (e.g., /* comment */)
|
|
94
|
+
// Don't change state, just skip the line
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Opening a multi-line comment
|
|
99
|
+
inMultiLineComment = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (inMultiLineComment) {
|
|
104
|
+
if (trimmed.includes("*/")) {
|
|
105
|
+
inMultiLineComment = false;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Skip single-line comments
|
|
110
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("#")) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Remove inline comments from the line before processing
|
|
114
|
+
// Use helper function to avoid removing comment delimiters inside strings
|
|
115
|
+
const commentIndex = findCommentStart(line);
|
|
116
|
+
if (commentIndex >= 0) {
|
|
117
|
+
line = line.substring(0, commentIndex);
|
|
118
|
+
}
|
|
119
|
+
nonCommentLines.push(line);
|
|
120
|
+
}
|
|
121
|
+
return nonCommentLines;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Detect session file paths referenced in test files
|
|
125
|
+
* Looks for storageState patterns in TypeScript/JavaScript/Python/Java/C# test files
|
|
126
|
+
* Excludes matches found in comments
|
|
127
|
+
*/
|
|
128
|
+
function detectSessionFiles(testFilePath) {
|
|
129
|
+
try {
|
|
130
|
+
const content = fs.readFileSync(testFilePath, "utf-8");
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
const sessionFiles = [];
|
|
133
|
+
// Pattern for TypeScript/JavaScript: storageState: '/path/to/file' or storageState: "/path/to/file"
|
|
134
|
+
const tsJsPattern = /storageState:\s*['"]([^'"]+)['"]/g;
|
|
135
|
+
// Pattern for Python: storage_state='/path/to/file' or storage_state="/path/to/file"
|
|
136
|
+
const pythonPattern = /storage_state\s*=\s*['"]([^'"]+)['"]/g;
|
|
137
|
+
// Pattern for Java: setStorageState(Paths.get("path")) or setStorageState("path")
|
|
138
|
+
// Enforces proper parenthesis matching: first ) is required, second ) is required when using Paths.get
|
|
139
|
+
const javaPattern = /setStorageState(?:Path)?\s*\(\s*(?:Paths\.get\s*\(\s*)?['"]([^'"]+)['"]\s*\)(?:\s*\))?/g;
|
|
140
|
+
// Pattern for C#: StorageStatePath = "path" or StorageState = "path"
|
|
141
|
+
const csharpPattern = /StorageState(?:Path)?\s*=\s*['"]([^'"]+)['"]/g;
|
|
142
|
+
// Filter out comments
|
|
143
|
+
const codeLines = filterComments(lines);
|
|
144
|
+
// Process each non-comment line
|
|
145
|
+
for (const line of codeLines) {
|
|
146
|
+
// Try all patterns on this line
|
|
147
|
+
let match;
|
|
148
|
+
tsJsPattern.lastIndex = 0;
|
|
149
|
+
while ((match = tsJsPattern.exec(line)) !== null) {
|
|
150
|
+
sessionFiles.push(match[1]);
|
|
151
|
+
}
|
|
152
|
+
pythonPattern.lastIndex = 0;
|
|
153
|
+
while ((match = pythonPattern.exec(line)) !== null) {
|
|
154
|
+
sessionFiles.push(match[1]);
|
|
155
|
+
}
|
|
156
|
+
javaPattern.lastIndex = 0;
|
|
157
|
+
while ((match = javaPattern.exec(line)) !== null) {
|
|
158
|
+
sessionFiles.push(match[1]);
|
|
159
|
+
}
|
|
160
|
+
csharpPattern.lastIndex = 0;
|
|
161
|
+
while ((match = csharpPattern.exec(line)) !== null) {
|
|
162
|
+
sessionFiles.push(match[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return sessionFiles;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
logger.error(`Failed to detect session files in ${testFilePath}`, {
|
|
169
|
+
error,
|
|
170
|
+
});
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
17
174
|
export class TestExecutionService {
|
|
18
175
|
docker;
|
|
19
|
-
imageReady;
|
|
176
|
+
imageReady = null;
|
|
20
177
|
constructor() {
|
|
21
178
|
this.docker = new Docker();
|
|
22
|
-
this.imageReady = this.ensureDockerImage();
|
|
23
179
|
}
|
|
24
180
|
/**
|
|
25
181
|
* Execute multiple tests in parallel batches
|
|
@@ -63,13 +219,42 @@ export class TestExecutionService {
|
|
|
63
219
|
}
|
|
64
220
|
/**
|
|
65
221
|
* Execute a single test
|
|
222
|
+
* @param options Test execution options
|
|
223
|
+
* @param onProgress Optional callback for progress updates
|
|
66
224
|
*/
|
|
67
|
-
async executeTest(options) {
|
|
225
|
+
async executeTest(options, onProgress) {
|
|
68
226
|
const startTime = Date.now();
|
|
69
227
|
const executedAt = new Date().toISOString();
|
|
70
228
|
logger.debug(`Executing test: ${options.testFile}`);
|
|
71
|
-
//
|
|
72
|
-
await
|
|
229
|
+
// Check and ensure Docker image is ready with progress reporting
|
|
230
|
+
await onProgress?.({
|
|
231
|
+
phase: "docker-check",
|
|
232
|
+
message: "Checking Docker image availability...",
|
|
233
|
+
percent: 5,
|
|
234
|
+
});
|
|
235
|
+
const imageResult = await this.ensureDockerImage(onProgress);
|
|
236
|
+
if (imageResult.cached) {
|
|
237
|
+
await onProgress?.({
|
|
238
|
+
phase: "docker-check",
|
|
239
|
+
message: "Using cached Docker image",
|
|
240
|
+
percent: 20,
|
|
241
|
+
details: { cached: true },
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
await onProgress?.({
|
|
246
|
+
phase: "docker-pull",
|
|
247
|
+
message: "Docker image pull completed",
|
|
248
|
+
percent: 20,
|
|
249
|
+
details: { cached: false },
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Report preparing phase
|
|
253
|
+
await onProgress?.({
|
|
254
|
+
phase: "preparing",
|
|
255
|
+
message: "Validating workspace and test file...",
|
|
256
|
+
percent: 25,
|
|
257
|
+
});
|
|
73
258
|
// Validate workspace path - use accessSync for better validation
|
|
74
259
|
const workspacePath = path.resolve(options.workspacePath);
|
|
75
260
|
try {
|
|
@@ -86,6 +271,11 @@ export class TestExecutionService {
|
|
|
86
271
|
if (!fs.existsSync(options.testFile)) {
|
|
87
272
|
throw new Error(`Test file does not exist: ${options.testFile}`);
|
|
88
273
|
}
|
|
274
|
+
await onProgress?.({
|
|
275
|
+
phase: "preparing",
|
|
276
|
+
message: "Preparing Docker container configuration...",
|
|
277
|
+
percent: 30,
|
|
278
|
+
});
|
|
89
279
|
const containerMountPath = "/home/user";
|
|
90
280
|
const dockerSocketPath = "/var/run/docker.sock";
|
|
91
281
|
// Calculate relative test file path
|
|
@@ -93,7 +283,7 @@ export class TestExecutionService {
|
|
|
93
283
|
testFilePath = path.resolve(containerMountPath, testFilePath);
|
|
94
284
|
// Prepare Docker command
|
|
95
285
|
const command = [
|
|
96
|
-
"
|
|
286
|
+
"/root/runner.sh",
|
|
97
287
|
options.language,
|
|
98
288
|
testFilePath,
|
|
99
289
|
options.testType,
|
|
@@ -117,6 +307,44 @@ export class TestExecutionService {
|
|
|
117
307
|
Target: path.join(containerMountPath, file),
|
|
118
308
|
Source: path.join(workspacePath, file),
|
|
119
309
|
})));
|
|
310
|
+
// Detect and mount session files
|
|
311
|
+
const sessionFiles = detectSessionFiles(options.testFile);
|
|
312
|
+
const mountedPaths = new Set(); // Track mounted file paths to prevent duplicates
|
|
313
|
+
for (const sessionFile of sessionFiles) {
|
|
314
|
+
let sessionFileSource;
|
|
315
|
+
let sessionFileTarget;
|
|
316
|
+
if (path.isAbsolute(sessionFile)) {
|
|
317
|
+
// Absolute path: mount at the same absolute path in container
|
|
318
|
+
sessionFileSource = sessionFile;
|
|
319
|
+
sessionFileTarget = sessionFile;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Relative path: resolve from test file directory on host
|
|
323
|
+
const testFileDir = path.dirname(options.testFile);
|
|
324
|
+
sessionFileSource = path.resolve(testFileDir, sessionFile);
|
|
325
|
+
// Mount at the corresponding relative path in the container
|
|
326
|
+
const testFileDirInContainer = path.dirname(testFilePath);
|
|
327
|
+
sessionFileTarget = path.resolve(testFileDirInContainer, sessionFile);
|
|
328
|
+
}
|
|
329
|
+
// Check if session file exists on host
|
|
330
|
+
if (fs.existsSync(sessionFileSource)) {
|
|
331
|
+
// Only mount if we haven't already mounted this path
|
|
332
|
+
if (!mountedPaths.has(sessionFileTarget)) {
|
|
333
|
+
logger.info(` docker run -v ${sessionFileSource}:${sessionFileTarget}:ro ...`);
|
|
334
|
+
hostConfig.Mounts?.push({
|
|
335
|
+
Type: "bind",
|
|
336
|
+
Target: sessionFileTarget,
|
|
337
|
+
Source: sessionFileSource,
|
|
338
|
+
ReadOnly: true,
|
|
339
|
+
});
|
|
340
|
+
mountedPaths.add(sessionFileTarget);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
logger.error(`✗ Session file not found: ${sessionFileSource}`);
|
|
345
|
+
logger.error(` Referenced in test as: ${sessionFile}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
120
348
|
// Prepare environment variables
|
|
121
349
|
const env = [
|
|
122
350
|
`SKYRAMP_TEST_TOKEN=${options.token || ""}`,
|
|
@@ -138,13 +366,40 @@ export class TestExecutionService {
|
|
|
138
366
|
}
|
|
139
367
|
const stream = new DockerStream();
|
|
140
368
|
try {
|
|
369
|
+
// Report executing phase
|
|
370
|
+
await onProgress?.({
|
|
371
|
+
phase: "executing",
|
|
372
|
+
message: "Starting test execution in Docker container...",
|
|
373
|
+
percent: 40,
|
|
374
|
+
});
|
|
141
375
|
let statusCode = 0;
|
|
142
376
|
let containerRef = null;
|
|
377
|
+
// Start periodic progress reporter during execution
|
|
378
|
+
// Runs in parallel with docker.run() to keep the AI agent informed
|
|
379
|
+
let progressIntervalHandle;
|
|
380
|
+
if (onProgress) {
|
|
381
|
+
let progressTick = 0;
|
|
382
|
+
progressIntervalHandle = setInterval(() => {
|
|
383
|
+
progressTick++;
|
|
384
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
385
|
+
// Progress moves from 40% to 79% during execution phase
|
|
386
|
+
// Slowly increment to show activity without reaching 80% (reserved for processing)
|
|
387
|
+
const percent = Math.min(40 + progressTick * 2, 79);
|
|
388
|
+
onProgress({
|
|
389
|
+
phase: "executing",
|
|
390
|
+
message: `Test running... (${elapsed}s elapsed)`,
|
|
391
|
+
percent,
|
|
392
|
+
}).catch(() => {
|
|
393
|
+
// Ignore progress notification errors
|
|
394
|
+
});
|
|
395
|
+
}, EXECUTION_PROGRESS_INTERVAL);
|
|
396
|
+
}
|
|
143
397
|
// Run container with timeout
|
|
144
398
|
const executionPromise = this.docker
|
|
145
399
|
.run(EXECUTOR_DOCKER_IMAGE, command, stream, {
|
|
146
400
|
Env: env,
|
|
147
401
|
HostConfig: hostConfig,
|
|
402
|
+
WorkingDir: containerMountPath, // Explicitly set working directory
|
|
148
403
|
})
|
|
149
404
|
.then(function (data) {
|
|
150
405
|
const result = data[0];
|
|
@@ -174,14 +429,18 @@ export class TestExecutionService {
|
|
|
174
429
|
});
|
|
175
430
|
try {
|
|
176
431
|
statusCode = await Promise.race([executionPromise, timeoutPromise]);
|
|
177
|
-
// Clear
|
|
432
|
+
// Clear timers on successful completion
|
|
178
433
|
if (timeoutHandle)
|
|
179
434
|
clearTimeout(timeoutHandle);
|
|
435
|
+
if (progressIntervalHandle)
|
|
436
|
+
clearInterval(progressIntervalHandle);
|
|
180
437
|
}
|
|
181
438
|
catch (error) {
|
|
182
|
-
// Clear
|
|
439
|
+
// Clear all timers on error
|
|
183
440
|
if (timeoutHandle)
|
|
184
441
|
clearTimeout(timeoutHandle);
|
|
442
|
+
if (progressIntervalHandle)
|
|
443
|
+
clearInterval(progressIntervalHandle);
|
|
185
444
|
// Cleanup container on timeout or other errors
|
|
186
445
|
if (containerRef !== null) {
|
|
187
446
|
const container = containerRef;
|
|
@@ -199,6 +458,12 @@ export class TestExecutionService {
|
|
|
199
458
|
}
|
|
200
459
|
throw error; // Re-throw the original error
|
|
201
460
|
}
|
|
461
|
+
// Report processing phase
|
|
462
|
+
await onProgress?.({
|
|
463
|
+
phase: "processing",
|
|
464
|
+
message: "Test execution completed, processing results...",
|
|
465
|
+
percent: 80,
|
|
466
|
+
});
|
|
202
467
|
const duration = Date.now() - startTime;
|
|
203
468
|
const cleanOutput = stripVTControlCharacters(output);
|
|
204
469
|
logger.info("Test execution completed", {
|
|
@@ -222,6 +487,14 @@ export class TestExecutionService {
|
|
|
222
487
|
output: cleanOutput,
|
|
223
488
|
exitCode: statusCode,
|
|
224
489
|
};
|
|
490
|
+
// Report completion
|
|
491
|
+
await onProgress?.({
|
|
492
|
+
phase: "processing",
|
|
493
|
+
message: result.passed
|
|
494
|
+
? "Test passed successfully"
|
|
495
|
+
: "Test execution completed with failures",
|
|
496
|
+
percent: 100,
|
|
497
|
+
});
|
|
225
498
|
logger.debug(`Test ${result.passed ? "passed" : "failed"}: ${options.testFile}`);
|
|
226
499
|
return result;
|
|
227
500
|
}
|
|
@@ -243,17 +516,25 @@ export class TestExecutionService {
|
|
|
243
516
|
}
|
|
244
517
|
/**
|
|
245
518
|
* Ensure Docker image is available
|
|
519
|
+
* @param onProgress Optional callback for progress updates during pull
|
|
520
|
+
* @returns Object indicating whether image was cached or pulled
|
|
246
521
|
*/
|
|
247
|
-
async ensureDockerImage() {
|
|
522
|
+
async ensureDockerImage(onProgress) {
|
|
248
523
|
try {
|
|
249
524
|
const images = await this.docker.listImages();
|
|
250
525
|
const imageExists = images.some((img) => (img.RepoTags && img.RepoTags.includes(EXECUTOR_DOCKER_IMAGE)) ||
|
|
251
526
|
(img.RepoDigests && img.RepoDigests.includes(EXECUTOR_DOCKER_IMAGE)));
|
|
252
527
|
if (imageExists) {
|
|
253
528
|
logger.debug(`Docker image ${EXECUTOR_DOCKER_IMAGE} already available`);
|
|
254
|
-
return;
|
|
529
|
+
return { cached: true };
|
|
255
530
|
}
|
|
256
531
|
logger.info(`Pulling Docker image ${EXECUTOR_DOCKER_IMAGE}...`);
|
|
532
|
+
await onProgress?.({
|
|
533
|
+
phase: "docker-pull",
|
|
534
|
+
message: `Pulling Docker image ${EXECUTOR_DOCKER_IMAGE}...`,
|
|
535
|
+
percent: 10,
|
|
536
|
+
details: { cached: false },
|
|
537
|
+
});
|
|
257
538
|
await new Promise((resolve, reject) => {
|
|
258
539
|
this.docker.pull(EXECUTOR_DOCKER_IMAGE, { platform: DOCKER_PLATFORM }, (err, stream) => {
|
|
259
540
|
if (err)
|
|
@@ -265,9 +546,39 @@ export class TestExecutionService {
|
|
|
265
546
|
return reject(err);
|
|
266
547
|
logger.info(`Docker image ${EXECUTOR_DOCKER_IMAGE} pulled successfully`);
|
|
267
548
|
resolve(res);
|
|
549
|
+
},
|
|
550
|
+
// Progress callback for each layer
|
|
551
|
+
(event) => {
|
|
552
|
+
if (event.status && onProgress) {
|
|
553
|
+
const progressMsg = event.progress || event.status;
|
|
554
|
+
const layerId = event.id || "";
|
|
555
|
+
// Calculate approximate progress (10-19% range during pull)
|
|
556
|
+
let percent = 10;
|
|
557
|
+
if (event.progressDetail?.current &&
|
|
558
|
+
event.progressDetail?.total) {
|
|
559
|
+
const layerProgress = event.progressDetail.current / event.progressDetail.total;
|
|
560
|
+
percent = 10 + Math.floor(layerProgress * 9); // 10-19%
|
|
561
|
+
}
|
|
562
|
+
onProgress({
|
|
563
|
+
phase: "docker-pull",
|
|
564
|
+
message: `${event.status}${layerId ? ` [${layerId}]` : ""}: ${progressMsg}`,
|
|
565
|
+
percent,
|
|
566
|
+
details: {
|
|
567
|
+
cached: false,
|
|
568
|
+
pullProgress: {
|
|
569
|
+
current: event.progressDetail?.current,
|
|
570
|
+
total: event.progressDetail?.total,
|
|
571
|
+
layer: layerId,
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
}).catch(() => {
|
|
575
|
+
// Ignore progress notification errors
|
|
576
|
+
});
|
|
577
|
+
}
|
|
268
578
|
});
|
|
269
579
|
});
|
|
270
580
|
});
|
|
581
|
+
return { cached: false };
|
|
271
582
|
}
|
|
272
583
|
catch (error) {
|
|
273
584
|
logger.error(`Failed to ensure Docker image: ${error.message}`);
|
|
@@ -323,7 +323,8 @@ export class TestHealthService {
|
|
|
323
323
|
// If execution data shows failure, escalate
|
|
324
324
|
if (execution && !execution.passed) {
|
|
325
325
|
priority = "HIGH";
|
|
326
|
-
rationale =
|
|
326
|
+
rationale =
|
|
327
|
+
"Drift analysis unavailable and test is failing - investigate immediately";
|
|
327
328
|
estimatedWork = "MEDIUM";
|
|
328
329
|
}
|
|
329
330
|
}
|
|
@@ -331,14 +332,16 @@ export class TestHealthService {
|
|
|
331
332
|
// No drift data but test is failing
|
|
332
333
|
action = "UPDATE";
|
|
333
334
|
priority = "HIGH";
|
|
334
|
-
rationale =
|
|
335
|
+
rationale =
|
|
336
|
+
"Test is failing but drift analysis unavailable - review test logic and dependencies";
|
|
335
337
|
estimatedWork = "MEDIUM";
|
|
336
338
|
}
|
|
337
339
|
else if (execution && execution.passed) {
|
|
338
340
|
// No drift data but test is passing
|
|
339
341
|
action = "VERIFY";
|
|
340
342
|
priority = "LOW";
|
|
341
|
-
rationale =
|
|
343
|
+
rationale =
|
|
344
|
+
"Drift analysis unavailable but test is passing - periodic verification recommended";
|
|
342
345
|
estimatedWork = "SMALL";
|
|
343
346
|
}
|
|
344
347
|
else {
|
|
@@ -566,10 +569,10 @@ export class TestHealthService {
|
|
|
566
569
|
}
|
|
567
570
|
else {
|
|
568
571
|
// This is a literal path part, escape special regex chars
|
|
569
|
-
return part.replace(/[.*+?^${}()|[\]\\]/g,
|
|
572
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
570
573
|
}
|
|
571
574
|
})
|
|
572
|
-
.join(
|
|
575
|
+
.join("");
|
|
573
576
|
// pathRegexString is already escaped, so we can use it directly
|
|
574
577
|
const pathRegex = new RegExp(pathRegexString);
|
|
575
578
|
const method = endpoint.method.toUpperCase();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SkyrampClient } from "@skyramp/skyramp";
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
|
+
const TOOL_NAME = "skyramp_login";
|
|
4
6
|
export function registerLoginTool(server) {
|
|
5
|
-
server.registerTool(
|
|
7
|
+
server.registerTool(TOOL_NAME, {
|
|
6
8
|
description: `Login to Skyramp platform
|
|
7
9
|
|
|
8
10
|
Logging into Skyramp provides access to additional platform features and services.
|
|
@@ -16,6 +18,7 @@ Logging into Skyramp provides access to additional platform features and service
|
|
|
16
18
|
keywords: ["login", "authenticate", "skyramp login"],
|
|
17
19
|
},
|
|
18
20
|
}, async (params) => {
|
|
21
|
+
let errorResult;
|
|
19
22
|
logger.info("Logging in to Skyramp", {
|
|
20
23
|
prompt: params.prompt,
|
|
21
24
|
});
|
|
@@ -34,7 +37,7 @@ Logging into Skyramp provides access to additional platform features and service
|
|
|
34
37
|
}
|
|
35
38
|
catch (error) {
|
|
36
39
|
const errorMessage = `Login to Skyramp failed: ${error.message}`;
|
|
37
|
-
|
|
40
|
+
errorResult = {
|
|
38
41
|
content: [
|
|
39
42
|
{
|
|
40
43
|
type: "text",
|
|
@@ -43,6 +46,13 @@ Logging into Skyramp provides access to additional platform features and service
|
|
|
43
46
|
],
|
|
44
47
|
isError: true,
|
|
45
48
|
};
|
|
49
|
+
return errorResult;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
const recordParams = {
|
|
53
|
+
prompt: params.prompt,
|
|
54
|
+
};
|
|
55
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
|
|
46
56
|
}
|
|
47
57
|
});
|
|
48
58
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SkyrampClient } from "@skyramp/skyramp";
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
|
+
const TOOL_NAME = "skyramp_logout";
|
|
4
6
|
export function registerLogoutTool(server) {
|
|
5
|
-
server.registerTool(
|
|
7
|
+
server.registerTool(TOOL_NAME, {
|
|
6
8
|
description: `Logout from Skyramp platform
|
|
7
9
|
|
|
8
10
|
Logout from Skyramp platform to end your authenticated session.`,
|
|
@@ -15,6 +17,7 @@ Logout from Skyramp platform to end your authenticated session.`,
|
|
|
15
17
|
keywords: ["logout", "sign out", "skyramp logout"],
|
|
16
18
|
},
|
|
17
19
|
}, async (params) => {
|
|
20
|
+
let errorResult;
|
|
18
21
|
logger.info("Logging out from Skyramp", {
|
|
19
22
|
prompt: params.prompt,
|
|
20
23
|
});
|
|
@@ -32,7 +35,7 @@ Logout from Skyramp platform to end your authenticated session.`,
|
|
|
32
35
|
}
|
|
33
36
|
catch (error) {
|
|
34
37
|
const errorMessage = `Logout from Skyramp failed: ${error.message}`;
|
|
35
|
-
|
|
38
|
+
errorResult = {
|
|
36
39
|
content: [
|
|
37
40
|
{
|
|
38
41
|
type: "text",
|
|
@@ -41,6 +44,13 @@ Logout from Skyramp platform to end your authenticated session.`,
|
|
|
41
44
|
],
|
|
42
45
|
isError: true,
|
|
43
46
|
};
|
|
47
|
+
return errorResult;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
const recordParams = {
|
|
51
|
+
prompt: params.prompt,
|
|
52
|
+
};
|
|
53
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, recordParams);
|
|
44
54
|
}
|
|
45
55
|
});
|
|
46
56
|
}
|
|
@@ -3,6 +3,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
3
3
|
import { getCodeReusePrompt } from "../../prompts/code-reuse.js";
|
|
4
4
|
import { codeRefactoringSchema, languageSchema, } from "../../types/TestTypes.js";
|
|
5
5
|
import { SKYRAMP_UTILS_HEADER } from "../../utils/utils.js";
|
|
6
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
6
7
|
const codeReuseSchema = z.object({
|
|
7
8
|
testFile: z
|
|
8
9
|
.string()
|
|
@@ -14,8 +15,9 @@ const codeReuseSchema = z.object({
|
|
|
14
15
|
.string()
|
|
15
16
|
.describe("The code content to analyze and optimize for code reuse"),
|
|
16
17
|
});
|
|
18
|
+
const TOOL_NAME = "skyramp_reuse_code";
|
|
17
19
|
export function registerCodeReuseTool(server) {
|
|
18
|
-
server.registerTool(
|
|
20
|
+
server.registerTool(TOOL_NAME, {
|
|
19
21
|
description: `Analyzes code for reuse opportunities and enforces code reuse principles.
|
|
20
22
|
|
|
21
23
|
This tool helps identify and reuse ONLY EXISTING helper functions from other test files (grep based on "${SKYRAMP_UTILS_HEADER}").
|
|
@@ -58,19 +60,43 @@ export function registerCodeReuseTool(server) {
|
|
|
58
60
|
],
|
|
59
61
|
},
|
|
60
62
|
}, async (params) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
let errorResult;
|
|
64
|
+
try {
|
|
65
|
+
logger.info("Analyzing code for reuse opportunities", {
|
|
66
|
+
testFile: params.testFile,
|
|
67
|
+
language: params.language,
|
|
68
|
+
framework: params.framework,
|
|
69
|
+
});
|
|
70
|
+
const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language);
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: codeReusePrompt,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const errorMessage = `Code reuse analysis failed: ${error.message}`;
|
|
82
|
+
errorResult = {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: errorMessage,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
return errorResult;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
95
|
+
prompt: params.prompt,
|
|
96
|
+
testFile: params.testFile,
|
|
97
|
+
language: params.language,
|
|
98
|
+
framework: params.framework,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
75
101
|
});
|
|
76
102
|
}
|