@skyramp/mcp 0.1.8 → 0.2.0-rc.2

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 (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -1,694 +0,0 @@
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, drift?.apiSchemaChanges);
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 endpointsRenamed = drift.changes.filter((c) => c.type === "endpoint_renamed");
269
- if (endpointsRenamed.length > 0) {
270
- issues.push({
271
- type: "endpoints_renamed",
272
- severity: "high",
273
- description: `${endpointsRenamed.length} API endpoint(s) renamed`,
274
- details: endpointsRenamed.map((c) => c.description).join("; "),
275
- });
276
- }
277
- const endpointsRemoved = drift.changes.filter((c) => c.type === "endpoint_removed");
278
- if (endpointsRemoved.length > 0) {
279
- issues.push({
280
- type: "endpoints_removed",
281
- severity: "high",
282
- description: `${endpointsRemoved.length} API endpoint(s) removed`,
283
- details: endpointsRemoved.map((c) => c.description).join("; "),
284
- });
285
- }
286
- const schemaChanges = drift.changes.filter((c) => ["endpoint_modified", "authentication_changed"].includes(c.type));
287
- if (schemaChanges.length > 0) {
288
- issues.push({
289
- type: "schema_changes",
290
- severity: "high",
291
- description: "API schema changes detected",
292
- details: schemaChanges.map((c) => c.description).join("; "),
293
- });
294
- }
295
- const authChanged = drift.changes.some((c) => c.type === "authentication_changed");
296
- if (authChanged) {
297
- issues.push({
298
- type: "authentication_changed",
299
- severity: "critical",
300
- description: "Authentication mechanism changed",
301
- });
302
- }
303
- }
304
- return issues;
305
- }
306
- /**
307
- * Generate recommendation for a test
308
- *
309
- * Recommendation logic (primarily drift-based):
310
- * 1. IF drift > 70: REGENERATE (HIGH priority)
311
- * 2. ELSE IF endpoint missing: DELETE (HIGH priority)
312
- * 3. ELSE IF 30 < drift <= 70: UPDATE (MEDIUM priority)
313
- * 4. ELSE IF drift > 10: VERIFY (LOW priority)
314
- * 5. ELSE: VERIFY (LOW priority, healthy test)
315
- *
316
- * Execution failures enhance rationale but don't change primary action
317
- */
318
- generateRecommendation(testFile, healthScore, driftScore, execution, issues, apiEndpoint, apiSchemaChanges) {
319
- const drift = driftScore !== undefined ? driftScore : -1; // -1 means no drift data
320
- let action;
321
- let priority;
322
- let rationale;
323
- let estimatedWork = "SMALL";
324
- // Handle missing drift data first
325
- if (drift === -1) {
326
- // No drift data available - base recommendation on health status and execution
327
- if (healthScore.status === "unknown") {
328
- action = "VERIFY";
329
- priority = "MEDIUM";
330
- rationale = "Unable to analyze drift - manual verification recommended";
331
- estimatedWork = "SMALL";
332
- // If execution data shows failure, escalate
333
- if (execution && !execution.passed) {
334
- priority = "HIGH";
335
- rationale =
336
- "Drift analysis unavailable and test is failing - investigate immediately";
337
- estimatedWork = "MEDIUM";
338
- }
339
- }
340
- else if (execution && !execution.passed) {
341
- // No drift data but test is failing
342
- action = "UPDATE";
343
- priority = "HIGH";
344
- rationale =
345
- "Test is failing but drift analysis unavailable - review test logic and dependencies";
346
- estimatedWork = "MEDIUM";
347
- }
348
- else if (execution && execution.passed) {
349
- // No drift data but test is passing
350
- action = "VERIFY";
351
- priority = "LOW";
352
- rationale =
353
- "Drift analysis unavailable but test is passing - periodic verification recommended";
354
- estimatedWork = "SMALL";
355
- }
356
- else {
357
- // No drift data, no execution data
358
- action = "VERIFY";
359
- priority = "MEDIUM";
360
- rationale = "No drift or execution data available - analysis needed";
361
- estimatedWork = "SMALL";
362
- }
363
- }
364
- else if (issues && issues.some((i) => i.type === "endpoints_renamed")) {
365
- // Endpoint renamed -> UPDATE with path substitution (regardless of drift score)
366
- action = "UPDATE";
367
- priority = "HIGH";
368
- rationale =
369
- "Endpoint path renamed - test URLs must be updated to match new path";
370
- estimatedWork = "SMALL";
371
- const renameIssue = issues.find((i) => i.type === "endpoints_renamed");
372
- if (renameIssue?.details) {
373
- rationale += `. ${renameIssue.details}`;
374
- }
375
- if (execution && !execution.passed) {
376
- rationale += ". Test is currently failing due to the path change";
377
- }
378
- }
379
- else if (drift > 70) {
380
- // High drift -> REGENERATE
381
- action = "REGENERATE";
382
- priority = "HIGH";
383
- rationale =
384
- "High drift detected - significant code changes since test creation";
385
- estimatedWork = "MEDIUM";
386
- // Enhance rationale with test failures if present
387
- if (execution && !execution.passed) {
388
- rationale += ". Test is also failing";
389
- }
390
- // Add specific issues
391
- if (issues && issues.length > 0) {
392
- const criticalIssues = issues.filter((i) => ["critical", "high"].includes(i.severity));
393
- if (criticalIssues.length > 0) {
394
- rationale += `. Critical issues: ${criticalIssues
395
- .map((i) => i.description)
396
- .join(", ")}`;
397
- }
398
- }
399
- }
400
- else if (apiEndpoint?.exists === false) {
401
- // Endpoint removed from schema -> DELETE
402
- action = "DELETE";
403
- priority = "HIGH";
404
- rationale = "Endpoint no longer exists in API schema";
405
- estimatedWork = "SMALL";
406
- }
407
- else if (drift > 30 && drift <= 70) {
408
- // Moderate drift -> UPDATE
409
- action = "UPDATE";
410
- priority = "MEDIUM";
411
- rationale =
412
- "Moderate drift detected - related code changes may affect test";
413
- estimatedWork = "SMALL";
414
- // Enhance with schema changes
415
- const schemaChanges = issues?.filter((i) => [
416
- "schema_changes",
417
- "endpoints_removed",
418
- "endpoints_renamed",
419
- "authentication_changed",
420
- ].includes(i.type));
421
- if (schemaChanges && schemaChanges.length > 0) {
422
- rationale += `. Schema changes: ${schemaChanges
423
- .map((i) => i.description)
424
- .join(", ")}`;
425
- estimatedWork = "MEDIUM";
426
- }
427
- // Note execution failures
428
- if (execution && !execution.passed) {
429
- rationale += ". Test is also failing - requires immediate attention";
430
- priority = "HIGH"; // Escalate priority for failing tests
431
- }
432
- }
433
- else if (drift > 10) {
434
- // Low drift -> VERIFY
435
- action = "VERIFY";
436
- priority = "LOW";
437
- rationale = "Minor changes detected - test likely still valid";
438
- estimatedWork = "SMALL";
439
- // Escalate to UPDATE if test is actually failing
440
- if (execution) {
441
- if (!execution.passed) {
442
- action = "UPDATE";
443
- priority = "HIGH";
444
- rationale =
445
- "Minor drift but test is failing - update needed to match code changes";
446
- estimatedWork = "MEDIUM";
447
- }
448
- else if (execution.warnings.length > 0) {
449
- rationale += ". Test passed with warnings";
450
- }
451
- }
452
- }
453
- else {
454
- // Minimal/no drift -> VERIFY
455
- action = "VERIFY";
456
- priority = "LOW";
457
- rationale = "Test appears healthy - periodic verification recommended";
458
- estimatedWork = "SMALL";
459
- // Handle edge case: low drift but test is failing
460
- if (execution && !execution.passed) {
461
- action = "UPDATE";
462
- priority = "HIGH";
463
- rationale =
464
- "Test is failing despite low drift - may indicate environmental issues or test flakiness";
465
- estimatedWork = "MEDIUM";
466
- }
467
- }
468
- // Determine endpoint status
469
- let endpointStatus;
470
- const renameIssues = issues?.filter((i) => i.type === "endpoints_renamed");
471
- if (renameIssues && renameIssues.length > 0) {
472
- endpointStatus = "renamed";
473
- }
474
- else if (apiEndpoint === undefined) {
475
- endpointStatus = undefined;
476
- }
477
- else if (apiEndpoint.exists) {
478
- endpointStatus = "exists";
479
- }
480
- else {
481
- endpointStatus = "missing";
482
- }
483
- // Extract rename mappings from apiSchemaChanges for downstream tools
484
- const renamedEndpoints = apiSchemaChanges?.endpointsRenamed &&
485
- apiSchemaChanges.endpointsRenamed.length > 0
486
- ? apiSchemaChanges.endpointsRenamed
487
- : undefined;
488
- return {
489
- testFile,
490
- action,
491
- priority,
492
- rationale,
493
- estimatedWork,
494
- issues,
495
- details: {
496
- driftScore: drift,
497
- executionPassed: execution?.passed,
498
- endpointStatus,
499
- renamedEndpoints,
500
- },
501
- };
502
- }
503
- /**
504
- * Parse OpenAPI schema and extract endpoints
505
- */
506
- async parseApiSchema(schemaPath) {
507
- let schema;
508
- try {
509
- if (schemaPath.startsWith("http://") ||
510
- schemaPath.startsWith("https://")) {
511
- // Fetch from URL
512
- const response = await fetch(schemaPath);
513
- if (!response.ok) {
514
- throw new Error(`HTTP error! status: ${response.status}`);
515
- }
516
- schema = await response.json();
517
- }
518
- else {
519
- // Read from file
520
- const content = fs.readFileSync(schemaPath, "utf-8");
521
- schema = JSON.parse(content);
522
- }
523
- }
524
- catch (error) {
525
- logger.error(`Failed to parse API schema: ${error.message}`);
526
- throw new Error(`Could not parse API schema at ${schemaPath}`);
527
- }
528
- const endpoints = [];
529
- const paths = schema.paths || {};
530
- for (const [pathStr, pathItem] of Object.entries(paths)) {
531
- for (const [method, operation] of Object.entries(pathItem)) {
532
- if (["get", "post", "put", "delete", "patch"].includes(method)) {
533
- const op = operation;
534
- const authRequired = this.checkAuthRequired(op, schema);
535
- endpoints.push({
536
- path: pathStr,
537
- method: method.toUpperCase(),
538
- operationId: op.operationId,
539
- authRequired,
540
- parameters: op.parameters || [],
541
- requestBody: op.requestBody,
542
- responses: op.responses,
543
- });
544
- }
545
- }
546
- }
547
- return endpoints;
548
- }
549
- /**
550
- * Check if endpoint requires authentication
551
- */
552
- checkAuthRequired(operation, schema) {
553
- // Check security at operation level
554
- if (operation.security && operation.security.length > 0) {
555
- return true;
556
- }
557
- // Check security at schema level
558
- if (schema.security && schema.security.length > 0) {
559
- return true;
560
- }
561
- return false;
562
- }
563
- /**
564
- * Analyze test coverage
565
- */
566
- async analyzeCoverage(tests, apiSchema) {
567
- const endpoints = await this.parseApiSchema(apiSchema);
568
- const coverage = [];
569
- for (const endpoint of endpoints) {
570
- const endpointKey = `${endpoint.method} ${endpoint.path}`;
571
- const coveredBy = tests.filter((t) => this.testCoversEndpoint(t.testFile, endpoint));
572
- coverage.push({
573
- endpoint: endpointKey,
574
- method: endpoint.method,
575
- covered: coveredBy.length > 0,
576
- testFiles: coveredBy.map((t) => t.testFile),
577
- endpointInfo: endpoint,
578
- });
579
- }
580
- const coveredEndpoints = coverage.filter((c) => c.covered).length;
581
- const uncoveredEndpoints = endpoints.filter((ep) => !coverage.find((c) => c.endpoint === `${ep.method} ${ep.path}`)
582
- ?.covered);
583
- return {
584
- totalEndpoints: endpoints.length,
585
- coveredEndpoints,
586
- coveragePercentage: endpoints.length > 0
587
- ? Math.round((coveredEndpoints / endpoints.length) * 100)
588
- : 0,
589
- uncoveredEndpoints,
590
- coverage,
591
- };
592
- }
593
- /**
594
- * Check if test covers endpoint
595
- */
596
- testCoversEndpoint(testFile, endpoint) {
597
- try {
598
- const content = fs.readFileSync(testFile, "utf-8");
599
- // Convert OpenAPI-style path to regex, e.g. /users/{id} -> /users/[^/]+
600
- // Replace path parameters with regex pattern, escaping the path parts but not the regex itself
601
- const pathRegexString = endpoint.path
602
- .split(/(\{[^}]+\})/) // Split on path parameters, keeping them in the result
603
- .map((part, index) => {
604
- if (part.match(/^\{[^}]+\}$/)) {
605
- // This is a path parameter like {id}, replace with regex pattern
606
- return "[^/]+";
607
- }
608
- else {
609
- // This is a literal path part, escape special regex chars
610
- return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
611
- }
612
- })
613
- .join("");
614
- // pathRegexString is already escaped, so we can use it directly
615
- const pathRegex = new RegExp(pathRegexString);
616
- const method = endpoint.method.toUpperCase();
617
- const methodLower = endpoint.method.toLowerCase();
618
- // Check if path matches
619
- if (!pathRegex.test(content)) {
620
- return false;
621
- }
622
- // More precise method detection patterns
623
- const methodPatterns = [
624
- // Python: method="PUT" or method='PUT'
625
- new RegExp(`method\\s*=\\s*["']${method}["']`, "i"),
626
- // JavaScript/TypeScript: method: 'PUT' or .put( or .PUT(
627
- new RegExp(`method\\s*:\\s*["']${method}["']`, "i"),
628
- new RegExp(`\\.${methodLower}\\s*\\(`, "i"),
629
- // HTTP request patterns: PUT /path
630
- new RegExp(`${method}\\s+${pathRegexString}`, "i"),
631
- // Axios/fetch: { method: 'PUT' }
632
- new RegExp(`["']method["']\\s*:\\s*["']${method}["']`, "i"),
633
- // RestAssured/Java: .put()
634
- new RegExp(`\\.${methodLower}\\(`, "i"),
635
- // Go: http.MethodPut or "PUT"
636
- new RegExp(`http\\.Method${method.charAt(0) + methodLower.slice(1)}`, "i"),
637
- new RegExp(`["']${method}["']`, "i"),
638
- ];
639
- // Check if any pattern matches
640
- return methodPatterns.some((pattern) => pattern.test(content));
641
- }
642
- catch (error) {
643
- return false;
644
- }
645
- }
646
- /**
647
- * Extract endpoint information from test file
648
- */
649
- async extractEndpointFromTest(testFile, apiSchema) {
650
- try {
651
- const content = fs.readFileSync(testFile, "utf-8");
652
- const endpoints = await this.parseApiSchema(apiSchema);
653
- // Find matching endpoint
654
- for (const endpoint of endpoints) {
655
- if (this.testCoversEndpoint(testFile, endpoint)) {
656
- return {
657
- path: endpoint.path,
658
- method: endpoint.method,
659
- exists: true,
660
- };
661
- }
662
- }
663
- return { path: "unknown", method: "unknown", exists: false };
664
- }
665
- catch (error) {
666
- return undefined;
667
- }
668
- }
669
- /**
670
- * Prioritize and sort recommendations
671
- */
672
- prioritizeRecommendations(recommendations) {
673
- const priorityOrder = {
674
- CRITICAL: 0,
675
- HIGH: 1,
676
- MEDIUM: 2,
677
- LOW: 3,
678
- };
679
- return recommendations.sort((a, b) => {
680
- const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
681
- if (priorityDiff !== 0)
682
- return priorityDiff;
683
- // Within same priority, sort by action
684
- const actionOrder = {
685
- REGENERATE: 0,
686
- DELETE: 1,
687
- UPDATE: 2,
688
- ADD: 3,
689
- VERIFY: 4,
690
- };
691
- return actionOrder[a.action] - actionOrder[b.action];
692
- });
693
- }
694
- }