@skyramp/mcp 0.0.65 → 0.1.0-rc.1

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 (36) hide show
  1. package/build/playwright/traceRecordingPrompt.js +30 -36
  2. package/build/prompts/architectPersona.js +19 -0
  3. package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
  5. package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
  6. package/build/prompts/test-recommendation/test-recommendation-prompt.js +25 -4
  7. package/build/prompts/testbot/testbot-prompts.js +87 -97
  8. package/build/prompts/testbot/testbot-prompts.test.js +142 -0
  9. package/build/services/ScenarioGenerationService.js +2 -2
  10. package/build/services/ScenarioGenerationService.test.js +35 -0
  11. package/build/services/TestExecutionService.js +1 -1
  12. package/build/tools/code-refactor/modularizationTool.js +2 -2
  13. package/build/tools/executeSkyrampTestTool.js +4 -3
  14. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +49 -20
  15. package/build/tools/generate-tests/generateContractRestTool.js +26 -4
  16. package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
  17. package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
  18. package/build/tools/generate-tests/generateUIRestTool.js +69 -4
  19. package/build/tools/submitReportTool.js +17 -12
  20. package/build/tools/test-management/analyzeChangesTool.js +8 -3
  21. package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
  22. package/build/types/TestTypes.js +16 -7
  23. package/build/utils/AnalysisStateManager.js +13 -5
  24. package/build/utils/AnalysisStateManager.test.js +35 -0
  25. package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
  26. package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
  27. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
  28. package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
  29. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
  30. package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
  31. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  32. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
  33. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
  34. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
  35. package/package.json +2 -2
  36. package/node_modules/playwright/lib/mcp/terminal/help.json +0 -32
@@ -0,0 +1,142 @@
1
+ jest.mock("@skyramp/skyramp", () => ({
2
+ WorkspaceConfigManager: jest.fn(),
3
+ }));
4
+ jest.mock("../../services/AnalyticsService.js", () => ({
5
+ AnalyticsService: { pushMCPToolEvent: jest.fn() },
6
+ }));
7
+ import { getTestbotPrompt } from "./testbot-prompts.js";
8
+ // Minimal args to invoke getTestbotPrompt — only services matter for these tests
9
+ const baseArgs = {
10
+ prTitle: "Test PR",
11
+ prDescription: "desc",
12
+ diffFile: ".skyramp_git_diff",
13
+ summaryOutputFile: "/tmp/summary.json",
14
+ repositoryPath: "/repo",
15
+ };
16
+ function callWithServices(services) {
17
+ return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.diffFile, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
18
+ undefined, // maxRecommendations
19
+ undefined, // maxGenerate
20
+ undefined, // maxCritical
21
+ undefined, // prNumber
22
+ undefined, // userPrompt
23
+ services);
24
+ }
25
+ function callWithStateOutputFile(stateOutputFile) {
26
+ return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.diffFile, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
27
+ undefined, // maxRecommendations
28
+ undefined, // maxGenerate
29
+ undefined, // maxCritical
30
+ undefined, // prNumber
31
+ undefined, // userPrompt
32
+ undefined, // services
33
+ stateOutputFile);
34
+ }
35
+ function callFollowUpWithStateOutputFile(stateOutputFile) {
36
+ return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.diffFile, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
37
+ undefined, // maxRecommendations
38
+ undefined, // maxGenerate
39
+ undefined, // maxCritical
40
+ undefined, // prNumber
41
+ "add more tests", // userPrompt — triggers follow-up path
42
+ undefined, // services
43
+ stateOutputFile);
44
+ }
45
+ describe("buildServiceContext (via getTestbotPrompt)", () => {
46
+ it("renders full service with all fields", () => {
47
+ const prompt = callWithServices([
48
+ {
49
+ serviceName: "backend",
50
+ language: "python",
51
+ framework: "pytest",
52
+ testDirectory: "tests/python",
53
+ api: { baseUrl: "http://localhost:8000" },
54
+ },
55
+ ]);
56
+ expect(prompt).toContain('<service name="backend">');
57
+ expect(prompt).toContain("<language>python</language>");
58
+ expect(prompt).toContain("<framework>pytest</framework>");
59
+ expect(prompt).toContain("<base_url>http://localhost:8000</base_url>");
60
+ expect(prompt).toContain("<output_dir>tests/python</output_dir>");
61
+ expect(prompt).toContain("</service>");
62
+ expect(prompt).toContain("<services>");
63
+ expect(prompt).toContain("</services>");
64
+ });
65
+ it("omits optional fields when absent", () => {
66
+ const prompt = callWithServices([{ serviceName: "minimal" }]);
67
+ expect(prompt).toContain('<service name="minimal">');
68
+ expect(prompt).not.toContain("<language>");
69
+ expect(prompt).not.toContain("<framework>");
70
+ expect(prompt).not.toContain("<base_url>");
71
+ expect(prompt).not.toContain("<output_dir>");
72
+ });
73
+ it("renders multiple services", () => {
74
+ const prompt = callWithServices([
75
+ { serviceName: "api", language: "python" },
76
+ { serviceName: "frontend", language: "typescript" },
77
+ ]);
78
+ expect(prompt).toContain('<service name="api">');
79
+ expect(prompt).toContain('<service name="frontend">');
80
+ });
81
+ it("does not render services block when services array is empty", () => {
82
+ const prompt = callWithServices([]);
83
+ expect(prompt).not.toContain("<services>");
84
+ expect(prompt).not.toContain("<service");
85
+ });
86
+ it("does not render services block when services is undefined", () => {
87
+ const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.diffFile, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
88
+ expect(prompt).not.toContain("<services>");
89
+ });
90
+ it("escapes XML special characters in service name", () => {
91
+ const prompt = callWithServices([
92
+ { serviceName: 'my<service>&"name' },
93
+ ]);
94
+ expect(prompt).toContain('<service name="my&lt;service&gt;&amp;&quot;name">');
95
+ expect(prompt).not.toContain('my<service>&"name">');
96
+ });
97
+ it("escapes XML special characters in field values", () => {
98
+ const prompt = callWithServices([
99
+ {
100
+ serviceName: "svc",
101
+ testDirectory: "tests/a&b",
102
+ api: { baseUrl: "http://host?a=1&b=2" },
103
+ },
104
+ ]);
105
+ expect(prompt).toContain("<output_dir>tests/a&amp;b</output_dir>");
106
+ expect(prompt).toContain("<base_url>http://host?a=1&amp;b=2</base_url>");
107
+ });
108
+ it("places services block between REPOSITORY PATH and instruction line", () => {
109
+ const prompt = callWithServices([{ serviceName: "svc" }]);
110
+ const repoIdx = prompt.indexOf("<REPOSITORY PATH>");
111
+ const servicesIdx = prompt.indexOf("<services>");
112
+ const instructionIdx = prompt.indexOf("Use the Skyramp MCP server tools");
113
+ expect(repoIdx).toBeLessThan(servicesIdx);
114
+ expect(servicesIdx).toBeLessThan(instructionIdx);
115
+ });
116
+ it("has no extra blank line when services are absent", () => {
117
+ const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.diffFile, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
118
+ // Should go directly from REPOSITORY PATH closing tag to "Use the Skyramp"
119
+ expect(prompt).toContain("</REPOSITORY PATH>\nUse the Skyramp MCP server tools");
120
+ });
121
+ });
122
+ describe("stateOutputFile in getTestbotPrompt", () => {
123
+ it("includes stateOutputFile in skyramp_analyze_changes call for first-run prompt", () => {
124
+ const stateFile = "/tmp/skyramp/analyze-changes-state.json";
125
+ const prompt = callWithStateOutputFile(stateFile);
126
+ // The prompt must pass stateOutputFile to skyramp_analyze_changes
127
+ expect(prompt).toContain(`\`stateOutputFile\`: "${stateFile}"`);
128
+ });
129
+ it("includes stateOutputFile in skyramp_analyze_changes call for follow-up prompt", () => {
130
+ const stateFile = "/tmp/skyramp/analyze-changes-state.json";
131
+ const prompt = callFollowUpWithStateOutputFile(stateFile);
132
+ expect(prompt).toContain(`\`stateOutputFile\`: "${stateFile}"`);
133
+ });
134
+ it("omits stateOutputFile from skyramp_analyze_changes call when not provided", () => {
135
+ const prompt = callWithStateOutputFile(undefined);
136
+ expect(prompt).not.toContain("stateOutputFile");
137
+ });
138
+ it("omits stateOutputFile from follow-up prompt when not provided", () => {
139
+ const prompt = callFollowUpWithStateOutputFile(undefined);
140
+ expect(prompt).not.toContain("stateOutputFile");
141
+ });
142
+ });
@@ -141,8 +141,8 @@ ${JSON.stringify(traceRequest, null, 2)}
141
141
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
142
142
  for (const [k, v] of Object.entries(parsed)) {
143
143
  queryParams[k] = Array.isArray(v)
144
- ? v.map(String)
145
- : [String(v)];
144
+ ? v.map((item) => typeof item === "object" && item !== null ? JSON.stringify(item) : String(item))
145
+ : [typeof v === "object" && v !== null ? JSON.stringify(v) : String(v)];
146
146
  }
147
147
  }
148
148
  else {
@@ -196,6 +196,41 @@ describe("ScenarioGenerationService — auth header flavors", () => {
196
196
  expect(trace.RequestHeaders["Authorization"]).toBeUndefined();
197
197
  });
198
198
  });
199
+ describe("ScenarioGenerationService — queryParams handling", () => {
200
+ it("serializes a flat primitive object correctly", () => {
201
+ const trace = generateTrace({ queryParams: '{"limit":"10","status":"active"}' });
202
+ expect(trace.QueryParams).toEqual({ limit: ["10"], status: ["active"] });
203
+ });
204
+ it("serializes numeric and boolean primitive values as strings", () => {
205
+ const trace = generateTrace({ queryParams: '{"page":2,"active":true}' });
206
+ expect(trace.QueryParams).toEqual({ page: ["2"], active: ["true"] });
207
+ });
208
+ it("JSON-stringifies nested object values instead of producing [object Object]", () => {
209
+ const trace = generateTrace({ queryParams: '{"filter":{"status":"active","min_price":10}}' });
210
+ expect(trace).not.toBeNull();
211
+ const filterVal = trace.QueryParams["filter"][0];
212
+ expect(filterVal).not.toBe("[object Object]");
213
+ expect(filterVal).toBe('{"status":"active","min_price":10}');
214
+ });
215
+ it("JSON-stringifies nested objects inside an array value", () => {
216
+ const trace = generateTrace({ queryParams: '{"ids":[{"id":1},{"id":2}]}' });
217
+ expect(trace).not.toBeNull();
218
+ expect(trace.QueryParams["ids"]).toEqual(['{"id":1}', '{"id":2}']);
219
+ });
220
+ it("passes through an array of primitive values unchanged", () => {
221
+ const trace = generateTrace({ queryParams: '{"tags":["a","b","c"]}' });
222
+ expect(trace.QueryParams["tags"]).toEqual(["a", "b", "c"]);
223
+ });
224
+ it("produces empty QueryParams when queryParams is omitted", () => {
225
+ const trace = generateTrace({});
226
+ expect(trace.QueryParams).toEqual({});
227
+ });
228
+ it("produces empty QueryParams and does not throw for invalid JSON", () => {
229
+ const trace = generateTrace({ queryParams: "not-valid-json" });
230
+ expect(trace).not.toBeNull();
231
+ expect(trace.QueryParams).toEqual({});
232
+ });
233
+ });
199
234
  describe("ScenarioGenerationService — baseURL parsing", () => {
200
235
  it("parses http baseURL correctly", () => {
201
236
  const trace = generateTrace({
@@ -8,7 +8,7 @@ import { logger } from "../utils/logger.js";
8
8
  import { buildContainerEnv } from "./containerEnv.js";
9
9
  const DEFAULT_TIMEOUT = 300000; // 5 minutes
10
10
  const MAX_CONCURRENT_EXECUTIONS = 5;
11
- export const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.18";
11
+ export const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.19";
12
12
  const DOCKER_PLATFORM = "linux/amd64";
13
13
  const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress updates during execution
14
14
  // Temp file with valid empty JSON — used instead of /dev/null for .json config files
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import fs from "fs";
3
3
  import { logger } from "../../utils/logger.js";
4
- import { TestType } from "../../types/TestTypes.js";
4
+ import { ProgrammingLanguage, TestType } from "../../types/TestTypes.js";
5
5
  import { ModularizationService, } from "../../services/ModularizationService.js";
6
6
  import { AnalyticsService } from "../../services/AnalyticsService.js";
7
7
  import { normalizeLanguageParams, resolveParamAliases, } from "../../utils/normalizeParams.js";
@@ -10,7 +10,7 @@ const modularizationSchema = {
10
10
  .string()
11
11
  .describe("The test file to process with modularization principles applied"),
12
12
  language: z
13
- .string()
13
+ .nativeEnum(ProgrammingLanguage)
14
14
  .optional()
15
15
  .describe("The programming language of the test file. Inferred from file extension if not provided."),
16
16
  testType: z
@@ -3,6 +3,7 @@ import { stripVTControlCharacters } from "util";
3
3
  import { TestExecutionService } from "../services/TestExecutionService.js";
4
4
  import { AnalyticsService } from "../services/AnalyticsService.js";
5
5
  import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
6
+ import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
6
7
  const TOOL_NAME = "skyramp_execute_test";
7
8
  export function registerExecuteSkyrampTestTool(server) {
8
9
  server.registerTool(TOOL_NAME, {
@@ -36,11 +37,11 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
36
37
  .string()
37
38
  .describe("The path to the workspace directory where the test file is located"),
38
39
  language: z
39
- .string()
40
+ .nativeEnum(ProgrammingLanguage)
40
41
  .describe("Programming language of the test file to execute (e.g., python, javascript, typescript, java)"),
41
42
  testType: z
42
- .string()
43
- .describe("Type of the test to execute (e.g., integration, contract, smoke, fuzz, load, e2e, ui). TEST TYPE MUST BE FROM [integration, contract, smoke, fuzz, load, e2e, ui]."),
43
+ .nativeEnum(TestType)
44
+ .describe("Type of the test to execute."),
44
45
  testFile: z
45
46
  .string()
46
47
  .describe("ALWAYS USE ABSOLUTE PATH to the test file to execute"),
@@ -2,25 +2,51 @@ import { z } from "zod";
2
2
  import path from "path";
3
3
  import { ScenarioGenerationService } from "../../services/ScenarioGenerationService.js";
4
4
  import fs from "fs";
5
- import { baseSchema, AUTH_PLACEHOLDER_TOKEN } from "../../types/TestTypes.js";
5
+ import { baseSchema, AUTH_PLACEHOLDER_TOKEN, HttpMethod } from "../../types/TestTypes.js";
6
6
  import { AnalyticsService } from "../../services/AnalyticsService.js";
7
7
  import { getWorkspaceAuthConfig, WorkspaceAuthType } from "../../utils/workspaceAuth.js";
8
8
  import { logger } from "../../utils/logger.js";
9
+ import { getPersonaPrefix } from "../../prompts/architectPersona.js";
10
+ function isJsonValue(v) {
11
+ if (v === undefined || v === null)
12
+ return true;
13
+ try {
14
+ JSON.parse(v);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ function isJsonObject(v) {
22
+ if (v === undefined || v === null)
23
+ return true;
24
+ try {
25
+ const p = JSON.parse(v);
26
+ return typeof p === "object" && !Array.isArray(p) && p !== null;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
9
32
  const stepSchema = z.object({
10
33
  method: z
11
- .string()
12
- .describe("HTTP method (GET, POST, PUT, DELETE, PATCH) for this step"),
34
+ .nativeEnum(HttpMethod)
35
+ .describe("HTTP method for this step."),
13
36
  path: z
14
37
  .string()
15
- .describe("API path for this step. CRITICAL: For requests that reference an ID created by a prior step, use the ACTUAL ID value from the prior step's responseBody, NOT a template variable."),
38
+ .startsWith("/", { message: "path must begin with '/' (e.g. '/api/v1/products/123')" })
39
+ .describe("API path for this step, must start with '/'. CRITICAL: For requests that reference an ID created by a prior step, use the ACTUAL ID value from the prior step's responseBody, NOT a template variable."),
16
40
  requestBody: z
17
41
  .string()
18
42
  .optional()
19
- .describe("JSON string of the request body for POST/PUT/PATCH requests"),
43
+ .refine(isJsonValue, { message: "requestBody must be valid JSON (e.g. '{\"name\":\"product\"}')." })
44
+ .describe("JSON string of the request body for POST/PUT/PATCH requests."),
20
45
  queryParams: z
21
46
  .string()
22
47
  .optional()
23
- .describe("JSON string of URL query parameters for GET search/filter/list requests"),
48
+ .refine(isJsonObject, { message: "queryParams must be a JSON object string (e.g. '{\"limit\":\"10\"}')." })
49
+ .describe("JSON string of URL query parameters as a flat object for GET search/filter/list requests."),
24
50
  responseBody: z
25
51
  .string()
26
52
  .optional()
@@ -61,7 +87,7 @@ const batchScenarioSchema = {
61
87
  authHeader: z
62
88
  .string()
63
89
  .optional()
64
- .describe("Which HTTP header carries the auth credential. Pass empty string or omit for unauthenticated endpoints."),
90
+ .describe("Which HTTP header carries the auth credential (e.g., 'Authorization', 'X-Api-Key'). Omit entirely to auto-resolve from workspace config. Pass empty string only for confirmed unauthenticated endpoints — empty string bypasses workspace auth resolution."),
65
91
  authScheme: z
66
92
  .string()
67
93
  .optional()
@@ -75,24 +101,27 @@ const batchScenarioSchema = {
75
101
  const TOOL_NAME = "skyramp_batch_scenario_test_generation";
76
102
  export function registerBatchScenarioTestTool(server) {
77
103
  server.registerTool(TOOL_NAME, {
78
- description: `Generate a complete multi-step scenario file in a single call.
104
+ description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
105
+ 1. Each step's method+path and confirmation it exists as a real endpoint (from OpenAPI spec, source code routes, or skyramp_analyze_changes output)
106
+ 2. Each step's requestBody or queryParams source — which schema field or prior step response provides these values
107
+ 3. The chaining strategy — which response fields from earlier steps are used as path params or body fields in later steps
108
+ 4. Auth resolution — authHeader/authScheme values and their source (workspace config / user input)
109
+ If any step's endpoint cannot be confirmed without guessing, STOP and ask the user before calling the tool.
79
110
 
80
- This tool generates ALL TraceRequest objects for a multi-step scenario at once, producing
81
- the complete scenario JSON file in one invocation. Use this instead of calling
82
- \`skyramp_scenario_test_generation\` multiple times for multi-step integration tests.
111
+ ---
83
112
 
84
- **When to use:**
85
- - Multi-step integration test scenarios (e.g., create product → create order → update order → verify)
86
- - Any scenario requiring 2+ sequential API requests
113
+ This tool generates the complete scenario JSON file for a multi-step integration test in a single call. Use this instead of calling skyramp_scenario_test_generation multiple times.
114
+
115
+ **Mandatory spec mapping (do this before every call):**
116
+ For each step in the \`steps\` array, confirm the method+path combination exists as a real endpoint (from OpenAPI spec, source code routes, or skyramp_analyze_changes output) before submitting. Do NOT invent paths. Do NOT use template variables — use CONCRETE ID values in paths (e.g. '/api/v1/products/70885', not '/api/v1/products/{id}').
87
117
 
88
- **What it does:**
89
- 1. Accepts an ordered array of steps, each with method, path, requestBody, etc.
90
- 2. Generates a TraceRequest for each step
91
- 3. Writes the complete scenario JSON file with all steps
118
+ **When to use:**
119
+ - Any scenario requiring 2+ sequential API requests (create update → verify, etc.)
120
+ - Single-step scenarios where you need the output scenarioFile path for skyramp_integration_test_generation
92
121
 
93
- **After this tool:** Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\` path.
122
+ **After this tool succeeds:** immediately call \`skyramp_integration_test_generation\` with the \`scenarioFile\` path returned in this tool's output.
94
123
 
95
- **CRITICAL:** Use CONCRETE ID values in paths (e.g., '/api/v1/products/70885'), not template variables.`,
124
+ **Error recovery:** If this tool returns an error for a specific step, the error message will tell you exactly which step failed (step N/total), the method+path, and the reason. Fix only the reported step and resubmit the full \`steps\` array — do NOT split into separate calls.`,
96
125
  inputSchema: batchScenarioSchema,
97
126
  }, async (params) => {
98
127
  if (params.authHeader === undefined) {
@@ -4,6 +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 { ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER } from "../../prompts/test-maintenance/enhanceAssertionSection.js";
7
+ import { getPersonaPrefix } from "../../prompts/architectPersona.js";
7
8
  const contractTestSchema = {
8
9
  ...baseTestSchema,
9
10
  pathParams: z
@@ -273,7 +274,21 @@ The generated consumer contract test contains a stub test function that uses Sky
273
274
  const TOOL_NAME = "skyramp_contract_test_generation";
274
275
  export function registerContractTestTool(server) {
275
276
  server.registerTool(TOOL_NAME, {
276
- description: `Generate a contract test using Skyramp's deterministic test generation platform.
277
+ description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
278
+ 1. The endpoint URL and HTTP method being tested
279
+ 2. Whether the endpoint is a nested resource (URL contains a path parameter like \`{id}\`, \`{flow_id}\`, etc.) — if YES, decide: do I have the request body to provision the parent, or should I use skipProvisionParents?
280
+ 3. Which assertions this test should validate (status code + key response schema fields with non-default values)
281
+ 4. Each required parameter and what value it will take, with source (workspace config / diff / schema / user input)
282
+ NEVER use a hardcoded ID (UUID or integer) as a path parameter value. If a real resource ID is needed and cannot be provisioned, use skipProvisionParents instead.
283
+
284
+ **Dynamic context (use this before generating):**
285
+ If \`skyramp_analyze_changes\` has already run and returned a \`sessionId\`, fetch the endpoint detail before generating:
286
+ \`skyramp://analysis/{sessionId}/endpoints/{path}/{method}\`
287
+ This gives you the exact request body shape, response schema, and auth config for this endpoint. Use it to fill parameters and write accurate assertions — do not infer from source code when this resource is available.
288
+
289
+ ---
290
+
291
+ Generate a contract test using Skyramp's deterministic test generation platform.
277
292
 
278
293
  Contract tests ensure your API implementation matches its OpenAPI/Swagger specification exactly. They validate request/response schemas, status codes, headers, and data types to prevent contract violations and API breaking changes.
279
294
 
@@ -281,6 +296,13 @@ Contract tests ensure your API implementation matches its OpenAPI/Swagger specif
281
296
 
282
297
  **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.**
283
298
 
299
+ **CRITICAL — Nested resource decision tree (follow this every time):**
300
+ Does the endpoint URL contain a path parameter (e.g. \`/flows/{id}\`, \`/work_queues/{id}/stats\`)?
301
+ - **YES, and \`apiSchema\` is provided** → use \`parentRequestData\` to supply the request body that creates the parent resource. The key must be the exact path parameter name (e.g. \`id\`, \`flow_id\`). The backend will provision the parent, extract the real ID, and inject it into the test.
302
+ - **YES, but \`apiSchema\` is NOT available** → set \`skipProvisionParents: true\` (with \`providerMode: true\`). The test will verify the error-path contract (404) rather than the success path.
303
+ - **NO path parameters** → no action needed; proceed normally.
304
+ NEVER substitute a hardcoded UUID or integer for a path parameter. A hardcoded ID will always 404 in a clean environment and produces a useless test.
305
+
284
306
  **Modes:**
285
307
  - Default (no mode set): both \`providerMode\` and \`consumerMode\` default to false. This generates both provider and consumer contract tests — equivalent to setting both modes to true.
286
308
  - \`providerMode\`: set to true ONLY if the user explicitly requests a provider-side contract test. Optionally specify \`providerOutput\` for the output file path.
@@ -288,11 +310,11 @@ Contract tests ensure your API implementation matches its OpenAPI/Swagger specif
288
310
  - Both \`providerMode\` and \`consumerMode\` can be enabled simultaneously to generate both sides.
289
311
 
290
312
  **Chaining (requires \`apiSchema\`):**
291
- - \`parentRequestData\`: map of parent request data for chained test generation. Not allowed with \`consumerMode\` or \`skipProvisionParents\`.
292
- - \`parentStatusCode\`: map of parent response status codes for chained test generation. Not allowed with \`consumerMode\` or \`skipProvisionParents\`.
313
+ - \`parentRequestData\`: map of parent request data for chained test generation. Key = exact path parameter name. Value = JSON string of the request body to create that parent resource. Not allowed with \`consumerMode\` or \`skipProvisionParents\`.
314
+ - \`parentStatusCode\`: expected HTTP status code for each parent provisioning call (e.g. \`{"id": "201"}\`). Not allowed with \`consumerMode\` or \`skipProvisionParents\`.
293
315
 
294
316
  **Provider setup/teardown:**
295
- - \`skipProvisionParents\`: when true, skips generating setup/teardown functions for the provider contract test. Requires \`providerMode\`. Not allowed with \`parentRequestData\` or \`parentStatusCode\`.`,
317
+ - \`skipProvisionParents\`: when true, skips generating setup/teardown functions for the provider contract test. Use this when \`apiSchema\` is unavailable and the endpoint requires a parent resource. Requires \`providerMode\`. Not allowed with \`parentRequestData\` or \`parentStatusCode\`.`,
296
318
  inputSchema: contractTestSchema,
297
319
  }, async (params) => {
298
320
  const service = new ContractTestService();
@@ -3,6 +3,7 @@ import { baseTestSchema, baseTraceSchema, TestType, codeRefactoringSchema, } fro
3
3
  import { TestGenerationService, } from "../../services/TestGenerationService.js";
4
4
  import { AnalyticsService } from "../../services/AnalyticsService.js";
5
5
  import { ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER } from "../../prompts/test-maintenance/enhanceAssertionSection.js";
6
+ import { getPersonaPrefix } from "../../prompts/architectPersona.js";
6
7
  const integrationTestSchema = z
7
8
  .object({
8
9
  ...baseTestSchema,
@@ -15,19 +16,20 @@ const integrationTestSchema = z
15
16
  exclude: baseTraceSchema.shape.exclude.optional(),
16
17
  scenarioFile: z
17
18
  .string()
18
- .describe("Path to the scenario file to be used for test generation. This file is generated by the skyramp_scenario_test_generation tool.")
19
- .optional(),
19
+ .endsWith(".json", { message: "scenarioFile must be a path to a .json file." })
20
+ .optional()
21
+ .describe("Absolute path to the scenario JSON file produced by skyramp_batch_scenario_test_generation. " +
22
+ "When provided, DO NOT also pass apiSchema or endpointURL — the scenario file already contains all endpoint information."),
20
23
  ...codeRefactoringSchema.shape,
21
24
  ...baseTestSchema,
22
25
  output: baseTestSchema.output.describe("Name of the output test file. " +
23
- "If the user does not specify a filename and a scenarioFile is provided, derive the output name from the scenario filename to avoid overwriting other tests. " +
24
- "The backend default 'integration_test.py' is generic and will collide when multiple tests are generated. " +
25
- "Derivation rule: take the scenario filename (no path, no extension), strip the leading 'scenario_' prefix, " +
26
- "replace every hyphen and non-alphanumeric character with an underscore, then append '_integration_test' and the language extension. " +
26
+ "When scenarioFile is provided and user did not specify a name, derive it: " +
27
+ "strip the path and 'scenario_' prefix, replace hyphens/non-alphanum with underscores, append '_integration_test' + language extension. " +
27
28
  "Examples: " +
28
29
  "'scenario_orders-patch-add-items-recalculate.json' → 'orders_patch_add_items_recalculate_integration_test.py' (Python) or 'orders_patch_add_items_recalculate_integration_test.spec.ts' (Playwright). " +
29
30
  "'scenario_products-crud.json' → 'products_crud_integration_test.py'. " +
30
- "Extensions: '.py' for pytest, '.spec.ts'/'.spec.js' for Playwright, '.java' for JUnit."),
31
+ "Extensions: '.py' for pytest, '.spec.ts'/'.spec.js' for Playwright, '.java' for JUnit. " +
32
+ "NEVER use the default 'integration_test.py' when scenarioFile is set — it collides with other generated tests."),
31
33
  endpointURL: baseTestSchema.endpointURL.default(""),
32
34
  })
33
35
  .omit({ method: true }).shape;
@@ -48,7 +50,7 @@ export class IntegrationTestService extends TestGenerationService {
48
50
  }
49
51
  buildAssertionEnhancementInstructions() {
50
52
  return `
51
- ⏭️ **CRITICAL NEXT STEP — Enhance response body assertions after each request:**
53
+ **CRITICAL NEXT STEP — Enhance response body assertions after each request:**
52
54
 
53
55
  The generated integration test contains only basic status-code assertions after each \`send_request\` / \`sendRequest\` call. For every request in the test (especially POST, PUT, and GET), add meaningful assertions on the response body using the rules below.
54
56
 
@@ -72,16 +74,45 @@ ${ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER}
72
74
  const TOOL_NAME = "skyramp_integration_test_generation";
73
75
  export function registerIntegrationTestTool(server) {
74
76
  server.registerTool(TOOL_NAME, {
75
- description: `Generate an integration test using Skyramp's deterministic test generation platform.
77
+ description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
78
+ 1. The endpoint URL(s) and HTTP method(s) involved in this multi-step workflow
79
+ 2. Why an integration test (multi-step workflow validation) is the right choice for this intent
80
+ 3. Which assertions this test should validate at each step (status code + key chained response fields)
81
+ 4. Each required parameter and what value it will take, with source (workspace config / diff / scenario file / user input)
82
+ If any required parameter cannot be determined without guessing, STOP and ask the user before calling the tool.
83
+
84
+ ---
85
+
86
+ Generate an integration test from a scenario file or a live endpoint trace.
87
+
88
+ **Two mutually exclusive modes — choose exactly one:**
89
+ 1. **Scenario mode** (preferred for multi-step flows): pass \`scenarioFile\` (absolute path to the .json file returned by skyramp_batch_scenario_test_generation). Do NOT pass \`apiSchema\` or \`endpointURL\` in this mode. Passing both causes: "scenarioFile is mutually exclusive with apiSchema and endpointURL."
90
+ 2. **Direct mode**: pass \`endpointURL\` and optionally \`apiSchema\`. Do NOT pass \`scenarioFile\`.
76
91
 
77
- Integration tests validate that multiple services, components, or modules work together correctly. They test complex user workflows, service interactions, data flow between systems, and ensure that integrated components function as expected in realistic scenarios.
92
+ **Auth scenario mode only:**
93
+ - If workspace has \`api.authType\` set: omit ALL auth params — workspace config handles the Bearer prefix. Passing auth alongside workspace authType causes: "Auth header and auth type cannot be supported at the same time."
94
+ - If workspace has no \`api.authType\`: pass \`authHeader\` only (no \`authScheme\`, no \`authToken\`).
78
95
 
79
- **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.**
96
+ **Output filename:** When \`scenarioFile\` is provided and user did not specify a name, derive it: strip path and 'scenario_' prefix, replace hyphens/non-alphanum with underscores, append '_integration_test' + language extension. Example: 'scenario_orders-patch.json' 'orders_patch_integration_test.py'. Never use the default 'integration_test.py' when scenarioFile is set it collides.
80
97
 
81
- **CRITICAL - When using scenarioFile or trace parameter:**
82
- If \`scenarioFile\` or \`trace\` parameter is provided, DO NOT pass \`apiSchema\` or \`endpointURL\` parameters. The scenario/trace file already contains all necessary endpoint and schema information. Passing both will cause test generation to fail.`,
98
+ **IMPORTANT:** If \`apiSchema\` is provided in direct mode, pass the path/URL as-is do NOT read the file contents. The backend processes it.`,
83
99
  inputSchema: integrationTestSchema,
84
100
  }, async (params) => {
101
+ if (params.scenarioFile && (params.apiSchema || params.endpointURL)) {
102
+ return {
103
+ content: [{
104
+ type: "text",
105
+ text: "**skyramp_integration_test_generation Error: Conflicting parameters**\n\n" +
106
+ "`scenarioFile` is mutually exclusive with `apiSchema` and `endpointURL`.\n\n" +
107
+ "**Received:** scenarioFile=" + params.scenarioFile +
108
+ (params.apiSchema ? ", apiSchema=" + params.apiSchema : "") +
109
+ (params.endpointURL ? ", endpointURL=" + params.endpointURL : "") + "\n\n" +
110
+ "**How to fix:** Remove `apiSchema` and `endpointURL` when passing `scenarioFile` — " +
111
+ "the scenario file already contains all endpoint and schema information.",
112
+ }],
113
+ isError: true,
114
+ };
115
+ }
85
116
  const service = new IntegrationTestService();
86
117
  const result = await service.generateTest(params);
87
118
  AnalyticsService.pushTestGenerationToolEvent(TOOL_NAME, result, params).catch(() => {
@@ -4,6 +4,7 @@ import { baseSchema, AUTH_PLACEHOLDER_TOKEN } from "../../types/TestTypes.js";
4
4
  import { AnalyticsService } from "../../services/AnalyticsService.js";
5
5
  import { getWorkspaceAuthConfig, WorkspaceAuthType } from "../../utils/workspaceAuth.js";
6
6
  import { logger } from "../../utils/logger.js";
7
+ import { getPersonaPrefix } from "../../prompts/architectPersona.js";
7
8
  const scenarioTestSchema = {
8
9
  scenarioName: z
9
10
  .string()
@@ -54,7 +55,7 @@ const scenarioTestSchema = {
54
55
  .string()
55
56
  .optional()
56
57
  .default("")
57
- .describe("Which HTTP header carries the auth credential. Examples: 'Authorization' (Bearer/Token auth), 'X-Api-Key' (API key auth), 'Cookie' (session auth). Pass empty string to skip auth for unauthenticated endpoints."),
58
+ .describe("Which HTTP header carries the auth credential (e.g., 'Authorization', 'X-Api-Key', 'Cookie'). Omit or pass empty string to auto-resolve from workspace config. To force an unauthenticated request, omit AND ensure no workspace auth is configured."),
58
59
  authScheme: z
59
60
  .string()
60
61
  .optional()
@@ -78,50 +79,27 @@ const scenarioTestSchema = {
78
79
  const TOOL_NAME = "skyramp_scenario_test_generation";
79
80
  export function registerScenarioTestTool(server) {
80
81
  server.registerTool(TOOL_NAME, {
81
- description: `Generate a single trace request from AI-parsed scenario parameters.
82
+ description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
83
+ 1. The specific API endpoint (method + concrete path with real IDs, not templates)
84
+ 2. The request body fields and their values, with source (schema / prior step response / user input)
85
+ 3. The expected response status code and key response fields to chain into subsequent steps
86
+ 4. Whether this step depends on a prior step's response ID — if so, confirm the ID value is known
87
+ If a required path parameter or request body field cannot be determined without guessing, STOP and ask the user before calling the tool.
82
88
 
83
- This tool generates a single TraceRequest object using parameters that have been parsed by AI from a natural language scenario. The AI should analyze the scenario and provide structured parameters instead of relying on hardcoded parsing logic.
89
+ ---
84
90
 
85
- **What it does:**
86
- 1. **Accept AI-Parsed Data**: Takes structured parameters parsed by AI from natural language
87
- 2. **Generate Trace Request**: Creates a single TraceRequest object with proper format
88
- 3. **File Management**: Appends the request to an existing trace file or creates a new one
89
- 4. **Dynamic Source**: IF DNS NAME IS PROVIDED, USE IT FOR SOURCE IP AND PORT
91
+ **Dynamic context (use this before generating):**
92
+ If \`skyramp_analyze_changes\` has already run and returned a \`sessionId\`, fetch the endpoint detail before building this step:
93
+ \`skyramp://analysis/{sessionId}/endpoints/{path}/{method}\`
94
+ This gives you the exact request body fields, types, and required vs optional distinction — use it to construct accurate request bodies instead of guessing from field names.
90
95
 
91
- **Output:**
92
- Returns a single TraceRequest object with:
93
- - Dynamic source IP and port
94
- - Destination host (extracted from API schema)
95
- - HTTP method and path (provided by AI)
96
- - Request and response bodies (provided by AI or generated)
97
- - Request and response headers
98
- - Status code and timestamp
99
- - Network details (port, scheme)
96
+ ---
100
97
 
101
- **AI Responsibilities:**
102
- The AI should parse the natural language scenario and provide:
103
- - HTTP method (POST, GET, PUT, DELETE)
104
- - API path with CONCRETE ID values, not templates (e.g., /api/v1/products/70885, NOT /api/v1/products/{product_id})
105
- - Request body (JSON string) for POST/PUT/PATCH requests
106
- - Query parameters (JSON string) for GET search/filter/list requests — NEVER put query params in requestBody
107
- - Response body (JSON string, if applicable)
108
- - Status code (optional, defaults based on method)
109
- - Entity details (name, price, quantity, ID as needed)
98
+ Generate a single-step scenario trace request. For multi-step scenarios, prefer \`skyramp_batch_scenario_test_generation\` which generates all steps in one call.
110
99
 
111
- **Requirements:**
112
- - Natural language scenario description
113
- - API schema (OpenAPI/Swagger file or URL) for destination extraction
114
- - AI-parsed HTTP method and path (required)
115
- - AI-parsed request/response bodies (optional)
100
+ **Path must use CONCRETE ID values** (e.g. '/api/v1/products/70885', not '/api/v1/products/{id}'). Use \`queryParams\` for GET filters/search — never \`requestBody\`.
116
101
 
117
- **Note:** This tool generates one request at a time. Call multiple times for multi-step scenarios.
118
-
119
- **CRITICAL - Integration Test Generation After Scenario Creation:**
120
- When generating an integration test using the scenario file created by this tool:
121
- 1. Pass the scenario file path to the \`scenarioFile\` parameter
122
- 2. DO NOT pass \`apiSchema\` or \`endpointURL\` parameters - the scenario file already contains all necessary endpoint and schema information
123
- 3. Provide: \`language\`, \`framework\`, \`outputDir\`, \`prompt\`, and \`scenarioFile\`. Auth parameters are automatically extracted from the scenario trace; only pass \`authHeader\`/\`authScheme\` if you need to override the trace values.
124
- Passing both scenarioFile and apiSchema/endpointURL will cause the test generation to fail.`,
102
+ **After this tool:** call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\` path. Do NOT also pass \`apiSchema\` or \`endpointURL\` — the scenario file contains all endpoint information.`,
125
103
  inputSchema: scenarioTestSchema,
126
104
  }, async (params) => {
127
105
  if (!params.authHeader) {