@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,5 +1,7 @@
1
1
  // @ts-nocheck - Jest ESM type inference issues
2
2
  import { jest } from "@jest/globals";
3
+ import * as os from "os";
4
+ import * as path from "path";
3
5
  import { buildContainerEnv, rewriteLocalhostForDocker } from "./containerEnv.js";
4
6
  // Mock dockerode before importing TestExecutionService
5
7
  const mockRun = jest.fn();
@@ -54,6 +56,32 @@ jest.unstable_mockModule("../utils/logger.js", () => ({
54
56
  warning: jest.fn(),
55
57
  },
56
58
  }));
59
+ // Real temp directory trees for tests that need actual directory traversal
60
+ // (findNestedNodeModules / findExcludedPaths walk the real fs, delegated to
61
+ // via mockReaddirSync — see each describe block's beforeEach). Every root
62
+ // created here is tracked and removed after its test so these don't leak
63
+ // into the OS temp dir across runs.
64
+ const makeTreeRoots = [];
65
+ function makeTree(files) {
66
+ const root = actualFs.mkdtempSync(path.join(os.tmpdir(), "nnm-"));
67
+ makeTreeRoots.push(root);
68
+ for (const [rel, content] of Object.entries(files)) {
69
+ const abs = path.join(root, rel);
70
+ if (content === null)
71
+ actualFs.mkdirSync(abs, { recursive: true });
72
+ else {
73
+ actualFs.mkdirSync(path.dirname(abs), { recursive: true });
74
+ actualFs.writeFileSync(abs, content);
75
+ }
76
+ }
77
+ return root;
78
+ }
79
+ afterEach(() => {
80
+ while (makeTreeRoots.length > 0) {
81
+ const root = makeTreeRoots.pop();
82
+ actualFs.rmSync(root, { recursive: true, force: true });
83
+ }
84
+ });
57
85
  describe("rewriteLocalhostForDocker", () => {
58
86
  it("rewrites http://localhost:port", () => {
59
87
  expect(rewriteLocalhostForDocker("http://localhost:8000")).toBe("http://host.docker.internal:8000");
@@ -633,3 +661,372 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
633
661
  expect(result.videoPath).toBeUndefined();
634
662
  });
635
663
  });
664
+ // SKYR-3819: tests delivered to a cross-repo test repository (testsRepoDir) live
665
+ // outside workspacePath. The executor must mount the test repo under a reserved
666
+ // /home/user subdir — inside the generated Playwright config's testDir — and
667
+ // anchor the container test path there, or the runner reports "No tests found" /
668
+ // "file or directory not found".
669
+ describe("TestExecutionService.executeTest - cross-repo test repo mounts", () => {
670
+ let TestExecutionService;
671
+ let TEST_REPO_CONTAINER_SUBDIR;
672
+ let EXECUTOR_DOCKER_IMAGE;
673
+ beforeAll(async () => {
674
+ const mod = await import("./TestExecutionService.js");
675
+ TestExecutionService = mod.TestExecutionService;
676
+ TEST_REPO_CONTAINER_SUBDIR = mod.TEST_REPO_CONTAINER_SUBDIR;
677
+ EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
678
+ });
679
+ const dirent = (name, dir = false) => ({
680
+ name,
681
+ isFile: () => !dir,
682
+ isDirectory: () => dir,
683
+ });
684
+ beforeEach(() => {
685
+ jest.clearAllMocks();
686
+ mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
687
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
688
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
689
+ // Path-aware directory layout:
690
+ // /workspace → SUT repo (src/)
691
+ // /test-repo → cross-repo test repo (tests/, node_modules/, package.json)
692
+ mockReaddirSync.mockImplementation((dir, options) => {
693
+ const listing = {
694
+ "/workspace": [dirent("src", true)],
695
+ "/workspace/src": [],
696
+ "/test-repo": [dirent("tests", true), dirent("node_modules", true), dirent("package.json")],
697
+ "/test-repo/tests": [dirent("orders_test.py")],
698
+ };
699
+ const entries = listing[dir] ?? [];
700
+ return options?.withFileTypes ? entries : entries.map((e) => e.name);
701
+ });
702
+ });
703
+ it("mounts test repo entries under the reserved /home/user subdir with host mirrors", async () => {
704
+ const service = new TestExecutionService();
705
+ await service.executeTest({
706
+ testFile: "/test-repo/tests/orders_test.py",
707
+ workspacePath: "/workspace",
708
+ testRepoPath: "/test-repo",
709
+ language: "python",
710
+ testType: "integration",
711
+ });
712
+ expect(mockRun).toHaveBeenCalledTimes(1);
713
+ const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
714
+ expect(mounts).toContainEqual(expect.objectContaining({
715
+ Source: "/test-repo/tests",
716
+ Target: `/home/user/${TEST_REPO_CONTAINER_SUBDIR}/tests`,
717
+ }));
718
+ // Host-absolute mirror so absolute references embedded in tests resolve
719
+ expect(mounts).toContainEqual(expect.objectContaining({ Source: "/test-repo/tests", Target: "/test-repo/tests" }));
720
+ });
721
+ it("anchors the container test path inside the reserved subdir", async () => {
722
+ const service = new TestExecutionService();
723
+ await service.executeTest({
724
+ testFile: "/test-repo/tests/orders_test.py",
725
+ workspacePath: "/workspace",
726
+ testRepoPath: "/test-repo",
727
+ language: "python",
728
+ testType: "integration",
729
+ });
730
+ const command = mockRun.mock.calls[0][1];
731
+ expect(command).toContain(`/home/user/${TEST_REPO_CONTAINER_SUBDIR}/tests/orders_test.py`);
732
+ });
733
+ it("applies mount exclusions and null shadows to the test repo", async () => {
734
+ const service = new TestExecutionService();
735
+ await service.executeTest({
736
+ testFile: "/test-repo/tests/orders_test.py",
737
+ workspacePath: "/workspace",
738
+ testRepoPath: "/test-repo",
739
+ language: "python",
740
+ testType: "integration",
741
+ });
742
+ const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
743
+ // node_modules is never mounted
744
+ expect(mounts.filter((m) => m.Source === "/test-repo/node_modules")).toHaveLength(0);
745
+ // package.json is shadowed with the empty-JSON file at both targets
746
+ const pkgShadows = mounts.filter((m) => m.Target === `/home/user/${TEST_REPO_CONTAINER_SUBDIR}/package.json` ||
747
+ m.Target === "/test-repo/package.json");
748
+ expect(pkgShadows).toHaveLength(2);
749
+ for (const m of pkgShadows) {
750
+ expect(m.Source).toMatch(/skyramp-empty\.json$/);
751
+ }
752
+ });
753
+ it("throws an actionable error when testFile is outside the workspace and no testRepoPath is known", async () => {
754
+ const service = new TestExecutionService();
755
+ await expect(service.executeTest({
756
+ testFile: "/test-repo/tests/orders_test.py",
757
+ workspacePath: "/workspace",
758
+ language: "python",
759
+ testType: "integration",
760
+ })).rejects.toThrow(/outside the workspace/);
761
+ });
762
+ it("does not add test repo mounts when testFile is inside the workspace", async () => {
763
+ mockReaddirSync.mockImplementation((dir, options) => {
764
+ const listing = {
765
+ "/workspace": [dirent("tests", true)],
766
+ "/workspace/tests": [dirent("orders_test.py")],
767
+ };
768
+ const entries = listing[dir] ?? [];
769
+ return options?.withFileTypes ? entries : entries.map((e) => e.name);
770
+ });
771
+ const service = new TestExecutionService();
772
+ await service.executeTest({
773
+ testFile: "/workspace/tests/orders_test.py",
774
+ workspacePath: "/workspace",
775
+ testRepoPath: "/test-repo",
776
+ language: "python",
777
+ testType: "integration",
778
+ });
779
+ const command = mockRun.mock.calls[0][1];
780
+ expect(command).toContain("/home/user/tests/orders_test.py");
781
+ const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
782
+ expect(mounts.filter((m) => m.Target.includes(TEST_REPO_CONTAINER_SUBDIR))).toHaveLength(0);
783
+ });
784
+ });
785
+ // Copilot review on PR #623: guard hazardous or stale cross-repo roots.
786
+ describe("TestExecutionService.executeTest - cross-repo root guards", () => {
787
+ let TestExecutionService;
788
+ let EXECUTOR_DOCKER_IMAGE;
789
+ beforeAll(async () => {
790
+ const mod = await import("./TestExecutionService.js");
791
+ TestExecutionService = mod.TestExecutionService;
792
+ EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
793
+ });
794
+ beforeEach(() => {
795
+ jest.clearAllMocks();
796
+ mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
797
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
798
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
799
+ });
800
+ afterEach(() => {
801
+ mockAccessSync.mockReset();
802
+ });
803
+ it("rejects a filesystem-root testRepoPath instead of mounting the host root", async () => {
804
+ const service = new TestExecutionService();
805
+ await expect(service.executeTest({
806
+ testFile: "/outside/tests/orders_test.py",
807
+ workspacePath: "/workspace",
808
+ testRepoPath: "/",
809
+ language: "python",
810
+ testType: "integration",
811
+ })).rejects.toThrow(/filesystem root/);
812
+ expect(mockRun).not.toHaveBeenCalled();
813
+ });
814
+ it("throws an actionable error when the cross-repo test repository is missing or unreadable", async () => {
815
+ mockAccessSync.mockImplementation((p) => {
816
+ if (String(p).startsWith("/test-repo")) {
817
+ throw new Error("EACCES");
818
+ }
819
+ });
820
+ const service = new TestExecutionService();
821
+ await expect(service.executeTest({
822
+ testFile: "/test-repo/tests/orders_test.py",
823
+ workspacePath: "/workspace",
824
+ testRepoPath: "/test-repo",
825
+ language: "python",
826
+ testType: "integration",
827
+ })).rejects.toThrow(/test repository path does not exist or is not readable/);
828
+ expect(mockRun).not.toHaveBeenCalled();
829
+ });
830
+ });
831
+ describe("findNestedNodeModules", () => {
832
+ let findNestedNodeModules;
833
+ beforeAll(async () => {
834
+ const mod = await import("./TestExecutionService.js");
835
+ findNestedNodeModules = mod.findNestedNodeModules;
836
+ });
837
+ beforeEach(() => {
838
+ // These tests need real directory traversal — delegate the readdirSync
839
+ // mock (used by TestExecutionService.ts's `fs` binding) back to the real
840
+ // fs so it reflects the on-disk tree built by makeTree() below.
841
+ mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
842
+ });
843
+ const relSorted = (root, abs) => abs.map((p) => path.relative(root, p).replace(/\\/g, "/")).sort();
844
+ it("returns nested node_modules but not the top-level one", () => {
845
+ const root = makeTree({
846
+ "node_modules/playwright/index.js": "x",
847
+ "tests/ui-testing/node_modules/@playwright/test/index.js": "x",
848
+ "tests/ui-testing/pages/dashboard-list.js": "x",
849
+ "packages/web/node_modules/react/index.js": "x",
850
+ });
851
+ expect(relSorted(root, findNestedNodeModules(root))).toEqual([
852
+ "packages/web/node_modules",
853
+ "tests/ui-testing/node_modules",
854
+ ]);
855
+ });
856
+ it("never descends into a node_modules (no nested-inside-node_modules hits)", () => {
857
+ const root = makeTree({
858
+ "tests/node_modules/foo/node_modules/bar/index.js": "x",
859
+ });
860
+ expect(relSorted(root, findNestedNodeModules(root))).toEqual([
861
+ "tests/node_modules",
862
+ ]);
863
+ });
864
+ it("returns empty when there are no nested node_modules", () => {
865
+ const root = makeTree({ "node_modules/a/index.js": "x", "src/app.js": "x" });
866
+ expect(findNestedNodeModules(root)).toEqual([]);
867
+ });
868
+ });
869
+ describe("TestExecutionService.executeTest - nested node_modules shadow + NODE_PATH wrap", () => {
870
+ let TestExecutionService;
871
+ let EXECUTOR_DOCKER_IMAGE;
872
+ beforeAll(async () => {
873
+ const mod = await import("./TestExecutionService.js");
874
+ TestExecutionService = mod.TestExecutionService;
875
+ EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
876
+ });
877
+ beforeEach(() => {
878
+ jest.clearAllMocks();
879
+ mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
880
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
881
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
882
+ // Real directory traversal so findNestedNodeModules / the workspace-mount
883
+ // scan see the on-disk tree built by makeTree() below (same delegation as
884
+ // the findNestedNodeModules describe block above).
885
+ mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
886
+ // Real existsSync too, so the production `if (!fs.existsSync(pkgPath))
887
+ // continue` package-existence gate is genuinely exercised against the
888
+ // on-disk tree instead of the file-level always-true default.
889
+ mockExistsSync.mockImplementation((...args) => actualFs.existsSync(...args));
890
+ });
891
+ afterEach(() => {
892
+ // Restore the file-level always-true default for describes that follow.
893
+ mockExistsSync.mockReturnValue(true);
894
+ });
895
+ it("shadows a leaked nested @playwright/test and wraps the run command with NODE_PATH", async () => {
896
+ const root = makeTree({
897
+ "tests/node_modules/@playwright/test/index.js": "module.exports = {};",
898
+ "tests/sample.spec.ts": "test('noop', () => {});",
899
+ });
900
+ const service = new TestExecutionService();
901
+ await service.executeTest({
902
+ testFile: path.join(root, "tests/sample.spec.ts"),
903
+ workspacePath: root,
904
+ language: "typescript",
905
+ testType: "smoke",
906
+ });
907
+ expect(mockRun).toHaveBeenCalledTimes(1);
908
+ const dockerOptions = mockRun.mock.calls[0][3];
909
+ const mounts = dockerOptions.HostConfig.Mounts;
910
+ const shadowMount = mounts.find((m) => m.ReadOnly && m.Target.endsWith("tests/node_modules/@playwright/test"));
911
+ expect(shadowMount).toBeDefined();
912
+ // The fixture only creates a nested @playwright/test — no
913
+ // @skyramp/skyramp package exists on disk, so the existence gate
914
+ // (`if (!fs.existsSync(pkgPath)) continue`) must skip it. Asserting its
915
+ // absence here only proves the gate is live because existsSync is now
916
+ // delegated to the real fs above (it would false-pass against the
917
+ // always-true mock default).
918
+ const nonexistentPkgShadow = mounts.find((m) => m.Target.endsWith("tests/node_modules/@skyramp/skyramp"));
919
+ expect(nonexistentPkgShadow).toBeUndefined();
920
+ const command = mockRun.mock.calls[0][1];
921
+ expect(command.slice(0, 2)).toEqual(["bash", "-c"]);
922
+ expect(command[2]).toContain("NODE_PATH");
923
+ expect(command[2]).toContain("/root/runner.sh");
924
+ });
925
+ });
926
+ describe("TestExecutionService.executeTest - cross-repo context nested node_modules shadow", () => {
927
+ let TestExecutionService;
928
+ let TEST_REPO_CONTAINER_SUBDIR;
929
+ let EXECUTOR_DOCKER_IMAGE;
930
+ beforeAll(async () => {
931
+ const mod = await import("./TestExecutionService.js");
932
+ TestExecutionService = mod.TestExecutionService;
933
+ TEST_REPO_CONTAINER_SUBDIR = mod.TEST_REPO_CONTAINER_SUBDIR;
934
+ EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
935
+ });
936
+ beforeEach(() => {
937
+ jest.clearAllMocks();
938
+ mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
939
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
940
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
941
+ // Real directory traversal so findNestedNodeModules sees the on-disk tree
942
+ // built by makeTree() below (same delegation as the sibling
943
+ // workspace-context describe block above).
944
+ mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
945
+ // Real existsSync too, so the production `if (!fs.existsSync(pkgPath))
946
+ // continue` package-existence gate is genuinely exercised against the
947
+ // on-disk tree instead of the file-level always-true default.
948
+ mockExistsSync.mockImplementation((...args) => actualFs.existsSync(...args));
949
+ });
950
+ afterEach(() => {
951
+ // Restore the file-level always-true default for describes that follow.
952
+ mockExistsSync.mockReturnValue(true);
953
+ });
954
+ it("shadows a leaked nested @playwright/test in the cross-repo test root, mirroring at the host path unconditionally", async () => {
955
+ const workspaceRoot = makeTree({});
956
+ const testRepoRoot = makeTree({
957
+ "e2e/node_modules/@playwright/test/index.js": "module.exports = {};",
958
+ "e2e/sample.spec.ts": "test('noop', () => {});",
959
+ });
960
+ const service = new TestExecutionService();
961
+ await service.executeTest({
962
+ testFile: path.join(testRepoRoot, "e2e/sample.spec.ts"),
963
+ workspacePath: workspaceRoot,
964
+ testRepoPath: testRepoRoot,
965
+ language: "typescript",
966
+ testType: "smoke",
967
+ });
968
+ expect(mockRun).toHaveBeenCalledTimes(1);
969
+ const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
970
+ // Container-side shadow lives under the reserved test-repo subdir.
971
+ const containerShadow = mounts.find((m) => m.ReadOnly &&
972
+ m.Target.includes(TEST_REPO_CONTAINER_SUBDIR) &&
973
+ m.Target.endsWith("e2e/node_modules/@playwright/test"));
974
+ expect(containerShadow).toBeDefined();
975
+ // Cross-repo mounts always mirror at the host-absolute path too (unlike
976
+ // the workspace context, where mirroring is conditional on
977
+ // workspacePath !== containerMountPath) — assert that unconditional
978
+ // mirror explicitly, since it's what distinguishes this context.
979
+ const hostPkgPath = path.join(testRepoRoot, "e2e/node_modules/@playwright/test");
980
+ const hostMirrorShadow = mounts.find((m) => m.ReadOnly && m.Target === hostPkgPath);
981
+ expect(hostMirrorShadow).toBeDefined();
982
+ expect(hostMirrorShadow.Source).toBe(containerShadow.Source);
983
+ });
984
+ });
985
+ describe("TestExecutionService.executeTest - root-level Playwright config shadow", () => {
986
+ let TestExecutionService;
987
+ let EXECUTOR_DOCKER_IMAGE;
988
+ beforeAll(async () => {
989
+ const mod = await import("./TestExecutionService.js");
990
+ TestExecutionService = mod.TestExecutionService;
991
+ EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
992
+ });
993
+ beforeEach(() => {
994
+ jest.clearAllMocks();
995
+ mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
996
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
997
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
998
+ // Real directory traversal so the entry-mount filter and the
999
+ // PLAYWRIGHT_CONFIG_FILES shadow scan see the on-disk tree built by
1000
+ // makeTree() below (same delegation as the sibling shadow describes above).
1001
+ mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
1002
+ mockExistsSync.mockImplementation((...args) => actualFs.existsSync(...args));
1003
+ });
1004
+ afterEach(() => {
1005
+ // Restore the file-level always-true default for describes that follow.
1006
+ mockExistsSync.mockReturnValue(true);
1007
+ });
1008
+ // Regression test: a root-level playwright.config.js used to win the
1009
+ // pushMount target-dedup race over its minimal-config shadow, because the
1010
+ // entry-mount filter excluded EXCLUDED_MOUNT_ITEMS/MOUNT_NULL_ITEMS but not
1011
+ // PLAYWRIGHT_CONFIG_FILES — so the repo's own root config (which may import
1012
+ // dotenv, or simply isn't the video-enabled config the executor expects)
1013
+ // leaked into the container instead of being shadowed.
1014
+ it("shadows a root-level playwright.config.js with the minimal/generated config instead of the workspace's own file", async () => {
1015
+ const root = makeTree({
1016
+ "playwright.config.js": "module.exports = { testDir: './tests' };",
1017
+ "tests/sample.spec.ts": "test('noop', () => {});",
1018
+ });
1019
+ const service = new TestExecutionService();
1020
+ await service.executeTest({
1021
+ testFile: path.join(root, "tests/sample.spec.ts"),
1022
+ workspacePath: root,
1023
+ language: "typescript",
1024
+ testType: "smoke",
1025
+ });
1026
+ expect(mockRun).toHaveBeenCalledTimes(1);
1027
+ const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
1028
+ const configMount = mounts.find((m) => m.Target === path.join("/home/user", "playwright.config.js"));
1029
+ expect(configMount).toBeDefined();
1030
+ expect(configMount.Source).not.toBe(path.join(root, "playwright.config.js"));
1031
+ });
1032
+ });
@@ -2,7 +2,7 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import { SkyrampClient } from "@skyramp/skyramp";
4
4
  import { analyzeOpenAPIWithGivenEndpoint } from "../utils/analyze-openapi.js";
5
- import { isAuthorizationHeaderName, KNOWN_AUTH_HEADERS, getWorkspaceAuthConfig, getDefaultAuthHeader, getAuthScheme, WorkspaceAuthType, } from "../utils/workspaceAuth.js";
5
+ import { isAuthorizationHeaderName, KNOWN_AUTH_HEADERS, getWorkspaceAuthConfig, getDefaultAuthHeader, getAuthScheme, WorkspaceAuthType, getWorkspaceSkipTLSVerify, } from "../utils/workspaceAuth.js";
6
6
  import { getPathParameterValidationError, OUTPUT_DIR_FIELD_NAME, PATH_PARAMS_FIELD_NAME, QUERY_PARAMS_FIELD_NAME, FORM_PARAMS_FIELD_NAME, validateParams, validatePath, validateRequestData, } from "../utils/utils.js";
7
7
  import { getEntryPoint } from "../utils/telemetry.js";
8
8
  import { getLanguageSteps } from "../utils/language-helper.js";
@@ -346,6 +346,24 @@ The generated test file remains unchanged and ready to use as-is.
346
346
  logger.warning("Could not resolve auth from workspace config");
347
347
  }
348
348
  }
349
+ // Workspace-declared self-signed TLS (SKYR-3961): api.skipTLSVerify maps
350
+ // onto the generation pipeline's generateInsecure option (the tool param
351
+ // `insecure` is renamed by buildBaseGenerationOptions; SkyrampClient reads
352
+ // generateInsecure). The tool schema defaults insecure to false, so
353
+ // "false" is indistinguishable from "unspecified" — a workspace
354
+ // declaration only ever turns it ON, never off.
355
+ if (generateOptions.generateInsecure !== true) {
356
+ try {
357
+ const repoPath = generateOptions.outputDir || process.cwd();
358
+ if (await getWorkspaceSkipTLSVerify(repoPath)) {
359
+ logger.info("generateInsecure enabled from workspace skipTLSVerify (self-signed TLS SUT)");
360
+ generateOptions.generateInsecure = true;
361
+ }
362
+ }
363
+ catch {
364
+ logger.warning("Could not resolve skipTLSVerify from workspace config");
365
+ }
366
+ }
349
367
  if (generateOptions.traceFilePath) {
350
368
  const traceAuth = this.extractAuthFromTrace(generateOptions.traceFilePath, generateOptions.generateInclude, generateOptions.generateExclude);
351
369
  if (traceAuth) {
@@ -518,3 +518,61 @@ describe("TestGenerationService — trace-based auth in executeGeneration", () =
518
518
  expect(callArgs.authScheme).toBeUndefined();
519
519
  });
520
520
  });
521
+ describe("TestGenerationService — workspace skipTLSVerify auto-resolve (SKYR-3961)", () => {
522
+ let mockGenerateRestTest;
523
+ let wsDir;
524
+ beforeEach(() => {
525
+ mockGenerateRestTest = jest.fn().mockResolvedValue("Test generation success");
526
+ SkyrampClient.mockImplementation(() => ({
527
+ generateRestTest: mockGenerateRestTest,
528
+ }));
529
+ wsDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-ws-"));
530
+ });
531
+ afterEach(() => {
532
+ fs.rmSync(wsDir, { recursive: true, force: true });
533
+ });
534
+ function writeWorkspace(api) {
535
+ const dir = path.join(wsDir, ".skyramp");
536
+ fs.mkdirSync(dir, { recursive: true });
537
+ fs.writeFileSync(path.join(dir, "workspace.yml"), [
538
+ "workspace:",
539
+ " repoName: org/sut",
540
+ "services:",
541
+ " - serviceName: sut",
542
+ " api:",
543
+ ...Object.entries(api).map(([k, v]) => ` ${k}: ${v}`),
544
+ ].join("\n"), "utf8");
545
+ }
546
+ it("sets insecure from workspace when the caller did not request it", async () => {
547
+ writeWorkspace({ baseUrl: "https://127.0.0.1:45643", skipTLSVerify: true });
548
+ const svc = new StubService();
549
+ svc.buildGenerationOptions = () => ({
550
+ authHeader: "",
551
+ authScheme: "",
552
+ outputDir: wsDir,
553
+ language: "python",
554
+ framework: "pytest",
555
+ });
556
+ svc.handleApiAnalysis = async () => null;
557
+ await svc.generateTest({ ...BASE });
558
+ const callArgs = mockGenerateRestTest.mock.calls[0][0];
559
+ // generateInsecure is the key SkyrampClient reads (buildBaseGenerationOptions
560
+ // maps the tool param `insecure` onto it).
561
+ expect(callArgs.generateInsecure).toBe(true);
562
+ });
563
+ it("leaves insecure unset when the workspace does not declare it", async () => {
564
+ writeWorkspace({ baseUrl: "https://127.0.0.1:45643" });
565
+ const svc = new StubService();
566
+ svc.buildGenerationOptions = () => ({
567
+ authHeader: "",
568
+ authScheme: "",
569
+ outputDir: wsDir,
570
+ language: "python",
571
+ framework: "pytest",
572
+ });
573
+ svc.handleApiAnalysis = async () => null;
574
+ await svc.generateTest({ ...BASE });
575
+ const callArgs = mockGenerateRestTest.mock.calls[0][0];
576
+ expect(callArgs.generateInsecure).not.toBe(true);
577
+ });
578
+ });
@@ -1,6 +1,7 @@
1
1
  export const TOOL_PHASE_MAP = {
2
2
  skyramp_recommend_tests: "analyzing",
3
3
  skyramp_analyze_changes: "analyzing",
4
+ skyramp_register_test_plan: "analyzing",
4
5
  skyramp_smoke_test_generation: "generating",
5
6
  skyramp_fuzz_test_generation: "generating",
6
7
  skyramp_contract_test_generation: "generating",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Canonical MCP tool-name constants for tools that are referenced OUTSIDE
3
+ * their own module — e.g. the register-plan checkpoint names the generation
4
+ * tools in its returned plan text, and the plan guard's rejection messages
5
+ * reference the register tool.
6
+ *
7
+ * Lives in a leaf module (no imports) so any tool can reference another
8
+ * tool's name without pulling in that tool's module graph — the generation
9
+ * tools import planGuard, so planGuard importing a generation tool's local
10
+ * TOOL_NAME back would be an import cycle.
11
+ *
12
+ * Keep in sync with TOOL_PHASE_MAP (src/tool-phases.ts) when renaming.
13
+ */
14
+ export declare const TOOL_CONTRACT_TEST_GENERATION = "skyramp_contract_test_generation";
15
+ export declare const TOOL_BATCH_SCENARIO_TEST_GENERATION = "skyramp_batch_scenario_test_generation";
16
+ export declare const TOOL_INTEGRATION_TEST_GENERATION = "skyramp_integration_test_generation";
17
+ export declare const TOOL_E2E_TEST_GENERATION = "skyramp_e2e_test_generation";
18
+ export declare const TOOL_UI_TEST_GENERATION = "skyramp_ui_test_generation";
19
+ export declare const TOOL_REGISTER_TEST_PLAN = "skyramp_register_test_plan";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Canonical MCP tool-name constants for tools that are referenced OUTSIDE
3
+ * their own module — e.g. the register-plan checkpoint names the generation
4
+ * tools in its returned plan text, and the plan guard's rejection messages
5
+ * reference the register tool.
6
+ *
7
+ * Lives in a leaf module (no imports) so any tool can reference another
8
+ * tool's name without pulling in that tool's module graph — the generation
9
+ * tools import planGuard, so planGuard importing a generation tool's local
10
+ * TOOL_NAME back would be an import cycle.
11
+ *
12
+ * Keep in sync with TOOL_PHASE_MAP (src/tool-phases.ts) when renaming.
13
+ */
14
+ export const TOOL_CONTRACT_TEST_GENERATION = "skyramp_contract_test_generation";
15
+ export const TOOL_BATCH_SCENARIO_TEST_GENERATION = "skyramp_batch_scenario_test_generation";
16
+ export const TOOL_INTEGRATION_TEST_GENERATION = "skyramp_integration_test_generation";
17
+ export const TOOL_E2E_TEST_GENERATION = "skyramp_e2e_test_generation";
18
+ export const TOOL_UI_TEST_GENERATION = "skyramp_ui_test_generation";
19
+ export const TOOL_REGISTER_TEST_PLAN = "skyramp_register_test_plan";
@@ -1,2 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type ScopedPom } from "../../utils/pom-scope/index.js";
3
+ import { type VerifyResult } from "../../utils/pom-verify/index.js";
4
+ /** Renders a `VerifyResult` (from `verifyReuse`) as the text report returned to the agent. */
5
+ export declare function formatVerifyReport(r: VerifyResult): string;
6
+ /** Renders the zero-reuse compliance-gate failure: a spec with 0 verified POM calls whose raw
7
+ * selectors nonetheless overlap tier-1 POM candidates — i.e. reuse was skipped, not unavailable. */
8
+ export declare function formatZeroReuseGateFailure(tier1: ScopedPom[]): string;
2
9
  export declare function registerCodeReuseTool(server: McpServer): void;