@skyramp/mcp 0.0.63-rc.6 → 0.0.63-rc.7
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/prompts/test-recommendation/recommendationSections.js +3 -7
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +40 -1
- package/build/services/TestGenerationService.js +1 -0
- package/build/tools/generate-tests/generateContractRestTool.js +69 -3
- package/build/tools/generate-tests/generateMockRestTool.js +202 -0
- package/build/types/TestRecommendation.js +1 -0
- package/build/types/TestTypes.js +8 -2
- package/build/utils/language-helper.js +30 -0
- package/package.json +1 -1
- package/build/tools/test-maintenance/actionsTool.js +0 -347
- package/build/tools/test-maintenance/actionsTool.test.js +0 -93
- package/build/tools/test-maintenance/analyzeTestDriftTool.js +0 -198
- package/build/tools/test-maintenance/calculateHealthScoresTool.js +0 -236
- package/build/tools/test-maintenance/discoverTestsTool.js +0 -143
- package/build/tools/test-maintenance/stateCleanupTool.js +0 -166
package/build/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { registerRecommendTestsPrompt } from "./prompts/test-recommendation/regi
|
|
|
23
23
|
import { registerModularizationTool } from "./tools/code-refactor/modularizationTool.js";
|
|
24
24
|
import { registerCodeReuseTool } from "./tools/code-refactor/codeReuseTool.js";
|
|
25
25
|
import { registerScenarioTestTool } from "./tools/generate-tests/generateScenarioRestTool.js";
|
|
26
|
+
import { registerMockTool } from "./tools/generate-tests/generateMockRestTool.js";
|
|
26
27
|
import { registerAnalyzeChangesTool, registerAnalyzeTestHealthTool, registerExecuteTestsTool, registerActionsTool, registerStateCleanupTool, } from "./tools/test-management/index.js";
|
|
27
28
|
import { registerTestbotPrompt, registerTestbotResource, } from "./prompts/testbot/testbot-prompts.js";
|
|
28
29
|
import { registerSubmitReportTool } from "./tools/submitReportTool.js";
|
|
@@ -177,6 +178,7 @@ const testGenerationTools = [
|
|
|
177
178
|
registerE2ETestTool,
|
|
178
179
|
registerUITestTool,
|
|
179
180
|
registerScenarioTestTool,
|
|
181
|
+
registerMockTool,
|
|
180
182
|
];
|
|
181
183
|
testGenerationTools.forEach((registerTool) => registerTool(server));
|
|
182
184
|
// Register modularization and code quality tools
|
|
@@ -20,13 +20,9 @@ export function getAuthSnippets(authHeaderValue) {
|
|
|
20
20
|
}
|
|
21
21
|
return { authSchemeSnippet: "", authTokenSnippet: "" };
|
|
22
22
|
}
|
|
23
|
-
export const PATH_PARAM_UUID_GUIDANCE = `**Path parameters
|
|
24
|
-
`
|
|
25
|
-
`
|
|
26
|
-
`This lets the CLI derive a clean filename from the resource name (\`coupons_GET_...\`) instead of the UUID. ` +
|
|
27
|
-
`Prefer example values from the OpenAPI schema (\`example\`, \`x-example\`, or enum values). ` +
|
|
28
|
-
`If no example exists, use a realistic UUID with varied hex digits such as \`3fa85f64-5717-4562-b3fc-2c963f66afa6\`. ` +
|
|
29
|
-
`Never use all-same-digit patterns like \`00000000-0000-0000-0000-000000000000\` or \`22222222-2222-2222-2222-222222222222\` — these look like placeholders and are rarely valid resource IDs.`;
|
|
23
|
+
export const PATH_PARAM_UUID_GUIDANCE = `**Path parameters:** keep the placeholder in \`endpointURL\` (e.g. \`/coupons/{coupon_id}\`). ` +
|
|
24
|
+
`Pass the value via \`pathParams\` (e.g. \`coupon_id=<random-uuid-v4>\`). ` +
|
|
25
|
+
`Use example values from the OpenAPI schema if available; otherwise generate a fresh random UUID v4 — not all-zeros or repeated-digit patterns.`;
|
|
30
26
|
export function buildPrioritizationDimensions() {
|
|
31
27
|
return `## Prioritization Dimensions (evaluate each candidate test)
|
|
32
28
|
|
|
@@ -2,7 +2,7 @@ jest.mock("@skyramp/skyramp", () => ({
|
|
|
2
2
|
WorkspaceConfigManager: { create: jest.fn() },
|
|
3
3
|
}));
|
|
4
4
|
import { buildRecommendationPrompt } from "./test-recommendation-prompt.js";
|
|
5
|
-
import { buildCoverageChecklist, MAX_CRITICAL_TESTS, MAX_TESTS_TO_GENERATE } from "./recommendationSections.js";
|
|
5
|
+
import { buildCoverageChecklist, PATH_PARAM_UUID_GUIDANCE, MAX_CRITICAL_TESTS, MAX_TESTS_TO_GENERATE } from "./recommendationSections.js";
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Minimal fixtures
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
@@ -287,3 +287,42 @@ describe("buildCoverageChecklist — Recommendation Stability section", () => {
|
|
|
287
287
|
expect(checklist).toContain(`up to ${Math.min(MAX_CRITICAL_TESTS, MAX_TESTS_TO_GENERATE)} of the ${MAX_TESTS_TO_GENERATE} generated tests MUST be from critical categories`);
|
|
288
288
|
});
|
|
289
289
|
});
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Tests — PATH_PARAM_UUID_GUIDANCE (no hardcoded UUID anchor)
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
const UUID_V4_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i;
|
|
294
|
+
describe("PATH_PARAM_UUID_GUIDANCE — no hardcoded UUID anchor", () => {
|
|
295
|
+
it("contains no hardcoded UUID that the LLM could anchor to", () => {
|
|
296
|
+
expect(PATH_PARAM_UUID_GUIDANCE).not.toMatch(UUID_V4_REGEX);
|
|
297
|
+
});
|
|
298
|
+
it("uses <random-uuid-v4> as the placeholder in the pathParams example", () => {
|
|
299
|
+
expect(PATH_PARAM_UUID_GUIDANCE).toContain("<random-uuid-v4>");
|
|
300
|
+
});
|
|
301
|
+
it("instructs the LLM to generate a fresh UUID v4", () => {
|
|
302
|
+
expect(PATH_PARAM_UUID_GUIDANCE).toContain("generate a fresh random UUID v4");
|
|
303
|
+
});
|
|
304
|
+
it("is included in the recommendation prompt for an endpoint with a UUID path param", () => {
|
|
305
|
+
const analysis = minimalAnalysis({
|
|
306
|
+
apiEndpoints: {
|
|
307
|
+
totalCount: 1,
|
|
308
|
+
baseUrl: "http://localhost:3000",
|
|
309
|
+
endpoints: [{
|
|
310
|
+
path: "/api/items/{item_id}",
|
|
311
|
+
resourceGroup: "items",
|
|
312
|
+
pathParams: [{ name: "item_id", type: "string", required: true }],
|
|
313
|
+
methods: [{
|
|
314
|
+
method: "GET",
|
|
315
|
+
description: "Get item by ID",
|
|
316
|
+
queryParams: [],
|
|
317
|
+
authRequired: false,
|
|
318
|
+
sourceFile: "routes/items.ts",
|
|
319
|
+
interactions: [{ description: "success", type: "success", request: {}, response: { statusCode: 200, description: "OK" } }],
|
|
320
|
+
}],
|
|
321
|
+
}],
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
const prompt = buildRecommendationPrompt(analysis, "current_branch_diff", 10);
|
|
325
|
+
expect(prompt).toContain("<random-uuid-v4>");
|
|
326
|
+
expect(prompt).not.toMatch(UUID_V4_REGEX);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -4,6 +4,13 @@ import { TestGenerationService, } from "../../services/TestGenerationService.js"
|
|
|
4
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
5
|
const contractTestSchema = {
|
|
6
6
|
...baseTestSchema,
|
|
7
|
+
pathParams: z
|
|
8
|
+
.string()
|
|
9
|
+
.default("")
|
|
10
|
+
.describe("Comma-separated path parameter values in the form 'key=value,key2=value2' (e.g., 'id=1,name=John'). " +
|
|
11
|
+
"IMPORTANT: If the user-provided URL contains path parameter placeholders (e.g., '/products/{product_id}'), do NOT fill in values for them here — pass the URL as-is and leave pathParams empty. " +
|
|
12
|
+
"The backend resolves path parameters automatically via the parent provisioning mechanism (parentRequestData). " +
|
|
13
|
+
"Only populate this field if the user has explicitly stated specific path parameter values to use."),
|
|
7
14
|
assertOptions: z
|
|
8
15
|
.string()
|
|
9
16
|
.optional()
|
|
@@ -57,6 +64,63 @@ export class ContractTestService extends TestGenerationService {
|
|
|
57
64
|
getTestType() {
|
|
58
65
|
return TestType.CONTRACT;
|
|
59
66
|
}
|
|
67
|
+
async generateTest(params) {
|
|
68
|
+
const result = await super.generateTest(params);
|
|
69
|
+
if (result.isError)
|
|
70
|
+
return result;
|
|
71
|
+
const postInstructions = this.buildSampleDataInstructions(params);
|
|
72
|
+
if (!postInstructions)
|
|
73
|
+
return result;
|
|
74
|
+
return {
|
|
75
|
+
...result,
|
|
76
|
+
content: [
|
|
77
|
+
...result.content,
|
|
78
|
+
{ type: "text", text: postInstructions },
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
buildSampleDataInstructions(params) {
|
|
83
|
+
const sections = [];
|
|
84
|
+
sections.push("- **Request body sample data**: the inline JSON/dict literal passed as the request body in the test function.");
|
|
85
|
+
if (!params.skipProvisionParents) {
|
|
86
|
+
sections.push("- **Parent provisioning request data**: the inline JSON/dict literals passed as request bodies inside setup/teardown functions that create parent resources.");
|
|
87
|
+
}
|
|
88
|
+
return `
|
|
89
|
+
⏭️ **CRITICAL NEXT STEP — Replace unrealistic placeholder values in sample data only:**
|
|
90
|
+
|
|
91
|
+
Inspect the following locations in the generated file and replace any placeholder values with realistic ones:
|
|
92
|
+
|
|
93
|
+
${sections.join("\n")}
|
|
94
|
+
|
|
95
|
+
**A value is a placeholder if it is any of the following — regardless of where it came from:**
|
|
96
|
+
- A JSON/OpenAPI type name: "string", "integer", "number", "boolean", "object", "array"
|
|
97
|
+
- A format descriptor: "uuid", "date", "date-time", "email", "uri", "url", "binary", "byte", "password"
|
|
98
|
+
- A generic filler: "example", "sample", "test", "foo", "bar", "baz", "value", "unknown", "none", "null", "N/A"
|
|
99
|
+
- An integer that is exactly 0 or 1 with no semantic meaning from the field name
|
|
100
|
+
- Any value that is clearly a type name or format name rather than a real datum
|
|
101
|
+
|
|
102
|
+
**How to replace:**
|
|
103
|
+
- Use the field name, OpenAPI schema descriptions/enums, and repository context to infer a realistic value.
|
|
104
|
+
- Examples: an "email" field → "user@example.com"; a "price" field → 29.99; a "status" field → the first valid enum value from the spec; a "uuid" value for an "id" field → "a1b2c3d4-e5f6-7890-abcd-ef1234567890".
|
|
105
|
+
|
|
106
|
+
**Exactly what to do:**
|
|
107
|
+
1. Open the generated file.
|
|
108
|
+
2. Find only the inline JSON/dict literals identified above.
|
|
109
|
+
3. Replace every placeholder value using the rules above.
|
|
110
|
+
4. Leave all realistic values unchanged.
|
|
111
|
+
5. Write the updated file. Stop there.
|
|
112
|
+
|
|
113
|
+
**What NOT to do — any of these is a violation:**
|
|
114
|
+
- Do NOT change function signatures, method names, class names, imports, or variable names.
|
|
115
|
+
- Do NOT add, remove, or reorder any functions, classes, or test cases.
|
|
116
|
+
- Do NOT change assertion logic, HTTP methods, URLs, headers, or status code checks.
|
|
117
|
+
- Do NOT reformat, reorder, or rewrite any code outside the identified JSON/dict literals.
|
|
118
|
+
- Do NOT add comments or docstrings.
|
|
119
|
+
- Do NOT extract, hoist, or expose any request body or data as a global variable, module-level constant, or shared fixture — keep all data inline exactly where it already is.
|
|
120
|
+
- Do NOT reference or reuse request body data across functions for comparison or any other purpose.
|
|
121
|
+
- Do NOT make any other "improvements" beyond the value replacements described above.
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
60
124
|
validateInputs(params) {
|
|
61
125
|
const errList = super.validateInputs(params);
|
|
62
126
|
if (errList.isError)
|
|
@@ -115,10 +179,12 @@ Contract tests ensure your API implementation matches its OpenAPI/Swagger specif
|
|
|
115
179
|
|
|
116
180
|
**IMPORTANT: If an apiSchema parameter (OpenAPI/Swagger file path or URL) is provided, DO NOT attempt to read or analyze the file contents. These files can be very large. Simply pass the path/URL to the tool - the backend will handle reading and processing the schema file.**
|
|
117
181
|
|
|
182
|
+
**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.**
|
|
183
|
+
|
|
118
184
|
**Modes:**
|
|
119
|
-
- Default (no mode set):
|
|
120
|
-
- \`providerMode\`:
|
|
121
|
-
- \`consumerMode\`:
|
|
185
|
+
- Default (no mode set): both \`providerMode\` and \`consumerMode\` default to false. Do NOT set either unless the user explicitly mentions "provider" or "consumer".
|
|
186
|
+
- \`providerMode\`: set to true ONLY if the user explicitly requests a provider-side contract test. Optionally specify \`providerOutput\` for the output file path.
|
|
187
|
+
- \`consumerMode\`: set to true ONLY if the user explicitly requests a consumer-side contract test. Optionally specify \`consumerOutput\` for the output file path.
|
|
122
188
|
- Both \`providerMode\` and \`consumerMode\` can be enabled simultaneously to generate both sides.
|
|
123
189
|
|
|
124
190
|
**Chaining (requires \`apiSchema\`):**
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { baseSchema, TestType } from "../../types/TestTypes.js";
|
|
3
|
+
import { TestGenerationService } from "../../services/TestGenerationService.js";
|
|
4
|
+
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
|
+
import { getEntryPoint } from "../../utils/telemetry.js";
|
|
6
|
+
const mockSchema = {
|
|
7
|
+
endpointURL: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe("The base URL or endpoint URL to generate a mock for. " +
|
|
10
|
+
"If only a base URL is provided (e.g., https://api.example.com) and an apiSchema is given, mocks will be generated for ALL endpoints and methods in the schema — do NOT append paths to the URL. " +
|
|
11
|
+
"Only include a specific path (e.g., https://api.example.com/api/v1/products) if the user explicitly requests mocking a single endpoint."),
|
|
12
|
+
method: z
|
|
13
|
+
.string()
|
|
14
|
+
.default("")
|
|
15
|
+
.describe('HTTP method to mock. DEFAULT: Use empty string ("") to mock ALL available methods from the OpenAPI schema. Only specify a specific method (GET, POST, PUT, DELETE, etc.) if the user explicitly requests mocking a single method.'),
|
|
16
|
+
apiSchema: z
|
|
17
|
+
.string()
|
|
18
|
+
.default("")
|
|
19
|
+
.describe("MUST be absolute path (/path/to/openapi.json) to the OpenAPI/Swagger schema file or a URL to it. NOTE TO AI ASSISTANTS: Do not read the file — simply pass the path/URL to the tool."),
|
|
20
|
+
responseData: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Sample response body data to use in the mock, provided either as an inline JSON/YAML string or as an absolute file path prefixed with '@' (e.g., @/absolute/path/to/file)."),
|
|
24
|
+
responseStatusCode: z
|
|
25
|
+
.string()
|
|
26
|
+
.default("")
|
|
27
|
+
.describe("HTTP status code the mock should return (e.g., '200', '201'). DO NOT ASSUME a status code if not provided."),
|
|
28
|
+
requestData: z
|
|
29
|
+
.string()
|
|
30
|
+
.default("")
|
|
31
|
+
.describe("Sample request body data for request-aware mocks, provided either as an inline JSON/YAML string or as an absolute file path prefixed with '@' (e.g., @/absolute/path/to/file)."),
|
|
32
|
+
formParams: z
|
|
33
|
+
.string()
|
|
34
|
+
.default("")
|
|
35
|
+
.describe("Comma-separated form parameters in the form 'key=value,key2=value2' for mocking form-encoded endpoints."),
|
|
36
|
+
requestAware: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.default(false)
|
|
39
|
+
.describe("When true, generates a dynamic mock that varies its response based on the incoming request data."),
|
|
40
|
+
trace: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("MUST be absolute path to a trace file (e.g., /path/to/trace.json) to generate the mock from captured traffic."),
|
|
44
|
+
...(({ authHeader, insecure, ...rest }) => rest)(baseSchema.shape),
|
|
45
|
+
};
|
|
46
|
+
export class MockGenerationService extends TestGenerationService {
|
|
47
|
+
getTestType() {
|
|
48
|
+
return TestType.MOCK;
|
|
49
|
+
}
|
|
50
|
+
async generateTest(params) {
|
|
51
|
+
const result = await super.generateTest(params);
|
|
52
|
+
if (result.isError)
|
|
53
|
+
return result;
|
|
54
|
+
if (!params.apiSchema)
|
|
55
|
+
return result;
|
|
56
|
+
return {
|
|
57
|
+
...result,
|
|
58
|
+
content: [
|
|
59
|
+
...result.content,
|
|
60
|
+
{ type: "text", text: this.buildReorganizationInstructions(params) },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
buildReorganizationInstructions(params) {
|
|
65
|
+
const language = params.language ?? "python";
|
|
66
|
+
const isTypeScript = language === "typescript" || language === "javascript";
|
|
67
|
+
const ext = isTypeScript ? (language === "typescript" ? "ts" : "js") : "py";
|
|
68
|
+
const indexFile = isTypeScript ? `index.${ext}` : `__init__.${ext}`;
|
|
69
|
+
const importStyle = isTypeScript
|
|
70
|
+
? "ES module imports (`import { ... } from './path'`)"
|
|
71
|
+
: "Python imports (`from mocks.path import ...`)";
|
|
72
|
+
return `
|
|
73
|
+
⏭️ **CRITICAL NEXT STEP — Reorganize generated mock files into a structured directory layout:**
|
|
74
|
+
|
|
75
|
+
The generated mock file(s) in \`${params.outputDir}\` must be reorganized as follows.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
**Step 1 — Create the \`mocks/\` directory structure**
|
|
80
|
+
|
|
81
|
+
For each endpoint path in the spec, derive its location using these rules:
|
|
82
|
+
1. Strip the leading \`/\` and split the path by \`/\`.
|
|
83
|
+
2. Remove all path parameter segments (anything matching \`{...}\`, e.g. \`{id}\`).
|
|
84
|
+
3. Take at most the first 3 non-parameter segments. The last segment becomes the **filename** (with \`.${ext}\` extension); any preceding segments become **directories**.
|
|
85
|
+
4. Group all HTTP methods that share the same effective file location into a single file.
|
|
86
|
+
|
|
87
|
+
Examples (assuming \`.${ext}\` files):
|
|
88
|
+
- \`/products\` → \`mocks/products.${ext}\`
|
|
89
|
+
- \`/products/{id}\` → \`mocks/products.${ext}\` (same file as above — same resource)
|
|
90
|
+
- \`/products/{id}/reviews\` → \`mocks/products/reviews.${ext}\`
|
|
91
|
+
- \`/products/{id}/reviews/{review_id}\` → \`mocks/products/reviews.${ext}\` (same file as above)
|
|
92
|
+
- \`/a/b/c/d/e\` → \`mocks/a/b/c.${ext}\` (truncated at 3 non-parameter segments)
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
**Step 2 — Write each mock file**
|
|
97
|
+
|
|
98
|
+
Each file must:
|
|
99
|
+
- Contain the mock definitions for all HTTP methods that map to it.
|
|
100
|
+
- Be **independently importable** — include all necessary imports at the top of the file so it can be used standalone without the index file.
|
|
101
|
+
- Export a function (e.g., \`apply_products_mock\` / \`applyProductsMock\`) that registers/applies all mocks in that file.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
**Step 3 — Write the top-level index file at \`mocks/${indexFile}\`**
|
|
106
|
+
|
|
107
|
+
This file must:
|
|
108
|
+
- Import from every mock file created in Step 2 using ${importStyle}.
|
|
109
|
+
- Export (or define) a single function (e.g., \`apply_all_mocks\` / \`applyAllMocks\`) that calls each individual apply function, so all mocks can be registered in one call.
|
|
110
|
+
- Re-export each individual apply function so consumers can also import them directly from the index.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
**Step 4 — Delete the original generated file(s)** from \`${params.outputDir}\` that were replaced by the structured layout (keep only the \`mocks/\` directory).
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
**What NOT to do:**
|
|
119
|
+
- Do NOT nest directories more than 3 levels deep inside \`mocks/\`.
|
|
120
|
+
- Do NOT create a separate file per HTTP method — group all methods for the same effective path in one file.
|
|
121
|
+
- Do NOT introduce circular imports between mock files.
|
|
122
|
+
- Do NOT modify any mock logic, response data, or function signatures — only reorganize and add import/export wiring.
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
buildGenerationOptions(params) {
|
|
126
|
+
return {
|
|
127
|
+
uri: params.endpointURL,
|
|
128
|
+
method: params.method,
|
|
129
|
+
apiSchema: params.apiSchema ? [params.apiSchema] : [],
|
|
130
|
+
language: params.language ?? "python",
|
|
131
|
+
framework: params.framework ?? "",
|
|
132
|
+
output: params.output,
|
|
133
|
+
outputDir: params.outputDir,
|
|
134
|
+
force: params.force ?? true,
|
|
135
|
+
deployDashboard: params.deployDashboard,
|
|
136
|
+
runtime: params.runtime,
|
|
137
|
+
dockerNetwork: params.dockerNetwork,
|
|
138
|
+
dockerWorkerPort: params.dockerWorkerPort?.toString() ?? "0",
|
|
139
|
+
k8sNamespace: params.k8sNamespace,
|
|
140
|
+
k8sConfig: params.k8sConfig,
|
|
141
|
+
k8sContext: params.k8sContext,
|
|
142
|
+
requestData: params.requestData,
|
|
143
|
+
responseData: params.responseData,
|
|
144
|
+
responseStatusCode: params.responseStatusCode,
|
|
145
|
+
formParams: params.formParams,
|
|
146
|
+
requestAware: params.requestAware,
|
|
147
|
+
traceFilePath: params.trace,
|
|
148
|
+
entrypoint: getEntryPoint(),
|
|
149
|
+
mockPort: params.mockPort ?? 0,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async executeGeneration(generateOptions) {
|
|
153
|
+
try {
|
|
154
|
+
const result = await this.client.generateRestMock(generateOptions);
|
|
155
|
+
if (result && result.length > 0) {
|
|
156
|
+
const lowerResult = result.toLowerCase();
|
|
157
|
+
if (!lowerResult.includes("success")) {
|
|
158
|
+
throw new Error(`Mock generation failed: ${result}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return `
|
|
162
|
+
**Generated Mock Details:**
|
|
163
|
+
- Output Directory: ${generateOptions.outputDir}
|
|
164
|
+
- Language: ${generateOptions.language}
|
|
165
|
+
- Framework: ${generateOptions.framework}
|
|
166
|
+
|
|
167
|
+
${result}`;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
171
|
+
throw new Error(`Failed to execute mock generation: ${errorMessage}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const TOOL_NAME = "skyramp_mock_generation";
|
|
176
|
+
export function registerMockTool(server) {
|
|
177
|
+
server.registerTool(TOOL_NAME, {
|
|
178
|
+
description: `Generate a mock server implementation using Skyramp's deterministic mock generation platform.
|
|
179
|
+
|
|
180
|
+
Mocks simulate API endpoints so that consumers can develop and test against them without requiring a live backend. They are useful for parallel development, isolating dependencies, and simulating edge-case responses.
|
|
181
|
+
|
|
182
|
+
**IMPORTANT: If an apiSchema parameter (OpenAPI/Swagger file path or URL) is provided, DO NOT attempt to read or analyze the file contents. Simply pass the path/URL to the tool — the backend will handle reading and processing the schema file.**
|
|
183
|
+
|
|
184
|
+
**IMPORTANT: When an apiSchema is provided and the user has not requested a specific endpoint, pass only the base URL (e.g., \`https://api.example.com\`) — do NOT append any path. The backend will generate mocks for all endpoints and methods defined in the schema.**
|
|
185
|
+
|
|
186
|
+
**Request-aware mocks:**
|
|
187
|
+
- \`requestAware: true\` generates a dynamic mock that inspects the incoming request and varies its response accordingly. Use this when the consumer sends different request bodies and expects different responses.
|
|
188
|
+
|
|
189
|
+
**Sources (in order of preference):**
|
|
190
|
+
1. Trace file (\`trace\`): generates the mock from captured real traffic.
|
|
191
|
+
2. OpenAPI schema (\`apiSchema\`): generates the mock from the spec.
|
|
192
|
+
3. Inline response data (\`responseData\`): generates a static mock with a fixed response.`,
|
|
193
|
+
inputSchema: mockSchema,
|
|
194
|
+
}, async (params) => {
|
|
195
|
+
const service = new MockGenerationService();
|
|
196
|
+
const result = await service.generateTest(params);
|
|
197
|
+
AnalyticsService.pushTestGenerationToolEvent(TOOL_NAME, result, params).catch(() => {
|
|
198
|
+
// Silently ignore analytics errors
|
|
199
|
+
});
|
|
200
|
+
return result;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -9,6 +9,7 @@ export const TEST_TYPE_DOCS = {
|
|
|
9
9
|
[TestType.LOAD]: "https://www.skyramp.dev/docs/load-tests",
|
|
10
10
|
[TestType.E2E]: "https://www.skyramp.dev/docs/e2e-tests",
|
|
11
11
|
[TestType.UI]: "https://www.skyramp.dev/docs/ui-tests",
|
|
12
|
+
[TestType.MOCK]: "https://www.skyramp.dev/docs/mocks",
|
|
12
13
|
};
|
|
13
14
|
// Zod schemas for validation
|
|
14
15
|
export const specificTestSchema = z.object({
|
package/build/types/TestTypes.js
CHANGED
|
@@ -10,6 +10,7 @@ export var TestType;
|
|
|
10
10
|
TestType["INTEGRATION"] = "integration";
|
|
11
11
|
TestType["E2E"] = "e2e";
|
|
12
12
|
TestType["UI"] = "ui";
|
|
13
|
+
TestType["MOCK"] = "mock";
|
|
13
14
|
})(TestType || (TestType = {}));
|
|
14
15
|
export const languageSchema = z.object({
|
|
15
16
|
language: z
|
|
@@ -96,6 +97,10 @@ export const baseSchema = z.object({
|
|
|
96
97
|
.boolean()
|
|
97
98
|
.default(true)
|
|
98
99
|
.describe("Whether to overwrite existing files in the output directory"),
|
|
100
|
+
mockPort: z
|
|
101
|
+
.number()
|
|
102
|
+
.default(0)
|
|
103
|
+
.describe("Port number for the mock server"),
|
|
99
104
|
prompt: z.string().describe("The prompt user provided to generate the test"),
|
|
100
105
|
});
|
|
101
106
|
export const basePlaywrightSchema = z.object({
|
|
@@ -154,8 +159,9 @@ export const baseTestSchema = {
|
|
|
154
159
|
.string()
|
|
155
160
|
.default("")
|
|
156
161
|
.describe("Comma-separated path parameter values in the form 'key=value,key2=value2' (e.g., 'id=1,name=John'). " +
|
|
157
|
-
"
|
|
158
|
-
"
|
|
162
|
+
"DO NOT populate this field unless the user has explicitly provided path parameter values. " +
|
|
163
|
+
"For contract tests, parent path parameters are automatically handled via the parent provisioning mechanism (parentRequestData) — do NOT invent or guess values for those here. " +
|
|
164
|
+
"Only supply this field if the user explicitly states specific path parameter values to use."),
|
|
159
165
|
queryParams: z
|
|
160
166
|
.string()
|
|
161
167
|
.default("")
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
export function getLanguageSteps(params) {
|
|
2
2
|
const { language, testType } = params;
|
|
3
|
+
if (testType === "mock") {
|
|
4
|
+
switch (language) {
|
|
5
|
+
case "python":
|
|
6
|
+
return `
|
|
7
|
+
# Install skyramp dependency
|
|
8
|
+
pip3 install skyramp
|
|
9
|
+
|
|
10
|
+
# Run the mock server
|
|
11
|
+
python3 <mock-file>.py
|
|
12
|
+
`;
|
|
13
|
+
case "typescript":
|
|
14
|
+
return `
|
|
15
|
+
# Install dependencies
|
|
16
|
+
npm install @skyramp/skyramp
|
|
17
|
+
|
|
18
|
+
# Run the mock server
|
|
19
|
+
npx ts-node <mock-file>.ts
|
|
20
|
+
`;
|
|
21
|
+
case "javascript":
|
|
22
|
+
return `
|
|
23
|
+
# Install dependencies
|
|
24
|
+
npm install @skyramp/skyramp
|
|
25
|
+
|
|
26
|
+
# Run the mock server
|
|
27
|
+
node <mock-file>.js
|
|
28
|
+
`;
|
|
29
|
+
default:
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
3
33
|
let pythonSteps = "";
|
|
4
34
|
switch (language) {
|
|
5
35
|
case "python":
|