@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.
Files changed (33) hide show
  1. package/build/index.js +6 -5
  2. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -7
  3. package/build/prompts/personas.js +2 -1
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -1
  5. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +28 -0
  6. package/build/prompts/test-maintenance/driftAnalysisSections.js +2 -2
  7. package/build/prompts/test-recommendation/analysisOutputPrompt.js +74 -16
  8. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -0
  9. package/build/prompts/test-recommendation/recommendationSections.js +13 -43
  10. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +19 -0
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.js +158 -70
  12. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +24 -117
  13. package/build/prompts/testbot/testbot-prompts.js +12 -18
  14. package/build/prompts/testbot/testbot-prompts.test.js +2 -2
  15. package/build/resources/analysisResources.js +1 -0
  16. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -1
  17. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +127 -4
  18. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +205 -18
  19. package/build/tools/generate-tests/generateContractRestTool.js +19 -19
  20. package/build/tools/generate-tests/generateIntegrationRestTool.js +9 -2
  21. package/build/tools/generate-tests/generateUIRestTool.js +23 -8
  22. package/build/tools/test-management/analyzeChangesTool.js +222 -11
  23. package/build/tools/test-management/analyzeChangesTool.test.js +233 -1
  24. package/build/types/TestRecommendation.js +0 -2
  25. package/build/utils/featureFlags.js +4 -22
  26. package/build/utils/featureFlags.test.js +81 -0
  27. package/build/utils/httpDefaults.js +6 -1
  28. package/build/utils/httpDefaults.test.js +21 -0
  29. package/build/utils/scenarioDrafting.js +511 -100
  30. package/build/utils/scenarioDrafting.test.js +545 -259
  31. package/build/utils/telemetry.js +2 -1
  32. package/build/utils/utils.js +23 -0
  33. package/package.json +1 -1
@@ -1,15 +1,40 @@
1
- jest.mock("../../services/ScenarioGenerationService.js", () => ({ ScenarioGenerationService: jest.fn() }));
2
- jest.mock("../../services/AnalyticsService.js", () => ({ AnalyticsService: { pushMCPToolEvent: jest.fn() } }));
3
- jest.mock("../../utils/workspaceAuth.js", () => ({ getWorkspaceAuthConfig: jest.fn(), WorkspaceAuthType: { None: "none" } }));
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 { stepSchema } from "./generateBatchScenarioRestTool.js";
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); // null is not an empty object — not rejected
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
- ⏭️ **CRITICAL NEXT STEP — Replace placeholder sample data:**
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
- return `
231
- ⏭️ **CRITICAL NEXT STEP Replace placeholder data and enhance assertions:**
232
-
233
- ⏭️ **CRITICAL — Part 1 — Replace placeholder values in the generated file:**
234
-
235
- ${this.buildPlaceholderReplacementBlock(params, "continue to Part 2")}
236
-
237
- ---
238
-
239
- ⏭️ **CRITICAL Part 2 Enhance response body assertions:**
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
- **CRITICAL NEXT STEP — Enhance response body assertions after each request:**
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.buildUIAssertionInstructions(params.modularizeCode !== false),
34
+ text: this.buildUIPostGenInstructions(!!params.enhanceAssertions, !!params.modularizeCode),
32
35
  });
33
36
  return { ...result, content };
34
37
  }
35
- buildUIAssertionInstructions(modularize = true) {
36
- const modularizeStep = modularize
37
- ? `\n⏭️ **Step 2 — Modularize [REQUIRED]**: Call \`skyramp_modularization\` with \`testFile\` set to the absolute path of the generated test file, \`language\`, \`testType: "ui"\`, and \`isTraceBased: true\`.`
38
- : "";
39
- return `**CRITICAL NEXT STEP — Enhance UI assertions in the generated test:**
40
-
41
- Call \`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.${modularizeStep}`;
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."),