@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.
- package/build/playwright/traceRecordingPrompt.js +30 -36
- package/build/prompts/architectPersona.js +19 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +25 -4
- package/build/prompts/testbot/testbot-prompts.js +87 -97
- package/build/prompts/testbot/testbot-prompts.test.js +142 -0
- package/build/services/ScenarioGenerationService.js +2 -2
- package/build/services/ScenarioGenerationService.test.js +35 -0
- package/build/services/TestExecutionService.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +2 -2
- package/build/tools/executeSkyrampTestTool.js +4 -3
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +49 -20
- package/build/tools/generate-tests/generateContractRestTool.js +26 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
- package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
- package/build/tools/generate-tests/generateUIRestTool.js +69 -4
- package/build/tools/submitReportTool.js +17 -12
- package/build/tools/test-management/analyzeChangesTool.js +8 -3
- package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
- package/build/types/TestTypes.js +16 -7
- package/build/utils/AnalysisStateManager.js +13 -5
- package/build/utils/AnalysisStateManager.test.js +35 -0
- package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
- package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
- package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
- package/package.json +2 -2
- 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<service>&"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&b</output_dir>");
|
|
106
|
+
expect(prompt).toContain("<base_url>http://host?a=1&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.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
43
|
-
.describe("Type of the test to execute
|
|
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
|
-
.
|
|
12
|
-
.describe("HTTP method
|
|
34
|
+
.nativeEnum(HttpMethod)
|
|
35
|
+
.describe("HTTP method for this step."),
|
|
13
36
|
path: z
|
|
14
37
|
.string()
|
|
15
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
**
|
|
89
|
-
|
|
90
|
-
|
|
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:**
|
|
122
|
+
**After this tool succeeds:** immediately call \`skyramp_integration_test_generation\` with the \`scenarioFile\` path returned in this tool's output.
|
|
94
123
|
|
|
95
|
-
**
|
|
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:
|
|
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\`:
|
|
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
|
-
.
|
|
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
|
-
"
|
|
24
|
-
"
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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.
|
|
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:
|
|
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
|
-
|
|
89
|
+
---
|
|
84
90
|
|
|
85
|
-
**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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) {
|