@skyramp/mcp 0.0.43 → 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.
Files changed (40) hide show
  1. package/build/index.js +15 -0
  2. package/build/prompts/code-reuse.js +1 -1
  3. package/build/prompts/driftAnalysisPrompt.js +159 -0
  4. package/build/prompts/modularization/ui-test-modularization.js +2 -0
  5. package/build/prompts/testGenerationPrompt.js +2 -2
  6. package/build/prompts/testHealthPrompt.js +82 -0
  7. package/build/services/DriftAnalysisService.js +924 -0
  8. package/build/services/ModularizationService.js +16 -1
  9. package/build/services/TestDiscoveryService.js +237 -0
  10. package/build/services/TestExecutionService.js +311 -0
  11. package/build/services/TestGenerationService.js +16 -2
  12. package/build/services/TestHealthService.js +653 -0
  13. package/build/tools/auth/loginTool.js +1 -1
  14. package/build/tools/auth/logoutTool.js +1 -1
  15. package/build/tools/code-refactor/codeReuseTool.js +5 -3
  16. package/build/tools/code-refactor/modularizationTool.js +8 -2
  17. package/build/tools/executeSkyrampTestTool.js +12 -122
  18. package/build/tools/fixErrorTool.js +1 -1
  19. package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
  20. package/build/tools/generate-tests/generateFuzzRestTool.js +1 -1
  21. package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
  22. package/build/tools/generate-tests/generateSmokeRestTool.js +1 -1
  23. package/build/tools/generate-tests/generateUIRestTool.js +1 -1
  24. package/build/tools/test-maintenance/actionsTool.js +202 -0
  25. package/build/tools/test-maintenance/analyzeTestDriftTool.js +188 -0
  26. package/build/tools/test-maintenance/calculateHealthScoresTool.js +248 -0
  27. package/build/tools/test-maintenance/discoverTestsTool.js +135 -0
  28. package/build/tools/test-maintenance/executeBatchTestsTool.js +188 -0
  29. package/build/tools/test-maintenance/stateCleanupTool.js +145 -0
  30. package/build/tools/test-recommendation/analyzeRepositoryTool.js +16 -1
  31. package/build/tools/test-recommendation/recommendTestsTool.js +6 -2
  32. package/build/tools/trace/startTraceCollectionTool.js +1 -1
  33. package/build/tools/trace/stopTraceCollectionTool.js +1 -1
  34. package/build/types/TestAnalysis.js +1 -0
  35. package/build/types/TestDriftAnalysis.js +1 -0
  36. package/build/types/TestExecution.js +6 -0
  37. package/build/types/TestHealth.js +4 -0
  38. package/build/utils/AnalysisStateManager.js +238 -0
  39. package/build/utils/utils.test.js +25 -9
  40. package/package.json +6 -3
@@ -9,8 +9,23 @@ export class ModularizationService {
9
9
  testType: params.testType,
10
10
  language: params.language,
11
11
  });
12
- let prompt = "";
13
12
  const testType = params.testType;
13
+ // Check if the test type is one that should not be modularized
14
+ if (!params.isTraceBased) {
15
+ return {
16
+ content: [
17
+ {
18
+ type: "text",
19
+ text: `⚠️ **MODULARIZATION NOT APPLICABLE**
20
+
21
+ Modularization is intentionally NOT applied to ${testType.toUpperCase()} tests if the test is not generated from traces.
22
+ The test file \`${params.testFile}\` will remain unchanged. No further action is needed.`,
23
+ },
24
+ ],
25
+ isError: false,
26
+ };
27
+ }
28
+ let prompt = "";
14
29
  switch (testType) {
15
30
  case TestType.UI:
16
31
  prompt = getModularizationPromptForUI(params.testFile);
@@ -0,0 +1,237 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { simpleGit } from "simple-git";
4
+ import { logger } from "../utils/logger.js";
5
+ import fg from "fast-glob";
6
+ export class TestDiscoveryService {
7
+ EXCLUDED_DIRS = [
8
+ "node_modules",
9
+ "venv",
10
+ ".venv",
11
+ "build",
12
+ "dist",
13
+ ".git",
14
+ "__pycache__",
15
+ ".pytest_cache",
16
+ "coverage",
17
+ ".next",
18
+ "out",
19
+ "target",
20
+ ];
21
+ SKYRAMP_MARKER = "Generated by Skyramp";
22
+ // Supported test file extensions
23
+ SUPPORTED_EXTENSIONS = [".py", ".js", ".ts", ".java"];
24
+ // Concurrency control for parallel operations
25
+ MAX_CONCURRENT_OPERATIONS = 10;
26
+ // Cache git client and repo status per repository
27
+ gitClientCache = new Map();
28
+ isGitRepoCache = new Map();
29
+ /**
30
+ * Discover all Skyramp tests in a repository
31
+ * Uses fast-glob for cross-platform file scanning
32
+ */
33
+ async discoverTests(repositoryPath) {
34
+ logger.info(`Starting test discovery in: ${repositoryPath}`);
35
+ if (!fs.existsSync(repositoryPath)) {
36
+ throw new Error(`Repository path does not exist: ${repositoryPath}`);
37
+ }
38
+ const stats = fs.statSync(repositoryPath);
39
+ if (!stats.isDirectory()) {
40
+ throw new Error(`Path is not a directory: ${repositoryPath}`);
41
+ }
42
+ // Initialize git client cache for this repository
43
+ await this.initializeGitClient(repositoryPath);
44
+ // Use cross-platform file search to find files containing Skyramp marker
45
+ const testFiles = this.findSkyrampTestsWithGrep(repositoryPath);
46
+ logger.info(`Found ${testFiles.length} Skyramp test files`);
47
+ // Process files in parallel with concurrency control
48
+ const skyrampTests = await this.processFilesInBatches(testFiles, repositoryPath);
49
+ logger.info(`Discovered ${skyrampTests.length} Skyramp tests`);
50
+ // Clean up cache to free memory
51
+ this.gitClientCache.clear();
52
+ this.isGitRepoCache.clear();
53
+ return {
54
+ tests: skyrampTests,
55
+ };
56
+ }
57
+ /**
58
+ * Initialize git client and check if repository is a git repo
59
+ */
60
+ async initializeGitClient(repositoryPath) {
61
+ try {
62
+ const git = simpleGit(repositoryPath);
63
+ this.gitClientCache.set(repositoryPath, git);
64
+ const isRepo = await git.checkIsRepo();
65
+ this.isGitRepoCache.set(repositoryPath, isRepo);
66
+ if (isRepo) {
67
+ logger.debug(`Git repository detected at: ${repositoryPath}`);
68
+ }
69
+ else {
70
+ logger.debug(`Not a git repository: ${repositoryPath}`);
71
+ }
72
+ }
73
+ catch (error) {
74
+ logger.debug(`Could not initialize git client: ${error.message}`);
75
+ this.isGitRepoCache.set(repositoryPath, false);
76
+ }
77
+ }
78
+ /**
79
+ * Process test files in parallel batches with concurrency control
80
+ */
81
+ async processFilesInBatches(testFiles, repositoryPath) {
82
+ const results = [];
83
+ // Process files in batches to control concurrency
84
+ for (let i = 0; i < testFiles.length; i += this.MAX_CONCURRENT_OPERATIONS) {
85
+ const batch = testFiles.slice(i, i + this.MAX_CONCURRENT_OPERATIONS);
86
+ const batchResults = await Promise.all(batch.map(async (testFile) => {
87
+ try {
88
+ return await this.extractTestMetadata(testFile, repositoryPath);
89
+ }
90
+ catch (error) {
91
+ logger.error(`Error processing test file ${testFile}: ${error}`);
92
+ return null;
93
+ }
94
+ }));
95
+ // Filter out null results and add to final results
96
+ results.push(...batchResults.filter((test) => test !== null));
97
+ }
98
+ return results;
99
+ }
100
+ /**
101
+ * Find files containing Skyramp marker using cross-platform Node.js file search
102
+ */
103
+ findSkyrampTestsWithGrep(repositoryPath) {
104
+ try {
105
+ // Build glob patterns for supported extensions
106
+ const globPatterns = this.SUPPORTED_EXTENSIONS.map((ext) => `**/*${ext}`);
107
+ // Build ignore patterns for excluded directories
108
+ const ignorePatterns = this.EXCLUDED_DIRS.map((dir) => `**/${dir}/**`);
109
+ // Use fast-glob to find all matching files
110
+ const allFiles = fg.sync(globPatterns, {
111
+ cwd: repositoryPath,
112
+ ignore: ignorePatterns,
113
+ absolute: true,
114
+ caseSensitiveMatch: false,
115
+ });
116
+ logger.debug(`Found ${allFiles.length} candidate files to search`);
117
+ // Read files and check for Skyramp marker
118
+ const matchingFiles = [];
119
+ const marker = this.SKYRAMP_MARKER;
120
+ // Process files in batches to avoid memory issues
121
+ const batchSize = 100;
122
+ for (let i = 0; i < allFiles.length; i += batchSize) {
123
+ const batch = allFiles.slice(i, i + batchSize);
124
+ const batchResults = batch.filter((file) => {
125
+ try {
126
+ // Read file and check for marker
127
+ const content = fs.readFileSync(file, "utf-8");
128
+ return content.includes(marker);
129
+ }
130
+ catch (error) {
131
+ // Skip files that can't be read (permissions, etc.)
132
+ logger.debug(`Skipping file ${file}: ${error}`);
133
+ return false;
134
+ }
135
+ });
136
+ matchingFiles.push(...batchResults);
137
+ }
138
+ logger.debug(`Found ${matchingFiles.length} files with Skyramp marker`);
139
+ return matchingFiles;
140
+ }
141
+ catch (error) {
142
+ logger.error(`File search failed: ${error.message}`);
143
+ logger.info("Falling back to directory scanning method");
144
+ return [];
145
+ }
146
+ }
147
+ /**
148
+ * Extract metadata from a test file
149
+ * File is already confirmed to contain Skyramp marker by file search
150
+ */
151
+ async extractTestMetadata(testFile, repositoryPath) {
152
+ let content;
153
+ try {
154
+ // Read full file (file search already confirmed it has Skyramp marker)
155
+ content = fs.readFileSync(testFile, "utf-8");
156
+ }
157
+ catch (error) {
158
+ logger.debug(`Could not read file ${testFile}: ${error}`);
159
+ return null;
160
+ }
161
+ const language = this.detectLanguage(testFile);
162
+ const testType = this.detectTestType(content, testFile);
163
+ const apiSchema = this.extractApiSchema(content);
164
+ const framework = this.extractFramework(content);
165
+ return {
166
+ testFile,
167
+ testType,
168
+ language,
169
+ framework,
170
+ apiSchema,
171
+ };
172
+ }
173
+ /**
174
+ * Detect programming language from file extension
175
+ */
176
+ detectLanguage(testFile) {
177
+ const ext = path.extname(testFile);
178
+ const languageMap = {
179
+ ".py": "python",
180
+ ".js": "javascript",
181
+ ".ts": "typescript",
182
+ ".java": "java",
183
+ };
184
+ return languageMap[ext] || "unknown";
185
+ }
186
+ /**
187
+ * Detect test type from file content and name
188
+ * Checks Skyramp command line, filename, and content patterns
189
+ */
190
+ detectTestType(content, testFile) {
191
+ const testTypes = [
192
+ "smoke",
193
+ "integration",
194
+ "load",
195
+ "contract",
196
+ "fuzz",
197
+ "e2e",
198
+ "ui",
199
+ ];
200
+ // First, try to extract from Skyramp command line
201
+ // Pattern: skyramp generate smoke rest ...
202
+ const commandMatch = content.match(/skyramp generate (\w+)/i);
203
+ if (commandMatch && commandMatch[1]) {
204
+ const type = commandMatch[1].toLowerCase();
205
+ if (testTypes.includes(type)) {
206
+ return type;
207
+ }
208
+ }
209
+ return "";
210
+ }
211
+ /**
212
+ * Extract API schema path from test content
213
+ * Looks for Skyramp command line first, then fallback to other patterns
214
+ */
215
+ extractApiSchema(content) {
216
+ // First, try to extract from Skyramp command line
217
+ // Pattern: --api-schema http://localhost:8000/openapi.json
218
+ const apiSchemaMatch = content.match(/--api-schema\s+([^\s\\]+)/);
219
+ if (apiSchemaMatch && apiSchemaMatch[1]) {
220
+ return apiSchemaMatch[1];
221
+ }
222
+ return "";
223
+ }
224
+ /**
225
+ * Extract Framework from test content
226
+ * Looks for Skyramp command line first, then fallback to other patterns
227
+ */
228
+ extractFramework(content) {
229
+ // First, try to extract from Skyramp command line
230
+ // Pattern: --framework pytest
231
+ const frameworkMatch = content.match(/--framework\s+([^\s\\]+)/);
232
+ if (frameworkMatch && frameworkMatch[1]) {
233
+ return frameworkMatch[1];
234
+ }
235
+ return "";
236
+ }
237
+ }
@@ -0,0 +1,311 @@
1
+ import Docker from "dockerode";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { Writable } from "stream";
5
+ import { stripVTControlCharacters } from "util";
6
+ import { logger } from "../utils/logger.js";
7
+ const DEFAULT_TIMEOUT = 300000; // 5 minutes
8
+ const MAX_CONCURRENT_EXECUTIONS = 5;
9
+ const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.2.38";
10
+ const DOCKER_PLATFORM = "linux/amd64";
11
+ // Files and directories to exclude when mounting workspace to Docker container
12
+ export const EXCLUDED_MOUNT_ITEMS = [
13
+ "package-lock.json",
14
+ "package.json",
15
+ "node_modules",
16
+ ];
17
+ export class TestExecutionService {
18
+ docker;
19
+ imageReady;
20
+ constructor() {
21
+ this.docker = new Docker();
22
+ this.imageReady = this.ensureDockerImage();
23
+ }
24
+ /**
25
+ * Execute multiple tests in parallel batches
26
+ */
27
+ async executeBatch(testOptions) {
28
+ logger.info(`Starting batch execution of ${testOptions.length} tests`);
29
+ const startTime = Date.now();
30
+ const results = [];
31
+ // Execute tests in batches to control concurrency
32
+ for (let i = 0; i < testOptions.length; i += MAX_CONCURRENT_EXECUTIONS) {
33
+ const batch = testOptions.slice(i, i + MAX_CONCURRENT_EXECUTIONS);
34
+ logger.debug(`Executing batch ${Math.floor(i / MAX_CONCURRENT_EXECUTIONS) + 1} (${batch.length} tests)`);
35
+ const batchPromises = batch.map((options) => this.executeTest(options).catch((error) => {
36
+ // If execution fails completely, return error result
37
+ return {
38
+ testFile: options.testFile,
39
+ passed: false,
40
+ executedAt: new Date().toISOString(),
41
+ duration: 0,
42
+ errors: [error.message],
43
+ warnings: [],
44
+ crashed: true,
45
+ };
46
+ }));
47
+ const batchResults = await Promise.all(batchPromises);
48
+ results.push(...batchResults);
49
+ }
50
+ const totalDuration = Date.now() - startTime;
51
+ const passed = results.filter((r) => r.passed).length;
52
+ const failed = results.filter((r) => !r.passed && !r.crashed).length;
53
+ const crashed = results.filter((r) => r.crashed).length;
54
+ logger.info(`Batch execution complete: ${passed} passed, ${failed} failed, ${crashed} crashed`);
55
+ return {
56
+ totalTests: testOptions.length,
57
+ passed,
58
+ failed,
59
+ crashed,
60
+ totalDuration,
61
+ results,
62
+ };
63
+ }
64
+ /**
65
+ * Execute a single test
66
+ */
67
+ async executeTest(options) {
68
+ const startTime = Date.now();
69
+ const executedAt = new Date().toISOString();
70
+ logger.debug(`Executing test: ${options.testFile}`);
71
+ // Wait for Docker image to be ready
72
+ await this.imageReady;
73
+ // Validate workspace path - use accessSync for better validation
74
+ const workspacePath = path.resolve(options.workspacePath);
75
+ try {
76
+ fs.accessSync(workspacePath, fs.constants.R_OK);
77
+ }
78
+ catch (err) {
79
+ throw new Error(`Workspace path does not exist or is not readable: ${workspacePath}`);
80
+ }
81
+ // Validate test file - use basename for safer filename extraction
82
+ const filename = path.basename(options.testFile);
83
+ if (!filename) {
84
+ throw new Error("Invalid test file path: could not extract filename");
85
+ }
86
+ if (!fs.existsSync(options.testFile)) {
87
+ throw new Error(`Test file does not exist: ${options.testFile}`);
88
+ }
89
+ const containerMountPath = "/home/user";
90
+ const dockerSocketPath = "/var/run/docker.sock";
91
+ // Calculate relative test file path
92
+ let testFilePath = path.relative(workspacePath, options.testFile);
93
+ testFilePath = path.resolve(containerMountPath, testFilePath);
94
+ // Prepare Docker command
95
+ const command = [
96
+ "./root/runner.sh",
97
+ options.language,
98
+ testFilePath,
99
+ options.testType,
100
+ ];
101
+ // Prepare host config with mounts
102
+ const hostConfig = {
103
+ ExtraHosts: ["host.docker.internal:host-gateway"],
104
+ Mounts: [
105
+ {
106
+ Type: "bind",
107
+ Target: dockerSocketPath,
108
+ Source: dockerSocketPath,
109
+ },
110
+ ],
111
+ };
112
+ // Mount workspace files (excluding unnecessary items)
113
+ const workspaceFiles = fs.readdirSync(workspacePath);
114
+ const filesToMount = workspaceFiles.filter((file) => !EXCLUDED_MOUNT_ITEMS.includes(file));
115
+ hostConfig.Mounts?.push(...filesToMount.map((file) => ({
116
+ Type: "bind",
117
+ Target: path.join(containerMountPath, file),
118
+ Source: path.join(workspacePath, file),
119
+ })));
120
+ // Prepare environment variables
121
+ const env = [
122
+ `SKYRAMP_TEST_TOKEN=${options.token || ""}`,
123
+ "SKYRAMP_IN_DOCKER=true",
124
+ ];
125
+ if (process.env.SKYRAMP_DEBUG) {
126
+ env.push(`SKYRAMP_DEBUG=${process.env.SKYRAMP_DEBUG}`);
127
+ }
128
+ if (process.env.API_KEY) {
129
+ env.push(`API_KEY=${process.env.API_KEY}`);
130
+ }
131
+ // Capture output
132
+ let output = "";
133
+ class DockerStream extends Writable {
134
+ _write(data, encode, cb) {
135
+ output += data.toString();
136
+ cb();
137
+ }
138
+ }
139
+ const stream = new DockerStream();
140
+ try {
141
+ let statusCode = 0;
142
+ let containerRef = null;
143
+ // Run container with timeout
144
+ const executionPromise = this.docker
145
+ .run(EXECUTOR_DOCKER_IMAGE, command, stream, {
146
+ Env: env,
147
+ HostConfig: hostConfig,
148
+ })
149
+ .then(function (data) {
150
+ const result = data[0];
151
+ const container = data[1];
152
+ containerRef = container; // Capture container reference for cleanup
153
+ stream.end();
154
+ statusCode = result.StatusCode;
155
+ logger.debug("Docker container execution completed");
156
+ return container.remove();
157
+ })
158
+ .then(function () {
159
+ logger.debug("Docker container removed successfully");
160
+ containerRef = null; // Container already removed
161
+ return statusCode;
162
+ })
163
+ .catch(function (err) {
164
+ logger.error("Docker container execution failed", {
165
+ error: err.message,
166
+ });
167
+ throw err;
168
+ });
169
+ // Apply timeout
170
+ const timeout = options.timeout || DEFAULT_TIMEOUT;
171
+ let timeoutHandle;
172
+ const timeoutPromise = new Promise((_, reject) => {
173
+ timeoutHandle = setTimeout(() => reject(new Error(`Test execution timeout after ${timeout}ms`)), timeout);
174
+ });
175
+ try {
176
+ statusCode = await Promise.race([executionPromise, timeoutPromise]);
177
+ // Clear the timeout timer if execution completes successfully
178
+ if (timeoutHandle)
179
+ clearTimeout(timeoutHandle);
180
+ }
181
+ catch (error) {
182
+ // Clear the timeout timer on any error
183
+ if (timeoutHandle)
184
+ clearTimeout(timeoutHandle);
185
+ // Cleanup container on timeout or other errors
186
+ if (containerRef !== null) {
187
+ const container = containerRef;
188
+ try {
189
+ logger.warning("Cleaning up orphaned Docker container after error");
190
+ await container.stop({ t: 5 }); // Give 5 seconds to stop gracefully
191
+ await container.remove({ force: true });
192
+ logger.debug("Orphaned container cleaned up successfully");
193
+ }
194
+ catch (cleanupError) {
195
+ logger.error("Failed to cleanup orphaned container", {
196
+ error: cleanupError.message,
197
+ });
198
+ }
199
+ }
200
+ throw error; // Re-throw the original error
201
+ }
202
+ const duration = Date.now() - startTime;
203
+ const cleanOutput = stripVTControlCharacters(output);
204
+ logger.info("Test execution completed", {
205
+ output: cleanOutput.substring(0, 200) +
206
+ (cleanOutput.length > 200 ? "..." : ""),
207
+ statusCode,
208
+ });
209
+ // Parse errors and warnings from output
210
+ const errors = this.parseErrors(cleanOutput);
211
+ const warnings = this.parseWarnings(cleanOutput);
212
+ // Check if test crashed
213
+ const crashed = statusCode !== 0 && statusCode !== 1;
214
+ const result = {
215
+ testFile: options.testFile,
216
+ passed: statusCode === 0,
217
+ executedAt,
218
+ duration,
219
+ errors,
220
+ warnings,
221
+ crashed,
222
+ output: cleanOutput,
223
+ exitCode: statusCode,
224
+ };
225
+ logger.debug(`Test ${result.passed ? "passed" : "failed"}: ${options.testFile}`);
226
+ return result;
227
+ }
228
+ catch (error) {
229
+ const duration = Date.now() - startTime;
230
+ const cleanOutput = stripVTControlCharacters(output);
231
+ logger.error(`Test execution error: ${error.message}`);
232
+ return {
233
+ testFile: options.testFile,
234
+ passed: false,
235
+ executedAt,
236
+ duration,
237
+ errors: [error.message],
238
+ warnings: [],
239
+ crashed: true,
240
+ output: cleanOutput,
241
+ };
242
+ }
243
+ }
244
+ /**
245
+ * Ensure Docker image is available
246
+ */
247
+ async ensureDockerImage() {
248
+ try {
249
+ const images = await this.docker.listImages();
250
+ const imageExists = images.some((img) => (img.RepoTags && img.RepoTags.includes(EXECUTOR_DOCKER_IMAGE)) ||
251
+ (img.RepoDigests && img.RepoDigests.includes(EXECUTOR_DOCKER_IMAGE)));
252
+ if (imageExists) {
253
+ logger.debug(`Docker image ${EXECUTOR_DOCKER_IMAGE} already available`);
254
+ return;
255
+ }
256
+ logger.info(`Pulling Docker image ${EXECUTOR_DOCKER_IMAGE}...`);
257
+ await new Promise((resolve, reject) => {
258
+ this.docker.pull(EXECUTOR_DOCKER_IMAGE, { platform: DOCKER_PLATFORM }, (err, stream) => {
259
+ if (err)
260
+ return reject(err);
261
+ if (!stream)
262
+ return reject(new Error("No stream received from docker pull"));
263
+ this.docker.modem.followProgress(stream, (err, res) => {
264
+ if (err)
265
+ return reject(err);
266
+ logger.info(`Docker image ${EXECUTOR_DOCKER_IMAGE} pulled successfully`);
267
+ resolve(res);
268
+ });
269
+ });
270
+ });
271
+ }
272
+ catch (error) {
273
+ logger.error(`Failed to ensure Docker image: ${error.message}`);
274
+ throw new Error(`Docker image setup failed: ${error.message}`);
275
+ }
276
+ }
277
+ /**
278
+ * Parse errors from test output
279
+ */
280
+ parseErrors(output) {
281
+ const errors = [];
282
+ const lines = output.split("\n");
283
+ for (const line of lines) {
284
+ // Common error patterns
285
+ if (line.includes("Error:") ||
286
+ line.includes("ERROR") ||
287
+ line.includes("AssertionError") ||
288
+ line.includes("Exception") ||
289
+ line.includes("FAILED") ||
290
+ line.includes("Traceback")) {
291
+ errors.push(line.trim());
292
+ }
293
+ }
294
+ return errors.slice(0, 20); // Limit to first 20 errors
295
+ }
296
+ /**
297
+ * Parse warnings from test output
298
+ */
299
+ parseWarnings(output) {
300
+ const warnings = [];
301
+ const lines = output.split("\n");
302
+ for (const line of lines) {
303
+ if (line.includes("Warning:") ||
304
+ line.includes("WARN") ||
305
+ line.includes("Deprecated")) {
306
+ warnings.push(line.trim());
307
+ }
308
+ }
309
+ return warnings.slice(0, 10); // Limit to first 10 warnings
310
+ }
311
+ }
@@ -44,12 +44,26 @@ export class TestGenerationService {
44
44
  `;
45
45
  }
46
46
  else if (params.modularizeCode) {
47
- // Only modularization
48
- postGenerationMessage += `
47
+ // Check if test type should be modularized
48
+ if (!params.trace && !params.scenarioFile && !params.playwrightInput) {
49
+ postGenerationMessage += `
50
+ Test generated successfully!
51
+
52
+ **NOTE**: Modularization is intentionally NOT applied to ${testType.toUpperCase()} tests.
53
+ ${testType.toUpperCase()} tests are designed to be simple, straightforward, and deterministic.
54
+ Modularization can introduce unpredictable results and unnecessary complexity for these test types.
55
+
56
+ The generated test file remains unchanged and ready to use as-is.
57
+ `;
58
+ }
59
+ else {
60
+ // Only modularization for UI, E2E, Integration tests
61
+ postGenerationMessage += `
49
62
  Test generated successfully!
50
63
 
51
64
  ⏭️**CRITICAL NEXT STEP**: Running modularization with skyramp_modularization tool.
52
65
  `;
66
+ }
53
67
  }
54
68
  return {
55
69
  content: [