@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
|
@@ -139,9 +139,9 @@ describe("TestGenerationService — extractAuthFromTrace", () => {
|
|
|
139
139
|
fs.writeFileSync(filePath, JSON.stringify(requests), "utf8");
|
|
140
140
|
return filePath;
|
|
141
141
|
}
|
|
142
|
-
function extract(filePath) {
|
|
142
|
+
function extract(filePath, include, exclude) {
|
|
143
143
|
const svc = new StubService();
|
|
144
|
-
return svc.extractAuthFromTrace(filePath);
|
|
144
|
+
return svc.extractAuthFromTrace(filePath, include, exclude);
|
|
145
145
|
}
|
|
146
146
|
it("extracts Authorization + Bearer scheme from trace", () => {
|
|
147
147
|
const fp = writeTrace([
|
|
@@ -203,6 +203,115 @@ describe("TestGenerationService — extractAuthFromTrace", () => {
|
|
|
203
203
|
it("returns null for non-existent file", () => {
|
|
204
204
|
expect(extract("/tmp/does-not-exist-xyz.json")).toBeNull();
|
|
205
205
|
});
|
|
206
|
+
it("skips Pragma header (standard HTTP/1.0 caching header, not auth)", () => {
|
|
207
|
+
const fp = writeTrace([
|
|
208
|
+
{
|
|
209
|
+
RequestHeaders: {
|
|
210
|
+
"Cache-Control": ["no-cache"],
|
|
211
|
+
Pragma: ["no-cache"],
|
|
212
|
+
"User-Agent": ["Mozilla/5.0"],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
expect(extract(fp)).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
it("skips Pragma and finds real auth header behind it", () => {
|
|
219
|
+
const fp = writeTrace([
|
|
220
|
+
{
|
|
221
|
+
RequestHeaders: {
|
|
222
|
+
"Cache-Control": ["no-cache"],
|
|
223
|
+
Pragma: ["no-cache"],
|
|
224
|
+
"User-Agent": ["Mozilla/5.0"],
|
|
225
|
+
Authorization: [`Bearer ${AUTH_PLACEHOLDER_TOKEN}`],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
expect(extract(fp)).toEqual({
|
|
230
|
+
authHeader: "Authorization",
|
|
231
|
+
authScheme: "Bearer",
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
it("uses include filter to skip noise and find auth on matching requests", () => {
|
|
235
|
+
const fp = writeTrace([
|
|
236
|
+
{
|
|
237
|
+
// requests[0]: unrelated traffic (e.g. Google time sync) with no auth
|
|
238
|
+
Destination: "clients2.google.com",
|
|
239
|
+
Path: "/time/1/current",
|
|
240
|
+
RequestHeaders: {
|
|
241
|
+
"Cache-Control": ["no-cache"],
|
|
242
|
+
"User-Agent": ["Mozilla/5.0"],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
// requests[1]: actual API request matching include filter
|
|
247
|
+
Destination: "demoshop.skyramp.dev",
|
|
248
|
+
Path: "/api/v1/products",
|
|
249
|
+
RequestHeaders: {
|
|
250
|
+
"Content-Type": ["application/json"],
|
|
251
|
+
Authorization: [`Bearer ${AUTH_PLACEHOLDER_TOKEN}`],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
expect(extract(fp, ["demoshop.skyramp.dev/api/*"])).toEqual({
|
|
256
|
+
authHeader: "Authorization",
|
|
257
|
+
authScheme: "Bearer",
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
it("uses exclude filter to skip noise and find auth on remaining requests", () => {
|
|
261
|
+
const fp = writeTrace([
|
|
262
|
+
{
|
|
263
|
+
Destination: "clients2.google.com",
|
|
264
|
+
Path: "/time/1/current",
|
|
265
|
+
RequestHeaders: {
|
|
266
|
+
"Cache-Control": ["no-cache"],
|
|
267
|
+
"User-Agent": ["Mozilla/5.0"],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
Destination: "demoshop.skyramp.dev",
|
|
272
|
+
Path: "/api/v1/products",
|
|
273
|
+
RequestHeaders: {
|
|
274
|
+
"Content-Type": ["application/json"],
|
|
275
|
+
Authorization: [`Bearer ${AUTH_PLACEHOLDER_TOKEN}`],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
expect(extract(fp, undefined, ["clients2.google.com/*"])).toEqual({
|
|
280
|
+
authHeader: "Authorization",
|
|
281
|
+
authScheme: "Bearer",
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
it("prefers Authorization over Cookie even when Cookie appears first", () => {
|
|
285
|
+
const fp = writeTrace([
|
|
286
|
+
{
|
|
287
|
+
RequestHeaders: {
|
|
288
|
+
"Content-Type": ["application/json"],
|
|
289
|
+
Cookie: ["cf_clearance=abc123; _dd_s=xyz"],
|
|
290
|
+
Authorization: [`Bearer ${AUTH_PLACEHOLDER_TOKEN}`],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
expect(extract(fp)).toEqual({
|
|
295
|
+
authHeader: "Authorization",
|
|
296
|
+
authScheme: "Bearer",
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
it("falls back to known auth header (X-Api-Key) when no Authorization present", () => {
|
|
300
|
+
const fp = writeTrace([
|
|
301
|
+
{
|
|
302
|
+
RequestHeaders: {
|
|
303
|
+
"Content-Type": ["application/json"],
|
|
304
|
+
"Sec-Fetch-Mode": ["cors"],
|
|
305
|
+
"X-Api-Key": ["my-secret-key"],
|
|
306
|
+
Traceparent: ["00-abc-def-01"],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
expect(extract(fp)).toEqual({
|
|
311
|
+
authHeader: "X-Api-Key",
|
|
312
|
+
authScheme: "",
|
|
313
|
+
});
|
|
314
|
+
});
|
|
206
315
|
it("skips standard headers like Accept", () => {
|
|
207
316
|
const fp = writeTrace([
|
|
208
317
|
{
|
|
@@ -34,8 +34,14 @@ describe("tool-phase-coverage", () => {
|
|
|
34
34
|
});
|
|
35
35
|
it("TOOL_PHASE_MAP contains only valid phases", () => {
|
|
36
36
|
const validPhases = new Set(["analyzing", "generating", "executing", "maintaining", "reporting"]);
|
|
37
|
-
for (const [tool,
|
|
38
|
-
|
|
37
|
+
for (const [tool, mapping] of Object.entries(TOOL_PHASE_MAP)) {
|
|
38
|
+
if (typeof mapping === "string") {
|
|
39
|
+
expect(validPhases.has(mapping)).toBe(true);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
expect(validPhases.has(mapping.before)).toBe(true);
|
|
43
|
+
expect(validPhases.has(mapping.after)).toBe(true);
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
});
|
|
41
47
|
});
|
package/build/tool-phases.js
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical mapping of Skyramp MCP tool names to testbot progress phases.
|
|
3
|
-
*
|
|
4
|
-
* The testbot progress UI reads this map at runtime to know which tool calls
|
|
5
|
-
* correspond to which progress steps. When adding or renaming tools, update
|
|
6
|
-
* this map so the progress UI stays accurate.
|
|
7
|
-
*
|
|
8
|
-
* Tools not in this map must be listed in TOOLS_WITHOUT_PHASE.
|
|
9
|
-
*
|
|
10
|
-
* Phases: analyzing, generating, executing, maintaining, reporting
|
|
11
|
-
*/
|
|
12
1
|
export const TOOL_PHASE_MAP = {
|
|
13
2
|
skyramp_recommend_tests: "analyzing",
|
|
14
3
|
skyramp_analyze_changes: "analyzing",
|
|
@@ -20,12 +9,21 @@ export const TOOL_PHASE_MAP = {
|
|
|
20
9
|
skyramp_e2e_test_generation: "generating",
|
|
21
10
|
skyramp_ui_test_generation: "generating",
|
|
22
11
|
skyramp_scenario_test_generation: "generating",
|
|
12
|
+
skyramp_batch_scenario_test_generation: "generating",
|
|
23
13
|
skyramp_mock_generation: "generating",
|
|
24
|
-
skyramp_execute_test: "executing",
|
|
25
|
-
skyramp_execute_tests: "executing",
|
|
14
|
+
skyramp_execute_test: { before: "maintaining", after: "executing" },
|
|
15
|
+
skyramp_execute_tests: { before: "maintaining", after: "executing" },
|
|
26
16
|
skyramp_analyze_test_health: "maintaining",
|
|
27
17
|
skyramp_submit_report: "reporting",
|
|
28
18
|
};
|
|
19
|
+
/** Tools whose phase depends on context — listed here for consumer discovery. */
|
|
20
|
+
export const CONTEXT_DEPENDENT_TOOLS = new Set(Object.entries(TOOL_PHASE_MAP)
|
|
21
|
+
.filter((entry) => typeof entry[1] === "object")
|
|
22
|
+
.map((entry) => entry[0]));
|
|
23
|
+
/** All tools that belong to the "generating" phase — used as the boundary. */
|
|
24
|
+
export const GENERATING_TOOLS = new Set(Object.entries(TOOL_PHASE_MAP)
|
|
25
|
+
.filter((entry) => typeof entry[1] === "string" && entry[1] === "generating")
|
|
26
|
+
.map((entry) => entry[0]));
|
|
29
27
|
/** Tools that intentionally have no progress phase (infrastructure/utility). */
|
|
30
28
|
export const TOOLS_WITHOUT_PHASE = new Set([
|
|
31
29
|
"skyramp_login",
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ScenarioGenerationService } from "../../services/ScenarioGenerationService.js";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { baseSchema, AUTH_PLACEHOLDER_TOKEN } from "../../types/TestTypes.js";
|
|
6
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
7
|
+
import { getWorkspaceAuthConfig, WorkspaceAuthType } from "../../utils/workspaceAuth.js";
|
|
8
|
+
import { logger } from "../../utils/logger.js";
|
|
9
|
+
const stepSchema = z.object({
|
|
10
|
+
method: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe("HTTP method (GET, POST, PUT, DELETE, PATCH) for this step"),
|
|
13
|
+
path: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe("API path for this step. CRITICAL: For requests that reference an ID created by a prior step, use the ACTUAL ID value from the prior step's responseBody, NOT a template variable."),
|
|
16
|
+
requestBody: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("JSON string of the request body for POST/PUT/PATCH requests"),
|
|
20
|
+
queryParams: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("JSON string of URL query parameters for GET search/filter/list requests"),
|
|
24
|
+
responseBody: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("JSON string of the expected response body"),
|
|
28
|
+
statusCode: z
|
|
29
|
+
.number()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Expected HTTP status code. Defaults: POST→201, DELETE→204, GET/PUT/PATCH→200."),
|
|
32
|
+
responseHeaders: z
|
|
33
|
+
.record(z.array(z.string()))
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Response headers as a JSON object. Defaults to Content-Type: application/json."),
|
|
36
|
+
});
|
|
37
|
+
const batchScenarioSchema = {
|
|
38
|
+
scenarioName: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe("Name of the test scenario. KEEP IT SHORT AND DESCRIPTIVE AS USED FOR FILE NAME."),
|
|
41
|
+
destination: z
|
|
42
|
+
.string()
|
|
43
|
+
.describe("Destination hostname or IP address (e.g., localhost). Do NOT include port numbers."),
|
|
44
|
+
baseURL: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Base URL for the API endpoints (e.g., http://localhost:8000/api/v1)."),
|
|
48
|
+
apiSchema: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Optional. Absolute path or URL to the OpenAPI/Swagger schema file."),
|
|
52
|
+
prompt: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Description of the overall multi-step scenario being tested"),
|
|
56
|
+
steps: z
|
|
57
|
+
.array(stepSchema)
|
|
58
|
+
.min(1)
|
|
59
|
+
.describe("Ordered array of API request steps. Each step is generated as a TraceRequest and appended to the scenario file."),
|
|
60
|
+
outputDir: baseSchema.shape.outputDir,
|
|
61
|
+
authHeader: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Which HTTP header carries the auth credential. Pass empty string or omit for unauthenticated endpoints."),
|
|
65
|
+
authScheme: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Only when authHeader is 'Authorization'. The prefix before the token (e.g., 'Bearer')."),
|
|
69
|
+
authToken: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Optional explicit auth token value. Leave empty — the service auto-generates "
|
|
73
|
+
+ `${AUTH_PLACEHOLDER_TOKEN}. Only provide for custom formats (e.g., 'session=${AUTH_PLACEHOLDER_TOKEN}' for Cookie).`),
|
|
74
|
+
};
|
|
75
|
+
const TOOL_NAME = "skyramp_batch_scenario_test_generation";
|
|
76
|
+
export function registerBatchScenarioTestTool(server) {
|
|
77
|
+
server.registerTool(TOOL_NAME, {
|
|
78
|
+
description: `Generate a complete multi-step scenario file in a single call.
|
|
79
|
+
|
|
80
|
+
This tool generates ALL TraceRequest objects for a multi-step scenario at once, producing
|
|
81
|
+
the complete scenario JSON file in one invocation. Use this instead of calling
|
|
82
|
+
\`skyramp_scenario_test_generation\` multiple times for multi-step integration tests.
|
|
83
|
+
|
|
84
|
+
**When to use:**
|
|
85
|
+
- Multi-step integration test scenarios (e.g., create product → create order → update order → verify)
|
|
86
|
+
- Any scenario requiring 2+ sequential API requests
|
|
87
|
+
|
|
88
|
+
**What it does:**
|
|
89
|
+
1. Accepts an ordered array of steps, each with method, path, requestBody, etc.
|
|
90
|
+
2. Generates a TraceRequest for each step
|
|
91
|
+
3. Writes the complete scenario JSON file with all steps
|
|
92
|
+
|
|
93
|
+
**After this tool:** Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\` path.
|
|
94
|
+
|
|
95
|
+
**CRITICAL:** Use CONCRETE ID values in paths (e.g., '/api/v1/products/70885'), not template variables.`,
|
|
96
|
+
inputSchema: batchScenarioSchema,
|
|
97
|
+
}, async (params) => {
|
|
98
|
+
if (params.authHeader === undefined) {
|
|
99
|
+
try {
|
|
100
|
+
const repoPath = params.outputDir || process.cwd();
|
|
101
|
+
const wsAuth = await getWorkspaceAuthConfig(repoPath);
|
|
102
|
+
if (wsAuth.authHeader && wsAuth.authType !== WorkspaceAuthType.None) {
|
|
103
|
+
logger.info("Auth header was empty — resolved from workspace config", {
|
|
104
|
+
authHeader: wsAuth.authHeader,
|
|
105
|
+
authType: wsAuth.authType,
|
|
106
|
+
});
|
|
107
|
+
params.authHeader = wsAuth.authHeader;
|
|
108
|
+
if (/^authorization$/i.test(wsAuth.authHeader) && wsAuth.authType) {
|
|
109
|
+
params.authScheme = params.authScheme || wsAuth.authType;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
logger.warning("Could not resolve auth from workspace config");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const service = new ScenarioGenerationService();
|
|
118
|
+
const steps = params.steps;
|
|
119
|
+
const scenarioSlug = params.scenarioName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "scenario";
|
|
120
|
+
const fileName = `scenario_${scenarioSlug}.json`;
|
|
121
|
+
const filePath = path.join(params.outputDir, fileName);
|
|
122
|
+
const resolvedOut = path.resolve(params.outputDir);
|
|
123
|
+
if (!path.resolve(filePath).startsWith(resolvedOut + path.sep) && path.resolve(filePath) !== resolvedOut) {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: `Error: scenarioName produced a path outside outputDir.` }],
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const traceRequests = [];
|
|
130
|
+
for (let i = 0; i < steps.length; i++) {
|
|
131
|
+
const step = steps[i];
|
|
132
|
+
const stepParams = {
|
|
133
|
+
scenarioName: params.scenarioName,
|
|
134
|
+
destination: params.destination,
|
|
135
|
+
baseURL: params.baseURL,
|
|
136
|
+
apiSchema: params.apiSchema,
|
|
137
|
+
method: step.method,
|
|
138
|
+
path: step.path,
|
|
139
|
+
requestBody: step.requestBody,
|
|
140
|
+
queryParams: step.queryParams,
|
|
141
|
+
responseBody: step.responseBody,
|
|
142
|
+
statusCode: step.statusCode,
|
|
143
|
+
responseHeaders: step.responseHeaders,
|
|
144
|
+
outputDir: params.outputDir,
|
|
145
|
+
authHeader: params.authHeader ?? "",
|
|
146
|
+
authScheme: params.authScheme ?? "",
|
|
147
|
+
authToken: params.authToken,
|
|
148
|
+
};
|
|
149
|
+
const traceRequest = service.generateTraceRequestFromInput(stepParams);
|
|
150
|
+
if (!traceRequest) {
|
|
151
|
+
const failResult = {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: `**Batch Scenario Generation Failed at Step ${i + 1}/${steps.length}**\n\n`
|
|
156
|
+
+ `${i}/${steps.length} steps succeeded before failure.\n\n`
|
|
157
|
+
+ `**Failed step:** ${step.method} ${step.path}\n`
|
|
158
|
+
+ `**Error:** Could not generate a trace request from the provided parameters.\n\n`
|
|
159
|
+
+ `Fix the failing step and retry with the full batch.`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
isError: true,
|
|
163
|
+
};
|
|
164
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, failResult, {
|
|
165
|
+
scenarioName: params.scenarioName,
|
|
166
|
+
stepCount: String(i),
|
|
167
|
+
failedStep: String(i + 1),
|
|
168
|
+
}).catch(() => { });
|
|
169
|
+
return failResult;
|
|
170
|
+
}
|
|
171
|
+
traceRequests.push(traceRequest);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
fs.writeFileSync(filePath, JSON.stringify(traceRequests, null, 2), "utf8");
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text", text: `Error writing scenario file: ${errorMessage}` }],
|
|
180
|
+
isError: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const stepCount = traceRequests.length;
|
|
184
|
+
const result = {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: `**Batch Scenario Generated — ${stepCount} steps**\n\n`
|
|
189
|
+
+ `**Scenario:** ${params.scenarioName}\n`
|
|
190
|
+
+ `**Steps:**\n${steps.map((s, i) => ` ${i + 1}. ${s.method} ${s.path} → ${s.statusCode ?? "default"}`).join("\n")}\n\n`
|
|
191
|
+
+ `**File:** ${filePath}\n\n`
|
|
192
|
+
+ `**Next:** Call \`skyramp_integration_test_generation\` with \`scenarioFile: "${filePath}"\``,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
isError: false,
|
|
196
|
+
};
|
|
197
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, result, {
|
|
198
|
+
scenarioName: params.scenarioName,
|
|
199
|
+
stepCount: String(stepCount),
|
|
200
|
+
}).catch(() => { });
|
|
201
|
+
return result;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { baseTestSchema, TestType } from "../../types/TestTypes.js";
|
|
4
4
|
import { TestGenerationService, } from "../../services/TestGenerationService.js";
|
|
5
5
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
6
|
+
import { ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER } from "../../prompts/test-maintenance/enhanceAssertionSection.js";
|
|
6
7
|
const contractTestSchema = {
|
|
7
8
|
...baseTestSchema,
|
|
8
9
|
pathParams: z
|
|
@@ -170,78 +171,7 @@ ${sections.join("\n")}
|
|
|
170
171
|
return `
|
|
171
172
|
⏭️ **CRITICAL STEP 2 OF 2 — Do this immediately along with test generation:**
|
|
172
173
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
The generated contract test contains only basic assertions (status code, top-level schema checks). For each **test function** (NOT \`beforeAll\` or \`afterAll\`), inspect the response body assertions and add or strengthen them using the rules below.
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
**IMPORTANT — How to access response body fields (use the SDK helpers, NOT dict/attribute access on the response variable):**
|
|
180
|
-
|
|
181
|
-
- **Python**: \`skyramp.get_response_value(<response_var>, "<json_path>")\`
|
|
182
|
-
- e.g. \`skyramp.get_response_value(products_POST_response, "id")\`
|
|
183
|
-
- e.g. \`skyramp.get_response_value(orders_POST_response, "items.0.product_id")\`
|
|
184
|
-
- **TypeScript (Playwright)**: \`getResponseValue(<response_var>, "<json_path>")\` (already imported from \`@skyramp/skyramp\`)
|
|
185
|
-
- e.g. \`getResponseValue(productsPostResponse, "id")\`
|
|
186
|
-
- **JavaScript (Playwright)**: \`getResponseValue(<response_var>, "<json_path>")\` (already imported from \`@skyramp/skyramp\`)
|
|
187
|
-
- e.g. \`getResponseValue(productsPostResponse, "id")\`
|
|
188
|
-
- **Java**: \`getValue(<response_var>, "<json_path>")\` (already imported)
|
|
189
|
-
- e.g. \`getValue(productsPostResponse, "id")\`
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
**What to assert after each request:**
|
|
194
|
-
|
|
195
|
-
1. **Non-null / non-empty fields** — After any POST or PUT that creates or updates a resource, assert that key identifying fields are present and non-empty:
|
|
196
|
-
- IDs, names, emails, and other primary fields must not be null/None/empty.
|
|
197
|
-
- Python: \`assert skyramp.get_response_value(products_POST_response, "id") is not None\`
|
|
198
|
-
- TypeScript: \`expect(getResponseValue(productsPostResponse, "id"), 'id').not.toBeNull();\`
|
|
199
|
-
- JavaScript: \`assert.notStrictEqual(getResponseValue(productsPostResponse, "id"), null, 'id should not be null');\`
|
|
200
|
-
- Java: \`assertNotNull(getValue(productsPostResponse, "id"));\`
|
|
201
|
-
|
|
202
|
-
2. **Echo-back values** — When the request body sends a value the response is expected to return unchanged (name, email, status, price, etc.), assert the response value equals the sent value:
|
|
203
|
-
- Python: \`assert skyramp.get_response_value(products_POST_response, "name") == "Skyramp Tester"\`
|
|
204
|
-
- TypeScript: \`expect(getResponseValue(productsPostResponse, "name"), 'name').toBe("Skyramp Tester");\`
|
|
205
|
-
- JavaScript: \`assert.strictEqual(getResponseValue(productsPostResponse, "name"), "Skyramp Tester", 'name should match request');\`
|
|
206
|
-
- Java: \`assertEquals("Skyramp Tester", getValue(productsPostResponse, "name"));\`
|
|
207
|
-
|
|
208
|
-
3. **Value ranges** — For numeric fields where a realistic range is inferable from the field name, schema constraints (minimum/maximum), or domain knowledge:
|
|
209
|
-
- Assert the value falls within the expected range.
|
|
210
|
-
- Python: \`assert skyramp.get_response_value(products_POST_response, "price") >= 0\`
|
|
211
|
-
- TypeScript: \`expect(getResponseValue(productsPostResponse, "price")).toBeGreaterThanOrEqual(0);\`
|
|
212
|
-
- JavaScript: \`assert.ok(getResponseValue(productsPostResponse, "price") >= 0, 'price should be non-negative');\`
|
|
213
|
-
- Include upper bounds when the OpenAPI schema defines them (e.g. \`maximum\`, \`minimum\`).
|
|
214
|
-
|
|
215
|
-
4. **Format/type constraints** — For string fields with a known format, assert the format rather than just the type:
|
|
216
|
-
- **UUID**: assert the value matches the UUID pattern (\`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\`).
|
|
217
|
-
- **Date / date-time**: assert the value is a valid ISO 8601 date or datetime string.
|
|
218
|
-
- **IP address**: assert the value matches IPv4 or IPv6 format.
|
|
219
|
-
- **Email**: assert the value contains \`@\` and a domain.
|
|
220
|
-
- **URL / URI**: assert the value starts with \`http://\` or \`https://\`.
|
|
221
|
-
- Use format info from the OpenAPI schema (\`format: uuid\`, \`format: date-time\`, etc.) to identify these fields.
|
|
222
|
-
|
|
223
|
-
5. **Specific known values** — For fields where the expected value can be determined from context:
|
|
224
|
-
- Check the OpenAPI schema for \`enum\`, \`const\`, or \`example\` values and assert accordingly.
|
|
225
|
-
- Python: \`assert skyramp.get_response_value(orders_POST_response, "status") == "pending"\`
|
|
226
|
-
- TypeScript: \`expect(getResponseValue(ordersPostResponse, "status"), 'status').toBe("pending");\`
|
|
227
|
-
- JavaScript: \`assert.strictEqual(getResponseValue(ordersPostResponse, "status"), "pending", 'status should be pending');\`
|
|
228
|
-
- Java: \`assertEquals("pending", getValue(ordersPostResponse, "status"));\`
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
**Scope rules:**
|
|
233
|
-
- Only modify test functions — do NOT touch \`beforeAll\`, \`afterAll\`, or any setup/teardown helper.
|
|
234
|
-
- Only add assertions that are clearly supported by the schema, field name, request body, or observable codebase evidence. Do not invent constraints.
|
|
235
|
-
- Add new assertions **immediately after** the existing status-code and schema assertions — do not restructure the test.
|
|
236
|
-
|
|
237
|
-
**What NOT to do — any of these is a violation:**
|
|
238
|
-
- Do NOT access response body fields via dict syntax (\`response["field"]\`) or attribute access (\`response.field\`) — always use the SDK helper (\`get_response_value\` / \`getResponseValue\` / \`getValue\`).
|
|
239
|
-
- Do NOT modify \`beforeAll\` or \`afterAll\` functions.
|
|
240
|
-
- Do NOT change existing assertions — only add new ones.
|
|
241
|
-
- Do NOT add assertions for fields where no constraint is clearly inferable.
|
|
242
|
-
- Do NOT restructure, reformat, or reorder any existing code.
|
|
243
|
-
- Do NOT add comments or docstrings.
|
|
244
|
-
- Do NOT change function signatures, imports, or variable names.
|
|
174
|
+
${ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER}
|
|
245
175
|
`;
|
|
246
176
|
}
|
|
247
177
|
buildConsumerStubReplacementInstructions() {
|
|
@@ -352,7 +282,7 @@ Contract tests ensure your API implementation matches its OpenAPI/Swagger specif
|
|
|
352
282
|
**IMPORTANT: If the endpoint URL contains path parameter placeholders (e.g., \`/products/{product_id}/reviews\`), pass the URL exactly as provided — do NOT substitute values for the placeholders. Leave \`pathParams\` empty unless the user has explicitly provided specific values.**
|
|
353
283
|
|
|
354
284
|
**Modes:**
|
|
355
|
-
- Default (no mode set): both \`providerMode\` and \`consumerMode\` default to false.
|
|
285
|
+
- Default (no mode set): both \`providerMode\` and \`consumerMode\` default to false. This generates both provider and consumer contract tests — equivalent to setting both modes to true.
|
|
356
286
|
- \`providerMode\`: set to true ONLY if the user explicitly requests a provider-side contract test. Optionally specify \`providerOutput\` for the output file path.
|
|
357
287
|
- \`consumerMode\`: set to true ONLY if the user explicitly requests a consumer-side contract test. Optionally specify \`consumerOutput\` for the output file path.
|
|
358
288
|
- Both \`providerMode\` and \`consumerMode\` can be enabled simultaneously to generate both sides.
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { baseTestSchema, baseTraceSchema, TestType, codeRefactoringSchema, } from "../../types/TestTypes.js";
|
|
3
3
|
import { TestGenerationService, } from "../../services/TestGenerationService.js";
|
|
4
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
|
+
import { ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER } from "../../prompts/test-maintenance/enhanceAssertionSection.js";
|
|
5
6
|
const integrationTestSchema = z
|
|
6
7
|
.object({
|
|
7
8
|
...baseTestSchema,
|
|
@@ -18,6 +19,15 @@ const integrationTestSchema = z
|
|
|
18
19
|
.optional(),
|
|
19
20
|
...codeRefactoringSchema.shape,
|
|
20
21
|
...baseTestSchema,
|
|
22
|
+
output: baseTestSchema.output.describe("Name of the output test file. " +
|
|
23
|
+
"If the user does not specify a filename and a scenarioFile is provided, derive the output name from the scenario filename to avoid overwriting other tests. " +
|
|
24
|
+
"The backend default 'integration_test.py' is generic and will collide when multiple tests are generated. " +
|
|
25
|
+
"Derivation rule: take the scenario filename (no path, no extension), strip the leading 'scenario_' prefix, " +
|
|
26
|
+
"replace every hyphen and non-alphanumeric character with an underscore, then append '_integration_test' and the language extension. " +
|
|
27
|
+
"Examples: " +
|
|
28
|
+
"'scenario_orders-patch-add-items-recalculate.json' → 'orders_patch_add_items_recalculate_integration_test.py' (Python) or 'orders_patch_add_items_recalculate_integration_test.spec.ts' (Playwright). " +
|
|
29
|
+
"'scenario_products-crud.json' → 'products_crud_integration_test.py'. " +
|
|
30
|
+
"Extensions: '.py' for pytest, '.spec.ts'/'.spec.js' for Playwright, '.java' for JUnit."),
|
|
21
31
|
endpointURL: baseTestSchema.endpointURL.default(""),
|
|
22
32
|
})
|
|
23
33
|
.omit({ method: true }).shape;
|
|
@@ -44,67 +54,7 @@ The generated integration test contains only basic status-code assertions after
|
|
|
44
54
|
|
|
45
55
|
---
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- **Python**: \`skyramp.get_response_value(<response_var>, "<json_path>")\`
|
|
50
|
-
- e.g. \`skyramp.get_response_value(products_POST_response, "id")\`
|
|
51
|
-
- e.g. \`skyramp.get_response_value(orders_POST_response, "items.0.product_id")\`
|
|
52
|
-
- **TypeScript (Playwright)**: \`getResponseValue(<response_var>, "<json_path>")\` (already imported from \`@skyramp/skyramp\`)
|
|
53
|
-
- e.g. \`getResponseValue(productsPostResponse, "id")\`
|
|
54
|
-
- **JavaScript (Playwright)**: \`getResponseValue(<response_var>, "<json_path>")\` (already imported from \`@skyramp/skyramp\`)
|
|
55
|
-
- e.g. \`getResponseValue(productsPostResponse, "id")\`
|
|
56
|
-
- **Java**: \`getValue(<response_var>, "<json_path>")\` (already imported)
|
|
57
|
-
- e.g. \`getValue(productsPostResponse, "id")\`
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
**What to assert after each request:**
|
|
62
|
-
|
|
63
|
-
1. **Non-null / non-empty fields** — After any POST or PUT that creates or updates a resource, assert that key identifying fields are present and non-empty:
|
|
64
|
-
- IDs, names, emails, and other primary fields must not be null/None/empty.
|
|
65
|
-
- Python: \`assert skyramp.get_response_value(products_POST_response, "id") is not None\`
|
|
66
|
-
- TypeScript: \`expect(getResponseValue(productsPostResponse, "id"), 'id').not.toBeNull();\`
|
|
67
|
-
- JavaScript: \`assert.notStrictEqual(getResponseValue(productsPostResponse, "id"), null, 'id should not be null');\`
|
|
68
|
-
- Java: \`assertNotNull(getValue(productsPostResponse, "id"));\`
|
|
69
|
-
|
|
70
|
-
2. **Echo-back values** — When the request body sends a value the response is expected to return unchanged (name, email, status, price, etc.), assert the response value equals the sent value:
|
|
71
|
-
- Python: \`assert skyramp.get_response_value(products_POST_response, "name") == "Skyramp Tester"\`
|
|
72
|
-
- TypeScript: \`expect(getResponseValue(productsPostResponse, "name"), 'name').toBe("Skyramp Tester");\`
|
|
73
|
-
- JavaScript: \`assert.strictEqual(getResponseValue(productsPostResponse, "name"), "Skyramp Tester", 'name should match request');\`
|
|
74
|
-
- Java: \`assertEquals("Skyramp Tester", getValue(productsPostResponse, "name"));\`
|
|
75
|
-
|
|
76
|
-
3. **Chained values** — When a value extracted from a prior response (e.g., a POST-created ID) is already used in a subsequent request's \`path_params\` / \`data_override\`, also assert the later response echoes that value back where applicable:
|
|
77
|
-
- Python: \`assert skyramp.get_response_value(product_GET_response, "id") == skyramp.get_response_value(products_POST_response, "id")\`
|
|
78
|
-
- TypeScript: \`expect(getResponseValue(productGetResponse, "id"), 'id').toBe(getResponseValue(productsPostResponse, "id"));\`
|
|
79
|
-
|
|
80
|
-
4. **Value ranges** — For numeric fields where a realistic range is inferable from the field name or domain:
|
|
81
|
-
- Python: \`assert skyramp.get_response_value(products_POST_response, "price") >= 0\`
|
|
82
|
-
- TypeScript: \`expect(getResponseValue(productsPostResponse, "price")).toBeGreaterThanOrEqual(0);\`
|
|
83
|
-
- JavaScript: \`assert.ok(getResponseValue(productsPostResponse, "price") >= 0, 'price should be non-negative');\`
|
|
84
|
-
|
|
85
|
-
5. **Format/type constraints** — For string fields with a known format:
|
|
86
|
-
- **UUID**: assert the value matches \`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\` (use a regex check or string length check).
|
|
87
|
-
- **Date / date-time**: assert the value is a non-empty string parseable as an ISO 8601 date.
|
|
88
|
-
- **Email**: assert the value contains \`@\`.
|
|
89
|
-
- **URL**: assert the value starts with \`http://\` or \`https://\`.
|
|
90
|
-
|
|
91
|
-
6. **Specific known values** — For enum/status fields where only one outcome is valid for this flow:
|
|
92
|
-
- Python: \`assert skyramp.get_response_value(orders_POST_response, "status") == "pending"\`
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
**Scope rules:**
|
|
97
|
-
- Apply to every \`send_request\` / \`sendRequest\` call — including GET and DELETE if they return a body.
|
|
98
|
-
- Only add assertions that are clearly supported by the request body, prior response values, field names, or codebase evidence. Do not invent constraints.
|
|
99
|
-
- Add new assertions **immediately after** the existing status-code assertion for each request — do not move or remove anything.
|
|
100
|
-
|
|
101
|
-
**What NOT to do — any of these is a violation:**
|
|
102
|
-
- Do NOT access response body fields via dict syntax (\`response["field"]\`) or attribute access (\`response.field\`) — always use the SDK helper (\`get_response_value\` / \`getResponseValue\` / \`getValue\`).
|
|
103
|
-
- Do NOT remove or modify existing assertions.
|
|
104
|
-
- Do NOT add assertions for fields where no constraint is clearly inferable.
|
|
105
|
-
- Do NOT restructure, reformat, or reorder any existing code.
|
|
106
|
-
- Do NOT add comments or docstrings.
|
|
107
|
-
- Do NOT change function signatures, imports, or variable names.
|
|
57
|
+
${ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER}
|
|
108
58
|
`;
|
|
109
59
|
}
|
|
110
60
|
buildGenerationOptions(params) {
|
|
@@ -17,14 +17,20 @@ const newTestSchema = z.object({
|
|
|
17
17
|
category: z.enum(["security_boundary", "business_rule", "breaking_change", "data_integrity", "workflow"]).describe("Test category — critical categories (security_boundary, business_rule, data_integrity, breaking_change) get generation priority over workflow"),
|
|
18
18
|
endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
|
|
19
19
|
fileName: z.string().describe("Name of the generated test file"),
|
|
20
|
-
description: z.string().optional().describe("What the test
|
|
20
|
+
description: z.string().optional().describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound."),
|
|
21
21
|
scenarioFile: z.string().optional().describe("Path to the scenario JSON file if one was generated (e.g. 'tests/scenario_collections-links.json')"),
|
|
22
22
|
traceFile: z.string().optional().describe("Path to the backend trace file if used or created"),
|
|
23
23
|
frontendTrace: z.string().optional().describe("Path to the Playwright/UI trace file if used or created"),
|
|
24
24
|
reasoning: z.string().describe("Why this test was created: what production risk it mitigates, what code pattern it targets, or what coverage gap it fills"),
|
|
25
25
|
});
|
|
26
26
|
const descriptionSchema = z.object({
|
|
27
|
-
description: z.string().describe("One-line description"),
|
|
27
|
+
description: z.string().describe("One-line description. Do NOT prefix with the severity level — severity is a separate field."),
|
|
28
|
+
severity: z
|
|
29
|
+
.enum(["critical", "high", "medium", "low"])
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Issue severity. critical = feature broken/unusable (e.g. page doesn't load, data corruption). " +
|
|
32
|
+
"high = incorrect behavior (e.g. wrong calculation, stale data returned). " +
|
|
33
|
+
"medium = minor functional gap. low = cosmetic or informational."),
|
|
28
34
|
});
|
|
29
35
|
const scenarioStepSchema = z.object({
|
|
30
36
|
method: z.string().optional().describe("HTTP method (e.g. 'POST', 'GET'). Required for API steps, omit for UI/E2E actions."),
|
|
@@ -41,13 +47,15 @@ const additionalRecommendationSchema = z.object({
|
|
|
41
47
|
scenarioName: z.string().describe("Name of the scenario, e.g. 'products_orders_workflow'"),
|
|
42
48
|
steps: z.array(scenarioStepSchema).describe("Ordered sequence of API/UI steps in this test scenario"),
|
|
43
49
|
description: z.string().describe("Why this test is valuable and what it would cover"),
|
|
44
|
-
priority: z.string().describe("Priority level: high, medium, or low"),
|
|
50
|
+
priority: z.preprocess((val) => (typeof val === "string" ? val.toLowerCase() : val), z.enum(["high", "medium", "low"])).describe("Priority level: high, medium, or low. First check diff relevance — does the test target an endpoint changed in this PR? HIGH: diff-relevant security/auth/error tests, cross-resource isolation for diff endpoints, CRUD lifecycle for NEW endpoints in the diff. MEDIUM: diff-relevant business-rule happy paths, multi-resource workflows involving diff endpoints, security/error tests for NON-diff endpoints. LOW: tests targeting only unchanged endpoints, trivially discoverable happy paths duplicating generated tests."),
|
|
45
51
|
openApiSpec: z.string().optional().describe("Path to OpenAPI/Swagger spec file if available, e.g. 'openapi.yaml'"),
|
|
46
52
|
backendTrace: z.string().optional().describe("Path to backend trace file if available, e.g. 'tests/skyramp-traces.json'. Used by integration and E2E tests."),
|
|
47
53
|
frontendTrace: z.string().optional().describe("Path to Playwright/UI trace file if available, e.g. 'tests/skyramp-playwright.zip'. UI tests need this; E2E tests need both frontend and backend traces."),
|
|
48
54
|
reasoning: z.string().describe("Why this test is recommended: the specific production risk, business rule, or security boundary it would validate"),
|
|
49
55
|
});
|
|
50
56
|
const testMaintenanceSchema = z.object({
|
|
57
|
+
testType: z.string().describe("Type of test: Contract, Integration, UI, etc."),
|
|
58
|
+
endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
|
|
51
59
|
fileName: z.string().describe("Test file that was maintained, e.g. 'products_smoke_test.py'"),
|
|
52
60
|
description: z.string().describe("What was changed and why"),
|
|
53
61
|
beforeStatus: z.enum(["Pass", "Fail", "Error"]).describe("Test result BEFORE modification"),
|
|
@@ -26,7 +26,7 @@ function sampleReportParams(outputFile) {
|
|
|
26
26
|
newTestsCreated: [
|
|
27
27
|
{ testId: "smoke-get-products-search", testType: "Smoke", category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", reasoning: "Validates product search returns correct results for category filter" },
|
|
28
28
|
],
|
|
29
|
-
testMaintenance: [{ description: "Updated auth flow in existing tests" }],
|
|
29
|
+
testMaintenance: [{ action: "UPDATE", testType: "Contract", endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Updated auth flow in existing tests", beforeStatus: "Fail", beforeDetails: "401 Unauthorized (1.5s)", afterStatus: "Pass", afterDetails: "All assertions passed (2.3s)" }],
|
|
30
30
|
testResults: [
|
|
31
31
|
{ testType: "Smoke", endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
|
|
32
32
|
{ testType: "Smoke", endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
|