@skyramp/mcp 0.0.64-rc.9 → 0.0.64

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 (27) hide show
  1. package/build/index.js +2 -0
  2. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -7
  3. package/build/prompts/test-maintenance/driftAnalysisSections.js +96 -34
  4. package/build/prompts/test-maintenance/enhanceAssertionSection.js +99 -0
  5. package/build/prompts/test-recommendation/recommendationSections.js +24 -9
  6. package/build/prompts/test-recommendation/test-recommendation-prompt.js +96 -27
  7. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +239 -2
  8. package/build/prompts/testbot/testbot-prompts.js +182 -125
  9. package/build/services/TestDiscoveryService.js +23 -0
  10. package/build/services/TestExecutionService.js +1 -1
  11. package/build/services/TestGenerationService.js +83 -12
  12. package/build/services/TestGenerationService.test.js +111 -2
  13. package/build/tool-phase-coverage.test.js +8 -2
  14. package/build/tool-phases.js +11 -13
  15. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +203 -0
  16. package/build/tools/generate-tests/generateContractRestTool.js +3 -73
  17. package/build/tools/generate-tests/generateIntegrationRestTool.js +11 -61
  18. package/build/tools/submitReportTool.js +11 -3
  19. package/build/tools/submitReportTool.test.js +1 -1
  20. package/build/tools/test-management/analyzeChangesTool.js +14 -4
  21. package/build/types/RepositoryAnalysis.js +1 -0
  22. package/build/utils/scenarioDrafting.js +121 -11
  23. package/build/utils/scenarioDrafting.test.js +266 -3
  24. package/node_modules/playwright/ThirdPartyNotices.txt +679 -3093
  25. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +117 -11
  26. package/package.json +2 -2
  27. package/build/tools/test-recommendation/recommendTestsTool.js +0 -274
@@ -102,7 +102,10 @@ class TraceRecordingBackend {
102
102
  rootPath: this._outputDir,
103
103
  harPath: this._harPath
104
104
  });
105
- return handler(parsed);
105
+ const exportResult = await handler(parsed);
106
+ if (!exportResult.isError)
107
+ this._trackedActions = [];
108
+ return exportResult;
106
109
  }
107
110
  if (name === import_assertTool.assertToolSchema.name) {
108
111
  const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
@@ -149,6 +152,36 @@ class TraceRecordingBackend {
149
152
  const result = await this._browserBackend.callTool("browser_select_option", args);
150
153
  const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
151
154
  if (!result.isError) {
155
+ const code = (0, import_response.parseResponse)(result)?.code ?? "";
156
+ const hasCssSelector = code.includes("page.locator('select") || code.includes('page.locator("select') || code.includes(".selectOption(") && !code.includes("getByTestId") && !code.includes("getByRole") && !code.includes("getByLabel");
157
+ if (hasCssSelector && args.ref) {
158
+ traceDebug("selectOption used CSS selector, trying to resolve a better one via hover");
159
+ const hoverResult = await this._browserBackend.callTool("browser_hover", {
160
+ element: args.element || "select",
161
+ ref: args.ref
162
+ });
163
+ if (!hoverResult.isError) {
164
+ const hoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? "";
165
+ const locatorMatch = hoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
166
+ if (locatorMatch) {
167
+ const locatorExpr = locatorMatch[1].trim();
168
+ const parsed = this._codeToLocator(locatorExpr);
169
+ if (parsed && parsed.locator.kind !== "text") {
170
+ const values2 = args.values || [];
171
+ const selectCode = `await page.${locatorExpr}.selectOption(${JSON.stringify(values2.length === 1 ? values2[0] : values2)});`;
172
+ traceDebug(`Improved select selector: ${selectCode}`);
173
+ const timestamp = Date.now();
174
+ this._trackedActions.push({
175
+ toolName: "browser_select_option",
176
+ args,
177
+ code: selectCode,
178
+ timestamp
179
+ });
180
+ return result;
181
+ }
182
+ }
183
+ }
184
+ }
152
185
  this._maybeTrackAction("browser_select_option", args, result);
153
186
  return result;
154
187
  }
@@ -232,18 +265,26 @@ Could not resolve element ref=${params.ref}. ${errText}` }], isError: true };
232
265
  Could not extract selector from hover result.` }], isError: true };
233
266
  }
234
267
  const locatorExpr = locatorMatch[1].trim();
235
- const parsed = this._codeToLocator(locatorExpr);
268
+ let parsed = this._codeToLocator(locatorExpr);
236
269
  if (!parsed) {
237
270
  return { content: [{ type: "text", text: `### Assertion Failed
238
271
  Could not parse locator: ${locatorExpr}` }], isError: true };
239
272
  }
240
273
  const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
241
274
  const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
242
- const refLine = snapText.split("\n").find((l) => l.includes(`[ref=${params.ref}]`)) || "";
275
+ const snapLines = snapText.split("\n");
276
+ const refLine = snapLines.find((l) => l.includes(`[ref=${params.ref}]`)) || "";
243
277
  const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
244
278
  const elementText = textMatch?.[1] || "";
245
279
  const valueMatch = refLine.match(/\]:\s*(.+)$/);
246
280
  const elementValue = valueMatch?.[1]?.trim() || "";
281
+ if (parsed.locator.kind === "text") {
282
+ const improved = await this._improveTextSelector(snapLines, params.ref, parsed);
283
+ if (improved) {
284
+ traceDebug(`Improved ambiguous text selector to: ${improved.selector}`);
285
+ parsed = improved;
286
+ }
287
+ }
247
288
  let passed = false;
248
289
  let details = "";
249
290
  if (params.type === "text") {
@@ -254,14 +295,19 @@ Could not parse locator: ${locatorExpr}` }], isError: true };
254
295
  details = passed ? `Value assertion passed: "${params.element}" has value "${params.expected}".` : `Value assertion FAILED: "${params.element}" has value "${elementValue}", expected "${params.expected}".`;
255
296
  }
256
297
  if (passed) {
257
- const assertName = params.type === "value" ? "assertValue" : "assertText";
258
- this._trackedActions.push({
259
- toolName: "browser_assert",
260
- args: { type: params.type, ref: params.ref, expected: params.expected },
261
- code: `${assertName}:${parsed.selector}:${params.expected}${params.type === "text" ? ":" + params.substring : ""}`,
262
- timestamp
263
- });
264
- traceDebug(`Assert: ${assertName} with selector ${parsed.selector}`);
298
+ const selectorText = parsed.locator.kind === "text" ? parsed.locator.body : parsed.locator.next?.kind === "text" ? parsed.locator.next.body : parsed.locator.options?.name ?? null;
299
+ if (selectorText && selectorText === params.expected) {
300
+ traceDebug(`Skipped tautological assertion: selector already matches "${params.expected}"`);
301
+ } else {
302
+ const assertName = params.type === "value" ? "assertValue" : "assertText";
303
+ this._trackedActions.push({
304
+ toolName: "browser_assert",
305
+ args: { type: params.type, ref: params.ref, expected: params.expected },
306
+ code: `${assertName}:${parsed.selector}:${params.expected}${params.type === "text" ? ":" + params.substring : ""}`,
307
+ timestamp
308
+ });
309
+ traceDebug(`Assert: ${assertName} with selector ${parsed.selector}`);
310
+ }
265
311
  }
266
312
  return {
267
313
  content: [{ type: "text", text: `### ${passed ? "Assertion Passed" : "Assertion Failed"}
@@ -304,6 +350,66 @@ ${details}` }]
304
350
  }
305
351
  return null;
306
352
  }
353
+ /**
354
+ * When hover resolves to a text-based locator (e.g. getByText("$849.98")),
355
+ * check if that text appears multiple times in the snapshot (ambiguous) and
356
+ * hover ancestor refs to find a parent with a test-id, producing a chained
357
+ * selector like `testid >> text` that uniquely identifies the element.
358
+ */
359
+ async _improveTextSelector(snapLines, ref, original) {
360
+ const textBody = original.locator.body;
361
+ const occurrences = snapLines.filter((l) => l.includes(textBody)).length;
362
+ if (occurrences <= 1)
363
+ return null;
364
+ traceDebug(`Text "${textBody}" appears ${occurrences} times in snapshot \u2014 looking for parent test-id`);
365
+ const refPattern = `[ref=${ref}]`;
366
+ const refIdx = snapLines.findIndex((l) => l.includes(refPattern));
367
+ if (refIdx < 0)
368
+ return null;
369
+ const refIndent = snapLines[refIdx].match(/^(\s*)/)?.[1]?.length ?? 0;
370
+ for (let i = refIdx - 1; i >= 0; i--) {
371
+ const line = snapLines[i];
372
+ const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
373
+ if (indent >= refIndent)
374
+ continue;
375
+ const ancestorRefMatch = line.match(/\[ref=(\w+)\]/);
376
+ if (!ancestorRefMatch)
377
+ continue;
378
+ const ancestorRef = ancestorRefMatch[1];
379
+ try {
380
+ const ancestorHover = await this._browserBackend.callTool("browser_hover", {
381
+ element: "parent element",
382
+ ref: ancestorRef
383
+ });
384
+ if (!ancestorHover.isError) {
385
+ const ancestorCode = (0, import_response.parseResponse)(ancestorHover)?.code ?? "";
386
+ const ancestorLocatorMatch = ancestorCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
387
+ if (ancestorLocatorMatch) {
388
+ const ancestorExpr = ancestorLocatorMatch[1].trim();
389
+ const testidMatch = ancestorExpr.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
390
+ if (testidMatch) {
391
+ const testid = testidMatch[1];
392
+ const chainedSelector = `internal:testid=[data-testid="${testid}"s] >> internal:text="${textBody}"i`;
393
+ traceDebug(`Improved to chained selector: ${chainedSelector}`);
394
+ return {
395
+ selector: chainedSelector,
396
+ locator: {
397
+ kind: "test-id",
398
+ body: testid,
399
+ options: {},
400
+ next: { kind: "text", body: textBody, options: { exact: false } }
401
+ }
402
+ };
403
+ }
404
+ }
405
+ }
406
+ } catch {
407
+ }
408
+ if (indent === 0)
409
+ break;
410
+ }
411
+ return null;
412
+ }
307
413
  static {
308
414
  /** Extract selector and locator info from a snapshot line for assertion tracking. */
309
415
  // Roles that map to valid Playwright getByRole() selectors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.0.64-rc.9",
3
+ "version": "0.0.64",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/sdk": "^1.24.3",
56
56
  "@playwright/test": "^1.55.0",
57
- "@skyramp/skyramp": "1.3.16",
57
+ "@skyramp/skyramp": "1.3.17",
58
58
  "dockerode": "^4.0.6",
59
59
  "fast-glob": "^3.3.3",
60
60
  "playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.9.0.tgz",
@@ -1,274 +0,0 @@
1
- import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
2
- import { z } from "zod";
3
- import { simpleGit } from "simple-git";
4
- import { StateManager, getSessionFilePath, getRegisteredSessions, getSessionData, hasSessionData, normalizeRecommendationState, } from "../../utils/AnalysisStateManager.js";
5
- import { logger } from "../../utils/logger.js";
6
- import { ANALYSIS_URI_PREFIX } from "../../resources/analysisResources.js";
7
- import { MAX_RECOMMENDATIONS, MAX_TESTS_TO_GENERATE } from "../../prompts/test-recommendation/recommendationSections.js";
8
- import { AnalyticsService } from "../../services/AnalyticsService.js";
9
- import { buildRecommendationPrompt } from "../../prompts/test-recommendation/test-recommendation-prompt.js";
10
- import { repositoryAnalysisSchema } from "../../types/RepositoryAnalysis.js";
11
- import { parsePRComments } from "../../utils/pr-comment-parser.js";
12
- import { getWorkspaceAuthConfig } from "../../utils/workspaceAuth.js";
13
- /**
14
- * Extract GitHub owner/repo from the origin remote URL.
15
- * Handles both SSH (git@github.com:owner/repo.git) and
16
- * HTTPS (https://github.com/owner/repo.git) formats.
17
- */
18
- async function getGitHubSlug(repoPath) {
19
- try {
20
- const git = simpleGit(repoPath);
21
- const remotes = await git.getRemotes(true);
22
- const origin = remotes.find((r) => r.name === "origin");
23
- const url = origin?.refs?.fetch;
24
- if (!url)
25
- return null;
26
- const sshMatch = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
27
- if (sshMatch)
28
- return { owner: sshMatch[1], repo: sshMatch[2] };
29
- const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
30
- if (httpsMatch)
31
- return { owner: httpsMatch[1], repo: httpsMatch[2] };
32
- return null;
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- const recommendTestsSchema = {
39
- sessionId: completable(z.string().optional().describe("Session ID from skyramp_analyze_repository. Optional if prNumber and repositoryPath are provided."), async (value) => {
40
- return Array.from(getRegisteredSessions().keys()).filter((id) => id.startsWith(value?.toString() || ""));
41
- }),
42
- repositoryPath: z
43
- .string()
44
- .optional()
45
- .describe("Absolute path to the repository. Required if sessionId is missing."),
46
- topN: z
47
- .number()
48
- .default(MAX_RECOMMENDATIONS)
49
- .describe(`Number of recommendations. Defaults to ${MAX_RECOMMENDATIONS}.`),
50
- prNumber: z
51
- .number()
52
- .optional()
53
- .describe("GitHub PR number. When provided, fetches previous TestBot comments to use as baseline recommendations."),
54
- };
55
- const TOOL_NAME = "skyramp_recommend_tests";
56
- export function registerRecommendTestsTool(server) {
57
- server.registerTool(TOOL_NAME, {
58
- description: `Generate actionable test recommendations based on enriched repository analysis.
59
-
60
- **PREREQUISITE**: Call skyramp_analyze_repository first to get the sessionId.
61
-
62
- This tool reads the enriched analysis (endpoints with interactions, drafted scenarios)
63
- and generates prioritized test recommendations using LLM reasoning over the actual data.
64
-
65
- For each recommended test, you'll get:
66
- - Priority (high/medium/low) with reasoning based on actual endpoint data
67
- - Specific test referencing concrete interactions (request→response pairs)
68
- - For integration/E2E: references to draftedScenarios by scenarioName
69
- - Which Skyramp tool to call for generation
70
-
71
- **Output guidelines:**
72
- - Use "high", "medium", or "low" for priority.
73
- - Never mark a test as blocked — recommend it with instructions for missing artifacts.
74
- - Reference specific interactions by description for contract/fuzz tests.
75
- - Reference draftedScenarios by scenarioName for integration/E2E tests.
76
-
77
- ** This tool is currently in Early Preview stage. Please verify the results. **`,
78
- inputSchema: recommendTestsSchema,
79
- }, async (params) => {
80
- let errorResult;
81
- try {
82
- logger.info("Recommend tests tool invoked", {
83
- sessionId: params.sessionId,
84
- topN: params.topN,
85
- prNumber: params.prNumber,
86
- });
87
- let stateData = null;
88
- let prContext = null;
89
- let repositoryPath = params.repositoryPath || "";
90
- let analysisScope = "current_branch_diff";
91
- if (params.sessionId) {
92
- // Load session data: try process memory first, then fall back to state file
93
- if (hasSessionData(params.sessionId)) {
94
- stateData = getSessionData(params.sessionId);
95
- logger.info("Loaded analysis from process memory", { sessionId: params.sessionId });
96
- }
97
- else {
98
- // Fall back to state file for backward compatibility
99
- const registeredPath = getSessionFilePath(params.sessionId);
100
- const stateManager = registeredPath
101
- ? StateManager.fromStatePath(registeredPath)
102
- : StateManager.fromSessionId(params.sessionId);
103
- if (stateManager.exists()) {
104
- stateData = await stateManager.readData();
105
- logger.info("Loaded analysis from state file (legacy)", { sessionId: params.sessionId });
106
- }
107
- }
108
- if (!stateData) {
109
- throw new Error(`Analysis session not found: ${params.sessionId}.`);
110
- }
111
- stateData = normalizeRecommendationState(stateData);
112
- repositoryPath = stateData.repositoryPath;
113
- analysisScope = stateData.analysisScope;
114
- prContext = stateData.prContext;
115
- }
116
- else if (params.prNumber && params.repositoryPath) {
117
- // No sessionId provided — fetch PR context directly to use as baseline
118
- const slug = await getGitHubSlug(params.repositoryPath);
119
- if (slug) {
120
- try {
121
- prContext = await parsePRComments(slug.owner, slug.repo, params.prNumber);
122
- logger.info("Fetched PR context for baseline recommendations", { prNumber: params.prNumber });
123
- }
124
- catch (err) {
125
- throw new Error(`Failed to fetch PR context for baseline: ${err instanceof Error ? err.message : String(err)}`);
126
- }
127
- }
128
- else {
129
- throw new Error(`Could not determine GitHub repository slug for ${params.repositoryPath}`);
130
- }
131
- if (!prContext || prContext.previousRecommendations.length === 0) {
132
- throw new Error(`No previous recommendations found for PR #${params.prNumber}. A full analysis is required first.`);
133
- }
134
- // Build a minimal "analysis" from the previous recommendations to satisfy the prompt builder
135
- stateData = {
136
- repositoryPath: params.repositoryPath,
137
- analysisScope: "current_branch_diff",
138
- analysis: {
139
- metadata: { repositoryName: slug.repo, analysisDate: new Date().toISOString(), scanDepth: "full", analysisScope: "current_branch_diff" },
140
- projectClassification: { projectType: "REST API", primaryLanguage: "unknown", primaryFramework: "unknown", deploymentPattern: "monolith" },
141
- technologyStack: { languages: [], frameworks: [], runtime: "", keyDependencies: [] },
142
- businessContext: { mainPurpose: "", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
143
- apiEndpoints: { totalCount: 0, baseUrl: "", endpoints: [] },
144
- authentication: { method: "none", configLocation: "", envVarsRequired: [], setupExample: "" },
145
- infrastructure: { isContainerized: false, hasDockerCompose: false, hasKubernetes: false, hasCiCd: false },
146
- existingTests: { frameworks: [], coverage: { unit: 0, integration: 0, e2e: 0, ui: 0, load: 0, contract: 0, smoke: 0 }, testLocations: {}, hasCoverageReports: false },
147
- },
148
- };
149
- }
150
- else {
151
- throw new Error("Either sessionId or (prNumber and repositoryPath) must be provided.");
152
- }
153
- if (!stateData) {
154
- throw new Error(`Failed to read analysis session: ${params.sessionId}. Run skyramp_analyze_repository first.`);
155
- }
156
- stateData = normalizeRecommendationState(stateData);
157
- analysisScope = stateData.analysisScope;
158
- repositoryPath = stateData.repositoryPath;
159
- const { analysis } = stateData;
160
- if (!analysis) {
161
- throw new Error("Session is missing analysis data.");
162
- }
163
- // Validate analysis against the Zod schema to catch malformed LLM output early
164
- const parseResult = repositoryAnalysisSchema.safeParse(analysis);
165
- if (!parseResult.success) {
166
- const issues = parseResult.error.issues.slice(0, 5).map((i) => `${i.path.join(".")}: ${i.message}`);
167
- logger.warning("Analysis data has schema issues (proceeding with best-effort)", {
168
- issueCount: parseResult.error.issues.length,
169
- sample: issues,
170
- });
171
- }
172
- // Guard critical nested fields the prompt builder accesses
173
- if (!analysis.apiEndpoints?.endpoints) {
174
- analysis.apiEndpoints = { totalCount: 0, baseUrl: "", endpoints: [] };
175
- }
176
- if (!analysis.businessContext?.draftedScenarios) {
177
- analysis.businessContext = {
178
- ...(analysis.businessContext || { mainPurpose: "", userFlows: [], dataFlows: [], integrationPatterns: [] }),
179
- draftedScenarios: analysis.businessContext?.draftedScenarios || [],
180
- };
181
- }
182
- if (!analysis.authentication) {
183
- analysis.authentication = { method: "none", configLocation: "", envVarsRequired: [], setupExample: "" };
184
- }
185
- if (!analysis.existingTests) {
186
- analysis.existingTests = {
187
- frameworks: [], coverage: { unit: 0, integration: 0, e2e: 0, ui: 0, load: 0, contract: 0, smoke: 0 },
188
- testLocations: {}, hasCoverageReports: false,
189
- };
190
- }
191
- const scope = analysisScope || "full_repo";
192
- const effectiveTopN = params.topN ?? MAX_RECOMMENDATIONS;
193
- // Fetch PR context when prNumber is provided and in diff scope (if not already fetched)
194
- if (!prContext && params.prNumber && scope === "current_branch_diff") {
195
- const slug = await getGitHubSlug(repositoryPath);
196
- if (slug) {
197
- try {
198
- prContext = await parsePRComments(slug.owner, slug.repo, params.prNumber);
199
- }
200
- catch (err) {
201
- logger.warning("Failed to fetch PR context", {
202
- error: err instanceof Error ? err.message : String(err),
203
- });
204
- }
205
- }
206
- else {
207
- logger.debug("Could not determine GitHub owner/repo from git remotes — skipping PR context");
208
- }
209
- }
210
- const wsAuthConfig = repositoryPath
211
- ? await getWorkspaceAuthConfig(repositoryPath)
212
- : {};
213
- const prompt = buildRecommendationPrompt(analysis, scope, effectiveTopN, prContext, wsAuthConfig.authHeader, wsAuthConfig.authType);
214
- // H3: Mode-specific output header
215
- const modeLabel = scope === "current_branch_diff" ? "PR Mode" : "Repo Mode";
216
- const modeDesc = scope === "current_branch_diff"
217
- ? `Focused on branch changes. Top ${effectiveTopN} recommendations — top 4 will be generated, remaining reported.`
218
- : `Comprehensive test strategy. Top ${effectiveTopN} tests across the entire application.`;
219
- // J4: PR awareness messaging
220
- let prAwareness = "";
221
- if (prContext && prContext.previousRecommendations.length > 0) {
222
- const implemented = prContext.previousRecommendations.filter((r) => r.status === "implemented").length;
223
- const pending = prContext.previousRecommendations.filter((r) => r.status === "recommended").length;
224
- prAwareness = `\n**PR History**: ${implemented} tests already implemented, ${pending} previously recommended.`;
225
- if (implemented > 0) {
226
- prAwareness += ` Building on existing coverage — new recommendations complement what\'s already been added.`;
227
- }
228
- }
229
- // G4: Include totalConsidered instruction in output
230
- const totalEndpointMethods = analysis.apiEndpoints.endpoints.reduce((acc, ep) => acc + (ep.methods?.length || 0), 0);
231
- const totalInteractions = analysis.apiEndpoints.endpoints.reduce((acc, ep) => acc + (ep.methods || []).reduce((a2, m) => a2 + (m.interactions?.length || 0), 0), 0);
232
- const totalScenarios = analysis.businessContext.draftedScenarios.length;
233
- const resourcesSection = params.sessionId ? `
234
- ## Available Resources
235
- - Summary: \`${ANALYSIS_URI_PREFIX}/${params.sessionId}/summary\`
236
- - Endpoints: \`${ANALYSIS_URI_PREFIX}/${params.sessionId}/endpoints\`
237
- - Scenarios: \`${ANALYSIS_URI_PREFIX}/${params.sessionId}/scenarios\`
238
- ` : '';
239
- const output = `# Test Recommendations (${modeLabel})
240
-
241
- ${params.sessionId ? `**Session**: \`${params.sessionId}\`\n` : ''}**Repository**: \`${repositoryPath}\`
242
- **Mode**: ${modeLabel} — ${modeDesc}${prAwareness}
243
- **Catalog**: ${totalEndpointMethods} endpoint methods, ${totalInteractions} interactions, ${totalScenarios} scenarios
244
- **Target**: Top ${effectiveTopN} recommendations ranked by value. Top ${MAX_TESTS_TO_GENERATE} = generate & execute. #${MAX_TESTS_TO_GENERATE + 1}-#${effectiveTopN} = report as additional recommendations.
245
- ${resourcesSection}
246
- ---
247
-
248
- ${prompt}
249
-
250
- ** This tool is currently in Early Preview stage. Please verify the results. **`;
251
- return {
252
- content: [{ type: "text", text: output }],
253
- isError: false,
254
- };
255
- }
256
- catch (error) {
257
- const errorMessage = error instanceof Error ? error.message : String(error);
258
- logger.error("Recommend tests tool failed", { error: errorMessage });
259
- errorResult = {
260
- content: [
261
- {
262
- type: "text",
263
- text: `Error generating recommendations: ${errorMessage}`,
264
- },
265
- ],
266
- isError: true,
267
- };
268
- return errorResult;
269
- }
270
- finally {
271
- AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {});
272
- }
273
- });
274
- }