@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.
@@ -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
- **After generation, fix chaining only:**
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
- - Change ONLY chaining values. Preserve everything else exactly as generated.
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
- expect(callArgs.authHeader).toBe("Authorization");
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 authHeader and authScheme from trace when not provided", async () => {
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
- expect(callArgs.authHeader).toBe("Authorization");
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
- expect(callArgs.authHeader).toBe("Authorization");
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
- expect(callArgs.authHeader).toBe("Authorization");
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
- // Inject SKYRAMP_TEST_BASE_URL from workspace if not already set in env.
86
- // Match by testFile path so the correct service URL is used when the
87
- // workspace has multiple services with different baseUrls.
88
- if (!process.env.SKYRAMP_TEST_BASE_URL && params.workspacePath) {
89
- const { baseUrl, candidates } = await getWorkspaceBaseUrl(params.workspacePath, params.testFile, params.language);
90
- if (baseUrl) {
91
- process.env.SKYRAMP_TEST_BASE_URL = baseUrl;
92
- didSetSkyrampBaseUrl = true;
93
- }
94
- else if (candidates.length > 0) {
95
- return {
96
- content: [{
97
- type: "text",
98
- text: [
99
- `Cannot determine SKYRAMP_TEST_BASE_URL — test file matches multiple services:`,
100
- ...candidates.map((c) => ` • ${c.serviceName}: ${c.baseUrl}`),
101
- ``,
102
- `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
103
- ].join("\n"),
104
- }],
105
- isError: true,
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
- if (!previousTestBaseUrl) {
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
- const ambiguous = results.filter((r) => r.candidates.length > 0);
134
- if (ambiguous.length > 0) {
135
- const lines = ambiguous.flatMap((r) => r.candidates.map((c) => ` • ${c.serviceName}: ${c.baseUrl}`));
136
- return {
137
- content: [{
138
- type: "text",
139
- text: [
140
- `Cannot determine SKYRAMP_TEST_BASE_URL — one or more test files match multiple services:`,
141
- ...lines,
142
- ``,
143
- `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
144
- ].join("\n"),
145
- }],
146
- isError: true,
147
- };
148
- }
149
- const allResolved = results.every((r) => r.baseUrl);
150
- if (allResolved) {
151
- const uniqueUrls = [...new Set(results.map((r) => r.baseUrl))];
152
- if (uniqueUrls.length === 1) {
153
- process.env.SKYRAMP_TEST_BASE_URL = uniqueUrls[0];
154
- didSetTestBaseUrl = true;
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(testOptions);
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 { baseUrl: `${parsed.protocol}//${parsed.host}`, candidates: [] };
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": "A high-level API to automate web browsers",
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/microsoft/playwright.git"
10
+ "url": "git+https://github.com/letsramp/playwright.git"
8
11
  },
9
- "homepage": "https://playwright.dev",
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.2",
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
  },