@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-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 (177) hide show
  1. package/build/index.js +2 -1
  2. package/build/prompts/code-reuse.d.ts +7 -1
  3. package/build/prompts/code-reuse.js +8 -5
  4. package/build/prompts/code-reuse.test.d.ts +1 -0
  5. package/build/prompts/code-reuse.test.js +62 -0
  6. package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
  7. package/build/prompts/pom-aware-code-reuse.js +100 -53
  8. package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
  9. package/build/prompts/pom-aware-code-reuse.test.js +11 -0
  10. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
  12. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
  13. package/build/prompts/test-recommendation/recommendationSections.js +5 -2
  14. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  15. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
  16. package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
  17. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
  18. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  19. package/build/prompts/testbot/testbot-prompts.js +67 -21
  20. package/build/prompts/testbot/testbot-prompts.test.js +44 -0
  21. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
  22. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
  23. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
  24. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
  25. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
  26. package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
  27. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
  28. package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
  29. package/build/recommendation/budgeters/shared.d.ts +19 -0
  30. package/build/recommendation/budgeters/shared.js +66 -0
  31. package/build/recommendation/discriminators.d.ts +31 -0
  32. package/build/recommendation/discriminators.js +355 -0
  33. package/build/recommendation/discriminators.test.d.ts +1 -0
  34. package/build/recommendation/discriminators.test.js +324 -0
  35. package/build/recommendation/diversity.d.ts +47 -0
  36. package/build/recommendation/diversity.js +101 -0
  37. package/build/recommendation/diversity.test.d.ts +1 -0
  38. package/build/recommendation/diversity.test.js +77 -0
  39. package/build/recommendation/planRanker.d.ts +50 -0
  40. package/build/recommendation/planRanker.js +67 -0
  41. package/build/recommendation/planRanker.test.d.ts +1 -0
  42. package/build/recommendation/planRanker.test.js +110 -0
  43. package/build/recommendation/testFixtures.d.ts +25 -0
  44. package/build/recommendation/testFixtures.js +45 -0
  45. package/build/resources/testbotResource.js +4 -1
  46. package/build/services/ScenarioGenerationService.d.ts +5 -0
  47. package/build/services/ScenarioGenerationService.js +16 -1
  48. package/build/services/ScenarioGenerationService.test.js +44 -0
  49. package/build/services/TestExecutionService.d.ts +15 -1
  50. package/build/services/TestExecutionService.js +210 -55
  51. package/build/services/TestExecutionService.test.js +397 -0
  52. package/build/services/TestGenerationService.js +19 -1
  53. package/build/services/TestGenerationService.test.js +58 -0
  54. package/build/tool-phases.js +1 -0
  55. package/build/toolNames.d.ts +19 -0
  56. package/build/toolNames.js +19 -0
  57. package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
  58. package/build/tools/code-refactor/codeReuseTool.js +130 -4
  59. package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
  60. package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
  61. package/build/tools/executeSkyrampTestTool.js +8 -2
  62. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
  65. package/build/tools/generate-tests/generateContractRestTool.js +11 -1
  66. package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
  67. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
  68. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
  69. package/build/tools/generate-tests/planGuard.d.ts +13 -0
  70. package/build/tools/generate-tests/planGuard.js +78 -0
  71. package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
  72. package/build/tools/generate-tests/planGuard.test.js +185 -0
  73. package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
  74. package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
  75. package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
  76. package/build/tools/generate-tests/scenarioLint.js +150 -0
  77. package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
  78. package/build/tools/generate-tests/scenarioLint.test.js +100 -0
  79. package/build/tools/submitReportTool.js +78 -0
  80. package/build/tools/submitReportTool.test.js +255 -0
  81. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  82. package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
  83. package/build/tools/test-management/index.d.ts +1 -0
  84. package/build/tools/test-management/index.js +1 -0
  85. package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
  86. package/build/tools/test-management/registerTestPlanTool.js +329 -0
  87. package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
  88. package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
  89. package/build/types/Recommendation.d.ts +97 -0
  90. package/build/types/Recommendation.js +48 -0
  91. package/build/types/RepositoryAnalysis.d.ts +14 -14
  92. package/build/types/TestExecution.d.ts +2 -0
  93. package/build/types/TestRecommendation.d.ts +12 -1
  94. package/build/types/TestRecommendation.js +26 -11
  95. package/build/types/TestTypes.js +1 -1
  96. package/build/utils/AnalysisStateManager.d.ts +47 -0
  97. package/build/utils/docker.test.js +1 -1
  98. package/build/utils/planMatchKeys.d.ts +61 -0
  99. package/build/utils/planMatchKeys.js +125 -0
  100. package/build/utils/pom-scope/import-expansion.d.ts +5 -0
  101. package/build/utils/pom-scope/import-expansion.js +32 -0
  102. package/build/utils/pom-scope/index.d.ts +39 -0
  103. package/build/utils/pom-scope/index.js +120 -0
  104. package/build/utils/pom-scope/index.test.d.ts +1 -0
  105. package/build/utils/pom-scope/index.test.js +239 -0
  106. package/build/utils/pom-scope/pom-files.d.ts +3 -0
  107. package/build/utils/pom-scope/pom-files.js +48 -0
  108. package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
  109. package/build/utils/pom-scope/pom-files.test.js +29 -0
  110. package/build/utils/pom-scope/scoring.d.ts +20 -0
  111. package/build/utils/pom-scope/scoring.js +45 -0
  112. package/build/utils/pom-scope/scoring.test.d.ts +1 -0
  113. package/build/utils/pom-scope/scoring.test.js +39 -0
  114. package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
  115. package/build/utils/pom-scope/selector-extractor.js +57 -0
  116. package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
  117. package/build/utils/pom-scope/selector-extractor.test.js +67 -0
  118. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
  119. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
  120. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
  121. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
  122. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
  123. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
  124. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
  125. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
  126. package/build/utils/pom-verify/bindings.d.ts +19 -0
  127. package/build/utils/pom-verify/bindings.js +161 -0
  128. package/build/utils/pom-verify/bindings.test.d.ts +1 -0
  129. package/build/utils/pom-verify/bindings.test.js +164 -0
  130. package/build/utils/pom-verify/calls.d.ts +16 -0
  131. package/build/utils/pom-verify/calls.js +42 -0
  132. package/build/utils/pom-verify/calls.test.d.ts +1 -0
  133. package/build/utils/pom-verify/calls.test.js +61 -0
  134. package/build/utils/pom-verify/index.d.ts +4 -0
  135. package/build/utils/pom-verify/index.js +4 -0
  136. package/build/utils/pom-verify/resolve.d.ts +7 -0
  137. package/build/utils/pom-verify/resolve.js +27 -0
  138. package/build/utils/pom-verify/resolve.test.d.ts +1 -0
  139. package/build/utils/pom-verify/resolve.test.js +68 -0
  140. package/build/utils/pom-verify/strip.d.ts +9 -0
  141. package/build/utils/pom-verify/strip.js +89 -0
  142. package/build/utils/pom-verify/verify.d.ts +14 -0
  143. package/build/utils/pom-verify/verify.js +158 -0
  144. package/build/utils/pom-verify/verify.test.d.ts +1 -0
  145. package/build/utils/pom-verify/verify.test.js +325 -0
  146. package/build/utils/reportVerification.d.ts +61 -0
  147. package/build/utils/reportVerification.js +104 -0
  148. package/build/utils/reportVerification.test.d.ts +1 -0
  149. package/build/utils/reportVerification.test.js +185 -0
  150. package/build/utils/scenarioDrafting.js +5 -5
  151. package/build/utils/versions.d.ts +3 -3
  152. package/build/utils/versions.js +1 -1
  153. package/build/utils/workspaceAuth.d.ts +9 -1
  154. package/build/utils/workspaceAuth.js +25 -5
  155. package/build/utils/workspaceAuth.test.js +48 -0
  156. package/build/workspace/workspace.d.ts +20 -0
  157. package/build/workspace/workspace.js +4 -0
  158. package/build/workspace/workspace.test.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
  160. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  161. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  170. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  171. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  172. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  173. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +3 -3
  177. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -1,7 +1,8 @@
1
1
  import { TestExecutionResult, BatchExecutionResult, TestExecutionOptions, ProgressCallback } from "../types/TestExecution.js";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.34";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.35";
3
3
  export declare const PLAYWRIGHT_CONFIG_FILES: string[];
4
4
  export declare const EXCLUDED_MOUNT_ITEMS: string[];
5
+ export declare const TEST_REPO_CONTAINER_SUBDIR = ".skyramp-test-repo";
5
6
  export declare const MOUNT_NULL_ITEMS: string[];
6
7
  /**
7
8
  * Check if a test type requires browser execution (UI/E2E)
@@ -35,6 +36,19 @@ export declare function detectSessionFiles(testFilePath: string): string[];
35
36
  * Skips recursing into directories that are themselves excluded.
36
37
  */
37
38
  export declare function findExcludedPaths(dir: string, excludedItems: string[]): string[];
39
+ /**
40
+ * Find every nested `node_modules` directory under `dir` — i.e. all except the
41
+ * top-level `<dir>/node_modules` (which the workspace mount already excludes).
42
+ * Never descends into a `node_modules`. Used to shadow repo-local node_modules
43
+ * that ride into the executor inside a parent directory's wholesale bind-mount;
44
+ * a leaked `@playwright/test` becomes a second physical copy and trips
45
+ * Playwright's "Requiring @playwright/test second time" guard.
46
+ * Skips `.git` (nothing importable lives there) and does not follow symlinks —
47
+ * by design (a symlinked nested node_modules either dangles in-container,
48
+ * falling through to NODE_PATH's global copy, or its real-path target is
49
+ * walked directly).
50
+ */
51
+ export declare function findNestedNodeModules(dir: string): string[];
38
52
  export declare class TestExecutionService {
39
53
  private docker;
40
54
  private imageReady;
@@ -20,6 +20,28 @@ const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress update
20
20
  // so Node.js doesn't throw ERR_INVALID_PACKAGE_CONFIG when reading them.
21
21
  const EMPTY_JSON_PATH = path.join(os.tmpdir(), "skyramp-empty.json");
22
22
  fs.writeFileSync(EMPTY_JSON_PATH, "{}");
23
+ // Empty directory used to shadow nested node_modules over their leaked mount
24
+ // (a directory target — /dev/null only works for file targets). Per-process
25
+ // mkdtemp guarantees it stays empty; best-effort removal on exit keeps tmp
26
+ // from accumulating one dir per process start on long-lived hosts.
27
+ const EMPTY_DIR_PATH = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-empty-"));
28
+ process.on("exit", () => {
29
+ try {
30
+ fs.rmdirSync(EMPTY_DIR_PATH);
31
+ }
32
+ catch {
33
+ // best-effort: bind mounts from a still-running container or a prior crash may block removal
34
+ }
35
+ });
36
+ // Packages that must never load from a repo-local (nested) node_modules: a
37
+ // second physical @playwright/test trips Playwright's "Requiring
38
+ // @playwright/test second time" guard, and a host-installed @skyramp/skyramp
39
+ // carries the wrong platform's native library. Shadow ONLY these inside each
40
+ // nested node_modules — customer test-support deps (winston, dotenv, …) keep
41
+ // resolving from the mounted repo. Node treats the empty shadow as a miss and
42
+ // walks up to NODE_PATH's global install (both packages are global in the
43
+ // executor image).
44
+ const SHADOWED_NESTED_PACKAGES = ["@playwright/test", "playwright", "@skyramp/skyramp"];
23
45
  // Minimal Playwright config — shadows repo configs that may import dotenv or other
24
46
  // dependencies not available in the executor container.
25
47
  const MINIMAL_PLAYWRIGHT_CONFIG_PATH = path.join(os.tmpdir(), "skyramp-playwright.config.ts");
@@ -36,6 +58,11 @@ export const PLAYWRIGHT_CONFIG_FILES = [
36
58
  export const EXCLUDED_MOUNT_ITEMS = [
37
59
  "node_modules",
38
60
  ];
61
+ // Reserved container subdir under /home/user where a cross-repo test repository
62
+ // (testsRepoDir, SKYR-3819) is mounted. Must live INSIDE /home/user: the generated
63
+ // Playwright config's testDir is anchored there, and a test path outside it is
64
+ // reported as "No tests found".
65
+ export const TEST_REPO_CONTAINER_SUBDIR = ".skyramp-test-repo";
39
66
  // Files to shadow with /dev/null recursively so the container ignores them
40
67
  export const MOUNT_NULL_ITEMS = [
41
68
  "package-lock.json",
@@ -336,6 +363,45 @@ export function findExcludedPaths(dir, excludedItems) {
336
363
  }
337
364
  return results;
338
365
  }
366
+ /**
367
+ * Find every nested `node_modules` directory under `dir` — i.e. all except the
368
+ * top-level `<dir>/node_modules` (which the workspace mount already excludes).
369
+ * Never descends into a `node_modules`. Used to shadow repo-local node_modules
370
+ * that ride into the executor inside a parent directory's wholesale bind-mount;
371
+ * a leaked `@playwright/test` becomes a second physical copy and trips
372
+ * Playwright's "Requiring @playwright/test second time" guard.
373
+ * Skips `.git` (nothing importable lives there) and does not follow symlinks —
374
+ * by design (a symlinked nested node_modules either dangles in-container,
375
+ * falling through to NODE_PATH's global copy, or its real-path target is
376
+ * walked directly).
377
+ */
378
+ export function findNestedNodeModules(dir) {
379
+ const results = [];
380
+ const walk = (current) => {
381
+ let entries;
382
+ try {
383
+ entries = fs.readdirSync(current, { withFileTypes: true });
384
+ }
385
+ catch {
386
+ return; // best-effort: skip unreadable directory, keep going
387
+ }
388
+ for (const entry of entries) {
389
+ if (!entry.isDirectory())
390
+ continue;
391
+ if (entry.name === ".git")
392
+ continue;
393
+ const full = path.join(current, entry.name);
394
+ if (entry.name === "node_modules") {
395
+ if (current !== dir)
396
+ results.push(full); // skip the top-level node_modules
397
+ continue; // never descend into a node_modules
398
+ }
399
+ walk(full);
400
+ }
401
+ };
402
+ walk(dir);
403
+ return results;
404
+ }
339
405
  export class TestExecutionService {
340
406
  docker;
341
407
  imageReady = null;
@@ -442,14 +508,59 @@ export class TestExecutionService {
442
508
  });
443
509
  const containerMountPath = "/home/user";
444
510
  const dockerSocketPath = "/var/run/docker.sock";
445
- // Calculate relative test file path
446
- let testFilePath = path.relative(workspacePath, options.testFile);
447
- testFilePath = path.resolve(containerMountPath, testFilePath);
511
+ const isUnder = (parent, child) => child === parent || child.startsWith(parent + path.sep);
512
+ // Cross-repo test file (SKYR-3819): a test delivered under the run's test
513
+ // repository lives outside workspacePath, so the workspace mounts below never
514
+ // include it and the relative-path computation would escape /home/user. Anchor
515
+ // it under the reserved subdir instead; the test repo is mounted there below.
516
+ const absTestFile = path.resolve(options.testFile);
517
+ const testRepoPath = options.testRepoPath ? path.resolve(options.testRepoPath) : undefined;
518
+ // A root-level test repo would bind-mount large portions of the host
519
+ // (read-write, alongside the Docker socket) into the executor — reject it
520
+ // explicitly rather than falling through to the generic path error below.
521
+ if (testRepoPath &&
522
+ testRepoPath === path.parse(testRepoPath).root &&
523
+ !isUnder(workspacePath, absTestFile)) {
524
+ throw new Error(`testRepoPath must not be the filesystem root, got: ${options.testRepoPath}`);
525
+ }
526
+ const crossRepoTestRoot = !isUnder(workspacePath, absTestFile) && testRepoPath && isUnder(testRepoPath, absTestFile)
527
+ ? testRepoPath
528
+ : undefined;
529
+ if (crossRepoTestRoot) {
530
+ try {
531
+ fs.accessSync(crossRepoTestRoot, fs.constants.R_OK);
532
+ }
533
+ catch {
534
+ throw new Error(`Cross-repo test repository path does not exist or is not readable: ${crossRepoTestRoot}`);
535
+ }
536
+ }
537
+ let testFilePath;
538
+ if (crossRepoTestRoot) {
539
+ testFilePath = path.join(containerMountPath, TEST_REPO_CONTAINER_SUBDIR, path.relative(crossRepoTestRoot, absTestFile));
540
+ }
541
+ else if (!isUnder(workspacePath, absTestFile)) {
542
+ throw new Error(`Test file ${options.testFile} is outside the workspace ${workspacePath}, so the executor container cannot reach it. ` +
543
+ `For tests delivered to a cross-repo test repository, run skyramp_analyze_changes with testsRepoDir first (it is remembered for the run), or move the test under the workspace.`);
544
+ }
545
+ else {
546
+ testFilePath = path.resolve(containerMountPath, path.relative(workspacePath, absTestFile));
547
+ }
448
548
  // Video recording setup for UI/E2E browser tests (no-op for API tests).
449
549
  const { browserTest, videoHostDir, videoContainerDir, playwrightConfigPath, pytestConfigPath } = setupVideoCapture(options, workspacePath, containerMountPath);
450
550
  // Prepare Docker command
451
551
  const command = [
452
- "/root/runner.sh",
552
+ "bash",
553
+ "-c",
554
+ // Bare requires from mounted/reused customer code must resolve the shadowed
555
+ // packages (@playwright/test, @skyramp/skyramp — see SHADOWED_NESTED_PACKAGES)
556
+ // from the image's single global install; npm root -g resolves that path
557
+ // regardless of the image's pinned Node version. The current executor image
558
+ // already sets NODE_PATH via ENV; this wrap makes the resolution
559
+ // image-independent rather than relying on that.
560
+ // The wrap assumes `bash` and `npm` exist in the executor image (both
561
+ // verified present; runner.sh already requires bash).
562
+ 'export NODE_PATH="$(npm root -g)"; exec /root/runner.sh "$@"',
563
+ "runner",
453
564
  options.language,
454
565
  testFilePath,
455
566
  options.testType,
@@ -466,20 +577,6 @@ export class TestExecutionService {
466
577
  },
467
578
  ],
468
579
  };
469
- // Mount workspace files, skipping EXCLUDED_MOUNT_ITEMS completely.
470
- //
471
- // Each workspace entry is bind-mounted at BOTH the canonical /home/user
472
- // path AND its host-absolute path. The dual mount lets the test resolve
473
- // any absolute reference the codegen happens to embed (storageState,
474
- // fixture paths, snapshots) — including the host workspace path that
475
- // Playwright's TypeScript loader sometimes produces from `__dirname` —
476
- // without needing source-code detection. EXCLUDED_MOUNT_ITEMS
477
- // (node_modules) stays excluded at both targets; MOUNT_NULL_ITEMS
478
- // shadows (package.json → empty JSON, etc.) and PLAYWRIGHT_CONFIG_FILES
479
- // shadows (minimal config) are applied at both targets too so the
480
- // protections survive regardless of which path the test resolves to.
481
- const workspaceFiles = fs.readdirSync(workspacePath);
482
- const filesToMount = workspaceFiles.filter((file) => !EXCLUDED_MOUNT_ITEMS.includes(file) && !MOUNT_NULL_ITEMS.includes(file));
483
580
  // Single Set tracks every mount target we've added so far. Used to dedupe
484
581
  // both the workspace-mirror push (when workspacePath happens to equal
485
582
  // containerMountPath) and the session-file push below.
@@ -490,6 +587,92 @@ export class TestExecutionService {
490
587
  mountedPaths.add(mount.Target);
491
588
  hostConfig.Mounts.push(mount);
492
589
  };
590
+ // Mount one repo tree (the workspace, or a cross-repo test repository) into
591
+ // the container, skipping EXCLUDED_MOUNT_ITEMS completely.
592
+ //
593
+ // Each top-level entry is bind-mounted at the canonical container path and —
594
+ // when mirrorToHost is set — at its host-absolute path too. The dual mount
595
+ // lets the test resolve any absolute reference the codegen happens to embed
596
+ // (storageState, fixture paths, snapshots) — including the host workspace
597
+ // path that Playwright's TypeScript loader sometimes produces from
598
+ // `__dirname` — without needing source-code detection. EXCLUDED_MOUNT_ITEMS
599
+ // (node_modules) stays excluded at both targets; MOUNT_NULL_ITEMS shadows
600
+ // (package.json → empty JSON, etc.), nested-package shadows, and
601
+ // PLAYWRIGHT_CONFIG_FILES shadows (minimal config) are applied at both
602
+ // targets too so the protections survive regardless of which path the test
603
+ // resolves to.
604
+ //
605
+ // Ordering matters between the broad mounts and the shadows: Docker layers
606
+ // later mounts over earlier ones, so the top-level entry mounts must be
607
+ // pushed BEFORE the shadow mounts that override paths inside them.
608
+ //
609
+ // The three recursive scans (findExcludedPaths × 2 + findNestedNodeModules)
610
+ // are deliberately kept as separate walks rather than fused into one pass:
611
+ // each skips node_modules/.git so the cost is small, and separate walks keep
612
+ // each shadow category's semantics independently testable.
613
+ const mountRepoTree = (hostRoot, containerRoot, mirrorToHost) => {
614
+ // Top-level entries. MOUNT_NULL_ITEMS are excluded here and shadowed
615
+ // recursively below instead. Root-level Playwright configs are excluded
616
+ // here too so the config-shadow loop below (or the pre-mounted generated
617
+ // config, which wins by dedup) supplies the container's config instead —
618
+ // otherwise the entry mount would win the dedup race and the repo's own
619
+ // config (which may import dotenv, or simply isn't the minimal/generated
620
+ // one) would leak into the container.
621
+ const entriesToMount = fs
622
+ .readdirSync(hostRoot)
623
+ .filter((file) => !EXCLUDED_MOUNT_ITEMS.includes(file) &&
624
+ !MOUNT_NULL_ITEMS.includes(file) &&
625
+ !PLAYWRIGHT_CONFIG_FILES.includes(file));
626
+ for (const file of entriesToMount) {
627
+ const source = path.join(hostRoot, file);
628
+ pushMount({ Type: "bind", Target: path.join(containerRoot, file), Source: source });
629
+ if (mirrorToHost) {
630
+ pushMount({ Type: "bind", Target: source, Source: source });
631
+ }
632
+ }
633
+ // Shadow MOUNT_NULL_ITEMS (found recursively) with /dev/null (or empty JSON for .json files).
634
+ for (const absolutePath of findExcludedPaths(hostRoot, MOUNT_NULL_ITEMS)) {
635
+ const rel = path.relative(hostRoot, absolutePath);
636
+ const source = absolutePath.endsWith(".json") ? EMPTY_JSON_PATH : "/dev/null";
637
+ pushMount({ Type: "bind", Source: source, Target: path.join(containerRoot, rel) });
638
+ if (mirrorToHost) {
639
+ pushMount({ Type: "bind", Source: source, Target: absolutePath });
640
+ }
641
+ }
642
+ // Shadow SHADOWED_NESTED_PACKAGES inside every nested node_modules leaked in
643
+ // via a parent-directory bind-mount (see findNestedNodeModules) — the
644
+ // top-level node_modules is already excluded above via EXCLUDED_MOUNT_ITEMS.
645
+ for (const nested of findNestedNodeModules(hostRoot)) {
646
+ for (const pkg of SHADOWED_NESTED_PACKAGES) {
647
+ const pkgPath = path.join(nested, pkg);
648
+ if (!fs.existsSync(pkgPath))
649
+ continue; // only shadow what actually exists
650
+ const rel = path.relative(hostRoot, pkgPath);
651
+ pushMount({
652
+ Type: "bind",
653
+ Source: EMPTY_DIR_PATH,
654
+ Target: path.join(containerRoot, rel),
655
+ ReadOnly: true,
656
+ });
657
+ if (mirrorToHost) {
658
+ pushMount({ Type: "bind", Source: EMPTY_DIR_PATH, Target: pkgPath, ReadOnly: true });
659
+ }
660
+ }
661
+ }
662
+ // Mount Playwright config files with the per-execution config (minimal or
663
+ // generated). Shadows ANY repo playwright.config.{ts,js,mjs} recursively so
664
+ // repo configs that import dotenv/etc. don't crash the executor. Applies to
665
+ // ALL tests — including non-browser TypeScript tests
666
+ // (contract/integration/fuzz/smoke/load) which per runner.sh also run via
667
+ // `npx playwright test <file>` and read playwright.config.ts.
668
+ for (const absolutePath of findExcludedPaths(hostRoot, PLAYWRIGHT_CONFIG_FILES)) {
669
+ const rel = path.relative(hostRoot, absolutePath);
670
+ pushMount({ Type: "bind", Source: playwrightConfigPath, Target: path.join(containerRoot, rel) });
671
+ if (mirrorToHost) {
672
+ pushMount({ Type: "bind", Source: playwrightConfigPath, Target: absolutePath });
673
+ }
674
+ }
675
+ };
493
676
  // For browser tests, mount generated video-enabled configs at canonical container
494
677
  // paths BEFORE the nullPaths loop so they WIN over any /dev/null shadows and work
495
678
  // even if the repo has no config file. The pushMount dedup ensures later shadows
@@ -512,44 +695,16 @@ export class TestExecutionService {
512
695
  });
513
696
  }
514
697
  }
698
+ // Mirror workspace mounts at their host-absolute paths unless the workspace
699
+ // already IS the canonical container path (mirroring would double-mount).
515
700
  const mirrorAtHostPath = workspacePath !== containerMountPath;
516
- for (const file of filesToMount) {
517
- const source = path.join(workspacePath, file);
518
- pushMount({
519
- Type: "bind",
520
- Target: path.join(containerMountPath, file),
521
- Source: source,
522
- });
523
- if (mirrorAtHostPath) {
524
- pushMount({ Type: "bind", Target: source, Source: source });
525
- }
526
- }
527
- // Mount MOUNT_NULL_ITEMS (found recursively) to /dev/null (or empty JSON for .json files)
528
- const nullPaths = findExcludedPaths(workspacePath, MOUNT_NULL_ITEMS);
529
- for (const absolutePath of nullPaths) {
530
- const rel = path.relative(workspacePath, absolutePath);
531
- const source = absolutePath.endsWith(".json") ? EMPTY_JSON_PATH : "/dev/null";
532
- pushMount({
533
- Type: "bind",
534
- Source: source,
535
- Target: path.join(containerMountPath, rel),
536
- });
537
- if (mirrorAtHostPath) {
538
- pushMount({ Type: "bind", Source: source, Target: absolutePath });
539
- }
540
- }
541
- // Mount Playwright config files with the per-execution config (minimal or generated).
542
- // Shadows ANY repo playwright.config.{ts,js,mjs} recursively so repo configs that
543
- // import dotenv/etc. don't crash the executor. Applies to ALL tests — including
544
- // non-browser TypeScript tests (contract/integration/fuzz/smoke/load) which per
545
- // runner.sh also run via `npx playwright test <file>` and read playwright.config.ts.
546
- const playwrightConfigPaths = findExcludedPaths(workspacePath, PLAYWRIGHT_CONFIG_FILES);
547
- for (const absolutePath of playwrightConfigPaths) {
548
- const rel = path.relative(workspacePath, absolutePath);
549
- pushMount({ Type: "bind", Source: playwrightConfigPath, Target: path.join(containerMountPath, rel) });
550
- if (mirrorAtHostPath) {
551
- pushMount({ Type: "bind", Source: playwrightConfigPath, Target: absolutePath });
552
- }
701
+ mountRepoTree(workspacePath, containerMountPath, mirrorAtHostPath);
702
+ // Cross-repo test repository mounts (SKYR-3819): expose the delivered tests
703
+ // under the reserved /home/user subdir — inside Playwright's testDir — with
704
+ // the same exclusions and shadows as the workspace, plus host-absolute
705
+ // mirrors so absolute references embedded in tests resolve.
706
+ if (crossRepoTestRoot) {
707
+ mountRepoTree(crossRepoTestRoot, path.join(containerMountPath, TEST_REPO_CONTAINER_SUBDIR), true);
553
708
  }
554
709
  // Mount video output directory for browser tests (writable)
555
710
  if (browserTest && videoHostDir) {