@skyramp/mcp 0.0.62 → 0.0.63-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.
- package/build/index.js +18 -26
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +59 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +153 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -9
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +34 -38
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +56 -9
- package/build/prompts/testbot/testbot-prompts.js +113 -100
- package/build/services/DriftAnalysisService.js +1 -1
- package/build/services/ScenarioGenerationService.js +5 -1
- package/build/services/TestExecutionService.js +2 -24
- package/build/services/TestExecutionService.test.js +167 -0
- package/build/services/containerEnv.js +35 -0
- package/build/tools/generate-tests/generateScenarioRestTool.js +7 -1
- package/build/tools/submitReportTool.js +6 -6
- package/build/tools/test-management/actionsTool.js +396 -0
- package/build/tools/test-management/analyzeChangesTool.js +750 -0
- package/build/tools/test-management/analyzeTestHealthTool.js +132 -0
- package/build/tools/test-management/executeTestsTool.js +198 -0
- package/build/tools/test-management/index.js +5 -0
- package/build/tools/test-management/stateCleanupTool.js +163 -0
- package/build/tools/test-recommendation/recommendTestsTool.js +1 -1
- package/build/utils/analyze-openapi.js +2 -2
- package/build/utils/pr-comment-parser.js +157 -36
- package/build/utils/pr-comment-parser.test.js +427 -0
- package/package.json +1 -1
- package/build/tools/initTestbotTool.js +0 -187
- package/build/tools/initTestbotTool.test.js +0 -194
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +0 -505
|
@@ -1,505 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import * as crypto from "crypto";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import { logger } from "../../utils/logger.js";
|
|
5
|
-
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
6
|
-
import { registerSession, storeSessionData, } from "../../utils/AnalysisStateManager.js";
|
|
7
|
-
import { WorkspaceConfigManager } from "@skyramp/skyramp";
|
|
8
|
-
import { TestDiscoveryService } from "../../services/TestDiscoveryService.js";
|
|
9
|
-
import { computeBranchDiff } from "../../utils/branchDiff.js";
|
|
10
|
-
import { parseEndpointsFromDiff } from "../../utils/routeParsers.js";
|
|
11
|
-
import { scanAllRepoEndpoints, scanRelatedEndpoints, grepRouterMountingContext } from "../../utils/repoScanner.js";
|
|
12
|
-
import { detectProjectMetadata } from "../../utils/projectMetadata.js";
|
|
13
|
-
import { draftScenariosFromEndpoints } from "../../utils/scenarioDrafting.js";
|
|
14
|
-
import { buildAnalysisOutputText } from "../../prompts/test-recommendation/analysisOutputPrompt.js";
|
|
15
|
-
import { parseTraceFile, discoverTraceFiles } from "../../utils/trace-parser.js";
|
|
16
|
-
const analyzeRepositorySchema = z.object({
|
|
17
|
-
repositoryPath: z
|
|
18
|
-
.string()
|
|
19
|
-
.describe("Absolute path to the repository to analyze (e.g., /path/to/my-repo)"),
|
|
20
|
-
scanDepth: z
|
|
21
|
-
.enum(["quick", "full"])
|
|
22
|
-
.default("full")
|
|
23
|
-
.describe("Analysis depth: 'quick' for basic info, 'full' for comprehensive analysis"),
|
|
24
|
-
analysisScope: z
|
|
25
|
-
.enum(["full_repo", "current_branch_diff"])
|
|
26
|
-
.default("full_repo")
|
|
27
|
-
.describe("Scope of analysis. 'full_repo' analyzes the entire repository. 'current_branch_diff' analyzes only the changes in the current branch compared to the default branch (e.g., main/master), useful for PR-scoped test recommendations."),
|
|
28
|
-
baseBranch: z
|
|
29
|
-
.string()
|
|
30
|
-
.optional()
|
|
31
|
-
.describe("Optional: PR base branch name (e.g. 'main', 'develop'). When provided, the diff is computed against origin/<baseBranch> instead of auto-detecting the default branch. Useful when the PR targets a non-default branch."),
|
|
32
|
-
});
|
|
33
|
-
const TOOL_NAME = "skyramp_analyze_repository";
|
|
34
|
-
export function registerAnalyzeRepositoryTool(server) {
|
|
35
|
-
server.registerTool(TOOL_NAME, {
|
|
36
|
-
description: `
|
|
37
|
-
⚠️ MANDATORY FIRST STEP - DISPLAY THIS WARNING:
|
|
38
|
-
Before ANY other response text, you MUST output this exact message:
|
|
39
|
-
"** This tool is currently in Early Preview stage. Please verify the results. **"
|
|
40
|
-
|
|
41
|
-
DO NOT proceed with tool calls until you have displayed this warning in your response.
|
|
42
|
-
|
|
43
|
-
❌ WRONG: Calling this tool without first displaying the warning message to the user
|
|
44
|
-
✅ CORRECT: First display the warning message in plain text, THEN call this tool
|
|
45
|
-
|
|
46
|
-
EXPECTED FLOW:
|
|
47
|
-
1. System displays: "** This tool is currently in Early Preview stage..."
|
|
48
|
-
2. System calls: skyramp_analyze_repository(...)
|
|
49
|
-
3. System processes results
|
|
50
|
-
|
|
51
|
-
Analyze a code repository to understand its structure, technology stack, and testing readiness.
|
|
52
|
-
|
|
53
|
-
This tool performs comprehensive repository analysis including:
|
|
54
|
-
- Project type classification (REST API, Frontend, Full-stack, Microservices, etc.)
|
|
55
|
-
- Technology stack identification (languages, frameworks, dependencies)
|
|
56
|
-
- Artifact discovery (OpenAPI specs, Playwright recordings, trace files)
|
|
57
|
-
- API endpoint detection and cataloging
|
|
58
|
-
- Authentication mechanism analysis
|
|
59
|
-
- Infrastructure configuration (Docker, Kubernetes, CI/CD)
|
|
60
|
-
- Existing test coverage assessment
|
|
61
|
-
|
|
62
|
-
The analysis provides enriched endpoint data (interactions, headers, cookies) and
|
|
63
|
-
drafted scenarios that can be used by skyramp_recommend_tests for LLM-powered recommendations.
|
|
64
|
-
|
|
65
|
-
When \`analysisScope\` is set to \`"current_branch_diff"\`, the analysis focuses specifically on the code changes in the current branch — identifying new/modified endpoints, changed services, and affected areas — rather than scanning the entire repository. This is ideal for PR-scoped test recommendations.
|
|
66
|
-
|
|
67
|
-
Example usage:
|
|
68
|
-
\`\`\`
|
|
69
|
-
{
|
|
70
|
-
"repositoryPath": "/Users/dev/my-api",
|
|
71
|
-
"scanDepth": "full",
|
|
72
|
-
"analysisScope": "current_branch_diff",
|
|
73
|
-
}
|
|
74
|
-
\`\`\`
|
|
75
|
-
|
|
76
|
-
**CRITICAL RULES**:
|
|
77
|
-
- DO NOT CREATE ANY .json or .md file during repository analysis.
|
|
78
|
-
|
|
79
|
-
Output: Detailed RepositoryAnalysis JSON object with all repository characteristics.`,
|
|
80
|
-
inputSchema: analyzeRepositorySchema.shape,
|
|
81
|
-
}, async (params, extra) => {
|
|
82
|
-
let errorResult;
|
|
83
|
-
const sendProgress = async (progress, total, message) => {
|
|
84
|
-
const progressToken = extra._meta?.progressToken;
|
|
85
|
-
if (progressToken !== undefined) {
|
|
86
|
-
const notification = {
|
|
87
|
-
method: "notifications/progress",
|
|
88
|
-
params: { progressToken, progress, total, message },
|
|
89
|
-
};
|
|
90
|
-
await extra.sendNotification(notification);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
try {
|
|
94
|
-
const analysisScope = params.analysisScope || "full_repo";
|
|
95
|
-
logger.info("Analyze repository tool invoked", {
|
|
96
|
-
repositoryPath: params.repositoryPath,
|
|
97
|
-
scanDepth: params.scanDepth,
|
|
98
|
-
analysisScope,
|
|
99
|
-
});
|
|
100
|
-
await sendProgress(0, 100, "Starting repository analysis...");
|
|
101
|
-
let diffData;
|
|
102
|
-
if (analysisScope === "current_branch_diff") {
|
|
103
|
-
await sendProgress(10, 100, "Computing branch diff...");
|
|
104
|
-
try {
|
|
105
|
-
diffData = await computeBranchDiff(params.repositoryPath, params.baseBranch);
|
|
106
|
-
logger.info("Branch diff computed via git", {
|
|
107
|
-
currentBranch: diffData.currentBranch,
|
|
108
|
-
baseBranch: diffData.baseBranch,
|
|
109
|
-
changedFiles: diffData.changedFiles.length,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
114
|
-
logger.error("Failed to obtain branch diff", { error: msg });
|
|
115
|
-
throw new Error(`Failed to obtain branch diff: ${msg}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
await sendProgress(30, 100, "Parsing endpoints from diff...");
|
|
119
|
-
const parsedDiff = diffData ? parseEndpointsFromDiff(diffData) : undefined;
|
|
120
|
-
// In PR mode, skip the expensive full-repo scan — the LLM will
|
|
121
|
-
// discover relevant endpoints from the diff and handler code.
|
|
122
|
-
// In full-repo mode, scan to give the LLM a head start.
|
|
123
|
-
let scannedEndpoints = [];
|
|
124
|
-
if (analysisScope !== "current_branch_diff") {
|
|
125
|
-
await sendProgress(40, 100, "Scanning repository for all endpoints...");
|
|
126
|
-
try {
|
|
127
|
-
scannedEndpoints = scanAllRepoEndpoints(params.repositoryPath);
|
|
128
|
-
logger.info("Pre-scanned repo endpoints", { count: scannedEndpoints.length });
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
logger.warning("Endpoint pre-scan failed, LLM will discover endpoints", {
|
|
132
|
-
error: err instanceof Error ? err.message : String(err),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
else if (diffData) {
|
|
137
|
-
await sendProgress(40, 100, "Scanning related endpoints from diff...");
|
|
138
|
-
try {
|
|
139
|
-
scannedEndpoints = scanRelatedEndpoints(params.repositoryPath, diffData.changedFiles);
|
|
140
|
-
logger.info("Scanned related endpoints for PR scope", { count: scannedEndpoints.length });
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
logger.warning("Related endpoint scan failed", {
|
|
144
|
-
error: err instanceof Error ? err.message : String(err),
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
if (scannedEndpoints.length === 0) {
|
|
148
|
-
await sendProgress(45, 100, "No related endpoints found, scanning full repo...");
|
|
149
|
-
try {
|
|
150
|
-
scannedEndpoints = scanAllRepoEndpoints(params.repositoryPath);
|
|
151
|
-
logger.info("Fallback: scanned all repo endpoints", { count: scannedEndpoints.length });
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
logger.warning("Full repo endpoint scan also failed", {
|
|
155
|
-
error: err instanceof Error ? err.message : String(err),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
await sendProgress(50, 100, "Building analysis session...");
|
|
161
|
-
// Read workspace config for baseUrl and auth info
|
|
162
|
-
let wsBaseUrl = "";
|
|
163
|
-
let wsAuthHeader = "";
|
|
164
|
-
let wsSchemaPath = "";
|
|
165
|
-
let wsAuthMethod = "none";
|
|
166
|
-
try {
|
|
167
|
-
const wsMgr = new WorkspaceConfigManager(params.repositoryPath);
|
|
168
|
-
if (await wsMgr.exists()) {
|
|
169
|
-
const wsConfig = await wsMgr.read();
|
|
170
|
-
const svc = wsConfig.services?.[0];
|
|
171
|
-
if (svc?.api?.baseUrl)
|
|
172
|
-
wsBaseUrl = svc.api.baseUrl;
|
|
173
|
-
if (svc?.api?.authHeader)
|
|
174
|
-
wsAuthHeader = svc.api.authHeader;
|
|
175
|
-
if (svc?.api?.schemaPath)
|
|
176
|
-
wsSchemaPath = svc.api.schemaPath;
|
|
177
|
-
if (wsAuthHeader) {
|
|
178
|
-
wsAuthMethod = /cookie|session/i.test(wsAuthHeader) ? "session" : "bearer";
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// workspace config not available
|
|
184
|
-
}
|
|
185
|
-
// Auto-detect project metadata from files
|
|
186
|
-
const projectMeta = detectProjectMetadata(params.repositoryPath);
|
|
187
|
-
const sessionId = crypto.randomUUID();
|
|
188
|
-
// Build skeleton endpoints — from full scan or diff-only
|
|
189
|
-
const skeletonResponse = (method) => method === "POST" ? { statusCode: 201, description: "Created" }
|
|
190
|
-
: method === "DELETE" ? { statusCode: 204, description: "No Content" }
|
|
191
|
-
: { statusCode: 200, description: "OK" };
|
|
192
|
-
const skeletonEndpoints = scannedEndpoints.length > 0
|
|
193
|
-
? scannedEndpoints.map((ep) => ({
|
|
194
|
-
path: ep.path,
|
|
195
|
-
resourceGroup: ep.path.split("/").filter(Boolean).pop() || "unknown",
|
|
196
|
-
pathParams: (ep.path.match(/\{(\w+)\}/g) || []).map((p) => ({
|
|
197
|
-
name: p.slice(1, -1),
|
|
198
|
-
type: "string",
|
|
199
|
-
required: true,
|
|
200
|
-
})),
|
|
201
|
-
methods: ep.methods.map((m) => ({
|
|
202
|
-
method: m,
|
|
203
|
-
description: "",
|
|
204
|
-
queryParams: [],
|
|
205
|
-
authRequired: true,
|
|
206
|
-
sourceFile: ep.sourceFile,
|
|
207
|
-
interactions: [{
|
|
208
|
-
description: `${m} ${ep.path}`,
|
|
209
|
-
type: "success",
|
|
210
|
-
request: {},
|
|
211
|
-
response: skeletonResponse(m),
|
|
212
|
-
}],
|
|
213
|
-
})),
|
|
214
|
-
}))
|
|
215
|
-
: (parsedDiff
|
|
216
|
-
? (() => {
|
|
217
|
-
const grouped = new Map();
|
|
218
|
-
for (const ep of [...parsedDiff.newEndpoints, ...parsedDiff.modifiedEndpoints]) {
|
|
219
|
-
let entry = grouped.get(ep.path);
|
|
220
|
-
if (!entry) {
|
|
221
|
-
entry = {
|
|
222
|
-
path: ep.path,
|
|
223
|
-
resourceGroup: ep.path.split("/").filter(Boolean).pop() || "unknown",
|
|
224
|
-
pathParams: (ep.path.match(/\{(\w+)\}/g) || []).map((p) => ({
|
|
225
|
-
name: p.slice(1, -1),
|
|
226
|
-
type: "string",
|
|
227
|
-
required: true,
|
|
228
|
-
})),
|
|
229
|
-
methods: [],
|
|
230
|
-
};
|
|
231
|
-
grouped.set(ep.path, entry);
|
|
232
|
-
}
|
|
233
|
-
entry.methods.push({
|
|
234
|
-
method: ep.method,
|
|
235
|
-
description: "",
|
|
236
|
-
queryParams: [],
|
|
237
|
-
authRequired: true,
|
|
238
|
-
sourceFile: ep.sourceFile,
|
|
239
|
-
interactions: [{
|
|
240
|
-
description: `${ep.method} ${ep.path}`,
|
|
241
|
-
type: "success",
|
|
242
|
-
request: {},
|
|
243
|
-
response: skeletonResponse(ep.method),
|
|
244
|
-
}],
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
return Array.from(grouped.values());
|
|
248
|
-
})()
|
|
249
|
-
: []);
|
|
250
|
-
const diffContext = parsedDiff ? {
|
|
251
|
-
currentBranch: parsedDiff.currentBranch,
|
|
252
|
-
baseBranch: parsedDiff.baseBranch,
|
|
253
|
-
changedFiles: parsedDiff.changedFiles,
|
|
254
|
-
newEndpoints: (() => {
|
|
255
|
-
const grouped = new Map();
|
|
256
|
-
for (const ep of parsedDiff.newEndpoints) {
|
|
257
|
-
let entry = grouped.get(ep.path);
|
|
258
|
-
if (!entry) {
|
|
259
|
-
entry = { path: ep.path, methods: [] };
|
|
260
|
-
grouped.set(ep.path, entry);
|
|
261
|
-
}
|
|
262
|
-
entry.methods.push({ method: ep.method, sourceFile: ep.sourceFile, interactionCount: 0 });
|
|
263
|
-
}
|
|
264
|
-
return Array.from(grouped.values());
|
|
265
|
-
})(),
|
|
266
|
-
modifiedEndpoints: (() => {
|
|
267
|
-
const grouped = new Map();
|
|
268
|
-
for (const ep of parsedDiff.modifiedEndpoints) {
|
|
269
|
-
let entry = grouped.get(ep.path);
|
|
270
|
-
if (!entry) {
|
|
271
|
-
entry = { path: ep.path, methods: [] };
|
|
272
|
-
grouped.set(ep.path, entry);
|
|
273
|
-
}
|
|
274
|
-
entry.methods.push({ method: ep.method, sourceFile: ep.sourceFile, changeType: "modified" });
|
|
275
|
-
}
|
|
276
|
-
return Array.from(grouped.values());
|
|
277
|
-
})(),
|
|
278
|
-
affectedServices: parsedDiff.affectedServices,
|
|
279
|
-
summary: "",
|
|
280
|
-
} : undefined;
|
|
281
|
-
// Run TestDiscoveryService to find existing Skyramp-generated tests
|
|
282
|
-
let discoveredTests = {
|
|
283
|
-
frameworks: [], coverage: { unit: 0, integration: 0, e2e: 0, ui: 0, load: 0, contract: 0, smoke: 0 }, testLocations: {}, hasCoverageReports: false,
|
|
284
|
-
};
|
|
285
|
-
try {
|
|
286
|
-
const discoveryService = new TestDiscoveryService();
|
|
287
|
-
const discoveryResult = await discoveryService.discoverTests(params.repositoryPath);
|
|
288
|
-
const byType = new Map();
|
|
289
|
-
for (const test of discoveryResult.tests) {
|
|
290
|
-
const type = test.testType || "unknown";
|
|
291
|
-
if (!byType.has(type))
|
|
292
|
-
byType.set(type, []);
|
|
293
|
-
byType.get(type).push(test.testFile);
|
|
294
|
-
if (test.framework && !discoveredTests.frameworks.includes(test.framework)) {
|
|
295
|
-
discoveredTests.frameworks.push(test.framework);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
for (const [type, files] of byType) {
|
|
299
|
-
discoveredTests.testLocations[type] = files.join(", ");
|
|
300
|
-
}
|
|
301
|
-
logger.info("Test discovery completed", { totalFiles: discoveryResult.tests.length, types: Object.fromEntries(Array.from(byType.entries()).map(([k, v]) => [k, v.length])) });
|
|
302
|
-
}
|
|
303
|
-
catch (err) {
|
|
304
|
-
logger.warning("TestDiscoveryService failed, existingTests will be empty", { error: err instanceof Error ? err.message : String(err) });
|
|
305
|
-
}
|
|
306
|
-
// I1/I2: Discover and parse trace files for enrichment
|
|
307
|
-
let traceResult;
|
|
308
|
-
const traceFiles = discoverTraceFiles(params.repositoryPath);
|
|
309
|
-
if (traceFiles.length > 0) {
|
|
310
|
-
try {
|
|
311
|
-
traceResult = await parseTraceFile(traceFiles[0]);
|
|
312
|
-
logger.info("Parsed trace file", {
|
|
313
|
-
file: traceFiles[0],
|
|
314
|
-
format: traceResult.format,
|
|
315
|
-
entries: traceResult.entries.length,
|
|
316
|
-
flows: traceResult.userFlows.length,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
catch (err) {
|
|
320
|
-
logger.warning("Trace parsing failed", { error: err instanceof Error ? err.message : String(err) });
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
// I4: Merge trace-derived interactions into skeleton endpoints
|
|
324
|
-
if (traceResult && traceResult.entries.length > 0) {
|
|
325
|
-
for (const entry of traceResult.entries) {
|
|
326
|
-
let rawPath = entry.path;
|
|
327
|
-
try {
|
|
328
|
-
rawPath = new URL(rawPath).pathname;
|
|
329
|
-
}
|
|
330
|
-
catch { /* already a path */ }
|
|
331
|
-
const normalizedPath = rawPath.replace(/\/[0-9a-f-]{20,}/gi, "/{id}").replace(/\/\d+/g, "/{id}");
|
|
332
|
-
const existing = skeletonEndpoints.find(ep => {
|
|
333
|
-
const epNorm = ep.path.replace(/\{\w+\}/g, "/{id}");
|
|
334
|
-
return epNorm === normalizedPath;
|
|
335
|
-
});
|
|
336
|
-
if (existing) {
|
|
337
|
-
const methodObj = existing.methods.find((m) => m.method === entry.method);
|
|
338
|
-
if (methodObj) {
|
|
339
|
-
const alreadyHasStatus = methodObj.interactions.some((i) => i.response.statusCode === entry.statusCode);
|
|
340
|
-
if (!alreadyHasStatus) {
|
|
341
|
-
methodObj.interactions.push({
|
|
342
|
-
description: `${entry.method} ${entry.path} \u2192 ${entry.statusCode} (from trace)`,
|
|
343
|
-
type: "success",
|
|
344
|
-
request: entry.requestBody ? { body: entry.requestBody } : {},
|
|
345
|
-
response: {
|
|
346
|
-
statusCode: entry.statusCode,
|
|
347
|
-
description: `Observed in trace (${traceResult.format})`,
|
|
348
|
-
...(entry.responseBody ? { body: entry.responseBody } : {}),
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// Combine trace-derived + code-inferred scenarios
|
|
357
|
-
const codeInferredScenarios = draftScenariosFromEndpoints(skeletonEndpoints);
|
|
358
|
-
let allDraftedScenarios = codeInferredScenarios;
|
|
359
|
-
if (traceResult && traceResult.userFlows.length > 0) {
|
|
360
|
-
const traceScenarios = traceResult.userFlows.slice(0, 5).map((flow, idx) => ({
|
|
361
|
-
scenarioName: `trace-flow-${idx + 1}`,
|
|
362
|
-
description: `User flow from trace: ${flow.entries.map(e => `${e.method} ${e.path}`).join(" \u2192 ")}`,
|
|
363
|
-
category: "workflow",
|
|
364
|
-
priority: "high",
|
|
365
|
-
steps: flow.entries.map((e, stepIdx) => ({
|
|
366
|
-
order: stepIdx + 1,
|
|
367
|
-
method: e.method,
|
|
368
|
-
path: e.path,
|
|
369
|
-
description: `${e.method} ${e.path} \u2192 ${e.statusCode}`,
|
|
370
|
-
interactionType: e.statusCode < 400 ? "success" : "error",
|
|
371
|
-
requestBody: e.requestBody,
|
|
372
|
-
responseBody: e.responseBody,
|
|
373
|
-
expectedStatusCode: e.statusCode,
|
|
374
|
-
})),
|
|
375
|
-
chainingKeys: [],
|
|
376
|
-
requiresAuth: true,
|
|
377
|
-
estimatedComplexity: flow.entries.length > 3 ? "complex" : "moderate",
|
|
378
|
-
source: "trace",
|
|
379
|
-
}));
|
|
380
|
-
allDraftedScenarios = [...traceScenarios, ...codeInferredScenarios];
|
|
381
|
-
}
|
|
382
|
-
const skeletonState = {
|
|
383
|
-
repositoryPath: params.repositoryPath,
|
|
384
|
-
analysisScope,
|
|
385
|
-
analysis: {
|
|
386
|
-
metadata: {
|
|
387
|
-
repositoryName: path.basename(params.repositoryPath),
|
|
388
|
-
analysisDate: new Date().toISOString(),
|
|
389
|
-
scanDepth: params.scanDepth || "full",
|
|
390
|
-
analysisScope,
|
|
391
|
-
},
|
|
392
|
-
projectClassification: {
|
|
393
|
-
projectType: projectMeta.projectType,
|
|
394
|
-
primaryLanguage: projectMeta.primaryLanguage,
|
|
395
|
-
primaryFramework: projectMeta.primaryFramework,
|
|
396
|
-
deploymentPattern: projectMeta.deploymentPattern,
|
|
397
|
-
},
|
|
398
|
-
technologyStack: { languages: projectMeta.languages, frameworks: projectMeta.frameworks, runtime: projectMeta.runtime, keyDependencies: [] },
|
|
399
|
-
businessContext: {
|
|
400
|
-
mainPurpose: "",
|
|
401
|
-
userFlows: [],
|
|
402
|
-
dataFlows: [],
|
|
403
|
-
integrationPatterns: [],
|
|
404
|
-
draftedScenarios: allDraftedScenarios,
|
|
405
|
-
},
|
|
406
|
-
artifacts: {
|
|
407
|
-
openApiSpecs: wsSchemaPath ? [{
|
|
408
|
-
path: wsSchemaPath,
|
|
409
|
-
version: "from-workspace-config",
|
|
410
|
-
endpointCount: 0,
|
|
411
|
-
baseUrl: wsBaseUrl,
|
|
412
|
-
authType: wsAuthMethod,
|
|
413
|
-
}] : [],
|
|
414
|
-
playwrightRecordings: [], traceFiles: [], notFound: [],
|
|
415
|
-
},
|
|
416
|
-
apiEndpoints: {
|
|
417
|
-
totalCount: skeletonEndpoints.reduce((acc, ep) => acc + ep.methods.length, 0),
|
|
418
|
-
baseUrl: wsBaseUrl,
|
|
419
|
-
endpoints: skeletonEndpoints,
|
|
420
|
-
},
|
|
421
|
-
authentication: {
|
|
422
|
-
method: wsAuthMethod,
|
|
423
|
-
configLocation: wsAuthHeader ? ".skyramp/workspace.yml" : "",
|
|
424
|
-
envVarsRequired: [],
|
|
425
|
-
setupExample: wsAuthHeader ? `${wsAuthHeader}: <token>` : "",
|
|
426
|
-
},
|
|
427
|
-
infrastructure: { isContainerized: projectMeta.isContainerized, hasDockerCompose: projectMeta.hasDockerCompose, hasKubernetes: false, hasCiCd: projectMeta.hasCiCd },
|
|
428
|
-
existingTests: discoveredTests,
|
|
429
|
-
...(diffContext ? { branchDiffContext: diffContext } : {}),
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
storeSessionData(sessionId, skeletonState);
|
|
433
|
-
registerSession(sessionId, `memory://${sessionId}`);
|
|
434
|
-
logger.info("Stored analysis skeleton in process memory", {
|
|
435
|
-
sessionId,
|
|
436
|
-
endpointCount: skeletonEndpoints.length,
|
|
437
|
-
});
|
|
438
|
-
// F2: Notify clients that new MCP resources are available
|
|
439
|
-
try {
|
|
440
|
-
await server.server.sendResourceListChanged();
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
// Client may not support resource list notifications
|
|
444
|
-
}
|
|
445
|
-
await sendProgress(70, 100, "Preparing LLM instructions...");
|
|
446
|
-
const routerMountContext = grepRouterMountingContext(params.repositoryPath);
|
|
447
|
-
await sendProgress(100, 100, "Analysis session ready.");
|
|
448
|
-
const outputText = buildAnalysisOutputText({
|
|
449
|
-
sessionId,
|
|
450
|
-
repositoryPath: params.repositoryPath,
|
|
451
|
-
analysisScope,
|
|
452
|
-
parsedDiff,
|
|
453
|
-
scannedEndpoints,
|
|
454
|
-
wsBaseUrl,
|
|
455
|
-
wsAuthHeader,
|
|
456
|
-
wsSchemaPath,
|
|
457
|
-
routerMountContext,
|
|
458
|
-
});
|
|
459
|
-
// B2: Structured JSON summary prepended to output
|
|
460
|
-
const structuredSummary = JSON.stringify({
|
|
461
|
-
sessionId,
|
|
462
|
-
summary: {
|
|
463
|
-
repositoryName: path.basename(params.repositoryPath),
|
|
464
|
-
projectType: projectMeta.projectType,
|
|
465
|
-
primaryFramework: projectMeta.primaryFramework,
|
|
466
|
-
endpointCount: skeletonEndpoints.reduce((acc, ep) => acc + ep.methods.length, 0),
|
|
467
|
-
scenarioCount: skeletonState.analysis.businessContext.draftedScenarios.length,
|
|
468
|
-
interactionCount: skeletonEndpoints.reduce((acc, ep) => acc + ep.methods.reduce((a2, m) => a2 + m.interactions.length, 0), 0),
|
|
469
|
-
},
|
|
470
|
-
resources: {
|
|
471
|
-
summary: `skyramp://analysis/${sessionId}/summary`,
|
|
472
|
-
endpoints: `skyramp://analysis/${sessionId}/endpoints`,
|
|
473
|
-
scenarios: `skyramp://analysis/${sessionId}/scenarios`,
|
|
474
|
-
diff: analysisScope === "current_branch_diff" ? `skyramp://analysis/${sessionId}/diff` : undefined,
|
|
475
|
-
},
|
|
476
|
-
nextStep: "Call skyramp_recommend_tests with sessionId, or read resources for details",
|
|
477
|
-
}, null, 2);
|
|
478
|
-
return {
|
|
479
|
-
content: [
|
|
480
|
-
{ type: "text", text: `\`\`\`json\n${structuredSummary}\n\`\`\`\n\n${outputText}` },
|
|
481
|
-
],
|
|
482
|
-
isError: false,
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
catch (error) {
|
|
486
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
487
|
-
logger.error("Analyze repository tool failed", { error: errorMessage });
|
|
488
|
-
errorResult = {
|
|
489
|
-
content: [
|
|
490
|
-
{
|
|
491
|
-
type: "text",
|
|
492
|
-
text: `Error analyzing repository: ${errorMessage}`,
|
|
493
|
-
},
|
|
494
|
-
],
|
|
495
|
-
isError: true,
|
|
496
|
-
};
|
|
497
|
-
return errorResult;
|
|
498
|
-
}
|
|
499
|
-
finally {
|
|
500
|
-
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
501
|
-
repositoryPath: params.repositoryPath,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
}
|