@skyramp/mcp 0.2.8 → 0.2.150-rc.ldw
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/commands/commandLibrary.d.ts +1 -0
- package/build/commands/commandLibrary.js +20 -14
- package/build/commands/commandLibrary.test.d.ts +1 -0
- package/build/commands/commandLibrary.test.js +59 -0
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +188 -0
- package/build/index.js +77 -13
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +9 -8
- package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
- package/build/prompts/local-dev/local-dev-plan.js +419 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +157 -0
- package/build/prompts/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
- package/build/prompts/sut-setup/shared.d.ts +20 -0
- package/build/prompts/sut-setup/shared.js +69 -7
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +4 -34
- package/build/resources/testbotResource.js +2 -1
- package/build/services/AnalyticsService.d.ts +1 -1
- package/build/services/TestExecutionService.d.ts +2 -1
- package/build/services/TestExecutionService.js +15 -10
- package/build/services/TestExecutionService.test.js +75 -1
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/tool-phases.js +4 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +47 -18
- package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
- package/build/tools/enrichTestWithMocksTool.js +726 -0
- package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
- package/build/tools/enrichTestWithMocksTool.test.js +266 -0
- package/build/tools/executeSkyrampTestTool.d.ts +4 -0
- package/build/tools/executeSkyrampTestTool.js +48 -24
- package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
- package/build/tools/executeSkyrampTestTool.test.js +16 -0
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +85 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +432 -0
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateMockRestTool.d.ts +129 -6
- package/build/tools/generate-tests/generateMockRestTool.js +234 -22
- package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +225 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.js +44 -0
- package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
- package/build/tools/localDevWorkflowFixes.test.js +255 -0
- package/build/tools/one-click/oneClickTool.d.ts +10 -0
- package/build/tools/one-click/oneClickTool.js +177 -21
- package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
- package/build/tools/one-click/oneClickTool.test.js +172 -0
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +96 -0
- package/build/tools/submitReportTool.d.ts +14 -14
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +59 -38
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
- package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/RepositoryAnalysis.d.ts +117 -0
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/TestExecution.d.ts +1 -0
- package/build/types/TestTypes.d.ts +20 -8
- package/build/types/TestTypes.js +19 -5
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/analyze-openapi.test.d.ts +1 -0
- package/build/utils/analyze-openapi.test.js +19 -0
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +96 -14
- package/build/utils/branchDiff.test.d.ts +1 -0
- package/build/utils/branchDiff.test.js +109 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/featureFlags.d.ts +20 -1
- package/build/utils/featureFlags.js +26 -2
- package/build/utils/featureFlags.test.js +57 -2
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/grpcMockValidation.test.d.ts +1 -0
- package/build/utils/grpcMockValidation.test.js +41 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +13 -0
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +82 -0
- package/build/utils/mockCompatibility.test.d.ts +1 -0
- package/build/utils/mockCompatibility.test.js +79 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +49 -35
- package/build/workspace/workspace.js +10 -5
- package/build/workspace/workspace.test.js +65 -6
- package/package.json +2 -2
|
@@ -1,18 +1,106 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { baseSchema,
|
|
2
|
+
import { baseSchema, MockProtocol, MOCK_TYPE } from "../../types/TestTypes.js";
|
|
3
|
+
export { MockProtocol } from "../../types/TestTypes.js";
|
|
3
4
|
import { TestGenerationService } from "../../services/TestGenerationService.js";
|
|
4
5
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
6
|
import { getEntryPoint } from "../../utils/telemetry.js";
|
|
7
|
+
import { getRestMockMethodValidationError, normalizeRestMockMethod, } from "../../utils/httpMethodValidation.js";
|
|
8
|
+
import { getGrpcMockResponseValidationError } from "../../utils/grpcMockValidation.js";
|
|
9
|
+
import { isLocalDevEnabled } from "../../utils/featureFlags.js";
|
|
10
|
+
export const MOCK_PROTOCOL_EXAMPLES = `
|
|
11
|
+
<example protocol="rest">
|
|
12
|
+
Generate a REST mock for one HTTP endpoint. Required fields: protocol="rest", endpointURL, method. Use apiSchema with an empty method only when intentionally generating all schema methods.
|
|
13
|
+
{
|
|
14
|
+
"protocol": "rest",
|
|
15
|
+
"endpointURL": "http://payment-service:5001/orders/:order_id/payment_authorizations",
|
|
16
|
+
"method": "PUT",
|
|
17
|
+
"requestAware": true,
|
|
18
|
+
"requestData": "{\"payment_method_id\":\"pm_123\"}",
|
|
19
|
+
"responseData": "{\"accepted\":true}",
|
|
20
|
+
"responseStatusCode": "200"
|
|
21
|
+
}
|
|
22
|
+
</example>
|
|
23
|
+
|
|
24
|
+
<example protocol="grpc">
|
|
25
|
+
Generate a gRPC mock. Required fields: protocol="grpc", endpointURL as host:port, protoPath, grpcServiceName. method is the RPC method name; omit or leave empty to generate all service methods. use responseData for successful protobuf-shaped responses, or grpcStatusCode/grpcStatusMessage for gRPC status errors.
|
|
26
|
+
{
|
|
27
|
+
"protocol": "grpc",
|
|
28
|
+
"endpointURL": "profile-service:50051",
|
|
29
|
+
"protoPath": "/repo/proto/profiles.proto",
|
|
30
|
+
"grpcServiceName": "ProfileService",
|
|
31
|
+
"method": "GetProfile",
|
|
32
|
+
"responseData": "{\"eligible\":true}"
|
|
33
|
+
}
|
|
34
|
+
</example>
|
|
35
|
+
|
|
36
|
+
<example protocol="grpc-status-error">
|
|
37
|
+
Generate a gRPC status error mock. Do not provide responseData with grpcStatusCode.
|
|
38
|
+
{
|
|
39
|
+
"protocol": "grpc",
|
|
40
|
+
"endpointURL": "profile-service:50051",
|
|
41
|
+
"protoPath": "/repo/proto/profiles.proto",
|
|
42
|
+
"grpcServiceName": "ProfileService",
|
|
43
|
+
"method": "GetProfile",
|
|
44
|
+
"grpcStatusCode": 5,
|
|
45
|
+
"grpcStatusMessage": "profile not found"
|
|
46
|
+
}
|
|
47
|
+
</example>
|
|
48
|
+
|
|
49
|
+
<example protocol="kafka-producer">
|
|
50
|
+
Generate a Kafka producer validation mock. Required fields: protocol="kafka", endpointURL as broker host:port, kafkaTopic, and requestData. Do not pass method or responseData.
|
|
51
|
+
{
|
|
52
|
+
"protocol": "kafka",
|
|
53
|
+
"endpointURL": "broker:9092",
|
|
54
|
+
"kafkaTopic": "events",
|
|
55
|
+
"requestData": "{\"name\":\"order.created\"}"
|
|
56
|
+
}
|
|
57
|
+
</example>
|
|
58
|
+
|
|
59
|
+
<example protocol="kafka-consumer">
|
|
60
|
+
Generate a Kafka consumer message mock. Required fields: protocol="kafka", endpointURL as broker host:port, kafkaTopic, and responseData. Do not pass method or requestData.
|
|
61
|
+
{
|
|
62
|
+
"protocol": "kafka",
|
|
63
|
+
"endpointURL": "broker:9092",
|
|
64
|
+
"kafkaTopic": "events",
|
|
65
|
+
"responseData": "{\"accepted\":true}"
|
|
66
|
+
}
|
|
67
|
+
</example>`;
|
|
6
68
|
const mockSchema = {
|
|
69
|
+
protocol: z
|
|
70
|
+
.nativeEnum(MockProtocol)
|
|
71
|
+
.default(MockProtocol.Rest)
|
|
72
|
+
.describe("Protocol for the mock to generate. Use 'rest' for HTTP/OpenAPI mocks, 'grpc' for protobuf/gRPC mocks, and 'kafka' for Kafka topic mocks. See <example> blocks in the tool description for protocol-specific required fields."),
|
|
7
73
|
endpointURL: z
|
|
8
74
|
.string()
|
|
9
|
-
.describe("
|
|
10
|
-
"
|
|
11
|
-
"
|
|
75
|
+
.describe("For REST: the base URL or endpoint URL to generate a mock for. " +
|
|
76
|
+
"For gRPC: the target service address as host:port (for example, profile-service:50051); do not include http://. " +
|
|
77
|
+
"For Kafka: the broker address as host:port (for example, broker:9092); do not include http://. " +
|
|
78
|
+
"If only a base URL is provided (e.g., https://api.example.com) and an apiSchema is given, REST mocks will be generated for ALL endpoints and methods in the schema — do NOT append paths to the URL. " +
|
|
79
|
+
"Only include a specific path (e.g., https://api.example.com/api/v1/products) if the user explicitly requests mocking a single REST endpoint."),
|
|
80
|
+
protoPath: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Required for protocol='grpc'. Absolute or repo-relative path to the .proto file that defines the gRPC service."),
|
|
84
|
+
grpcServiceName: z
|
|
85
|
+
.string()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Required for protocol='grpc'. gRPC service name from the proto file, e.g. Greeter or ProfileService."),
|
|
88
|
+
protobufExtraFiles: z
|
|
89
|
+
.array(z.string())
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Optional extra .proto files needed to compile the primary proto for gRPC mocks."),
|
|
92
|
+
protobufExtraImportPath: z
|
|
93
|
+
.array(z.string())
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("Optional extra import directories used when compiling protobuf files for gRPC mocks."),
|
|
96
|
+
kafkaTopic: z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Required for protocol='kafka'. Kafka topic name to mock, e.g. order.created or orders."),
|
|
12
100
|
method: z
|
|
13
|
-
.
|
|
101
|
+
.string()
|
|
14
102
|
.default("")
|
|
15
|
-
.describe('HTTP method to mock
|
|
103
|
+
.describe('For REST: HTTP method to mock; required unless apiSchema is provided for schema-wide generation. Empty string ("") with apiSchema mocks ALL available schema methods. For gRPC: RPC method name to mock; empty string generates all service methods. For Kafka: MUST be empty; use kafkaTopic for the topic name.'),
|
|
16
104
|
apiSchema: z
|
|
17
105
|
.string()
|
|
18
106
|
.default("")
|
|
@@ -20,16 +108,29 @@ const mockSchema = {
|
|
|
20
108
|
responseData: z
|
|
21
109
|
.string()
|
|
22
110
|
.optional()
|
|
23
|
-
.describe("
|
|
111
|
+
.describe("REST/gRPC: sample response body data to return. Kafka: consumer messages to inject. Provide inline JSON/YAML or an absolute file path prefixed with '@' (e.g., @/absolute/path/to/file)."),
|
|
24
112
|
responseStatusCode: z
|
|
25
113
|
.string()
|
|
26
114
|
.default("")
|
|
27
|
-
.refine((val) => !val || /^\d{3}$/.test(val), {
|
|
115
|
+
.refine((val) => !val || /^\d{3}$/.test(val), {
|
|
116
|
+
message: "Must be a 3-digit HTTP status code (e.g., '200', '201') or empty string",
|
|
117
|
+
})
|
|
28
118
|
.describe("HTTP status code the mock should return (e.g., '200', '201'). DO NOT ASSUME a status code if not provided."),
|
|
119
|
+
grpcStatusCode: z
|
|
120
|
+
.number()
|
|
121
|
+
.int()
|
|
122
|
+
.min(1)
|
|
123
|
+
.max(16)
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("For protocol='grpc' error mocks only. Non-OK gRPC status code number, e.g. 5 for NOT_FOUND or 14 for UNAVAILABLE. Do not combine with responseData."),
|
|
126
|
+
grpcStatusMessage: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("For protocol='grpc' error mocks only. gRPC status message to return with grpcStatusCode."),
|
|
29
130
|
requestData: z
|
|
30
131
|
.string()
|
|
31
132
|
.default("")
|
|
32
|
-
.describe("
|
|
133
|
+
.describe("REST: request body sample for request-aware matching. Kafka: producer validation payload/schema. Provide inline JSON/YAML or an absolute file path prefixed with '@' (e.g., @/absolute/path/to/file)."),
|
|
33
134
|
formParams: z
|
|
34
135
|
.string()
|
|
35
136
|
.default("")
|
|
@@ -42,17 +143,18 @@ const mockSchema = {
|
|
|
42
143
|
.string()
|
|
43
144
|
.optional()
|
|
44
145
|
.describe("MUST be absolute path to a trace file (e.g., /path/to/trace.json) to generate the mock from captured traffic."),
|
|
45
|
-
...(({ authHeader, insecure, ...rest }) => rest)(baseSchema.shape),
|
|
146
|
+
...(({ authHeader: _authHeader, insecure: _insecure, ...rest }) => rest)(baseSchema.shape),
|
|
46
147
|
};
|
|
47
148
|
export class MockGenerationService extends TestGenerationService {
|
|
48
149
|
getTestType() {
|
|
49
|
-
return
|
|
150
|
+
return MOCK_TYPE;
|
|
50
151
|
}
|
|
51
152
|
async generateTest(params) {
|
|
52
153
|
const result = await super.generateTest(params);
|
|
53
154
|
if (result.isError)
|
|
54
155
|
return result;
|
|
55
|
-
if (!params.apiSchema
|
|
156
|
+
if (!params.apiSchema ||
|
|
157
|
+
(params.protocol ?? MockProtocol.Rest) !== MockProtocol.Rest)
|
|
56
158
|
return result;
|
|
57
159
|
return {
|
|
58
160
|
...result,
|
|
@@ -123,11 +225,74 @@ This file must:
|
|
|
123
225
|
- Do NOT modify any mock logic, response data, or function signatures — only reorganize and add import/export wiring.
|
|
124
226
|
`;
|
|
125
227
|
}
|
|
126
|
-
|
|
228
|
+
validateInputs(params) {
|
|
229
|
+
const result = super.validateInputs(params);
|
|
230
|
+
const protocol = params.protocol ?? MockProtocol.Rest;
|
|
231
|
+
if (protocol === MockProtocol.Rest) {
|
|
232
|
+
const methodError = getRestMockMethodValidationError(params.method);
|
|
233
|
+
if (methodError) {
|
|
234
|
+
result.content.push({
|
|
235
|
+
type: "text",
|
|
236
|
+
text: `Error: ${methodError}.`,
|
|
237
|
+
});
|
|
238
|
+
result.isError = true;
|
|
239
|
+
}
|
|
240
|
+
if (!params.apiSchema && !params.method) {
|
|
241
|
+
result.content.push({
|
|
242
|
+
type: "text",
|
|
243
|
+
text: "Error: REST mock method is required unless apiSchema is provided for schema-wide generation.",
|
|
244
|
+
});
|
|
245
|
+
result.isError = true;
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
const missing = [];
|
|
250
|
+
if (protocol === MockProtocol.Grpc) {
|
|
251
|
+
if (!params.protoPath)
|
|
252
|
+
missing.push("protoPath is required for protocol='grpc'");
|
|
253
|
+
if (!params.grpcServiceName)
|
|
254
|
+
missing.push("grpcServiceName is required for protocol='grpc'");
|
|
255
|
+
if (params.grpcStatusCode !== undefined && params.responseData?.trim()) {
|
|
256
|
+
missing.push("gRPC status error mocks must use grpcStatusCode/grpcStatusMessage instead of responseData");
|
|
257
|
+
}
|
|
258
|
+
if (params.grpcStatusMessage && params.grpcStatusCode === undefined) {
|
|
259
|
+
missing.push("grpcStatusMessage requires grpcStatusCode");
|
|
260
|
+
}
|
|
261
|
+
const grpcResponseError = getGrpcMockResponseValidationError(params.responseData);
|
|
262
|
+
if (grpcResponseError)
|
|
263
|
+
missing.push(grpcResponseError);
|
|
264
|
+
}
|
|
265
|
+
if (protocol === MockProtocol.Kafka) {
|
|
266
|
+
if (!params.kafkaTopic) {
|
|
267
|
+
missing.push("kafkaTopic is required for protocol='kafka'");
|
|
268
|
+
}
|
|
269
|
+
if (params.method) {
|
|
270
|
+
missing.push("Kafka mock method must be empty; use kafkaTopic for the topic name. Produce validation uses requestData and consumer messages use responseData");
|
|
271
|
+
}
|
|
272
|
+
const hasRequestData = Boolean(params.requestData && params.requestData.trim());
|
|
273
|
+
const hasResponseData = Boolean(params.responseData && params.responseData.trim());
|
|
274
|
+
if (hasRequestData === hasResponseData) {
|
|
275
|
+
missing.push("Kafka mock requires exactly one of requestData (producer validation) or responseData (consumer messages)");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (missing.length === 0) {
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
127
281
|
return {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
282
|
+
content: [
|
|
283
|
+
...result.content,
|
|
284
|
+
{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: `Error: ${missing.join("; ")}.`,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
isError: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
buildGenerationOptions(params) {
|
|
293
|
+
const protocol = params.protocol ?? MockProtocol.Rest;
|
|
294
|
+
const common = {
|
|
295
|
+
protocol,
|
|
131
296
|
language: params.language ?? "python",
|
|
132
297
|
framework: params.framework ??
|
|
133
298
|
(params.language === "typescript" || params.language === "javascript"
|
|
@@ -143,20 +308,59 @@ This file must:
|
|
|
143
308
|
k8sNamespace: params.k8sNamespace,
|
|
144
309
|
k8sConfig: params.k8sConfig,
|
|
145
310
|
k8sContext: params.k8sContext,
|
|
311
|
+
entrypoint: getEntryPoint(),
|
|
312
|
+
};
|
|
313
|
+
const responseDataOption = params.responseData !== undefined &&
|
|
314
|
+
!(protocol === MockProtocol.Grpc && params.grpcStatusCode !== undefined)
|
|
315
|
+
? { responseData: params.responseData }
|
|
316
|
+
: {};
|
|
317
|
+
if (protocol === MockProtocol.Grpc) {
|
|
318
|
+
return {
|
|
319
|
+
...common,
|
|
320
|
+
...responseDataOption,
|
|
321
|
+
method: params.method,
|
|
322
|
+
url: params.endpointURL,
|
|
323
|
+
protoPath: params.protoPath,
|
|
324
|
+
service: params.grpcServiceName,
|
|
325
|
+
grpcStatusCode: params.grpcStatusCode,
|
|
326
|
+
grpcStatusMessage: params.grpcStatusMessage,
|
|
327
|
+
extraFiles: params.protobufExtraFiles ?? [],
|
|
328
|
+
extraImports: params.protobufExtraImportPath ?? [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (protocol === MockProtocol.Kafka) {
|
|
332
|
+
return {
|
|
333
|
+
...common,
|
|
334
|
+
...responseDataOption,
|
|
335
|
+
url: params.endpointURL,
|
|
336
|
+
topic: params.kafkaTopic,
|
|
337
|
+
requestData: params.requestData?.trim() ? params.requestData : undefined,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
...common,
|
|
342
|
+
...responseDataOption,
|
|
343
|
+
method: normalizeRestMockMethod(params.method),
|
|
344
|
+
uri: params.endpointURL,
|
|
345
|
+
apiSchema: params.apiSchema ? [params.apiSchema] : [],
|
|
146
346
|
requestData: params.requestData,
|
|
147
|
-
responseData: params.responseData,
|
|
148
347
|
responseStatusCode: params.responseStatusCode,
|
|
149
348
|
formParams: params.formParams,
|
|
150
349
|
requestAware: params.requestAware,
|
|
151
350
|
traceFilePath: params.trace,
|
|
152
|
-
entrypoint: getEntryPoint(),
|
|
153
351
|
mockPort: params.mockPort ?? 0,
|
|
154
352
|
optionalFields: params.optionalFields ?? false,
|
|
155
353
|
};
|
|
156
354
|
}
|
|
157
355
|
async executeGeneration(generateOptions) {
|
|
158
356
|
try {
|
|
159
|
-
const
|
|
357
|
+
const protocol = generateOptions.protocol ?? MockProtocol.Rest;
|
|
358
|
+
const client = this.client;
|
|
359
|
+
const result = protocol === MockProtocol.Grpc
|
|
360
|
+
? await client.generateGrpcMock(generateOptions)
|
|
361
|
+
: protocol === MockProtocol.Kafka
|
|
362
|
+
? await client.generateKafkaMock(generateOptions)
|
|
363
|
+
: await client.generateRestMock(generateOptions);
|
|
160
364
|
if (result && result.length > 0) {
|
|
161
365
|
const lowerResult = result.toLowerCase();
|
|
162
366
|
if (!lowerResult.includes("success")) {
|
|
@@ -177,12 +381,18 @@ ${result}`;
|
|
|
177
381
|
}
|
|
178
382
|
}
|
|
179
383
|
}
|
|
384
|
+
const LOCAL_DEV_GUARD = `
|
|
385
|
+
**LOCAL-DEV GUARD:** If the user asked to run a local-dev workflow, test local changes, validate a feature branch, test against a base/prod/main branch, or mock downstream services for local testing, do NOT start with this tool. First call \`skyramp_one_click_tool\` with workflow \`local_dev_test_changes\`, \`callingAgent\` set to the current MCP client, \`repositoryPath\`, and \`baseBranch\`; that workflow installs skills, initializes the workspace, analyzes the diff, and then calls mock generation at the correct phase.
|
|
386
|
+
`;
|
|
387
|
+
function getLocalDevGuard() {
|
|
388
|
+
return isLocalDevEnabled() ? LOCAL_DEV_GUARD : "";
|
|
389
|
+
}
|
|
180
390
|
const TOOL_NAME = "skyramp_mock_generation";
|
|
181
391
|
export function registerMockTool(server) {
|
|
182
392
|
server.registerTool(TOOL_NAME, {
|
|
183
|
-
description:
|
|
393
|
+
description: `${getLocalDevGuard()}Generate a mock server implementation using Skyramp's deterministic mock generation platform.
|
|
184
394
|
|
|
185
|
-
Mocks simulate
|
|
395
|
+
Mocks simulate REST, gRPC, or Kafka dependencies 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.
|
|
186
396
|
|
|
187
397
|
**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.**
|
|
188
398
|
|
|
@@ -194,7 +404,9 @@ Mocks simulate API endpoints so that consumers can develop and test against them
|
|
|
194
404
|
**Sources (in order of preference):**
|
|
195
405
|
1. Trace file (\`trace\`): generates the mock from captured real traffic.
|
|
196
406
|
2. OpenAPI schema (\`apiSchema\`): generates the mock from the spec.
|
|
197
|
-
3. Inline response data (\`responseData\`): generates a static mock with a fixed response
|
|
407
|
+
3. Inline response data (\`responseData\`): generates a static mock with a fixed response.
|
|
408
|
+
|
|
409
|
+
${MOCK_PROTOCOL_EXAMPLES}`,
|
|
198
410
|
inputSchema: mockSchema,
|
|
199
411
|
}, async (params) => {
|
|
200
412
|
const service = new MockGenerationService();
|
|
@@ -12,7 +12,7 @@ export declare const loadTestSchema: {
|
|
|
12
12
|
exclude: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
13
13
|
endpointURL: z.ZodDefault<z.ZodString>;
|
|
14
14
|
deployDashboard: z.ZodDefault<z.ZodBoolean>;
|
|
15
|
-
runtime: z.ZodDefault<z.ZodNativeEnum<typeof import("../../types/TestTypes.js").RuntimeEnvironment>>;
|
|
15
|
+
runtime: z.ZodDefault<z.ZodEffects<z.ZodNativeEnum<typeof import("../../types/TestTypes.js").RuntimeEnvironment>, import("../../types/TestTypes.js").RuntimeEnvironment, unknown>>;
|
|
16
16
|
dockerNetwork: z.ZodDefault<z.ZodString>;
|
|
17
17
|
dockerWorkerPort: z.ZodDefault<z.ZodNumber>;
|
|
18
18
|
k8sNamespace: z.ZodDefault<z.ZodString>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* skyramp_generate_enriched_integration_test
|
|
5
|
+
*
|
|
6
|
+
* Combines integration test generation (from a scenario file) and mock enrichment
|
|
7
|
+
* into a single atomic operation. Eliminates the need to call
|
|
8
|
+
* skyramp_integration_test_generation and skyramp_enrich_test_with_mocks separately.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerGenerateEnrichedIntegrationTestTool(server: McpServer): void;
|
|
11
|
+
export declare function executeGenerateEnrichedIntegrationTest(params: {
|
|
12
|
+
scenarioFile: string;
|
|
13
|
+
mockFiles: string[];
|
|
14
|
+
scenarioName: string;
|
|
15
|
+
outputDir: string;
|
|
16
|
+
output?: string;
|
|
17
|
+
runtime?: string;
|
|
18
|
+
dockerNetwork?: string;
|
|
19
|
+
authHeader?: string;
|
|
20
|
+
authScheme?: string;
|
|
21
|
+
language?: string;
|
|
22
|
+
framework?: string;
|
|
23
|
+
traceComments?: string[];
|
|
24
|
+
}): Promise<CallToolResult>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SkyrampClient } from "@skyramp/skyramp";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
import { RuntimeEnvironment, runtimeEnvironmentInputSchema } from "../types/TestTypes.js";
|
|
5
|
+
import { injectMockImports, isSupportedMockEnrichmentLanguage } from "./enrichTestWithMocksTool.js";
|
|
6
|
+
import { isAuthorizationHeaderName } from "../utils/workspaceAuth.js";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
const TOOL_NAME = "skyramp_generate_enriched_integration_test";
|
|
10
|
+
const enrichedIntegrationTestSchema = {
|
|
11
|
+
scenarioFile: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Absolute path to the scenario JSON file (from skyramp_batch_scenario_test_generation). Used as trace input for integration test generation."),
|
|
14
|
+
mockFiles: z
|
|
15
|
+
.array(z.string())
|
|
16
|
+
.describe("Absolute paths to the generated mock files (from skyramp_mock_generation or skyramp_batch_mock_generation) that this test needs."),
|
|
17
|
+
scenarioName: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe("Human-readable scenario name (e.g., order-happy-path). Used as the SCENARIO_NAME constant in the enriched test."),
|
|
20
|
+
outputDir: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe("Absolute path to the directory where the test file will be written."),
|
|
23
|
+
output: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Output test file name (e.g., order_happy_path_integration_test.py). Derived from scenario if omitted."),
|
|
27
|
+
runtime: runtimeEnvironmentInputSchema
|
|
28
|
+
.optional()
|
|
29
|
+
.default(RuntimeEnvironment.DOCKER)
|
|
30
|
+
.describe("Runtime environment: 'docker' or 'k8s' (also accepts 'kubernetes'). Defaults to docker."),
|
|
31
|
+
dockerNetwork: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Docker network name for the test runtime."),
|
|
35
|
+
authHeader: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("HTTP header that carries the auth credential (e.g., Authorization, X-Api-Key)."),
|
|
39
|
+
authScheme: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Auth scheme prefix (e.g., Bearer). Mapped to authType for the backend."),
|
|
43
|
+
language: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Programming language for the generated test. Automatic mock enrichment supports Python, TypeScript, JavaScript, and Java."),
|
|
47
|
+
framework: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Test framework (e.g., pytest, playwright)."),
|
|
51
|
+
traceComments: z
|
|
52
|
+
.array(z.string())
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Optional trace comments showing how mock values flow to assertions."),
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* skyramp_generate_enriched_integration_test
|
|
58
|
+
*
|
|
59
|
+
* Combines integration test generation (from a scenario file) and mock enrichment
|
|
60
|
+
* into a single atomic operation. Eliminates the need to call
|
|
61
|
+
* skyramp_integration_test_generation and skyramp_enrich_test_with_mocks separately.
|
|
62
|
+
*/
|
|
63
|
+
export function registerGenerateEnrichedIntegrationTestTool(server) {
|
|
64
|
+
server.registerTool(TOOL_NAME, {
|
|
65
|
+
description: "Generate an integration test from a scenario file AND enrich it with mock imports in one step. " +
|
|
66
|
+
"Combines skyramp_integration_test_generation + mock enrichment into a single call. " +
|
|
67
|
+
"Use this after skyramp_batch_scenario_test_generation and skyramp_batch_mock_generation when the target test language is Python, TypeScript, JavaScript, or Java.",
|
|
68
|
+
inputSchema: enrichedIntegrationTestSchema,
|
|
69
|
+
}, async (params) => {
|
|
70
|
+
return executeGenerateEnrichedIntegrationTest(params);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function executeGenerateEnrichedIntegrationTest(params) {
|
|
74
|
+
const { scenarioFile, mockFiles, scenarioName, outputDir, output, runtime, dockerNetwork, authHeader, authScheme, language, framework, traceComments, } = params;
|
|
75
|
+
try {
|
|
76
|
+
const resolvedLanguage = (language ?? "python").toLowerCase();
|
|
77
|
+
if (!isSupportedMockEnrichmentLanguage(resolvedLanguage)) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: `**Error:** Mock enrichment for ${resolvedLanguage} tests is not supported. ` +
|
|
83
|
+
"Supported languages: python, typescript, javascript, java.",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Step 1b: Validate scenarioFile exists
|
|
90
|
+
if (!fs.existsSync(scenarioFile)) {
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: `**Error:** Scenario file not found: ${scenarioFile}`,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Step 1b: Validate mock files exist
|
|
102
|
+
const missingFiles = mockFiles.filter((f) => !fs.existsSync(f));
|
|
103
|
+
if (missingFiles.length > 0) {
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `**Error:** Mock files not found:\n` +
|
|
109
|
+
missingFiles.map((f) => ` - ${f}`).join("\n") +
|
|
110
|
+
`\n\nGenerate them first with \`skyramp_batch_mock_generation\`.`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
isError: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Step 2: Generate the integration test
|
|
117
|
+
// Normalize auth params: the Go CLI treats --auth-type and --auth-header as
|
|
118
|
+
// mutually exclusive when authHeader is the standard Authorization header.
|
|
119
|
+
// Mirror TestGenerationService.executeGeneration() normalization:
|
|
120
|
+
// if authScheme is set and authHeader is Authorization, use authType only.
|
|
121
|
+
let resolvedAuthHeader = authHeader;
|
|
122
|
+
const resolvedAuthType = authScheme;
|
|
123
|
+
if (resolvedAuthType !== undefined && isAuthorizationHeaderName(resolvedAuthHeader || "")) {
|
|
124
|
+
resolvedAuthHeader = undefined;
|
|
125
|
+
}
|
|
126
|
+
const client = new SkyrampClient();
|
|
127
|
+
const result = await client.generateRestTest({
|
|
128
|
+
traceFilePath: scenarioFile,
|
|
129
|
+
outputDir,
|
|
130
|
+
output,
|
|
131
|
+
runtime: runtime ?? RuntimeEnvironment.DOCKER,
|
|
132
|
+
force: true,
|
|
133
|
+
dockerNetwork,
|
|
134
|
+
authHeader: resolvedAuthHeader,
|
|
135
|
+
authType: resolvedAuthType,
|
|
136
|
+
language: resolvedLanguage,
|
|
137
|
+
framework,
|
|
138
|
+
testType: "integration",
|
|
139
|
+
});
|
|
140
|
+
// Check if generation succeeded
|
|
141
|
+
if (!result || !result.toLowerCase().includes("success")) {
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: `**Error:** Integration test generation failed: ${result || "no output from backend"}`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Step 3: Determine the test file path and read it
|
|
153
|
+
const testFileName = output || deriveTestFileName(scenarioFile, resolvedLanguage);
|
|
154
|
+
const testFilePath = path.join(outputDir, testFileName);
|
|
155
|
+
if (!fs.existsSync(testFilePath)) {
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: `**Error:** Generated test file not found at expected path: ${testFilePath}\n` +
|
|
161
|
+
`Backend output: ${result}`,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
isError: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const testContent = fs.readFileSync(testFilePath, "utf-8");
|
|
168
|
+
// Step 4: Enrich with mock imports
|
|
169
|
+
const enrichedContent = injectMockImports(testContent, testFilePath, scenarioName, mockFiles, traceComments, resolvedLanguage);
|
|
170
|
+
// Step 5: Write the enriched content back
|
|
171
|
+
fs.writeFileSync(testFilePath, enrichedContent);
|
|
172
|
+
logger.info(`Generated and enriched integration test: ${testFilePath}`, {
|
|
173
|
+
scenarioName,
|
|
174
|
+
mockFileCount: mockFiles.length,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: [
|
|
181
|
+
`**Integration test generated and enriched with mocks for scenario "${scenarioName}".**\n`,
|
|
182
|
+
`**Test file:** ${testFilePath}`,
|
|
183
|
+
`**Mock files imported:** ${mockFiles.length}`,
|
|
184
|
+
...mockFiles.map((f) => ` - ${path.basename(f)}`),
|
|
185
|
+
`\nThe test is ready for execution with: \`skyramp_execute_test({testFile: "${testFilePath}", ...})\``,
|
|
186
|
+
].join("\n"),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
logger.error(`Failed to generate enriched integration test: ${error.message}`);
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: `**Failed to generate enriched integration test:** ${error.message}`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
isError: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function deriveTestFileName(scenarioFile, language) {
|
|
205
|
+
const base = path.basename(scenarioFile, ".json");
|
|
206
|
+
const name = base.replace(/^scenario_/, "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
207
|
+
const normalizedLanguage = language?.toLowerCase();
|
|
208
|
+
if (normalizedLanguage === "typescript") {
|
|
209
|
+
return `${name}_integration_test.spec.ts`;
|
|
210
|
+
}
|
|
211
|
+
if (normalizedLanguage === "javascript") {
|
|
212
|
+
return `${name}_integration_test.spec.js`;
|
|
213
|
+
}
|
|
214
|
+
if (normalizedLanguage === "java") {
|
|
215
|
+
return `${toPascalCase(name)}IntegrationTest.java`;
|
|
216
|
+
}
|
|
217
|
+
return `${name}_integration_test.py`;
|
|
218
|
+
}
|
|
219
|
+
function toPascalCase(value) {
|
|
220
|
+
return value
|
|
221
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
224
|
+
.join("");
|
|
225
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
const generateRestTest = jest.fn();
|
|
6
|
+
jest.unstable_mockModule("@skyramp/skyramp", () => ({
|
|
7
|
+
SkyrampClient: jest.fn().mockImplementation(() => ({
|
|
8
|
+
generateRestTest,
|
|
9
|
+
})),
|
|
10
|
+
}));
|
|
11
|
+
const { executeGenerateEnrichedIntegrationTest } = await import("./generateEnrichedIntegrationTestTool.js");
|
|
12
|
+
describe("executeGenerateEnrichedIntegrationTest", () => {
|
|
13
|
+
let tmpDir;
|
|
14
|
+
let scenarioFile;
|
|
15
|
+
let mockFile;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-enriched-gen-"));
|
|
18
|
+
scenarioFile = path.join(tmpDir, "scenario.json");
|
|
19
|
+
mockFile = path.join(tmpDir, "mock.py");
|
|
20
|
+
fs.writeFileSync(scenarioFile, "[]");
|
|
21
|
+
fs.writeFileSync(mockFile, "def get_all_mocks(): return []\n");
|
|
22
|
+
generateRestTest.mockReset();
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
it("rejects unsupported mock enrichment languages before generation", async () => {
|
|
28
|
+
const output = "unsupported_go_integration_test.go";
|
|
29
|
+
const result = await executeGenerateEnrichedIntegrationTest({
|
|
30
|
+
scenarioFile,
|
|
31
|
+
mockFiles: [mockFile],
|
|
32
|
+
scenarioName: "unsupported-go",
|
|
33
|
+
outputDir: tmpDir,
|
|
34
|
+
output,
|
|
35
|
+
language: "go",
|
|
36
|
+
framework: "go-test",
|
|
37
|
+
});
|
|
38
|
+
expect(result.isError).toBe(true);
|
|
39
|
+
const message = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
40
|
+
expect(message).toContain("Mock enrichment for go tests is not supported");
|
|
41
|
+
expect(generateRestTest).not.toHaveBeenCalled();
|
|
42
|
+
expect(fs.existsSync(path.join(tmpDir, output))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|