@skyramp/mcp 0.1.8 → 0.2.0-rc.2

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 (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -156,14 +156,25 @@ function filterComments(lines) {
156
156
  * Detect session file paths referenced in test files
157
157
  * Looks for storageState patterns in TypeScript/JavaScript/Python/Java/C# test files
158
158
  * Excludes matches found in comments
159
+ *
160
+ * Also handles the codegen pattern `path.join(__dirname, '<filename>')` (TS/JS) —
161
+ * the filename is resolved relative to the test file's directory on the host so
162
+ * the existing absolute-path mount branch makes it visible at the same path
163
+ * inside the container (Playwright's TS loader resolves __dirname to the host
164
+ * workspace path at runtime).
159
165
  */
160
- function detectSessionFiles(testFilePath) {
166
+ export function detectSessionFiles(testFilePath) {
161
167
  try {
162
168
  const content = fs.readFileSync(testFilePath, "utf-8");
163
169
  const lines = content.split("\n");
164
170
  const sessionFiles = [];
165
171
  // Pattern for TypeScript/JavaScript: storageState: '/path/to/file' or storageState: "/path/to/file"
166
172
  const tsJsPattern = /storageState:\s*['"]([^'"]+)['"]/g;
173
+ // Pattern for TypeScript/JavaScript with path.join(__dirname, 'filename') — covers
174
+ // both the inline form (`storageState: path.join(__dirname, '...')`) and the
175
+ // variable-assignment form (`const X = path.join(__dirname, '...')` then
176
+ // `storageState: X`) the skyramp codegen emits.
177
+ const tsJsPathJoinPattern = /path\.join\s*\(\s*__dirname\s*,\s*['"]([^'"]+)['"]\s*\)/g;
167
178
  // Pattern for Python: storage_state='/path/to/file' or storage_state="/path/to/file"
168
179
  const pythonPattern = /storage_state\s*=\s*['"]([^'"]+)['"]/g;
169
180
  // Pattern for Java: setStorageState(Paths.get("path")) or setStorageState("path")
@@ -173,6 +184,7 @@ function detectSessionFiles(testFilePath) {
173
184
  const csharpPattern = /StorageState(?:Path)?\s*=\s*['"]([^'"]+)['"]/g;
174
185
  // Filter out comments
175
186
  const codeLines = filterComments(lines);
187
+ const testFileDir = path.dirname(testFilePath);
176
188
  // Process each non-comment line
177
189
  for (const line of codeLines) {
178
190
  // Try all patterns on this line
@@ -181,6 +193,12 @@ function detectSessionFiles(testFilePath) {
181
193
  while ((match = tsJsPattern.exec(line)) !== null) {
182
194
  sessionFiles.push(match[1]);
183
195
  }
196
+ tsJsPathJoinPattern.lastIndex = 0;
197
+ while ((match = tsJsPathJoinPattern.exec(line)) !== null) {
198
+ // Resolve relative to the test file's host directory so the absolute-
199
+ // path branch below mounts it at the same path inside the container.
200
+ sessionFiles.push(path.resolve(testFileDir, match[1]));
201
+ }
184
202
  pythonPattern.lastIndex = 0;
185
203
  while ((match = pythonPattern.exec(line)) !== null) {
186
204
  sessionFiles.push(match[1]);
@@ -357,39 +375,79 @@ export class TestExecutionService {
357
375
  },
358
376
  ],
359
377
  };
360
- // Mount workspace files, skipping EXCLUDED_MOUNT_ITEMS completely
378
+ // Mount workspace files, skipping EXCLUDED_MOUNT_ITEMS completely.
379
+ //
380
+ // Each workspace entry is bind-mounted at BOTH the canonical /home/user
381
+ // path AND its host-absolute path. The dual mount lets the test resolve
382
+ // any absolute reference the codegen happens to embed (storageState,
383
+ // fixture paths, snapshots) — including the host workspace path that
384
+ // Playwright's TypeScript loader sometimes produces from `__dirname` —
385
+ // without needing source-code detection. EXCLUDED_MOUNT_ITEMS
386
+ // (node_modules) stays excluded at both targets; MOUNT_NULL_ITEMS
387
+ // shadows (package.json → empty JSON, etc.) and PLAYWRIGHT_CONFIG_FILES
388
+ // shadows (minimal config) are applied at both targets too so the
389
+ // protections survive regardless of which path the test resolves to.
361
390
  const workspaceFiles = fs.readdirSync(workspacePath);
362
391
  const filesToMount = workspaceFiles.filter((file) => !EXCLUDED_MOUNT_ITEMS.includes(file) && !MOUNT_NULL_ITEMS.includes(file));
363
- hostConfig.Mounts?.push(...filesToMount.map((file) => ({
364
- Type: "bind",
365
- Target: path.join(containerMountPath, file),
366
- Source: path.join(workspacePath, file),
367
- })));
392
+ // Single Set tracks every mount target we've added so far. Used to dedupe
393
+ // both the workspace-mirror push (when workspacePath happens to equal
394
+ // containerMountPath) and the session-file push below.
395
+ const mountedPaths = new Set();
396
+ const pushMount = (mount) => {
397
+ if (mountedPaths.has(mount.Target))
398
+ return;
399
+ mountedPaths.add(mount.Target);
400
+ hostConfig.Mounts.push(mount);
401
+ };
402
+ const mirrorAtHostPath = workspacePath !== containerMountPath;
403
+ for (const file of filesToMount) {
404
+ const source = path.join(workspacePath, file);
405
+ pushMount({
406
+ Type: "bind",
407
+ Target: path.join(containerMountPath, file),
408
+ Source: source,
409
+ });
410
+ if (mirrorAtHostPath) {
411
+ pushMount({ Type: "bind", Target: source, Source: source });
412
+ }
413
+ }
368
414
  // Mount MOUNT_NULL_ITEMS (found recursively) to /dev/null (or empty JSON for .json files)
369
415
  const nullPaths = findExcludedPaths(workspacePath, MOUNT_NULL_ITEMS);
370
416
  for (const absolutePath of nullPaths) {
371
- const target = path.join(containerMountPath, path.relative(workspacePath, absolutePath));
417
+ const rel = path.relative(workspacePath, absolutePath);
372
418
  const source = absolutePath.endsWith(".json") ? EMPTY_JSON_PATH : "/dev/null";
373
- hostConfig.Mounts?.push({
419
+ pushMount({
374
420
  Type: "bind",
375
421
  Source: source,
376
- Target: target,
422
+ Target: path.join(containerMountPath, rel),
377
423
  });
424
+ if (mirrorAtHostPath) {
425
+ pushMount({ Type: "bind", Source: source, Target: absolutePath });
426
+ }
378
427
  }
379
428
  // Mount Playwright config files with minimal config (shadows repo configs that may
380
429
  // import dotenv or other dependencies not available in the executor container)
381
430
  const playwrightConfigPaths = findExcludedPaths(workspacePath, PLAYWRIGHT_CONFIG_FILES);
382
431
  for (const absolutePath of playwrightConfigPaths) {
383
- const target = path.join(containerMountPath, path.relative(workspacePath, absolutePath));
384
- hostConfig.Mounts?.push({
432
+ const rel = path.relative(workspacePath, absolutePath);
433
+ pushMount({
385
434
  Type: "bind",
386
435
  Source: MINIMAL_PLAYWRIGHT_CONFIG_PATH,
387
- Target: target,
436
+ Target: path.join(containerMountPath, rel),
388
437
  });
438
+ if (mirrorAtHostPath) {
439
+ pushMount({
440
+ Type: "bind",
441
+ Source: MINIMAL_PLAYWRIGHT_CONFIG_PATH,
442
+ Target: absolutePath,
443
+ });
444
+ }
389
445
  }
390
- // Detect and mount session files
446
+ // Detect and mount session files referenced outside the workspace
447
+ // (anything inside the workspace is already covered by the dual mount
448
+ // above; the session-file loop is the safety net for tests that point
449
+ // at a session in some other directory).
391
450
  const sessionFiles = detectSessionFiles(options.testFile);
392
- const mountedPaths = new Set(); // Track mounted file paths to prevent duplicates
393
451
  for (const sessionFile of sessionFiles) {
394
452
  let sessionFileSource;
395
453
  let sessionFileTarget;
@@ -142,6 +142,49 @@ describe("buildContainerEnv", () => {
142
142
  expect(env).toContain("API_KEY=my-key");
143
143
  });
144
144
  });
145
+ describe("detectSessionFiles", () => {
146
+ // Import after mocks are set up so the fs mock applies
147
+ let detectSessionFiles;
148
+ let mockReadFileSync;
149
+ beforeAll(async () => {
150
+ const mod = await import("./TestExecutionService.js");
151
+ detectSessionFiles = mod.detectSessionFiles;
152
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
153
+ mockReadFileSync = require("fs").readFileSync;
154
+ });
155
+ it("detects string-literal storageState (TS/JS)", () => {
156
+ mockReadFileSync.mockReturnValueOnce(`test.use({ storageState: '/abs/path/session.json' });`);
157
+ expect(detectSessionFiles("/ws/test.spec.ts")).toEqual([
158
+ "/abs/path/session.json",
159
+ ]);
160
+ });
161
+ it("detects skyramp codegen path.join(__dirname, '<file>') pattern and resolves to host-absolute path", () => {
162
+ // Reproduces SKYR-3321 generated test shape — must resolve to the host
163
+ // absolute path so the executor's absolute-path mount branch makes the
164
+ // file visible at that same path inside the container.
165
+ mockReadFileSync.mockReturnValueOnce(`
166
+ import path from 'path';
167
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
168
+ const SESSION_STORAGE = path.join(__dirname, 'skyramp_session_storage.json');
169
+ test.use({ storageState: SESSION_STORAGE });
170
+ `);
171
+ expect(detectSessionFiles("/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts")).toEqual([
172
+ "/Users/pedro/projects/cisco-xdr-tests/skyramp_session_storage.json",
173
+ ]);
174
+ });
175
+ it("detects inline storageState: path.join(__dirname, '<file>')", () => {
176
+ mockReadFileSync.mockReturnValueOnce(`test.use({ storageState: path.join(__dirname, 'session.json') });`);
177
+ expect(detectSessionFiles("/ws/spec.ts")).toEqual(["/ws/session.json"]);
178
+ });
179
+ it("ignores storageState references inside comments", () => {
180
+ mockReadFileSync.mockReturnValueOnce(`
181
+ // storageState: '/should/not/match.json'
182
+ // path.join(__dirname, 'also-not.json')
183
+ test('x', () => {});
184
+ `);
185
+ expect(detectSessionFiles("/ws/spec.ts")).toEqual([]);
186
+ });
187
+ });
145
188
  describe("TestExecutionService.executeTest - Docker env forwarding", () => {
146
189
  // Import after mocks are set up
147
190
  let TestExecutionService;
@@ -210,4 +253,66 @@ describe("TestExecutionService.executeTest - Docker env forwarding", () => {
210
253
  e.startsWith("SKYRAMP_TEST_SERVICE_URL_"));
211
254
  expect(envWithBaseUrl).toHaveLength(0);
212
255
  });
256
+ // Approach B: every workspace mount is mirrored at the host-absolute path so
257
+ // tests that embed absolute references (storageState, fixtures, snapshots)
258
+ // resolve correctly inside the executor regardless of which path-shape the
259
+ // codegen happens to emit.
260
+ it("mirrors each workspace file mount at both /home/user/<f> and the host-absolute path", async () => {
261
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
262
+ const fs = require("fs");
263
+ fs.readdirSync.mockImplementation((_path, options) => {
264
+ if (options?.withFileTypes) {
265
+ return [
266
+ { name: "xdr_dashboard.spec.ts", isFile: () => true, isDirectory: () => false },
267
+ { name: "skyramp_session_storage.json", isFile: () => true, isDirectory: () => false },
268
+ ];
269
+ }
270
+ return ["xdr_dashboard.spec.ts", "skyramp_session_storage.json"];
271
+ });
272
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
273
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
274
+ const service = new TestExecutionService();
275
+ await service.executeTest({
276
+ testFile: "/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts",
277
+ workspacePath: "/Users/pedro/projects/cisco-xdr-tests",
278
+ language: "typescript",
279
+ testType: "ui",
280
+ });
281
+ const dockerOptions = mockRun.mock.calls[0][3];
282
+ const targets = dockerOptions.HostConfig.Mounts.map((m) => m.Target);
283
+ // Canonical /home/user mount
284
+ expect(targets).toContain("/home/user/xdr_dashboard.spec.ts");
285
+ expect(targets).toContain("/home/user/skyramp_session_storage.json");
286
+ // Host-absolute mirror — the fix for absolute paths leaking out of `__dirname`
287
+ expect(targets).toContain("/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts");
288
+ expect(targets).toContain("/Users/pedro/projects/cisco-xdr-tests/skyramp_session_storage.json");
289
+ });
290
+ it("does not double-mount when workspacePath equals /home/user", async () => {
291
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
292
+ const fs = require("fs");
293
+ fs.readdirSync.mockImplementation((_path, options) => {
294
+ if (options?.withFileTypes) {
295
+ return [{ name: "test_file.py", isFile: () => true, isDirectory: () => false }];
296
+ }
297
+ return ["test_file.py"];
298
+ });
299
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
300
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
301
+ const service = new TestExecutionService();
302
+ await service.executeTest({
303
+ testFile: "/home/user/test_file.py",
304
+ workspacePath: "/home/user",
305
+ language: "python",
306
+ testType: "smoke",
307
+ });
308
+ const dockerOptions = mockRun.mock.calls[0][3];
309
+ const targetCounts = {};
310
+ for (const m of dockerOptions.HostConfig.Mounts) {
311
+ targetCounts[m.Target] = (targetCounts[m.Target] ?? 0) + 1;
312
+ }
313
+ // No mount target should appear twice (no host-absolute mirror when workspace == /home/user)
314
+ for (const [t, n] of Object.entries(targetCounts)) {
315
+ expect({ target: t, count: n }).toEqual({ target: t, count: 1 });
316
+ }
317
+ });
213
318
  });
@@ -8,7 +8,8 @@ import { getEntryPoint } from "../utils/telemetry.js";
8
8
  import { getLanguageSteps } from "../utils/language-helper.js";
9
9
  import { logger } from "../utils/logger.js";
10
10
  import { normalizeLanguageParams } from "../utils/normalizeParams.js";
11
- import { stageGeneratedPaths } from "../utils/gitStaging.js";
11
+ import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
12
+ import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
12
13
  export class TestGenerationService {
13
14
  client;
14
15
  constructor() {
@@ -18,6 +19,15 @@ export class TestGenerationService {
18
19
  try {
19
20
  // Normalize language/framework to handle LLM case variations
20
21
  normalizeLanguageParams(params);
22
+ // In cross-repo mode, redirect outputDir to the test repo clone.
23
+ const resolved = resolveOutputDir(params.outputDir, getTestsRepoDir());
24
+ if (resolved !== params.outputDir) {
25
+ logger.info("Cross-repo: redirecting outputDir to test repo", {
26
+ original: params.outputDir,
27
+ redirected: resolved,
28
+ });
29
+ params.outputDir = resolved;
30
+ }
21
31
  // Log prompt parameter using reusable utility
22
32
  logger.info("Generating test", {
23
33
  prompt: params.prompt,
@@ -147,16 +147,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
147
147
  if (stateData && stateData.existingTests) {
148
148
  const testIndex = stateData.existingTests.findIndex((t) => t.testFile === params.testFile);
149
149
  if (testIndex >= 0) {
150
- stateData.existingTests[testIndex].execution = {
151
- passed: result.passed,
152
- duration: result.duration || 0,
153
- errors: result.errors || [],
154
- warnings: result.warnings || [],
155
- crashed: result.crashed || false,
156
- stdout: result.output || "",
157
- stderr: result.errors?.join("\n") || "",
158
- executionTimestamp: new Date().toISOString(),
159
- };
150
+ stateData.existingTests[testIndex].execution = result;
160
151
  await stateManager.writeData(stateData);
161
152
  logger.info(`Updated stateFile with execution results for ${params.testFile}`);
162
153
  }
@@ -48,10 +48,22 @@ export const stepSchema = z.object({
48
48
  .string()
49
49
  .optional()
50
50
  .refine(isJsonObject, { message: "queryParams must be a JSON object string (e.g. '{\"limit\":\"10\"}')." })
51
- .describe("JSON string of URL query parameters (e.g., '{\"q\": \"bear\", \"limit\": 10}'). "
52
- + "Use this for GET request filters, search terms, pagination, sorting — any parameter that belongs in the URL query string. "
53
- + "CRITICAL: For search/filter/list endpoints (e.g., GET /products/search?q=bear&limit=10), parameters MUST go here, NOT in requestBody. "
54
- + "GET request bodies are non-standard and may be ignored or rejected by servers and frameworks, so always encode these parameters in the URL query string instead of the request body."),
51
+ .describe(`JSON string of URL query parameters. Provide a FLAT object where each key is the exact URL parameter name and each value is a string or number (single value per key).
52
+
53
+ <examples>
54
+ <example label="simple params">{"q": "bear", "limit": 10}</example>
55
+ <example label="bracket-notation keys">{"filter[title][_neq]": "not-a-number"}</example>
56
+ <example label="multi-value as comma-separated">{"tags": "sale,new,featured"}</example>
57
+ <example label="JSON-encoded string value">{"filter": "{\\\"status\\\":\\\"active\\\"}"}</example>
58
+ </examples>
59
+
60
+ For multi-value params (e.g. ?tags=sale,new), provide a comma-separated string: "tags": "sale,new". Arrays are accepted but will be comma-joined into one value — note this produces a single ?tags=sale,new param, not repeated ?tags=sale&tags=new keys.
61
+
62
+ If you provide nested objects, the service JSON-stringifies them as a fallback, but you should inspect the target API source to determine the correct key format and use that directly.
63
+
64
+ Use this for GET request filters, search terms, pagination, sorting — any parameter that belongs in the URL query string.
65
+
66
+ CRITICAL: For search/filter/list endpoints (e.g., GET /products/search?q=bear&limit=10), parameters MUST go here, NOT in requestBody. GET request bodies are non-standard and may be ignored or rejected by servers and frameworks.`),
55
67
  responseBody: z
56
68
  .string()
57
69
  .optional()
@@ -28,6 +28,8 @@ const integrationTestSchema = z
28
28
  "When provided, DO NOT also pass apiSchema or endpointURL — the scenario file already contains all endpoint information."),
29
29
  ...codeRefactoringSchema.shape,
30
30
  ...baseTestSchema,
31
+ apiSchema: baseTestSchema.apiSchema.describe("MUST be absolute path(/path/to/openapi.json) to the OpenAPI/Swagger schema file or a URL to the OpenAPI/Swagger schema file(e.g. https://demoshop.skyramp.dev/openapi.json). DO NOT TRY TO ASSUME THE OPENAPI SCHEMA IF NOT PROVIDED. NOTE TO AI ASSISTANTS: You do not need to read the contents of this file - simply pass the file path as the backend will read and process it. " +
32
+ "When an OpenAPI schema is provided, this tool automatically derives a CRUD scenario flow (Create → Read → Update → Delete) directly from the schema."),
31
33
  output: baseTestSchema.output.describe("Name of the output test file. " +
32
34
  "When scenarioFile is provided and user did not specify a name, derive it: " +
33
35
  "strip the path and 'scenario_' prefix, replace hyphens/non-alphanum with underscores, append '_integration_test' + language extension. " +
@@ -96,6 +96,8 @@ This tells you exactly which frontend files changed so you record traces for the
96
96
 
97
97
  **Typical pipeline:** Use the \`browser_*\` tools (\`browser_navigate\`, \`browser_click\`, \`browser_type\`, etc.) to record user interactions, then call \`skyramp_export_zip\` to export a trace zip, then pass the absolute path to that zip as \`playwrightInput\` here.
98
98
 
99
+ **DOM Analyzer tools for blueprint-aware recording:** alongside the basic \`browser_*\` interaction tools, the Skyramp MCP exposes \`browser_blueprint\` (canonical PageBlueprint capture), \`browser_blueprint_diff\` (structured before/after delta), \`browser_widget_contract_lookup\` (interaction recipe for custom widgets by fingerprint), and the sitemap tools \`browser_sitemap_build\` / \`browser_sitemap_query\`. These enable semantic target selection and delta-derived assertions: capture a blueprint before each meaningful action, perform the action, then capture again — the diff between the two grounds your assertions in observable state changes rather than author guesses about what "success" looks like.
100
+
99
101
  **CRITICAL: Do NOT use skyramp_start_trace_collection/skyramp_stop_trace_collection for UI test recording — use browser_* tools + skyramp_export_zip instead.**`,
100
102
  inputSchema: uiTestSchema,
101
103
  _meta: {