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