@skyramp/mcp 0.0.64-rc.8 → 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 (31) hide show
  1. package/build/index.js +2 -0
  2. package/build/playwright/registerPlaywrightTools.js +1 -1
  3. package/build/playwright/traceRecordingPrompt.js +9 -3
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -7
  5. package/build/prompts/test-maintenance/driftAnalysisSections.js +96 -34
  6. package/build/prompts/test-maintenance/enhanceAssertionSection.js +99 -0
  7. package/build/prompts/test-recommendation/recommendationSections.js +24 -9
  8. package/build/prompts/test-recommendation/test-recommendation-prompt.js +96 -27
  9. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +239 -2
  10. package/build/prompts/testbot/testbot-prompts.js +185 -120
  11. package/build/services/TestDiscoveryService.js +23 -0
  12. package/build/services/TestExecutionService.js +1 -1
  13. package/build/services/TestGenerationService.js +83 -12
  14. package/build/services/TestGenerationService.test.js +111 -2
  15. package/build/tool-phase-coverage.test.js +8 -2
  16. package/build/tool-phases.js +11 -13
  17. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +203 -0
  18. package/build/tools/generate-tests/generateContractRestTool.js +3 -73
  19. package/build/tools/generate-tests/generateIntegrationRestTool.js +11 -61
  20. package/build/tools/submitReportTool.js +11 -3
  21. package/build/tools/submitReportTool.test.js +1 -1
  22. package/build/tools/test-management/analyzeChangesTool.js +14 -4
  23. package/build/types/RepositoryAnalysis.js +1 -0
  24. package/build/utils/scenarioDrafting.js +121 -11
  25. package/build/utils/scenarioDrafting.test.js +266 -3
  26. package/node_modules/playwright/ThirdPartyNotices.txt +679 -3093
  27. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +52 -0
  28. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +290 -15
  29. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +60 -0
  30. package/package.json +2 -2
  31. package/build/tools/test-recommendation/recommendTestsTool.js +0 -274
@@ -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
- }