@skyramp/mcp 0.0.59 → 0.0.60-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 +32 -5
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +98 -0
- package/build/prompts/test-recommendation/recommendationSections.js +226 -0
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +71 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +166 -104
- package/build/prompts/testGenerationPrompt.js +2 -3
- package/build/prompts/testbot/testbot-prompts.js +96 -93
- package/build/resources/analysisResources.js +254 -0
- package/build/services/ScenarioGenerationService.js +70 -26
- package/build/tools/generate-tests/generateIntegrationRestTool.js +54 -1
- package/build/tools/generate-tests/generateScenarioRestTool.js +8 -5
- package/build/tools/submitReportTool.js +28 -0
- package/build/tools/test-maintenance/stateCleanupTool.js +8 -0
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +349 -217
- package/build/tools/test-recommendation/recommendTestsTool.js +163 -159
- package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
- package/build/types/RepositoryAnalysis.js +99 -12
- package/build/utils/AnalysisStateManager.js +40 -23
- package/build/utils/branchDiff.js +47 -0
- package/build/utils/pr-comment-parser.js +124 -0
- package/build/utils/projectMetadata.js +188 -0
- package/build/utils/projectMetadata.test.js +81 -0
- package/build/utils/repoScanner.js +378 -0
- package/build/utils/routeParsers.js +213 -0
- package/build/utils/routeParsers.test.js +87 -0
- package/build/utils/scenarioDrafting.js +119 -0
- package/build/utils/scenarioDrafting.test.js +66 -0
- package/build/utils/trace-parser.js +166 -0
- package/build/utils/workspaceAuth.js +16 -0
- package/package.json +1 -1
- package/build/prompts/test-recommendation/repository-analysis-prompt.js +0 -326
- package/build/prompts/test-recommendation/test-mapping-prompt.js +0 -266
- package/build/tools/test-recommendation/mapTestsTool.js +0 -243
- package/build/types/TestMapping.js +0 -173
- package/build/utils/scoring-engine.js +0 -380
|
@@ -1,207 +1,211 @@
|
|
|
1
|
+
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { simpleGit } from "simple-git";
|
|
4
|
+
import { StateManager, getSessionFilePath, getRegisteredSessions, getSessionData, hasSessionData, } from "../../utils/AnalysisStateManager.js";
|
|
4
5
|
import { logger } from "../../utils/logger.js";
|
|
5
6
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
[TestType.INTEGRATION]: "skyramp_integration_test_generation",
|
|
11
|
-
[TestType.FUZZ]: "skyramp_fuzz_test_generation",
|
|
12
|
-
[TestType.E2E]: "skyramp_e2e_test_generation",
|
|
13
|
-
[TestType.LOAD]: "skyramp_load_test_generation",
|
|
14
|
-
[TestType.UI]: "skyramp_ui_test_generation",
|
|
15
|
-
};
|
|
7
|
+
import { buildRecommendationPrompt } from "../../prompts/test-recommendation/test-recommendation-prompt.js";
|
|
8
|
+
import { repositoryAnalysisSchema } from "../../types/RepositoryAnalysis.js";
|
|
9
|
+
import { parsePRComments } from "../../utils/pr-comment-parser.js";
|
|
10
|
+
import { getWorkspaceAuthHeader } from "../../utils/workspaceAuth.js";
|
|
16
11
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* Extract GitHub owner/repo from the origin remote URL.
|
|
13
|
+
* Handles both SSH (git@github.com:owner/repo.git) and
|
|
14
|
+
* HTTPS (https://github.com/owner/repo.git) formats.
|
|
19
15
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
async function getGitHubSlug(repoPath) {
|
|
17
|
+
try {
|
|
18
|
+
const git = simpleGit(repoPath);
|
|
19
|
+
const remotes = await git.getRemotes(true);
|
|
20
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
21
|
+
const url = origin?.refs?.fetch;
|
|
22
|
+
if (!url)
|
|
23
|
+
return null;
|
|
24
|
+
const sshMatch = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
25
|
+
if (sshMatch)
|
|
26
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
27
|
+
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
28
|
+
if (httpsMatch)
|
|
29
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const recommendTestsSchema = {
|
|
37
|
+
sessionId: completable(z.string().describe("Session ID from skyramp_analyze_repository"), async (value) => {
|
|
38
|
+
return Array.from(getRegisteredSessions().keys()).filter((id) => id.startsWith(value.toString()));
|
|
39
|
+
}),
|
|
24
40
|
topN: z
|
|
25
41
|
.number()
|
|
26
|
-
.
|
|
27
|
-
.describe("Number of
|
|
28
|
-
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Number of recommendations. Defaults to 5."),
|
|
44
|
+
prNumber: z
|
|
29
45
|
.number()
|
|
30
|
-
.
|
|
31
|
-
.describe("
|
|
32
|
-
}
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("GitHub PR number. When provided in diff scope, fetches previous TestBot comments to avoid re-recommendations."),
|
|
48
|
+
};
|
|
33
49
|
const TOOL_NAME = "skyramp_recommend_tests";
|
|
34
50
|
export function registerRecommendTestsTool(server) {
|
|
35
51
|
server.registerTool(TOOL_NAME, {
|
|
36
|
-
description: `Generate actionable test recommendations
|
|
37
|
-
|
|
38
|
-
**PREREQUISITE**: Call skyramp_map_tests first to get the stateFile path.
|
|
52
|
+
description: `Generate actionable test recommendations based on enriched repository analysis.
|
|
39
53
|
|
|
40
|
-
|
|
41
|
-
- Ordered list of recommended test types (high/medium/low priority)
|
|
42
|
-
- Specific test scenarios based on actual repository endpoints and flows
|
|
43
|
-
- Artifact availability status and guidance for missing artifacts
|
|
44
|
-
- Step-by-step next actions
|
|
54
|
+
**PREREQUISITE**: Call skyramp_analyze_repository first to get the sessionId.
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Recommended Tests (Prioritized)
|
|
49
|
-
Priority: <PRIORITY HIGH, MEDIUM, OR LOW>
|
|
50
|
-
1. <TEST TYPE> Tests [**CRITICAL: NEVER MENTION THE SCORE HERE IN THE OUTPUT**]
|
|
51
|
-
Rationale:
|
|
52
|
-
Specific Tests to Create:
|
|
53
|
-
Test 1: <TEST NAME>
|
|
54
|
-
Description: <TEST DESCRIPTION>
|
|
55
|
-
Target Flow: <TARGET FLOW>
|
|
56
|
-
\`\`\`
|
|
56
|
+
This tool reads the enriched analysis (endpoints with interactions, drafted scenarios)
|
|
57
|
+
and generates prioritized test recommendations using LLM reasoning over the actual data.
|
|
57
58
|
|
|
58
|
-
For each recommended test
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
59
|
+
For each recommended test, you'll get:
|
|
60
|
+
- Priority (high/medium/low) with reasoning based on actual endpoint data
|
|
61
|
+
- Specific test referencing concrete interactions (request→response pairs)
|
|
62
|
+
- For integration/E2E: references to draftedScenarios by scenarioName
|
|
63
|
+
- Which Skyramp tool to call for generation
|
|
62
64
|
|
|
63
65
|
**CRITICAL RULES**:
|
|
64
66
|
- THE PRIORITY SHOULD ONLY BE DEFINED AS HIGH, MEDIUM, OR LOW NOTHING ELSE.
|
|
65
67
|
- DO NOT SHOW ANY PRIORITY BREAKDOWN IN THE OUTPUT.
|
|
66
68
|
- DON'T MARK ANY TEST BLOCKED EVEN IF REQUIRED ARTIFACTS ARE MISSING.
|
|
69
|
+
- Reference specific interactions by description when recommending smoke/contract/fuzz tests.
|
|
70
|
+
- Reference specific draftedScenarios by scenarioName when recommending integration/E2E tests.
|
|
67
71
|
|
|
68
|
-
Output: TestRecommendation with prioritized, actionable test recommendations.
|
|
69
|
-
**CRITICAL:** At the end of the tool execution, MUST display the below message:
|
|
70
72
|
** This tool is currently in Early Preview stage. Please verify the results. **`,
|
|
71
|
-
inputSchema: recommendTestsSchema
|
|
73
|
+
inputSchema: recommendTestsSchema,
|
|
72
74
|
}, async (params) => {
|
|
73
75
|
let errorResult;
|
|
74
76
|
try {
|
|
75
77
|
logger.info("Recommend tests tool invoked", {
|
|
76
|
-
|
|
78
|
+
sessionId: params.sessionId,
|
|
77
79
|
topN: params.topN,
|
|
78
|
-
minScore: params.minScore,
|
|
79
80
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
throw new Error("stateFile is required");
|
|
81
|
+
if (!params.sessionId) {
|
|
82
|
+
throw new Error("sessionId is required");
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
// Load session data: try process memory first, then fall back to state file
|
|
85
|
+
let stateData = null;
|
|
86
|
+
if (hasSessionData(params.sessionId)) {
|
|
87
|
+
stateData = getSessionData(params.sessionId);
|
|
88
|
+
logger.info("Loaded analysis from process memory", { sessionId: params.sessionId });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Fall back to state file for backward compatibility
|
|
92
|
+
const registeredPath = getSessionFilePath(params.sessionId);
|
|
93
|
+
const stateManager = registeredPath
|
|
94
|
+
? StateManager.fromStatePath(registeredPath)
|
|
95
|
+
: StateManager.fromSessionId(params.sessionId);
|
|
96
|
+
if (!stateManager.exists()) {
|
|
97
|
+
throw new Error(`Analysis session not found: ${params.sessionId}. Run skyramp_analyze_repository first.`);
|
|
98
|
+
}
|
|
99
|
+
stateData = await stateManager.readData();
|
|
100
|
+
logger.info("Loaded analysis from state file (legacy)", { sessionId: params.sessionId });
|
|
87
101
|
}
|
|
88
|
-
const stateData = await stateManager.readData();
|
|
89
102
|
if (!stateData) {
|
|
90
|
-
throw new Error(`Failed to read
|
|
103
|
+
throw new Error(`Failed to read analysis session: ${params.sessionId}. Run skyramp_analyze_repository first.`);
|
|
104
|
+
}
|
|
105
|
+
const { analysisScope } = stateData;
|
|
106
|
+
let { repositoryPath, analysis } = stateData;
|
|
107
|
+
// Fallback: if the LLM saved RepositoryAnalysis at the root level
|
|
108
|
+
// (without the wrapper), detect it and use the data directly.
|
|
109
|
+
if (!analysis && stateData.apiEndpoints) {
|
|
110
|
+
logger.info("Detected unwrapped RepositoryAnalysis — using root data");
|
|
111
|
+
analysis = stateData;
|
|
112
|
+
repositoryPath = repositoryPath || analysis?.metadata?.repositoryName || "unknown";
|
|
91
113
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
if (!analysis) {
|
|
115
|
+
throw new Error("Session is missing analysis data. Run skyramp_analyze_repository first.");
|
|
116
|
+
}
|
|
117
|
+
// Validate analysis against the Zod schema to catch malformed LLM output early
|
|
118
|
+
const parseResult = repositoryAnalysisSchema.safeParse(analysis);
|
|
119
|
+
if (!parseResult.success) {
|
|
120
|
+
const issues = parseResult.error.issues.slice(0, 5).map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
121
|
+
logger.warning("Analysis data has schema issues (proceeding with best-effort)", {
|
|
122
|
+
issueCount: parseResult.error.issues.length,
|
|
123
|
+
sample: issues,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Guard critical nested fields the prompt builder accesses
|
|
127
|
+
if (!analysis.apiEndpoints?.endpoints) {
|
|
128
|
+
analysis.apiEndpoints = { totalCount: 0, baseUrl: "", endpoints: [] };
|
|
129
|
+
}
|
|
130
|
+
if (!analysis.businessContext?.draftedScenarios) {
|
|
131
|
+
analysis.businessContext = {
|
|
132
|
+
...(analysis.businessContext || { mainPurpose: "", userFlows: [], dataFlows: [], integrationPatterns: [] }),
|
|
133
|
+
draftedScenarios: analysis.businessContext?.draftedScenarios || [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!analysis.authentication) {
|
|
137
|
+
analysis.authentication = { method: "none", configLocation: "", envVarsRequired: [], setupExample: "" };
|
|
138
|
+
}
|
|
139
|
+
if (!analysis.existingTests) {
|
|
140
|
+
analysis.existingTests = {
|
|
141
|
+
frameworks: [], coverage: { unit: 0, integration: 0, e2e: 0, ui: 0, load: 0, contract: 0, smoke: 0 },
|
|
142
|
+
testLocations: {}, hasCoverageReports: false,
|
|
143
|
+
};
|
|
95
144
|
}
|
|
96
145
|
const scope = analysisScope || "full_repo";
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
if (
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (hasEndpoints) {
|
|
107
|
-
const lines = [];
|
|
108
|
-
for (const ep of diff.newEndpoints) {
|
|
109
|
-
lines.push(`- **${ep.method} ${apiBaseUrl}${ep.path}** (new) — \`${ep.sourceFile}\``);
|
|
146
|
+
const sessionId = params.sessionId;
|
|
147
|
+
const effectiveTopN = params.topN ?? (scope === "current_branch_diff" ? 7 : 10);
|
|
148
|
+
// Fetch PR context when prNumber is provided and in diff scope
|
|
149
|
+
let prContext = stateData.prContext;
|
|
150
|
+
if (!prContext && params.prNumber && scope === "current_branch_diff") {
|
|
151
|
+
const slug = await getGitHubSlug(repositoryPath);
|
|
152
|
+
if (slug) {
|
|
153
|
+
try {
|
|
154
|
+
prContext = await parsePRComments(slug.owner, slug.repo, params.prNumber);
|
|
110
155
|
}
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
catch (err) {
|
|
157
|
+
logger.warning("Failed to fetch PR context", {
|
|
158
|
+
error: err instanceof Error ? err.message : String(err),
|
|
159
|
+
});
|
|
113
160
|
}
|
|
114
|
-
diffContextSection = `
|
|
115
|
-
## Changed Endpoints in This PR
|
|
116
|
-
${lines.join("\n")}
|
|
117
|
-
|
|
118
|
-
`;
|
|
119
161
|
}
|
|
120
|
-
else
|
|
121
|
-
|
|
122
|
-
// LLM can infer which endpoints are affected
|
|
123
|
-
diffContextSection = `
|
|
124
|
-
## Changed Files in This PR
|
|
125
|
-
${diff.changedFiles.map((f) => `- \`${f}\``).join("\n")}
|
|
126
|
-
|
|
127
|
-
`;
|
|
162
|
+
else {
|
|
163
|
+
logger.debug("Could not determine GitHub owner/repo from git remotes — skipping PR context");
|
|
128
164
|
}
|
|
129
165
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Look in the test directory, repository root, and \`.skyramp/\` directories.
|
|
155
|
-
|
|
156
|
-
**IMPORTANT — Verify trace relevance before using it:**
|
|
157
|
-
Before passing a trace file to a test generation tool, inspect its contents to confirm it actually exercises the UI components or pages affected by the PR. A trace recorded before the current changes will not cover new UI elements. If the trace does NOT cover the changed UI:
|
|
158
|
-
- Do NOT use it for generating tests for the new changes.
|
|
159
|
-
- Report in \`issuesFound\`: "A Playwright trace file was found (<filename>) but it does not cover the new UI changes in this PR. To generate UI tests for the new functionality, record a new trace that exercises the changed pages/components and commit it, then re-run the Testbot."
|
|
166
|
+
const workspaceAuthHeader = repositoryPath
|
|
167
|
+
? await getWorkspaceAuthHeader(repositoryPath)
|
|
168
|
+
: undefined;
|
|
169
|
+
const prompt = buildRecommendationPrompt(analysis, scope, "all", effectiveTopN, prContext, workspaceAuthHeader);
|
|
170
|
+
// H3: Mode-specific output header
|
|
171
|
+
const modeLabel = scope === "current_branch_diff" ? "PR Mode" : "Repo Mode";
|
|
172
|
+
const modeDesc = scope === "current_branch_diff"
|
|
173
|
+
? `Focused on branch changes. Top ${effectiveTopN} recommendations — top 4 will be generated, remaining reported.`
|
|
174
|
+
: `Comprehensive test strategy. Top ${effectiveTopN} tests across the entire application.`;
|
|
175
|
+
// J4: PR awareness messaging
|
|
176
|
+
let prAwareness = "";
|
|
177
|
+
if (prContext && prContext.previousRecommendations.length > 0) {
|
|
178
|
+
const implemented = prContext.previousRecommendations.filter(r => r.status === "implemented").length;
|
|
179
|
+
const pending = prContext.previousRecommendations.filter(r => r.status === "recommended").length;
|
|
180
|
+
prAwareness = `\n**PR History**: ${implemented} tests already implemented, ${pending} previously recommended.`;
|
|
181
|
+
if (implemented > 0) {
|
|
182
|
+
prAwareness += ` Building on existing coverage — new recommendations complement what\'s already been added.`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// G4: Include totalConsidered instruction in output
|
|
186
|
+
const totalEndpointMethods = analysis.apiEndpoints.endpoints.reduce((acc, ep) => acc + (ep.methods?.length || 0), 0);
|
|
187
|
+
const totalInteractions = analysis.apiEndpoints.endpoints.reduce((acc, ep) => acc + (ep.methods || []).reduce((a2, m) => a2 + (m.interactions?.length || 0), 0), 0);
|
|
188
|
+
const totalScenarios = analysis.businessContext.draftedScenarios.length;
|
|
189
|
+
const output = `# Test Recommendations (${modeLabel})
|
|
160
190
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const nextActionsSection = mapping.summary.highPriority.length > 0 ||
|
|
167
|
-
mapping.summary.mediumPriority.length > 0
|
|
168
|
-
? `
|
|
169
|
-
## Required Next Actions
|
|
191
|
+
**Session**: \`${sessionId}\`
|
|
192
|
+
**Repository**: \`${repositoryPath}\`
|
|
193
|
+
**Mode**: ${modeLabel} — ${modeDesc}${prAwareness}
|
|
194
|
+
**Catalog**: ${totalEndpointMethods} endpoint methods, ${totalInteractions} interactions, ${totalScenarios} scenarios
|
|
195
|
+
**Target**: Top ${effectiveTopN} recommendations ranked by value. Top 4 = generate & execute. #5-#${effectiveTopN} = report as additional recommendations.
|
|
170
196
|
|
|
171
|
-
|
|
172
|
-
|
|
197
|
+
## Available Resources
|
|
198
|
+
- Summary: \`skyramp://analysis/${sessionId}/summary\`
|
|
199
|
+
- Endpoints: \`skyramp://analysis/${sessionId}/endpoints\`
|
|
200
|
+
- Scenarios: \`skyramp://analysis/${sessionId}/scenarios\`
|
|
173
201
|
|
|
174
|
-
|
|
175
|
-
${highActions || "none"}
|
|
202
|
+
---
|
|
176
203
|
|
|
177
|
-
${
|
|
178
|
-
`
|
|
179
|
-
: "";
|
|
180
|
-
const output = `# Test Recommendations
|
|
181
|
-
|
|
182
|
-
**State File**: \`${params.stateFile}\`
|
|
183
|
-
**Repository**: \`${repositoryPath}\`
|
|
184
|
-
**Scope**: ${isDiffScope ? "current branch diff" : "full repository"}
|
|
185
|
-
${diffContextSection}
|
|
186
|
-
## Priority Summary
|
|
187
|
-
- High: ${mapping.summary.highPriority.join(", ") || "none"}
|
|
188
|
-
- Medium: ${mapping.summary.mediumPriority.join(", ") || "none"}
|
|
189
|
-
- Low: ${mapping.summary.lowPriority.join(", ") || "none"}
|
|
190
|
-
${nextActionsSection}
|
|
191
|
-
**MANDATORY RULES**:
|
|
192
|
-
1. Use ONLY "high", "medium", "low" for priority — NO numeric scores.
|
|
193
|
-
2. DO NOT mark any test "blocked" even if artifacts are missing.
|
|
194
|
-
3. DO NOT create or save any files — output everything inline.
|
|
195
|
-
4. For each generated test, pass the proper endpoint URL as the \`endpointURL\` parameter to the generate tool and DO NOT pass base URL as the \`endpointURL\` parameter.
|
|
204
|
+
${prompt}
|
|
196
205
|
|
|
197
206
|
** This tool is currently in Early Preview stage. Please verify the results. **`;
|
|
198
207
|
return {
|
|
199
|
-
content: [
|
|
200
|
-
{
|
|
201
|
-
type: "text",
|
|
202
|
-
text: output,
|
|
203
|
-
},
|
|
204
|
-
],
|
|
208
|
+
content: [{ type: "text", text: output }],
|
|
205
209
|
isError: false,
|
|
206
210
|
};
|
|
207
211
|
}
|
|
@@ -29,7 +29,7 @@ Fields per service:
|
|
|
29
29
|
python → pytest or robot
|
|
30
30
|
typescript / javascript → playwright
|
|
31
31
|
java → junit
|
|
32
|
-
outputDir — service root relative to repo root, e.g. "services/api-gateway"
|
|
32
|
+
outputDir — service root relative to repo root where the test files will be generated, e.g. "services/api-gateway"
|
|
33
33
|
api.schemaPath — path or URL to OpenAPI/Protobuf/GraphQL schema
|
|
34
34
|
Search for: openapi.json, swagger.yaml, *.proto, *.graphql
|
|
35
35
|
Framework defaults: FastAPI → /openapi.json, Express → /api-docs, Spring → /v3/api-docs
|
|
@@ -1,19 +1,111 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
// Zod schemas
|
|
2
|
+
// ── Zod schemas ──
|
|
3
3
|
export const analysisScopeSchema = z.enum(["full_repo", "current_branch_diff"]);
|
|
4
|
+
export const paramInfoSchema = z.object({
|
|
5
|
+
name: z.string(),
|
|
6
|
+
type: z.string(),
|
|
7
|
+
required: z.boolean(),
|
|
8
|
+
description: z.string().optional(),
|
|
9
|
+
constraints: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
export const cookieInfoSchema = z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
sampleValue: z.string(),
|
|
14
|
+
purpose: z.string(),
|
|
15
|
+
httpOnly: z.boolean(),
|
|
16
|
+
secure: z.boolean(),
|
|
17
|
+
});
|
|
18
|
+
export const endpointInteractionSchema = z.object({
|
|
19
|
+
description: z.string(),
|
|
20
|
+
type: z.enum(["success", "error", "edge-case"]),
|
|
21
|
+
request: z.object({
|
|
22
|
+
contentType: z.string().optional(),
|
|
23
|
+
body: z.record(z.any()).optional(),
|
|
24
|
+
headers: z.record(z.string()).optional(),
|
|
25
|
+
queryParams: z.record(z.any()).optional(),
|
|
26
|
+
}),
|
|
27
|
+
response: z.object({
|
|
28
|
+
statusCode: z.number(),
|
|
29
|
+
description: z.string(),
|
|
30
|
+
body: z.record(z.any()).optional(),
|
|
31
|
+
headers: z.record(z.string()).optional(),
|
|
32
|
+
cookies: z.array(cookieInfoSchema).optional(),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
export const endpointMethodSchema = z.object({
|
|
36
|
+
method: z.string(),
|
|
37
|
+
description: z.string(),
|
|
38
|
+
queryParams: z.array(paramInfoSchema),
|
|
39
|
+
authRequired: z.boolean(),
|
|
40
|
+
authType: z.string().optional(),
|
|
41
|
+
scopes: z.array(z.string()).optional(),
|
|
42
|
+
sourceFile: z.string(),
|
|
43
|
+
interactions: z.array(endpointInteractionSchema),
|
|
44
|
+
dependsOn: z.array(z.string()).optional(),
|
|
45
|
+
createsResource: z.string().optional(),
|
|
46
|
+
});
|
|
47
|
+
export const enrichedEndpointSchema = z.object({
|
|
48
|
+
path: z.string(),
|
|
49
|
+
resourceGroup: z.string(),
|
|
50
|
+
pathParams: z.array(paramInfoSchema),
|
|
51
|
+
methods: z.array(endpointMethodSchema),
|
|
52
|
+
});
|
|
53
|
+
export const chainingRefSchema = z.object({
|
|
54
|
+
sourceStep: z.number(),
|
|
55
|
+
sourceField: z.string(),
|
|
56
|
+
sourceLocation: z.enum(["body", "header", "cookie"]),
|
|
57
|
+
targetParam: z.string(),
|
|
58
|
+
targetLocation: z.enum(["path", "body", "query", "header", "cookie"]),
|
|
59
|
+
});
|
|
60
|
+
export const scenarioStepSchema = z.object({
|
|
61
|
+
order: z.number(),
|
|
62
|
+
method: z.string(),
|
|
63
|
+
path: z.string(),
|
|
64
|
+
description: z.string(),
|
|
65
|
+
interactionType: z.enum(["success", "error", "edge-case"]),
|
|
66
|
+
requestBody: z.record(z.any()).optional(),
|
|
67
|
+
expectedStatusCode: z.number(),
|
|
68
|
+
expectedResponseFields: z.array(z.string()).optional(),
|
|
69
|
+
chainsFrom: z
|
|
70
|
+
.union([chainingRefSchema, z.array(chainingRefSchema)])
|
|
71
|
+
.optional(),
|
|
72
|
+
});
|
|
73
|
+
export const draftedScenarioSchema = z.object({
|
|
74
|
+
scenarioName: z.string(),
|
|
75
|
+
description: z.string(),
|
|
76
|
+
category: z.enum([
|
|
77
|
+
"crud",
|
|
78
|
+
"workflow",
|
|
79
|
+
"auth",
|
|
80
|
+
"error-handling",
|
|
81
|
+
"data-validation",
|
|
82
|
+
]),
|
|
83
|
+
priority: z.enum(["high", "medium", "low"]),
|
|
84
|
+
steps: z.array(scenarioStepSchema),
|
|
85
|
+
chainingKeys: z.array(z.string()),
|
|
86
|
+
requiresAuth: z.boolean(),
|
|
87
|
+
estimatedComplexity: z.enum(["simple", "moderate", "complex"]),
|
|
88
|
+
source: z.enum(["code-inferred", "trace", "documentation"]).optional(),
|
|
89
|
+
});
|
|
4
90
|
export const branchDiffContextSchema = z.object({
|
|
5
91
|
currentBranch: z.string(),
|
|
6
92
|
baseBranch: z.string(),
|
|
7
93
|
changedFiles: z.array(z.string()),
|
|
8
94
|
newEndpoints: z.array(z.object({
|
|
9
95
|
path: z.string(),
|
|
10
|
-
|
|
11
|
-
|
|
96
|
+
methods: z.array(z.object({
|
|
97
|
+
method: z.string(),
|
|
98
|
+
sourceFile: z.string(),
|
|
99
|
+
interactionCount: z.number(),
|
|
100
|
+
})),
|
|
12
101
|
})),
|
|
13
102
|
modifiedEndpoints: z.array(z.object({
|
|
14
103
|
path: z.string(),
|
|
15
|
-
|
|
16
|
-
|
|
104
|
+
methods: z.array(z.object({
|
|
105
|
+
method: z.string(),
|
|
106
|
+
sourceFile: z.string(),
|
|
107
|
+
changeType: z.enum(["added", "modified", "removed"]),
|
|
108
|
+
})),
|
|
17
109
|
})),
|
|
18
110
|
affectedServices: z.array(z.string()),
|
|
19
111
|
summary: z.string().optional(),
|
|
@@ -62,6 +154,7 @@ export const repositoryAnalysisSchema = z.object({
|
|
|
62
154
|
userFlows: z.array(z.string()),
|
|
63
155
|
dataFlows: z.array(z.string()),
|
|
64
156
|
integrationPatterns: z.array(z.string()),
|
|
157
|
+
draftedScenarios: z.array(draftedScenarioSchema),
|
|
65
158
|
}),
|
|
66
159
|
artifacts: z.object({
|
|
67
160
|
openApiSpecs: z.array(z.object({
|
|
@@ -86,13 +179,7 @@ export const repositoryAnalysisSchema = z.object({
|
|
|
86
179
|
apiEndpoints: z.object({
|
|
87
180
|
totalCount: z.number(),
|
|
88
181
|
baseUrl: z.string(),
|
|
89
|
-
endpoints: z.array(
|
|
90
|
-
path: z.string(),
|
|
91
|
-
method: z.string(),
|
|
92
|
-
resourceGroup: z.string(),
|
|
93
|
-
authRequired: z.boolean(),
|
|
94
|
-
sourceFile: z.string().optional(),
|
|
95
|
-
})),
|
|
182
|
+
endpoints: z.array(enrichedEndpointSchema),
|
|
96
183
|
}),
|
|
97
184
|
authentication: z.object({
|
|
98
185
|
method: z.enum(["bearer", "api-key", "oauth2", "basic", "jwt", "none"]),
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as os from "os";
|
|
4
|
+
import * as crypto from "crypto";
|
|
4
5
|
import { logger } from "./logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* Per-process registry mapping sessionId → stateFilePath.
|
|
8
|
+
* Resource list callbacks iterate this map so each stdio process only
|
|
9
|
+
* advertises its own sessions, eliminating cross-process leakage when
|
|
10
|
+
* multiple MCP clients share the same filesystem (e.g. /tmp).
|
|
11
|
+
*/
|
|
12
|
+
const processSessionRegistry = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* In-memory session store: sessionId → analysis data.
|
|
15
|
+
* Eliminates the need for the LLM to read/write state files on disk.
|
|
16
|
+
* The analyze tool stores a skeleton here; the recommend tool reads it.
|
|
17
|
+
*/
|
|
18
|
+
const inMemorySessionStore = new Map();
|
|
19
|
+
export function registerSession(sessionId, stateFilePath) {
|
|
20
|
+
processSessionRegistry.set(sessionId, stateFilePath);
|
|
21
|
+
}
|
|
22
|
+
export function getSessionFilePath(sessionId) {
|
|
23
|
+
return processSessionRegistry.get(sessionId);
|
|
24
|
+
}
|
|
25
|
+
export function getRegisteredSessions() {
|
|
26
|
+
return processSessionRegistry;
|
|
27
|
+
}
|
|
28
|
+
export function storeSessionData(sessionId, data) {
|
|
29
|
+
inMemorySessionStore.set(sessionId, data);
|
|
30
|
+
}
|
|
31
|
+
export function getSessionData(sessionId) {
|
|
32
|
+
return inMemorySessionStore.get(sessionId);
|
|
33
|
+
}
|
|
34
|
+
export function hasSessionData(sessionId) {
|
|
35
|
+
return inMemorySessionStore.has(sessionId);
|
|
36
|
+
}
|
|
5
37
|
/**
|
|
6
38
|
* File prefix mapping for each state type
|
|
7
39
|
*/
|
|
@@ -24,16 +56,22 @@ export class StateManager {
|
|
|
24
56
|
/**
|
|
25
57
|
* Create a new state manager
|
|
26
58
|
* @param stateType Type of state (analysis, recommendation)
|
|
27
|
-
* @param sessionId Unique session identifier (defaults to
|
|
59
|
+
* @param sessionId Unique session identifier (defaults to UUID)
|
|
28
60
|
* @param stateDir Directory to store state files (defaults to /tmp)
|
|
29
61
|
*/
|
|
30
62
|
constructor(stateType = "analysis", sessionId, stateDir) {
|
|
31
63
|
this.stateType = stateType;
|
|
32
|
-
this.sessionId = sessionId ||
|
|
64
|
+
this.sessionId = sessionId || crypto.randomUUID();
|
|
33
65
|
const baseDir = stateDir || os.tmpdir();
|
|
34
66
|
const prefix = STATE_FILE_PREFIXES[stateType];
|
|
35
67
|
this.stateFile = path.join(baseDir, `${prefix}-${this.sessionId}.json`);
|
|
36
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Create state manager from a sessionId (resolves the state file path internally)
|
|
71
|
+
*/
|
|
72
|
+
static fromSessionId(sessionId, stateType = "recommendation", stateDir) {
|
|
73
|
+
return new StateManager(stateType, sessionId, stateDir);
|
|
74
|
+
}
|
|
37
75
|
/**
|
|
38
76
|
* Create state manager from existing state file path
|
|
39
77
|
*/
|
|
@@ -118,42 +156,24 @@ export class StateManager {
|
|
|
118
156
|
throw new Error(`Failed to write state file: ${error.message}`);
|
|
119
157
|
}
|
|
120
158
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Get the state file path
|
|
123
|
-
*/
|
|
124
159
|
getStatePath() {
|
|
125
160
|
return this.stateFile;
|
|
126
161
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Get the session ID
|
|
129
|
-
*/
|
|
130
162
|
getSessionId() {
|
|
131
163
|
return this.sessionId;
|
|
132
164
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Get the state type
|
|
135
|
-
*/
|
|
136
165
|
getStateType() {
|
|
137
166
|
return this.stateType;
|
|
138
167
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Check if state file exists
|
|
141
|
-
*/
|
|
142
168
|
exists() {
|
|
143
169
|
return fs.existsSync(this.stateFile);
|
|
144
170
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Delete the state file
|
|
147
|
-
*/
|
|
148
171
|
async delete() {
|
|
149
172
|
if (this.exists()) {
|
|
150
173
|
await fs.promises.unlink(this.stateFile);
|
|
151
174
|
logger.debug(`Deleted state file: ${this.stateFile}`);
|
|
152
175
|
}
|
|
153
176
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Get state file size in bytes
|
|
156
|
-
*/
|
|
157
177
|
async getSize() {
|
|
158
178
|
if (!this.exists()) {
|
|
159
179
|
return 0;
|
|
@@ -161,9 +181,6 @@ export class StateManager {
|
|
|
161
181
|
const stats = await fs.promises.stat(this.stateFile);
|
|
162
182
|
return stats.size;
|
|
163
183
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Get human-readable state file size
|
|
166
|
-
*/
|
|
167
184
|
async getSizeFormatted() {
|
|
168
185
|
const bytes = await this.getSize();
|
|
169
186
|
if (bytes === 0)
|