@skyramp/mcp 0.0.64-rc.2 → 0.0.64-rc.4
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/prompts/testbot/testbot-prompts.js +11 -2
- package/build/services/TestExecutionService.js +1 -0
- package/build/services/TestGenerationService.js +6 -0
- package/build/services/TestGenerationService.test.js +11 -7
- package/build/tools/executeSkyrampTestTool.js +26 -22
- package/build/tools/test-management/executeTestsTool.js +33 -24
- package/build/utils/workspaceAuth.js +7 -2
- package/build/utils/workspaceAuth.test.js +52 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +1 -1
- package/node_modules/playwright/package.json +7 -4
- package/package.json +2 -2
|
@@ -151,10 +151,19 @@ If a test generation tool call fails:
|
|
|
151
151
|
4. You MUST generate **at least 1 test** for any PR that touches application code. Zero generated tests is NOT acceptable.
|
|
152
152
|
5. Log skipped candidates in \`issuesFound\` with the error message.
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
### UI Test Execution Fix-up
|
|
155
|
+
If a generated UI test fails with a timeout waiting for an element after navigation (e.g. \`TimeoutError\` on \`getByTestId\` or \`locator\`), add a hydration wait after each \`page.goto()\` call:
|
|
156
|
+
\`\`\`
|
|
157
|
+
// Wait for React/framework hydration to complete
|
|
158
|
+
await page.waitForTimeout(1500);
|
|
159
|
+
\`\`\`
|
|
160
|
+
Then re-run the test. This is a common issue with SSR/SPA frameworks where the DOM is rendered but not yet interactive.
|
|
161
|
+
|
|
162
|
+
**After generation, fix chaining and enhance assertions only:**
|
|
155
163
|
- Path params like \`id = 'id'\` → \`skyramp.get_response_value(prev_response, "id")\`
|
|
156
164
|
- Hardcoded IDs in request bodies → dynamic values from prior response
|
|
157
|
-
-
|
|
165
|
+
- **Integration tests and contract provider tests**: after the test generation, you MUST enhance response body assertions as instructed in the tool output.
|
|
166
|
+
- Change ONLY chaining values and enhance assertions. Preserve everything else exactly as generated.
|
|
158
167
|
|
|
159
168
|
After all actions, execute ONLY the test files you created (ADD), regenerated (REGENERATE),
|
|
160
169
|
or edited (UPDATE). Do NOT execute VERIFY'd tests — they are unaffected by the diff and do not
|
|
@@ -335,6 +335,7 @@ export class TestExecutionService {
|
|
|
335
335
|
// Prepare host config with mounts
|
|
336
336
|
const hostConfig = {
|
|
337
337
|
ExtraHosts: ["host.docker.internal:host-gateway"],
|
|
338
|
+
...(options.useHostNetwork && process.platform === "linux" ? { NetworkMode: "host" } : {}),
|
|
338
339
|
Mounts: [
|
|
339
340
|
{
|
|
340
341
|
Type: "bind",
|
|
@@ -226,6 +226,12 @@ The generated test file remains unchanged and ready to use as-is.
|
|
|
226
226
|
// Map authScheme → authType for the npm client which uses authType internally
|
|
227
227
|
if (generateOptions.authScheme !== undefined) {
|
|
228
228
|
generateOptions.authType = generateOptions.authScheme;
|
|
229
|
+
// authType handles the Authorization header implicitly — the Go CLI treats
|
|
230
|
+
// --auth-type and --auth-header as mutually exclusive, so clear authHeader
|
|
231
|
+
// when it's the standard Authorization header to avoid the conflict.
|
|
232
|
+
if (/^authorization$/i.test(generateOptions.authHeader || '')) {
|
|
233
|
+
delete generateOptions.authHeader;
|
|
234
|
+
}
|
|
229
235
|
}
|
|
230
236
|
delete generateOptions.authScheme;
|
|
231
237
|
const result = await this.client.generateRestTest(generateOptions);
|
|
@@ -107,7 +107,8 @@ describe("TestGenerationService — authType/authScheme not passed to library",
|
|
|
107
107
|
const callArgs = mockGenerateRestTest.mock.calls[0][0];
|
|
108
108
|
expect(callArgs.authType).toBe("Token");
|
|
109
109
|
expect(callArgs.authScheme).toBeUndefined();
|
|
110
|
-
|
|
110
|
+
// authHeader is cleared for Authorization when authType is set (CLI conflict fix)
|
|
111
|
+
expect(callArgs.authHeader).toBeUndefined();
|
|
111
112
|
});
|
|
112
113
|
it("passes authHeader through to library without modification", async () => {
|
|
113
114
|
const svc = new StubService();
|
|
@@ -237,7 +238,7 @@ describe("TestGenerationService — trace-based auth in executeGeneration", () =
|
|
|
237
238
|
fs.writeFileSync(filePath, JSON.stringify(requests), "utf8");
|
|
238
239
|
return filePath;
|
|
239
240
|
}
|
|
240
|
-
it("populates
|
|
241
|
+
it("populates authScheme from trace when not provided (clears Authorization authHeader)", async () => {
|
|
241
242
|
const traceFile = writeTrace([
|
|
242
243
|
{
|
|
243
244
|
RequestHeaders: {
|
|
@@ -258,7 +259,8 @@ describe("TestGenerationService — trace-based auth in executeGeneration", () =
|
|
|
258
259
|
svc.handleApiAnalysis = async () => null;
|
|
259
260
|
await svc.generateTest({ ...BASE });
|
|
260
261
|
const callArgs = mockGenerateRestTest.mock.calls[0][0];
|
|
261
|
-
|
|
262
|
+
// authHeader is cleared for Authorization when authType is set (CLI conflict fix)
|
|
263
|
+
expect(callArgs.authHeader).toBeUndefined();
|
|
262
264
|
expect(callArgs.authType).toBe("Token");
|
|
263
265
|
expect(callArgs.authScheme).toBeUndefined();
|
|
264
266
|
});
|
|
@@ -287,7 +289,7 @@ describe("TestGenerationService — trace-based auth in executeGeneration", () =
|
|
|
287
289
|
expect(callArgs.authType).toBe("");
|
|
288
290
|
expect(callArgs.authScheme).toBeUndefined();
|
|
289
291
|
});
|
|
290
|
-
it("overrides conflicting authScheme with trace value", async () => {
|
|
292
|
+
it("overrides conflicting authScheme with trace value and clears Authorization authHeader", async () => {
|
|
291
293
|
const traceFile = writeTrace([
|
|
292
294
|
{
|
|
293
295
|
RequestHeaders: {
|
|
@@ -308,11 +310,12 @@ describe("TestGenerationService — trace-based auth in executeGeneration", () =
|
|
|
308
310
|
svc.handleApiAnalysis = async () => null;
|
|
309
311
|
await svc.generateTest({ ...BASE });
|
|
310
312
|
const callArgs = mockGenerateRestTest.mock.calls[0][0];
|
|
311
|
-
|
|
313
|
+
// authHeader is cleared for Authorization when authType is set (CLI conflict fix)
|
|
314
|
+
expect(callArgs.authHeader).toBeUndefined();
|
|
312
315
|
expect(callArgs.authType).toBe("Token");
|
|
313
316
|
expect(callArgs.authScheme).toBeUndefined();
|
|
314
317
|
});
|
|
315
|
-
it("does not override auth when trace has no auth header", async () => {
|
|
318
|
+
it("does not override auth when trace has no auth header (clears Authorization authHeader)", async () => {
|
|
316
319
|
const traceFile = writeTrace([
|
|
317
320
|
{
|
|
318
321
|
RequestHeaders: {
|
|
@@ -332,7 +335,8 @@ describe("TestGenerationService — trace-based auth in executeGeneration", () =
|
|
|
332
335
|
svc.handleApiAnalysis = async () => null;
|
|
333
336
|
await svc.generateTest({ ...BASE });
|
|
334
337
|
const callArgs = mockGenerateRestTest.mock.calls[0][0];
|
|
335
|
-
|
|
338
|
+
// authHeader is cleared for Authorization when authType is set (CLI conflict fix)
|
|
339
|
+
expect(callArgs.authHeader).toBeUndefined();
|
|
336
340
|
expect(callArgs.authType).toBe("Bearer");
|
|
337
341
|
expect(callArgs.authScheme).toBeUndefined();
|
|
338
342
|
});
|
|
@@ -79,31 +79,34 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
79
79
|
};
|
|
80
80
|
const previousBaseUrl = process.env.SKYRAMP_TEST_BASE_URL;
|
|
81
81
|
let didSetSkyrampBaseUrl = false;
|
|
82
|
+
let useHostNetwork = false;
|
|
82
83
|
try {
|
|
83
84
|
// Send initial progress
|
|
84
85
|
await sendProgress(0, 100, "Starting test execution...");
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
// Always resolve workspace config for dockerNetwork (host networking)
|
|
87
|
+
// and optionally inject SKYRAMP_TEST_BASE_URL if not already set.
|
|
88
|
+
if (params.workspacePath) {
|
|
89
|
+
const { baseUrl, dockerNetwork, candidates } = await getWorkspaceBaseUrl(params.workspacePath, params.testFile, params.language);
|
|
90
|
+
useHostNetwork = !!dockerNetwork;
|
|
91
|
+
if (!process.env.SKYRAMP_TEST_BASE_URL) {
|
|
92
|
+
if (baseUrl) {
|
|
93
|
+
process.env.SKYRAMP_TEST_BASE_URL = baseUrl;
|
|
94
|
+
didSetSkyrampBaseUrl = true;
|
|
95
|
+
}
|
|
96
|
+
else if (candidates.length > 0) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: [
|
|
101
|
+
`Cannot determine SKYRAMP_TEST_BASE_URL — test file matches multiple services:`,
|
|
102
|
+
...candidates.map((c) => ` • ${c.serviceName}: ${c.baseUrl}`),
|
|
103
|
+
``,
|
|
104
|
+
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
|
|
105
|
+
].join("\n"),
|
|
106
|
+
}],
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
const executionService = new TestExecutionService();
|
|
@@ -115,6 +118,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
115
118
|
testType: params.testType,
|
|
116
119
|
token: params.token,
|
|
117
120
|
playwrightSaveStoragePath: params.playwrightSaveStoragePath,
|
|
121
|
+
useHostNetwork,
|
|
118
122
|
}, onExecutionProgress);
|
|
119
123
|
// Progress is already reported by TestExecutionService
|
|
120
124
|
// Only report final status if not already at 100%
|
|
@@ -128,38 +128,47 @@ back into the state file for use by \`skyramp_actions\`.
|
|
|
128
128
|
// candidates so the LLM can resolve and re-invoke.
|
|
129
129
|
const previousTestBaseUrl = process.env.SKYRAMP_TEST_BASE_URL;
|
|
130
130
|
let didSetTestBaseUrl = false;
|
|
131
|
-
|
|
131
|
+
let useHostNetwork = false;
|
|
132
|
+
// Always resolve workspace config for dockerNetwork (host networking)
|
|
133
|
+
// and optionally inject SKYRAMP_TEST_BASE_URL if not already set.
|
|
134
|
+
{
|
|
132
135
|
const results = await Promise.all(testOptions.map((t) => getWorkspaceBaseUrl(absoluteWorkspacePath, t.testFile, t.language)));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
// Enable host networking if any matched service has dockerNetwork
|
|
137
|
+
useHostNetwork = results.some((r) => !!r.dockerNetwork);
|
|
138
|
+
if (!previousTestBaseUrl) {
|
|
139
|
+
const ambiguous = results.filter((r) => r.candidates.length > 0);
|
|
140
|
+
if (ambiguous.length > 0) {
|
|
141
|
+
const lines = ambiguous.flatMap((r) => r.candidates.map((c) => ` • ${c.serviceName}: ${c.baseUrl}`));
|
|
142
|
+
return {
|
|
143
|
+
content: [{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: [
|
|
146
|
+
`Cannot determine SKYRAMP_TEST_BASE_URL — one or more test files match multiple services:`,
|
|
147
|
+
...lines,
|
|
148
|
+
``,
|
|
149
|
+
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
|
|
150
|
+
].join("\n"),
|
|
151
|
+
}],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const allResolved = results.every((r) => r.baseUrl);
|
|
156
|
+
if (allResolved) {
|
|
157
|
+
const uniqueUrls = [...new Set(results.map((r) => r.baseUrl))];
|
|
158
|
+
if (uniqueUrls.length === 1) {
|
|
159
|
+
process.env.SKYRAMP_TEST_BASE_URL = uniqueUrls[0];
|
|
160
|
+
didSetTestBaseUrl = true;
|
|
161
|
+
}
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
}
|
|
165
|
+
// Pass useHostNetwork to each test option
|
|
166
|
+
const enrichedTestOptions = testOptions.map((t) => ({ ...t, useHostNetwork }));
|
|
158
167
|
let executionResult;
|
|
159
168
|
try {
|
|
160
169
|
logger.info(`Executing ${testOptions.length} tests in parallel batches (max 5 concurrent)`);
|
|
161
170
|
const executionService = new TestExecutionService();
|
|
162
|
-
executionResult = await executionService.executeBatch(
|
|
171
|
+
executionResult = await executionService.executeBatch(enrichedTestOptions);
|
|
163
172
|
logger.info(`Batch execution complete: ${executionResult.passed} passed, ` +
|
|
164
173
|
`${executionResult.failed} failed, ${executionResult.crashed} crashed`);
|
|
165
174
|
}
|
|
@@ -29,7 +29,7 @@ export async function getWorkspaceAuthHeader(repositoryPath) {
|
|
|
29
29
|
* → { baseUrl: "http://localhost:8000", candidates: [] }
|
|
30
30
|
*/
|
|
31
31
|
export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
|
|
32
|
-
const noMatch = { baseUrl: undefined, candidates: [] };
|
|
32
|
+
const noMatch = { baseUrl: undefined, dockerNetwork: undefined, candidates: [] };
|
|
33
33
|
if (!testFile)
|
|
34
34
|
return noMatch;
|
|
35
35
|
try {
|
|
@@ -57,12 +57,17 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
|
|
|
57
57
|
}
|
|
58
58
|
if (matches.length === 1) {
|
|
59
59
|
const parsed = new URL(matches[0].api.baseUrl);
|
|
60
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
baseUrl: `${parsed.protocol}//${parsed.host}`,
|
|
62
|
+
dockerNetwork: matches[0].runtimeDetails?.dockerNetwork,
|
|
63
|
+
candidates: [],
|
|
64
|
+
};
|
|
61
65
|
}
|
|
62
66
|
// Still ambiguous — return candidates for the LLM to resolve
|
|
63
67
|
if (matches.length > 1) {
|
|
64
68
|
return {
|
|
65
69
|
baseUrl: undefined,
|
|
70
|
+
dockerNetwork: undefined,
|
|
66
71
|
candidates: matches.map((s) => ({
|
|
67
72
|
serviceName: s.serviceName ?? s.testDirectory,
|
|
68
73
|
baseUrl: s.api.baseUrl,
|
|
@@ -146,6 +146,58 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
146
146
|
expect(result.baseUrl).toBe("http://localhost:9000");
|
|
147
147
|
expect(result.candidates).toHaveLength(0);
|
|
148
148
|
});
|
|
149
|
+
it("should return dockerNetwork when matched service has runtimeDetails.dockerNetwork", async () => {
|
|
150
|
+
const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
|
|
151
|
+
WorkspaceConfigManager.mockImplementationOnce(() => ({
|
|
152
|
+
exists: jest.fn().mockResolvedValue(true),
|
|
153
|
+
read: jest.fn().mockResolvedValue({
|
|
154
|
+
services: [
|
|
155
|
+
{
|
|
156
|
+
serviceName: "backend",
|
|
157
|
+
testDirectory: "tests",
|
|
158
|
+
api: { baseUrl: "http://localhost:8000" },
|
|
159
|
+
runtimeDetails: { dockerNetwork: "myapp_default" },
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/tests/test_api.py`);
|
|
165
|
+
expect(result.baseUrl).toBe("http://localhost:8000");
|
|
166
|
+
expect(result.dockerNetwork).toBe("myapp_default");
|
|
167
|
+
});
|
|
168
|
+
it("should return undefined dockerNetwork when service has no runtimeDetails", async () => {
|
|
169
|
+
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/backend/tests/test_api.py`);
|
|
170
|
+
expect(result.baseUrl).toBe("http://localhost:8000");
|
|
171
|
+
expect(result.dockerNetwork).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
it("should return undefined dockerNetwork when ambiguous", async () => {
|
|
174
|
+
const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
|
|
175
|
+
WorkspaceConfigManager.mockImplementationOnce(() => ({
|
|
176
|
+
exists: jest.fn().mockResolvedValue(true),
|
|
177
|
+
read: jest.fn().mockResolvedValue({
|
|
178
|
+
services: [
|
|
179
|
+
{
|
|
180
|
+
serviceName: "svc-a",
|
|
181
|
+
language: "python",
|
|
182
|
+
testDirectory: "shared/tests",
|
|
183
|
+
api: { baseUrl: "http://localhost:8000" },
|
|
184
|
+
runtimeDetails: { dockerNetwork: "net-a" },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
serviceName: "svc-b",
|
|
188
|
+
language: "python",
|
|
189
|
+
testDirectory: "shared/tests",
|
|
190
|
+
api: { baseUrl: "http://localhost:9000" },
|
|
191
|
+
runtimeDetails: { dockerNetwork: "net-b" },
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
}),
|
|
195
|
+
}));
|
|
196
|
+
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/shared/tests/test_api.py`, "python");
|
|
197
|
+
expect(result.baseUrl).toBeUndefined();
|
|
198
|
+
expect(result.dockerNetwork).toBeUndefined();
|
|
199
|
+
expect(result.candidates).toHaveLength(2);
|
|
200
|
+
});
|
|
149
201
|
it("should return no match when matched service baseUrl is not a valid URL", async () => {
|
|
150
202
|
const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
|
|
151
203
|
WorkspaceConfigManager.mockImplementationOnce(() => ({
|
|
@@ -146,7 +146,7 @@ class TraceRecordingBackend {
|
|
|
146
146
|
this._maybeTrackAction("browser_select_option", args, result);
|
|
147
147
|
return result;
|
|
148
148
|
}
|
|
149
|
-
const shouldFallback = resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout");
|
|
149
|
+
const shouldFallback = resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout") || resultText.includes("not found in the current page snapshot");
|
|
150
150
|
if (!shouldFallback)
|
|
151
151
|
return result;
|
|
152
152
|
traceDebug("selectOption failed on custom dropdown, trying combobox fallback");
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "playwright",
|
|
2
|
+
"name": "@skyramp/playwright",
|
|
3
3
|
"version": "1.58.2-skyramp.8.9.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Skyramp's fork of Playwright with trace recording for UI test generation",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
5
8
|
"repository": {
|
|
6
9
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/letsramp/playwright.git"
|
|
8
11
|
},
|
|
9
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://www.skyramp.dev",
|
|
10
13
|
"engines": {
|
|
11
14
|
"node": ">=18"
|
|
12
15
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.0.64-rc.
|
|
3
|
+
"version": "0.0.64-rc.4",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@skyramp/skyramp": "1.3.16",
|
|
53
53
|
"dockerode": "^4.0.6",
|
|
54
54
|
"fast-glob": "^3.3.3",
|
|
55
|
-
"playwright": "file:vendor/playwright-1.58.2-skyramp.8.9.0.tgz",
|
|
55
|
+
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.9.0.tgz",
|
|
56
56
|
"simple-git": "^3.30.0",
|
|
57
57
|
"zod": "^3.25.3"
|
|
58
58
|
},
|