@skyramp/mcp 0.1.4 → 0.1.6
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 +6 -5
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -7
- package/build/prompts/personas.js +2 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +28 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +2 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +74 -16
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -0
- package/build/prompts/test-recommendation/recommendationSections.js +13 -43
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +19 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +158 -70
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +24 -117
- package/build/prompts/testbot/testbot-prompts.js +12 -18
- package/build/prompts/testbot/testbot-prompts.test.js +2 -2
- package/build/resources/analysisResources.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +127 -4
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +205 -18
- package/build/tools/generate-tests/generateContractRestTool.js +19 -19
- package/build/tools/generate-tests/generateIntegrationRestTool.js +9 -2
- package/build/tools/generate-tests/generateUIRestTool.js +23 -8
- package/build/tools/test-management/analyzeChangesTool.js +222 -11
- package/build/tools/test-management/analyzeChangesTool.test.js +233 -1
- package/build/types/TestRecommendation.js +0 -2
- package/build/utils/featureFlags.js +4 -22
- package/build/utils/featureFlags.test.js +81 -0
- package/build/utils/httpDefaults.js +6 -1
- package/build/utils/httpDefaults.test.js +21 -0
- package/build/utils/scenarioDrafting.js +511 -100
- package/build/utils/scenarioDrafting.test.js +545 -259
- package/build/utils/telemetry.js +2 -1
- package/build/utils/utils.js +23 -0
- package/package.json +1 -1
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
jest.mock("../../services/ScenarioGenerationService.js", () => ({
|
|
2
|
-
jest.
|
|
3
|
-
|
|
1
|
+
jest.mock("../../services/ScenarioGenerationService.js", () => ({
|
|
2
|
+
ScenarioGenerationService: jest.fn().mockImplementation(() => ({
|
|
3
|
+
generateTraceRequestFromInput: jest.fn().mockReturnValue(null),
|
|
4
|
+
})),
|
|
5
|
+
}));
|
|
6
|
+
jest.mock("../../services/AnalyticsService.js", () => ({ AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) } }));
|
|
7
|
+
jest.mock("../../utils/workspaceAuth.js", () => ({
|
|
8
|
+
getWorkspaceAuthConfig: jest.fn().mockResolvedValue({ authHeader: undefined, authType: "none" }),
|
|
9
|
+
WorkspaceAuthType: { None: "none" },
|
|
10
|
+
getDefaultAuthHeader: jest.fn().mockReturnValue(""),
|
|
11
|
+
isAuthorizationHeaderName: jest.fn().mockReturnValue(false),
|
|
12
|
+
getAuthScheme: jest.fn().mockReturnValue(""),
|
|
13
|
+
readWorkspaceConfigRaw: jest.fn().mockResolvedValue(null),
|
|
14
|
+
}));
|
|
4
15
|
jest.mock("../../utils/logger.js", () => ({ logger: { info: jest.fn(), warning: jest.fn(), error: jest.fn() } }));
|
|
5
16
|
jest.mock("../../prompts/personas.js", () => ({ getPersonaPrefix: () => "" }));
|
|
6
|
-
import
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import { stepSchema, registerBatchScenarioTestTool } from "./generateBatchScenarioRestTool.js";
|
|
19
|
+
/** Build a minimal mock McpServer, register the tool, and return the captured handler. */
|
|
20
|
+
function captureHandler() {
|
|
21
|
+
let capturedHandler;
|
|
22
|
+
const mockServer = {
|
|
23
|
+
registerTool: jest.fn((_name, _meta, handler) => {
|
|
24
|
+
capturedHandler = handler;
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
registerBatchScenarioTestTool(mockServer);
|
|
28
|
+
return capturedHandler;
|
|
29
|
+
}
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// stepSchema — requestBody validation
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
33
|
describe("stepSchema — requestBody validation", () => {
|
|
8
34
|
it("accepts a POST step with a non-empty requestBody", () => {
|
|
9
35
|
const result = stepSchema.safeParse({
|
|
10
36
|
method: "POST",
|
|
11
37
|
path: "/api/v1/products",
|
|
12
|
-
statusCode: 201,
|
|
13
38
|
requestBody: JSON.stringify({ name: "Widget-123", price: 9.99 }),
|
|
14
39
|
});
|
|
15
40
|
expect(result.success).toBe(true);
|
|
@@ -18,7 +43,6 @@ describe("stepSchema — requestBody validation", () => {
|
|
|
18
43
|
const result = stepSchema.safeParse({
|
|
19
44
|
method: "POST",
|
|
20
45
|
path: "/api/v1/products",
|
|
21
|
-
statusCode: 201,
|
|
22
46
|
requestBody: "{}",
|
|
23
47
|
});
|
|
24
48
|
expect(result.success).toBe(false);
|
|
@@ -30,7 +54,6 @@ describe("stepSchema — requestBody validation", () => {
|
|
|
30
54
|
const result = stepSchema.safeParse({
|
|
31
55
|
method: "PATCH",
|
|
32
56
|
path: "/api/v1/orders/1",
|
|
33
|
-
statusCode: 200,
|
|
34
57
|
requestBody: "{}",
|
|
35
58
|
});
|
|
36
59
|
expect(result.success).toBe(false);
|
|
@@ -40,7 +63,6 @@ describe("stepSchema — requestBody validation", () => {
|
|
|
40
63
|
const result = stepSchema.safeParse({
|
|
41
64
|
method: "PUT",
|
|
42
65
|
path: "/api/v1/products/1",
|
|
43
|
-
statusCode: 200,
|
|
44
66
|
requestBody: "{}",
|
|
45
67
|
});
|
|
46
68
|
expect(result.success).toBe(false);
|
|
@@ -49,29 +71,24 @@ describe("stepSchema — requestBody validation", () => {
|
|
|
49
71
|
expect(issue?.message).toContain("PUT");
|
|
50
72
|
});
|
|
51
73
|
it("accepts a POST step with requestBody omitted entirely", () => {
|
|
52
|
-
// The validation only rejects {} when present — omitting requestBody is still allowed
|
|
53
74
|
const result = stepSchema.safeParse({
|
|
54
75
|
method: "POST",
|
|
55
76
|
path: "/api/v1/products",
|
|
56
|
-
statusCode: 201,
|
|
57
77
|
});
|
|
58
78
|
expect(result.success).toBe(true);
|
|
59
79
|
});
|
|
60
80
|
it("does not throw on requestBody: 'null' (valid JSON null)", () => {
|
|
61
|
-
// parsed === null must not cause Object.keys to throw
|
|
62
81
|
const result = stepSchema.safeParse({
|
|
63
82
|
method: "POST",
|
|
64
83
|
path: "/api/v1/products",
|
|
65
|
-
statusCode: 201,
|
|
66
84
|
requestBody: "null",
|
|
67
85
|
});
|
|
68
|
-
expect(result.success).toBe(true);
|
|
86
|
+
expect(result.success).toBe(true);
|
|
69
87
|
});
|
|
70
88
|
it("accepts a GET step with no requestBody", () => {
|
|
71
89
|
const result = stepSchema.safeParse({
|
|
72
90
|
method: "GET",
|
|
73
91
|
path: "/api/v1/products/1",
|
|
74
|
-
statusCode: 200,
|
|
75
92
|
});
|
|
76
93
|
expect(result.success).toBe(true);
|
|
77
94
|
});
|
|
@@ -79,19 +96,189 @@ describe("stepSchema — requestBody validation", () => {
|
|
|
79
96
|
const result = stepSchema.safeParse({
|
|
80
97
|
method: "DELETE",
|
|
81
98
|
path: "/api/v1/products/1",
|
|
82
|
-
statusCode: 204,
|
|
83
99
|
});
|
|
84
100
|
expect(result.success).toBe(true);
|
|
85
101
|
});
|
|
86
102
|
it("does not reject a GET step that happens to have an empty requestBody", () => {
|
|
87
|
-
// GET with empty body is unusual but not blocked — the validation only
|
|
88
|
-
// applies to body methods (POST/PUT/PATCH)
|
|
89
103
|
const result = stepSchema.safeParse({
|
|
90
104
|
method: "GET",
|
|
91
105
|
path: "/api/v1/products",
|
|
92
|
-
statusCode: 200,
|
|
93
106
|
requestBody: "{}",
|
|
94
107
|
});
|
|
95
108
|
expect(result.success).toBe(true);
|
|
96
109
|
});
|
|
97
110
|
});
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
// GraphQL step rejection (Change 10b)
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
describe("generateBatchScenarioRestTool — GraphQL step rejection (Change 10b)", () => {
|
|
115
|
+
let handler;
|
|
116
|
+
const baseParams = {
|
|
117
|
+
scenarioName: "test_scenario",
|
|
118
|
+
destination: "localhost",
|
|
119
|
+
outputDir: "/tmp/skyramp-test",
|
|
120
|
+
};
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
jest.clearAllMocks();
|
|
123
|
+
handler = captureHandler();
|
|
124
|
+
});
|
|
125
|
+
it("rejects a step list containing /graphql", async () => {
|
|
126
|
+
const result = await handler({
|
|
127
|
+
...baseParams,
|
|
128
|
+
steps: [{ method: "POST", path: "/graphql", requestBody: '{"query":"{ users { id } }"}' }],
|
|
129
|
+
});
|
|
130
|
+
expect(result.isError).toBe(true);
|
|
131
|
+
expect(result.content[0].text).toContain("GraphQL endpoints are not supported");
|
|
132
|
+
expect(result.content[0].text).toContain("POST /graphql");
|
|
133
|
+
});
|
|
134
|
+
it("rejects a step list containing /api/graphql (nested segment)", async () => {
|
|
135
|
+
const result = await handler({
|
|
136
|
+
...baseParams,
|
|
137
|
+
steps: [
|
|
138
|
+
{ method: "GET", path: "/api/v1/users" },
|
|
139
|
+
{ method: "POST", path: "/api/graphql", requestBody: '{"query":"{ id }"}' },
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
expect(result.isError).toBe(true);
|
|
143
|
+
expect(result.content[0].text).toContain("GraphQL endpoints are not supported");
|
|
144
|
+
expect(result.content[0].text).toContain("POST /api/graphql");
|
|
145
|
+
});
|
|
146
|
+
it("does not reject a path whose segment is 'graphql-config' (not an exact segment match)", async () => {
|
|
147
|
+
// 'graphql-config' !== 'graphql' — the guard uses exact segment comparison.
|
|
148
|
+
// The GraphQL rejection message must never appear, regardless of whether the
|
|
149
|
+
// handler succeeds or fails for some other reason.
|
|
150
|
+
const result = await handler({
|
|
151
|
+
...baseParams,
|
|
152
|
+
steps: [{ method: "GET", path: "/api/v1/graphql-config" }],
|
|
153
|
+
});
|
|
154
|
+
expect(result.content[0].text).not.toContain("GraphQL endpoints are not supported");
|
|
155
|
+
});
|
|
156
|
+
it("does not throw on a non-string path (typeof guard)", async () => {
|
|
157
|
+
// typeof guard returns false for non-string — no crash from .replace()
|
|
158
|
+
const result = await handler({
|
|
159
|
+
...baseParams,
|
|
160
|
+
steps: [{ method: "GET", path: undefined }],
|
|
161
|
+
});
|
|
162
|
+
expect(result).toBeDefined();
|
|
163
|
+
if (result.isError) {
|
|
164
|
+
expect(result.content[0].text).not.toContain("Cannot read");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// Spec path validation (Change 3)
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
describe("generateBatchScenarioRestTool — spec path validation (Change 3)", () => {
|
|
172
|
+
let handler;
|
|
173
|
+
let readFileSyncSpy;
|
|
174
|
+
const fakeSpec = JSON.stringify({
|
|
175
|
+
openapi: "3.0.0",
|
|
176
|
+
paths: {
|
|
177
|
+
"/api/v1/users": {},
|
|
178
|
+
"/api/v1/users/{id}": {},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const baseParams = {
|
|
182
|
+
scenarioName: "test_scenario",
|
|
183
|
+
destination: "localhost",
|
|
184
|
+
outputDir: "/tmp/skyramp-test",
|
|
185
|
+
apiSchema: "/tmp/openapi.json",
|
|
186
|
+
};
|
|
187
|
+
let writeFileSyncSpy;
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
jest.clearAllMocks();
|
|
190
|
+
handler = captureHandler();
|
|
191
|
+
readFileSyncSpy = jest.spyOn(fs, "readFileSync").mockReturnValue(fakeSpec);
|
|
192
|
+
writeFileSyncSpy = jest.spyOn(fs, "writeFileSync").mockImplementation(() => { });
|
|
193
|
+
// Return a valid trace so tests reach the success path (where the warning is appended)
|
|
194
|
+
const ScenarioSvc = require("../../services/ScenarioGenerationService.js").ScenarioGenerationService;
|
|
195
|
+
ScenarioSvc.mockImplementation(() => ({
|
|
196
|
+
generateTraceRequestFromInput: jest.fn().mockReturnValue({ method: "GET", path: "/mock" }),
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
readFileSyncSpy.mockRestore();
|
|
201
|
+
writeFileSyncSpy.mockRestore();
|
|
202
|
+
});
|
|
203
|
+
it("warns (does not hard-reject) when step paths are not in the OpenAPI spec", async () => {
|
|
204
|
+
// Spec may lag code — missing path != phantom path, so we warn and proceed.
|
|
205
|
+
const result = await handler({
|
|
206
|
+
...baseParams,
|
|
207
|
+
steps: [{ method: "GET", path: "/api/v1/phantom-endpoint" }],
|
|
208
|
+
});
|
|
209
|
+
expect(result.isError).toBeFalsy();
|
|
210
|
+
expect(result.content[0].text).toContain("Spec warning");
|
|
211
|
+
expect(result.content[0].text).toContain("/api/v1/phantom-endpoint");
|
|
212
|
+
expect(result.content[0].text).toContain("Known spec paths");
|
|
213
|
+
});
|
|
214
|
+
it("includes no spec warning when step path is present in the OpenAPI spec", async () => {
|
|
215
|
+
const result = await handler({
|
|
216
|
+
...baseParams,
|
|
217
|
+
steps: [{ method: "GET", path: "/api/v1/users" }],
|
|
218
|
+
});
|
|
219
|
+
expect(result.content[0].text).not.toContain("Spec warning");
|
|
220
|
+
});
|
|
221
|
+
it("does not warn for Express-style :param paths that normalise to a spec path", async () => {
|
|
222
|
+
const result = await handler({
|
|
223
|
+
...baseParams,
|
|
224
|
+
steps: [{ method: "GET", path: "/api/v1/users/:id" }],
|
|
225
|
+
});
|
|
226
|
+
expect(result.content[0].text).not.toContain("Spec warning");
|
|
227
|
+
});
|
|
228
|
+
it("does not throw on a non-string path (typeof guard)", async () => {
|
|
229
|
+
const result = await handler({
|
|
230
|
+
...baseParams,
|
|
231
|
+
steps: [{ method: "GET", path: undefined }],
|
|
232
|
+
});
|
|
233
|
+
expect(result).toBeDefined();
|
|
234
|
+
expect(result.content[0].text).not.toContain("TypeError");
|
|
235
|
+
expect(result.content[0].text).not.toContain("Cannot read");
|
|
236
|
+
});
|
|
237
|
+
it("includes known spec paths hint in the warning message", async () => {
|
|
238
|
+
const result = await handler({
|
|
239
|
+
...baseParams,
|
|
240
|
+
steps: [{ method: "POST", path: "/does/not/exist" }],
|
|
241
|
+
});
|
|
242
|
+
expect(result.isError).toBeFalsy();
|
|
243
|
+
expect(result.content[0].text).toContain("/api/v1/users");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe("generateBatchScenarioRestTool — spec URL fetch safety (comment 3203006665)", () => {
|
|
247
|
+
let handler;
|
|
248
|
+
const baseParams = {
|
|
249
|
+
scenarioName: "test_scenario",
|
|
250
|
+
destination: "localhost",
|
|
251
|
+
outputDir: "/tmp/skyramp-test",
|
|
252
|
+
apiSchema: "https://example.com/openapi.json",
|
|
253
|
+
steps: [{ method: "GET", path: "/api/v1/users" }],
|
|
254
|
+
};
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
jest.clearAllMocks();
|
|
257
|
+
handler = captureHandler();
|
|
258
|
+
});
|
|
259
|
+
it("skips spec check (no warning emitted) when URL returns non-2xx", async () => {
|
|
260
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
261
|
+
ok: false,
|
|
262
|
+
status: 404,
|
|
263
|
+
statusText: "Not Found",
|
|
264
|
+
text: async () => "<html>Not Found</html>",
|
|
265
|
+
});
|
|
266
|
+
const result = await handler({ ...baseParams });
|
|
267
|
+
// Non-2xx throws → caught → spec check skipped entirely, no warning in output
|
|
268
|
+
expect(result.content[0].text).not.toContain("Spec warning");
|
|
269
|
+
});
|
|
270
|
+
it("skips spec check (no warning emitted) when spec has no paths", async () => {
|
|
271
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
272
|
+
ok: true,
|
|
273
|
+
status: 200,
|
|
274
|
+
statusText: "OK",
|
|
275
|
+
text: async () => JSON.stringify({ openapi: "3.0.0", info: { title: "Test", version: "1.0" } }),
|
|
276
|
+
});
|
|
277
|
+
const result = await handler({ ...baseParams });
|
|
278
|
+
expect(result.content[0].text).not.toContain("Spec warning");
|
|
279
|
+
});
|
|
280
|
+
afterEach(() => {
|
|
281
|
+
// Restore fetch
|
|
282
|
+
delete global.fetch;
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -4,7 +4,7 @@ import { baseTestSchema, TestType } from "../../types/TestTypes.js";
|
|
|
4
4
|
import { TestGenerationService, } from "../../services/TestGenerationService.js";
|
|
5
5
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
6
6
|
import { getPersonaPrefix } from "../../prompts/personas.js";
|
|
7
|
-
import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
|
|
7
|
+
import { isContractConsumerModeEnabled, isTestbotEnabled } from "../../utils/featureFlags.js";
|
|
8
8
|
// SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE gates BOTH:
|
|
9
9
|
// 1. Consumer-side contract test generation (`consumerMode` /
|
|
10
10
|
// `consumerOutput` schema fields, validation, post-gen instructions,
|
|
@@ -16,6 +16,7 @@ import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
|
|
|
16
16
|
//
|
|
17
17
|
// `providerMode` itself is exposed regardless of the flag.
|
|
18
18
|
const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
|
|
19
|
+
const ADD_ASSERTIONS_DEFAULT = isTestbotEnabled();
|
|
19
20
|
// "Requires apiSchema." plus a mode-aware "Not allowed with ..." clause for
|
|
20
21
|
// the parent-provisioning fields. Generated once so wording stays in lockstep
|
|
21
22
|
// with the feature flag.
|
|
@@ -23,6 +24,10 @@ const PARENT_FIELD_NOT_ALLOWED_NOTE = CONSUMER_MODE_ENABLED
|
|
|
23
24
|
? "Requires apiSchema. Not allowed with consumerMode or skipProvisionParents."
|
|
24
25
|
: "Requires apiSchema. Not allowed with skipProvisionParents.";
|
|
25
26
|
const baseContractTestSchema = {
|
|
27
|
+
enhanceAssertions: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.default(ADD_ASSERTIONS_DEFAULT)
|
|
30
|
+
.describe("When true, calls skyramp_enhance_assertions after test generation to add richer response-body assertions. Disabled by default. Automatically enabled when running as testbot-feature. Do not override the default value of this parameter, unless the user explicitly asks to enable it."),
|
|
26
31
|
...baseTestSchema,
|
|
27
32
|
pathParams: z
|
|
28
33
|
.string()
|
|
@@ -149,7 +154,7 @@ export class ContractTestService extends TestGenerationService {
|
|
|
149
154
|
if (params.providerMode) {
|
|
150
155
|
content.push({
|
|
151
156
|
type: "text",
|
|
152
|
-
text: this.buildProviderPostGenInstructions(params),
|
|
157
|
+
text: this.buildProviderPostGenInstructions(params, params.enhanceAssertions),
|
|
153
158
|
});
|
|
154
159
|
}
|
|
155
160
|
else {
|
|
@@ -219,27 +224,22 @@ ${step5}
|
|
|
219
224
|
}
|
|
220
225
|
buildSampleDataInstructions(params) {
|
|
221
226
|
return `
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
**Part 1 — Replace placeholder values in the generated file:**
|
|
227
|
+
### CRITICAL NEXT STEP — Replace placeholder sample data
|
|
225
228
|
|
|
226
229
|
${this.buildPlaceholderReplacementBlock(params)}
|
|
227
230
|
`;
|
|
228
231
|
}
|
|
229
|
-
buildProviderPostGenInstructions(params) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated provider contract test file, \`testType: "contract"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.
|
|
242
|
-
`;
|
|
232
|
+
buildProviderPostGenInstructions(params, enhanceAssertions = false) {
|
|
233
|
+
const steps = [];
|
|
234
|
+
const nextStepNote = enhanceAssertions ? "continue to Step 2" : undefined;
|
|
235
|
+
steps.push(`### Step ${steps.length + 1} — Replace placeholder values [REQUIRED]\n\n${this.buildPlaceholderReplacementBlock(params, nextStepNote)}`);
|
|
236
|
+
if (enhanceAssertions) {
|
|
237
|
+
steps.push(`### Step ${steps.length + 1} — Enhance response body assertions [REQUIRED]\nCall \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated provider contract test file, \`testType: "contract"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.`);
|
|
238
|
+
}
|
|
239
|
+
const heading = enhanceAssertions
|
|
240
|
+
? "Replace placeholder data and enhance assertions"
|
|
241
|
+
: "Replace placeholder data";
|
|
242
|
+
return `\n### CRITICAL NEXT STEP — ${heading}\n\n${steps.join("\n\n")}`;
|
|
243
243
|
}
|
|
244
244
|
buildConsumerStubReplacementInstructions() {
|
|
245
245
|
return `
|
|
@@ -4,8 +4,14 @@ import { TestGenerationService, } from "../../services/TestGenerationService.js"
|
|
|
4
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
5
|
import { AUTH_CONFLICT_ERROR_MSG } from "../../prompts/test-recommendation/recommendationSections.js";
|
|
6
6
|
import { getPersonaPrefix } from "../../prompts/personas.js";
|
|
7
|
+
import { isTestbotEnabled } from "../../utils/featureFlags.js";
|
|
8
|
+
const ADD_ASSERTIONS_DEFAULT = isTestbotEnabled();
|
|
7
9
|
const integrationTestSchema = z
|
|
8
10
|
.object({
|
|
11
|
+
enhanceAssertions: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.default(ADD_ASSERTIONS_DEFAULT)
|
|
14
|
+
.describe("When true, calls skyramp_enhance_assertions after test generation to add richer response-body assertions. Disabled by default. Automatically enabled when running as testbot-feature. Do not override the default value of this parameter, unless the user explicitly asks to enable it."),
|
|
9
15
|
...baseTestSchema,
|
|
10
16
|
chainingKey: z
|
|
11
17
|
.string()
|
|
@@ -41,6 +47,8 @@ export class IntegrationTestService extends TestGenerationService {
|
|
|
41
47
|
const result = await super.generateTest(params);
|
|
42
48
|
if (result.isError)
|
|
43
49
|
return result;
|
|
50
|
+
if (!params.enhanceAssertions)
|
|
51
|
+
return result;
|
|
44
52
|
const content = [...result.content];
|
|
45
53
|
content.push({
|
|
46
54
|
type: "text",
|
|
@@ -50,8 +58,7 @@ export class IntegrationTestService extends TestGenerationService {
|
|
|
50
58
|
}
|
|
51
59
|
buildAssertionEnhancementInstructions() {
|
|
52
60
|
return `
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
### CRITICAL NEXT STEP — Enhance response body assertions after each request
|
|
55
62
|
Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated test file, \`testType: "integration"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.
|
|
56
63
|
`;
|
|
57
64
|
}
|
|
@@ -4,6 +4,7 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
|
4
4
|
import { TestGenerationService, } from "../../services/TestGenerationService.js";
|
|
5
5
|
import { normalizeLanguageParams, resolveParamAliases, } from "../../utils/normalizeParams.js";
|
|
6
6
|
import { getPersonaPrefix } from "../../prompts/personas.js";
|
|
7
|
+
import { isTestbotEnabled } from "../../utils/featureFlags.js";
|
|
7
8
|
const TOOL_NAME = "skyramp_ui_test_generation";
|
|
8
9
|
export class UITestService extends TestGenerationService {
|
|
9
10
|
getTestType() {
|
|
@@ -25,25 +26,39 @@ export class UITestService extends TestGenerationService {
|
|
|
25
26
|
const result = await super.generateTest({ ...params, modularizeCode: false });
|
|
26
27
|
if (result.isError)
|
|
27
28
|
return result;
|
|
29
|
+
if (!params.enhanceAssertions && !params.modularizeCode)
|
|
30
|
+
return result;
|
|
28
31
|
const content = [...result.content];
|
|
29
32
|
content.push({
|
|
30
33
|
type: "text",
|
|
31
|
-
text: this.
|
|
34
|
+
text: this.buildUIPostGenInstructions(!!params.enhanceAssertions, !!params.modularizeCode),
|
|
32
35
|
});
|
|
33
36
|
return { ...result, content };
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
: "";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
buildUIPostGenInstructions(enhanceAssertions, modularize) {
|
|
39
|
+
const steps = [];
|
|
40
|
+
if (enhanceAssertions) {
|
|
41
|
+
steps.push(`### Step ${steps.length + 1} — Enhance UI assertions [REQUIRED]\nCall \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated test file, \`testType: "ui"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.`);
|
|
42
|
+
}
|
|
43
|
+
if (modularize) {
|
|
44
|
+
steps.push(`### Step ${steps.length + 1} — Modularize [REQUIRED]\nCall \`skyramp_modularization\` with \`testFile\` set to the absolute path of the generated test file, \`language\`, \`testType: "ui"\`, and \`isTraceBased: true\`.`);
|
|
45
|
+
}
|
|
46
|
+
const heading = enhanceAssertions && modularize
|
|
47
|
+
? "Enhance UI assertions and modularize the generated test"
|
|
48
|
+
: enhanceAssertions
|
|
49
|
+
? "Enhance UI assertions in the generated test"
|
|
50
|
+
: "Modularize the generated test";
|
|
51
|
+
return `### CRITICAL NEXT STEP — ${heading}\n\n${steps.join("\n\n")}`;
|
|
42
52
|
}
|
|
43
53
|
}
|
|
54
|
+
const ADD_ASSERTIONS_DEFAULT = isTestbotEnabled();
|
|
44
55
|
// Only include the original params in the schema
|
|
45
56
|
const uiTestSchema = {
|
|
46
57
|
...languageSchema.shape,
|
|
58
|
+
enhanceAssertions: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.default(ADD_ASSERTIONS_DEFAULT)
|
|
61
|
+
.describe("When true, calls skyramp_enhance_assertions after test generation to add richer response-body assertions. Disabled by default. Automatically enabled when running as testbot-feature. Do not override the default value of this parameter, unless the user explicitly asks to enable it."),
|
|
47
62
|
playwrightInput: z
|
|
48
63
|
.string()
|
|
49
64
|
.describe("MUST be absolute path to the playwright trace zip file like /path/to/trace.zip. Capture using browser_* tools + skyramp_export_zip, or via manual trace collection with skyramp_start_trace_collection/skyramp_stop_trace_collection."),
|