@skyramp/mcp 0.3.2-rc.pom-3 → 0.3.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 (289) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +19 -13
  3. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  4. package/build/commands/localDevTestChangesCommand.js +201 -0
  5. package/build/index.js +80 -6
  6. package/build/prompts/code-reuse.js +3 -0
  7. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  8. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  9. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
  10. package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
  11. package/build/prompts/local-dev/local-dev-plan.js +429 -0
  12. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  13. package/build/prompts/local-dev/local-dev-prompts.js +190 -0
  14. package/build/prompts/pom-aware-code-reuse.js +12 -0
  15. package/build/prompts/prompt-utils.d.ts +8 -0
  16. package/build/prompts/prompt-utils.js +33 -0
  17. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +37 -14
  18. package/build/prompts/sut-setup/shared.js +16 -12
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  20. package/build/prompts/test-recommendation/scopeAssessment.d.ts +24 -7
  21. package/build/prompts/test-recommendation/scopeAssessment.js +111 -12
  22. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  23. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  24. package/build/prompts/testbot/testbot-prompts.js +32 -52
  25. package/build/recommendation/planRanker.d.ts +15 -2
  26. package/build/recommendation/planRanker.js +76 -5
  27. package/build/resources/testbotResource.js +2 -1
  28. package/build/services/AnalyticsService.d.ts +1 -1
  29. package/build/services/TestExecutionService.d.ts +2 -1
  30. package/build/services/TestExecutionService.js +8 -3
  31. package/build/services/TestGenerationService.d.ts +2 -2
  32. package/build/services/TestGenerationService.js +39 -21
  33. package/build/services/containerEnv.js +3 -1
  34. package/build/tool-phases.js +6 -0
  35. package/build/tools/code-refactor/codeReuseTool.js +43 -4
  36. package/build/tools/code-refactor/enhanceAssertionsTool.js +68 -18
  37. package/build/tools/code-refactor/reuse-outcome.d.ts +109 -0
  38. package/build/tools/code-refactor/reuse-outcome.js +158 -0
  39. package/build/tools/code-refactor/reuse-state.d.ts +45 -0
  40. package/build/tools/code-refactor/reuse-state.js +140 -0
  41. package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
  42. package/build/tools/enrichTestWithMocksTool.js +726 -0
  43. package/build/tools/executeSkyrampTestTool.d.ts +11 -0
  44. package/build/tools/executeSkyrampTestTool.js +62 -21
  45. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +106 -0
  46. package/build/tools/generate-tests/batchMockGenerationTool.js +545 -0
  47. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +25 -23
  48. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  49. package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -1
  50. package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
  51. package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -1
  52. package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
  53. package/build/tools/generate-tests/generateMockRestTool.d.ts +179 -6
  54. package/build/tools/generate-tests/generateMockRestTool.js +391 -22
  55. package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
  56. package/build/tools/generate-tests/planGuard.js +2 -22
  57. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
  58. package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
  59. package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
  60. package/build/tools/localDevWorkerComposeTool.js +264 -0
  61. package/build/tools/one-click/oneClickTool.d.ts +13 -0
  62. package/build/tools/one-click/oneClickTool.js +195 -24
  63. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  64. package/build/tools/preflightMockCheckTool.js +96 -0
  65. package/build/tools/queryProxyMocksTool.d.ts +70 -0
  66. package/build/tools/queryProxyMocksTool.js +522 -0
  67. package/build/tools/runExistingTestsTool.d.ts +31 -0
  68. package/build/tools/runExistingTestsTool.js +214 -13
  69. package/build/tools/submitReportTool.js +38 -3
  70. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  71. package/build/tools/test-management/analyzeChangesTool.js +63 -40
  72. package/build/tools/test-management/registerTestPlanTool.js +55 -7
  73. package/build/tools/trace/startTraceCollectionTool.js +3 -3
  74. package/build/types/FrontendIntegration.d.ts +3 -0
  75. package/build/types/FrontendIntegration.js +3 -0
  76. package/build/types/OneClickCommands.d.ts +1 -1
  77. package/build/types/Recommendation.d.ts +20 -0
  78. package/build/types/Recommendation.js +32 -6
  79. package/build/types/RepositoryAnalysis.d.ts +131 -14
  80. package/build/types/RepositoryAnalysis.js +16 -2
  81. package/build/types/ReuseOutcome.d.ts +63 -0
  82. package/build/types/ReuseOutcome.js +33 -0
  83. package/build/types/TestExecution.d.ts +1 -0
  84. package/build/types/TestTypes.d.ts +25 -7
  85. package/build/types/TestTypes.js +22 -6
  86. package/build/types/TestbotReport.d.ts +7 -0
  87. package/build/types/index.d.ts +2 -0
  88. package/build/types/index.js +1 -0
  89. package/build/utils/AnalysisStateManager.d.ts +26 -0
  90. package/build/utils/AnalysisStateManager.js +31 -1
  91. package/build/utils/analyze-openapi.js +18 -1
  92. package/build/utils/branchDiff.d.ts +17 -1
  93. package/build/utils/branchDiff.js +99 -14
  94. package/build/utils/featureFlags.d.ts +31 -0
  95. package/build/utils/featureFlags.js +37 -0
  96. package/build/utils/frontendIntegration.js +8 -1
  97. package/build/utils/grpcMockValidation.d.ts +1 -0
  98. package/build/utils/grpcMockValidation.js +49 -0
  99. package/build/utils/httpMethodValidation.d.ts +4 -0
  100. package/build/utils/httpMethodValidation.js +15 -0
  101. package/build/utils/logger.js +1 -1
  102. package/build/utils/mockCompatibility.d.ts +49 -0
  103. package/build/utils/mockCompatibility.js +82 -0
  104. package/build/utils/pom-scope/pom-files.js +7 -0
  105. package/build/utils/pom-verify/bindings.js +11 -1
  106. package/build/utils/pom-verify/re-exports.d.ts +10 -0
  107. package/build/utils/pom-verify/re-exports.js +61 -0
  108. package/build/utils/pom-verify/resolve.d.ts +4 -1
  109. package/build/utils/pom-verify/resolve.js +7 -4
  110. package/build/utils/pom-verify/verify.d.ts +5 -0
  111. package/build/utils/pom-verify/verify.js +25 -3
  112. package/build/utils/progress.js +10 -5
  113. package/build/utils/proxy-terminal.js +3 -3
  114. package/build/utils/routeParsers.d.ts +3 -9
  115. package/build/utils/routeParsers.js +79 -4
  116. package/build/utils/utils.js +2 -2
  117. package/build/utils/versions.d.ts +4 -3
  118. package/build/utils/versions.js +3 -1
  119. package/build/utils/workspaceAuth.d.ts +46 -0
  120. package/build/utils/workspaceAuth.js +156 -1
  121. package/build/workspace/testSuites.d.ts +2 -1
  122. package/build/workspace/testSuites.js +1 -1
  123. package/build/workspace/workspace.d.ts +108 -32
  124. package/build/workspace/workspace.js +32 -4
  125. package/package.json +5 -2
  126. package/build/adapters/jestAdapter.test.d.ts +0 -1
  127. package/build/adapters/jestAdapter.test.js +0 -93
  128. package/build/adapters/mochaAdapter.test.d.ts +0 -1
  129. package/build/adapters/mochaAdapter.test.js +0 -63
  130. package/build/adapters/playwrightAdapter.test.d.ts +0 -1
  131. package/build/adapters/playwrightAdapter.test.js +0 -169
  132. package/build/adapters/pytestAdapter.test.d.ts +0 -1
  133. package/build/adapters/pytestAdapter.test.js +0 -90
  134. package/build/prompts/code-reuse.test.d.ts +0 -1
  135. package/build/prompts/code-reuse.test.js +0 -62
  136. package/build/prompts/pom-aware-code-reuse.test.d.ts +0 -1
  137. package/build/prompts/pom-aware-code-reuse.test.js +0 -11
  138. package/build/prompts/test-maintenance/drift-analysis-prompt.test.d.ts +0 -1
  139. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +0 -51
  140. package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +0 -1
  141. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -264
  142. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +0 -1
  143. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +0 -126
  144. package/build/prompts/test-recommendation/promptPlan.test.d.ts +0 -1
  145. package/build/prompts/test-recommendation/promptPlan.test.js +0 -336
  146. package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +0 -1
  147. package/build/prompts/test-recommendation/scopeAssessment.test.js +0 -371
  148. package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +0 -1
  149. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +0 -1925
  150. package/build/prompts/testbot/testbot-prompts.test.d.ts +0 -1
  151. package/build/prompts/testbot/testbot-prompts.test.js +0 -451
  152. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +0 -1
  153. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +0 -138
  154. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +0 -1
  155. package/build/recommendation/budgeters/fixedNBudgeter.test.js +0 -66
  156. package/build/recommendation/discriminators.test.d.ts +0 -1
  157. package/build/recommendation/discriminators.test.js +0 -324
  158. package/build/recommendation/diversity.test.d.ts +0 -1
  159. package/build/recommendation/diversity.test.js +0 -77
  160. package/build/recommendation/planRanker.test.d.ts +0 -1
  161. package/build/recommendation/planRanker.test.js +0 -110
  162. package/build/resources/testbotResource.test.d.ts +0 -1
  163. package/build/resources/testbotResource.test.js +0 -44
  164. package/build/services/AnalyticsService.test.d.ts +0 -1
  165. package/build/services/AnalyticsService.test.js +0 -86
  166. package/build/services/ScenarioGenerationService.integration.test.d.ts +0 -1
  167. package/build/services/ScenarioGenerationService.integration.test.js +0 -162
  168. package/build/services/ScenarioGenerationService.test.d.ts +0 -1
  169. package/build/services/ScenarioGenerationService.test.js +0 -414
  170. package/build/services/TestDiscoveryService.test.d.ts +0 -1
  171. package/build/services/TestDiscoveryService.test.js +0 -983
  172. package/build/services/TestExecutionService.test.d.ts +0 -1
  173. package/build/services/TestExecutionService.test.js +0 -1032
  174. package/build/services/TestGenerationService.test.d.ts +0 -1
  175. package/build/services/TestGenerationService.test.js +0 -578
  176. package/build/tool-phase-coverage.test.d.ts +0 -1
  177. package/build/tool-phase-coverage.test.js +0 -49
  178. package/build/tools/code-refactor/codeReuseTool.test.d.ts +0 -1
  179. package/build/tools/code-refactor/codeReuseTool.test.js +0 -572
  180. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +0 -1
  181. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +0 -433
  182. package/build/tools/generate-tests/generateContractRestTool.test.d.ts +0 -1
  183. package/build/tools/generate-tests/generateContractRestTool.test.js +0 -142
  184. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +0 -1
  185. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +0 -159
  186. package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +0 -1
  187. package/build/tools/generate-tests/generateLoadRestTool.test.js +0 -169
  188. package/build/tools/generate-tests/generateUIRestTool.test.d.ts +0 -1
  189. package/build/tools/generate-tests/generateUIRestTool.test.js +0 -32
  190. package/build/tools/generate-tests/planGuard.test.d.ts +0 -1
  191. package/build/tools/generate-tests/planGuard.test.js +0 -185
  192. package/build/tools/generate-tests/scenarioLint.test.d.ts +0 -1
  193. package/build/tools/generate-tests/scenarioLint.test.js +0 -100
  194. package/build/tools/runExistingTestsTool.test.d.ts +0 -1
  195. package/build/tools/runExistingTestsTool.test.js +0 -379
  196. package/build/tools/submitReportTool.test.d.ts +0 -1
  197. package/build/tools/submitReportTool.test.js +0 -1428
  198. package/build/tools/test-management/actionsTool.test.d.ts +0 -1
  199. package/build/tools/test-management/actionsTool.test.js +0 -436
  200. package/build/tools/test-management/analyzeChangesTool.test.d.ts +0 -1
  201. package/build/tools/test-management/analyzeChangesTool.test.js +0 -522
  202. package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +0 -1
  203. package/build/tools/test-management/analyzeTestHealthTool.test.js +0 -385
  204. package/build/tools/test-management/registerTestPlanTool.test.d.ts +0 -1
  205. package/build/tools/test-management/registerTestPlanTool.test.js +0 -296
  206. package/build/tools/trace/resolveSaveStoragePath.test.d.ts +0 -1
  207. package/build/tools/trace/resolveSaveStoragePath.test.js +0 -17
  208. package/build/tools/trace/resolveSessionPaths.test.d.ts +0 -1
  209. package/build/tools/trace/resolveSessionPaths.test.js +0 -104
  210. package/build/tools/trace/sessionState.test.d.ts +0 -1
  211. package/build/tools/trace/sessionState.test.js +0 -17
  212. package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +0 -1
  213. package/build/tools/workspace/initializeWorkspaceTool.test.js +0 -139
  214. package/build/tools/workspace/serviceUpsert.test.d.ts +0 -1
  215. package/build/tools/workspace/serviceUpsert.test.js +0 -50
  216. package/build/utils/AnalysisStateManager.test.d.ts +0 -1
  217. package/build/utils/AnalysisStateManager.test.js +0 -134
  218. package/build/utils/dartRouteExtractor.test.d.ts +0 -1
  219. package/build/utils/dartRouteExtractor.test.js +0 -307
  220. package/build/utils/docker.test.d.ts +0 -1
  221. package/build/utils/docker.test.js +0 -114
  222. package/build/utils/featureFlags.test.d.ts +0 -1
  223. package/build/utils/featureFlags.test.js +0 -81
  224. package/build/utils/fileWalk.test.d.ts +0 -1
  225. package/build/utils/fileWalk.test.js +0 -252
  226. package/build/utils/frontendIntegration.test.d.ts +0 -1
  227. package/build/utils/frontendIntegration.test.js +0 -229
  228. package/build/utils/frontendSelectors.test.d.ts +0 -1
  229. package/build/utils/frontendSelectors.test.js +0 -118
  230. package/build/utils/gitStaging.test.d.ts +0 -1
  231. package/build/utils/gitStaging.test.js +0 -111
  232. package/build/utils/httpDefaults.test.d.ts +0 -1
  233. package/build/utils/httpDefaults.test.js +0 -21
  234. package/build/utils/importerHop.test.d.ts +0 -1
  235. package/build/utils/importerHop.test.js +0 -469
  236. package/build/utils/pathAffinityClassification.test.d.ts +0 -1
  237. package/build/utils/pathAffinityClassification.test.js +0 -208
  238. package/build/utils/planMatchKeys.test.d.ts +0 -1
  239. package/build/utils/planMatchKeys.test.js +0 -123
  240. package/build/utils/pom-scope/index.test.d.ts +0 -1
  241. package/build/utils/pom-scope/index.test.js +0 -278
  242. package/build/utils/pom-scope/pom-files.test.d.ts +0 -1
  243. package/build/utils/pom-scope/pom-files.test.js +0 -29
  244. package/build/utils/pom-scope/scoring.test.d.ts +0 -1
  245. package/build/utils/pom-scope/scoring.test.js +0 -39
  246. package/build/utils/pom-scope/selector-extractor.test.d.ts +0 -1
  247. package/build/utils/pom-scope/selector-extractor.test.js +0 -67
  248. package/build/utils/pom-verify/bindings.test.d.ts +0 -1
  249. package/build/utils/pom-verify/bindings.test.js +0 -164
  250. package/build/utils/pom-verify/calls.test.d.ts +0 -1
  251. package/build/utils/pom-verify/calls.test.js +0 -61
  252. package/build/utils/pom-verify/resolve.test.d.ts +0 -1
  253. package/build/utils/pom-verify/resolve.test.js +0 -114
  254. package/build/utils/pom-verify/verify.test.d.ts +0 -1
  255. package/build/utils/pom-verify/verify.test.js +0 -374
  256. package/build/utils/pr-comment-parser.test.d.ts +0 -1
  257. package/build/utils/pr-comment-parser.test.js +0 -428
  258. package/build/utils/progress.test.d.ts +0 -1
  259. package/build/utils/progress.test.js +0 -37
  260. package/build/utils/projectMetadata.test.d.ts +0 -1
  261. package/build/utils/projectMetadata.test.js +0 -172
  262. package/build/utils/pythonMountPrefixes.test.d.ts +0 -1
  263. package/build/utils/pythonMountPrefixes.test.js +0 -113
  264. package/build/utils/repoScanner.test.d.ts +0 -1
  265. package/build/utils/repoScanner.test.js +0 -190
  266. package/build/utils/reportVerification.test.d.ts +0 -1
  267. package/build/utils/reportVerification.test.js +0 -185
  268. package/build/utils/routeParsers.test.d.ts +0 -1
  269. package/build/utils/routeParsers.test.js +0 -1011
  270. package/build/utils/scenarioDrafting.test.d.ts +0 -1
  271. package/build/utils/scenarioDrafting.test.js +0 -876
  272. package/build/utils/sourceRouteExtractor.test.d.ts +0 -1
  273. package/build/utils/sourceRouteExtractor.test.js +0 -738
  274. package/build/utils/telemetry.test.d.ts +0 -1
  275. package/build/utils/telemetry.test.js +0 -70
  276. package/build/utils/trace-parser.test.d.ts +0 -6
  277. package/build/utils/trace-parser.test.js +0 -140
  278. package/build/utils/uiPageEnumerator.test.d.ts +0 -1
  279. package/build/utils/uiPageEnumerator.test.js +0 -824
  280. package/build/utils/utils.test.d.ts +0 -1
  281. package/build/utils/utils.test.js +0 -103
  282. package/build/utils/walkerCharacterization.test.d.ts +0 -1
  283. package/build/utils/walkerCharacterization.test.js +0 -233
  284. package/build/utils/workspaceAuth.test.d.ts +0 -1
  285. package/build/utils/workspaceAuth.test.js +0 -260
  286. package/build/workspace/testSuites.test.d.ts +0 -1
  287. package/build/workspace/testSuites.test.js +0 -23
  288. package/build/workspace/workspace.test.d.ts +0 -1
  289. package/build/workspace/workspace.test.js +0 -264
@@ -1,1032 +0,0 @@
1
- // @ts-nocheck - Jest ESM type inference issues
2
- import { jest } from "@jest/globals";
3
- import * as os from "os";
4
- import * as path from "path";
5
- import { buildContainerEnv, rewriteLocalhostForDocker } from "./containerEnv.js";
6
- // Mock dockerode before importing TestExecutionService
7
- const mockRun = jest.fn();
8
- const mockListImages = jest.fn();
9
- jest.unstable_mockModule("dockerode", () => ({
10
- default: jest.fn().mockImplementation(() => ({
11
- run: mockRun,
12
- listImages: mockListImages,
13
- })),
14
- }));
15
- // Get actual fs for spreading, then mock specific methods
16
- const actualFs = await import("fs");
17
- const mockAccessSync = jest.fn();
18
- const mockExistsSync = jest.fn().mockReturnValue(true);
19
- const mockReaddirSync = jest.fn().mockImplementation((_path, options) => {
20
- if (options?.withFileTypes) {
21
- return [{ name: "test_file.py", isFile: () => true, isDirectory: () => false }];
22
- }
23
- return ["test_file.py"];
24
- });
25
- const mockReadFileSync = jest.fn().mockReturnValue("");
26
- const mockWriteFileSync = jest.fn();
27
- const mockMkdirSync = jest.fn();
28
- const mockRmSync = jest.fn();
29
- jest.unstable_mockModule("fs", () => ({
30
- ...actualFs,
31
- default: {
32
- ...actualFs.default,
33
- accessSync: mockAccessSync,
34
- existsSync: mockExistsSync,
35
- readdirSync: mockReaddirSync,
36
- readFileSync: mockReadFileSync,
37
- writeFileSync: mockWriteFileSync,
38
- mkdirSync: mockMkdirSync,
39
- rmSync: mockRmSync,
40
- constants: actualFs.default.constants,
41
- },
42
- accessSync: mockAccessSync,
43
- existsSync: mockExistsSync,
44
- readdirSync: mockReaddirSync,
45
- readFileSync: mockReadFileSync,
46
- writeFileSync: mockWriteFileSync,
47
- mkdirSync: mockMkdirSync,
48
- rmSync: mockRmSync,
49
- }));
50
- // Mock logger
51
- jest.unstable_mockModule("../utils/logger.js", () => ({
52
- logger: {
53
- debug: jest.fn(),
54
- info: jest.fn(),
55
- error: jest.fn(),
56
- warning: jest.fn(),
57
- },
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
- });
85
- describe("rewriteLocalhostForDocker", () => {
86
- it("rewrites http://localhost:port", () => {
87
- expect(rewriteLocalhostForDocker("http://localhost:8000")).toBe("http://host.docker.internal:8000");
88
- });
89
- it("rewrites http://127.0.0.1:port", () => {
90
- expect(rewriteLocalhostForDocker("http://127.0.0.1:4200")).toBe("http://host.docker.internal:4200");
91
- });
92
- it("rewrites https://localhost:port", () => {
93
- expect(rewriteLocalhostForDocker("https://localhost:443")).toBe("https://host.docker.internal:443");
94
- });
95
- it("rewrites localhost without port", () => {
96
- expect(rewriteLocalhostForDocker("http://localhost/api")).toBe("http://host.docker.internal/api");
97
- });
98
- it("rewrites localhost with no path or port", () => {
99
- expect(rewriteLocalhostForDocker("http://localhost")).toBe("http://host.docker.internal");
100
- });
101
- it("does not rewrite non-localhost URLs", () => {
102
- expect(rewriteLocalhostForDocker("http://api.example.com:8000")).toBe("http://api.example.com:8000");
103
- });
104
- it("does not rewrite localhost in path", () => {
105
- expect(rewriteLocalhostForDocker("http://api.example.com/localhost")).toBe("http://api.example.com/localhost");
106
- });
107
- it("does not rewrite partial matches like localhost-extra", () => {
108
- expect(rewriteLocalhostForDocker("http://localhost-extra:8000")).toBe("http://localhost-extra:8000");
109
- });
110
- });
111
- describe("buildContainerEnv", () => {
112
- const baseOptions = { token: "test-token", language: "python" };
113
- const emptyHostEnv = {};
114
- it("always includes SKYRAMP_TEST_TOKEN and SKYRAMP_IN_DOCKER", () => {
115
- const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
116
- expect(env).toContain("SKYRAMP_TEST_TOKEN=test-token");
117
- expect(env).toContain("SKYRAMP_IN_DOCKER=true");
118
- });
119
- it("defaults token to empty string when not provided", () => {
120
- const env = buildContainerEnv({ language: "typescript" }, undefined, emptyHostEnv);
121
- expect(env).toContain("SKYRAMP_TEST_TOKEN=");
122
- });
123
- it("adds PYTEST_ADDOPTS for python language", () => {
124
- const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
125
- expect(env).toContain("PYTEST_ADDOPTS=--noconftest -c /dev/null");
126
- });
127
- it("does not add PYTEST_ADDOPTS for non-python language", () => {
128
- const env = buildContainerEnv({ ...baseOptions, language: "typescript" }, undefined, emptyHostEnv);
129
- expect(env.find((e) => e.startsWith("PYTEST_ADDOPTS="))).toBeUndefined();
130
- });
131
- it("adds PLAYWRIGHT_SAVE_STORAGE_PATH when saveStoragePath provided", () => {
132
- const env = buildContainerEnv(baseOptions, "/tmp/storage", emptyHostEnv);
133
- expect(env).toContain("PLAYWRIGHT_SAVE_STORAGE_PATH=/tmp/storage");
134
- });
135
- it("does not add PLAYWRIGHT_SAVE_STORAGE_PATH when not provided", () => {
136
- const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
137
- expect(env.find((e) => e.startsWith("PLAYWRIGHT_SAVE_STORAGE_PATH="))).toBeUndefined();
138
- });
139
- describe("base URL env var forwarding", () => {
140
- it("rewrites localhost to host.docker.internal in bridge mode (default)", () => {
141
- const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://localhost:8000" };
142
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
143
- expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://host.docker.internal:8000");
144
- });
145
- it("rewrites 127.0.0.1 to host.docker.internal in bridge mode", () => {
146
- const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://127.0.0.1:4200" };
147
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
148
- expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://host.docker.internal:4200");
149
- });
150
- it("preserves localhost when useHostNetwork is true", () => {
151
- const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://localhost:8000" };
152
- const env = buildContainerEnv({ ...baseOptions, useHostNetwork: true }, undefined, hostEnv);
153
- expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://localhost:8000");
154
- });
155
- it("does not rewrite non-localhost URLs", () => {
156
- const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://api.example.com:8000" };
157
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
158
- expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://api.example.com:8000");
159
- });
160
- it("rewrites SKYRAMP_TEST_SERVICE_URL_* vars in bridge mode", () => {
161
- const hostEnv = {
162
- SKYRAMP_TEST_SERVICE_URL_BACKEND: "http://localhost:8000",
163
- SKYRAMP_TEST_SERVICE_URL_FRONTEND: "http://localhost:5173",
164
- };
165
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
166
- expect(env).toContain("SKYRAMP_TEST_SERVICE_URL_BACKEND=http://host.docker.internal:8000");
167
- expect(env).toContain("SKYRAMP_TEST_SERVICE_URL_FRONTEND=http://host.docker.internal:5173");
168
- });
169
- it("skips SKYRAMP_TEST_BASE_URL when value is empty", () => {
170
- const hostEnv = { SKYRAMP_TEST_BASE_URL: "" };
171
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
172
- expect(env.find((e) => e.startsWith("SKYRAMP_TEST_BASE_URL="))).toBeUndefined();
173
- });
174
- it("does not forward unrelated SKYRAMP_ vars", () => {
175
- const hostEnv = {
176
- SKYRAMP_TEST_BASE_URL: "http://localhost:8000",
177
- SKYRAMP_OTHER_VAR: "should-not-appear",
178
- SKYRAMP_TEST_SERVICE_NOT_URL: "also-no",
179
- };
180
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
181
- expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://host.docker.internal:8000");
182
- expect(env.find((e) => e.includes("SKYRAMP_OTHER_VAR"))).toBeUndefined();
183
- expect(env.find((e) => e.includes("SKYRAMP_TEST_SERVICE_NOT_URL"))).toBeUndefined();
184
- });
185
- });
186
- it("forwards SKYRAMP_DEBUG from host env", () => {
187
- const hostEnv = { SKYRAMP_DEBUG: "true" };
188
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
189
- expect(env).toContain("SKYRAMP_DEBUG=true");
190
- });
191
- it("forwards API_KEY from host env", () => {
192
- const hostEnv = { API_KEY: "my-key" };
193
- const env = buildContainerEnv(baseOptions, undefined, hostEnv);
194
- expect(env).toContain("API_KEY=my-key");
195
- });
196
- });
197
- describe("detectSessionFiles", () => {
198
- // Import after mocks are set up so the fs mock applies
199
- let detectSessionFiles;
200
- beforeAll(async () => {
201
- const mod = await import("./TestExecutionService.js");
202
- detectSessionFiles = mod.detectSessionFiles;
203
- });
204
- it("detects string-literal storageState (TS/JS)", () => {
205
- mockReadFileSync.mockReturnValueOnce(`test.use({ storageState: '/abs/path/session.json' });`);
206
- expect(detectSessionFiles("/ws/test.spec.ts")).toEqual([
207
- "/abs/path/session.json",
208
- ]);
209
- });
210
- it("detects skyramp codegen path.join(__dirname, '<file>') pattern and resolves to host-absolute path", () => {
211
- // Reproduces SKYR-3321 generated test shape — must resolve to the host
212
- // absolute path so the executor's absolute-path mount branch makes the
213
- // file visible at that same path inside the container.
214
- mockReadFileSync.mockReturnValueOnce(`
215
- import path from 'path';
216
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
217
- const SESSION_STORAGE = path.join(__dirname, 'skyramp_session_storage.json');
218
- test.use({ storageState: SESSION_STORAGE });
219
- `);
220
- expect(detectSessionFiles("/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts")).toEqual([
221
- "/Users/pedro/projects/cisco-xdr-tests/skyramp_session_storage.json",
222
- ]);
223
- });
224
- it("detects inline storageState: path.join(__dirname, '<file>')", () => {
225
- mockReadFileSync.mockReturnValueOnce(`test.use({ storageState: path.join(__dirname, 'session.json') });`);
226
- expect(detectSessionFiles("/ws/spec.ts")).toEqual(["/ws/session.json"]);
227
- });
228
- it("ignores storageState references inside comments", () => {
229
- mockReadFileSync.mockReturnValueOnce(`
230
- // storageState: '/should/not/match.json'
231
- // path.join(__dirname, 'also-not.json')
232
- test('x', () => {});
233
- `);
234
- expect(detectSessionFiles("/ws/spec.ts")).toEqual([]);
235
- });
236
- });
237
- describe("isBrowserTest", () => {
238
- let isBrowserTest;
239
- beforeAll(async () => {
240
- const mod = await import("./TestExecutionService.js");
241
- isBrowserTest = mod.isBrowserTest;
242
- });
243
- it("returns true for UI test type", () => {
244
- expect(isBrowserTest("ui")).toBe(true);
245
- });
246
- it("returns true for E2E test type", () => {
247
- expect(isBrowserTest("e2e")).toBe(true);
248
- });
249
- it("returns false for contract test type", () => {
250
- expect(isBrowserTest("contract")).toBe(false);
251
- });
252
- it("returns false for integration test type", () => {
253
- expect(isBrowserTest("integration")).toBe(false);
254
- });
255
- it("returns false for fuzz test type", () => {
256
- expect(isBrowserTest("fuzz")).toBe(false);
257
- });
258
- it("returns false for smoke test type", () => {
259
- expect(isBrowserTest("smoke")).toBe(false);
260
- });
261
- it("returns false for load test type", () => {
262
- expect(isBrowserTest("load")).toBe(false);
263
- });
264
- it("returns false for mock test type", () => {
265
- expect(isBrowserTest("mock")).toBe(false);
266
- });
267
- });
268
- describe("getVideoSubdir", () => {
269
- let getVideoSubdir;
270
- beforeAll(async () => {
271
- const mod = await import("./TestExecutionService.js");
272
- getVideoSubdir = mod.getVideoSubdir;
273
- });
274
- it("generates deterministic subdirectory name with basename and hash", () => {
275
- const testFile = "/workspace/tests/login.spec.ts";
276
- const result = getVideoSubdir(testFile);
277
- // Should be "basename-8hexchars" format
278
- // Note: path.extname only strips the final extension (.ts), so .spec remains in basename
279
- expect(result).toMatch(/^login\.spec-[0-9a-f]{8}$/);
280
- });
281
- it("strips file extension from basename", () => {
282
- const result = getVideoSubdir("/workspace/checkout.spec.ts");
283
- // path.extname('.spec.ts') returns '.ts', so 'checkout.spec' is the basename
284
- expect(result).toMatch(/^checkout\.spec-[0-9a-f]{8}$/);
285
- expect(result).toContain(".spec");
286
- });
287
- it("produces different hashes for different paths", () => {
288
- const path1 = "/workspace/tests/login.spec.ts";
289
- const path2 = "/workspace/tests/checkout.spec.ts";
290
- const result1 = getVideoSubdir(path1);
291
- const result2 = getVideoSubdir(path2);
292
- expect(result1).not.toBe(result2);
293
- // Different hashes
294
- expect(result1.split("-")[1]).not.toBe(result2.split("-")[1]);
295
- });
296
- it("produces same hash for the same path (deterministic)", () => {
297
- const testFile = "/workspace/tests/products.spec.ts";
298
- const result1 = getVideoSubdir(testFile);
299
- const result2 = getVideoSubdir(testFile);
300
- expect(result1).toBe(result2);
301
- });
302
- it("handles paths with no extension", () => {
303
- const result = getVideoSubdir("/workspace/test_file");
304
- expect(result).toMatch(/^test_file-[0-9a-f]{8}$/);
305
- });
306
- it("handles paths with multiple dots", () => {
307
- const result = getVideoSubdir("/workspace/test.spec.e2e.ts");
308
- // Should strip only the last extension (.ts)
309
- expect(result).toMatch(/^test\.spec\.e2e-[0-9a-f]{8}$/);
310
- });
311
- });
312
- describe("TestExecutionService.executeTest - Docker env forwarding", () => {
313
- // Import after mocks are set up
314
- let TestExecutionService;
315
- let EXECUTOR_DOCKER_IMAGE;
316
- beforeAll(async () => {
317
- const mod = await import("./TestExecutionService.js");
318
- TestExecutionService = mod.TestExecutionService;
319
- EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
320
- });
321
- beforeEach(() => {
322
- jest.clearAllMocks();
323
- // Simulate image already cached
324
- mockListImages.mockResolvedValue([
325
- { RepoTags: [EXECUTOR_DOCKER_IMAGE] },
326
- ]);
327
- });
328
- afterEach(() => {
329
- // Clean up env vars we set during tests
330
- delete process.env.SKYRAMP_TEST_BASE_URL;
331
- delete process.env.SKYRAMP_TEST_SERVICE_URL_BACKEND;
332
- delete process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND;
333
- });
334
- it("passes SKYRAMP_TEST_BASE_URL to Docker container Env", async () => {
335
- process.env.SKYRAMP_TEST_BASE_URL = "http://external-host:8000";
336
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
337
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
338
- const service = new TestExecutionService();
339
- await service.executeTest({
340
- testFile: "/workspace/test_file.py",
341
- workspacePath: "/workspace",
342
- language: "python",
343
- testType: "smoke",
344
- });
345
- expect(mockRun).toHaveBeenCalledTimes(1);
346
- const dockerOptions = mockRun.mock.calls[0][3];
347
- expect(dockerOptions.Env).toContain("SKYRAMP_TEST_BASE_URL=http://external-host:8000");
348
- });
349
- it("passes multiple SKYRAMP_TEST_SERVICE_URL_* to Docker container Env", async () => {
350
- process.env.SKYRAMP_TEST_SERVICE_URL_BACKEND = "http://host1:8000";
351
- process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND = "http://host2:5173";
352
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
353
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
354
- const service = new TestExecutionService();
355
- await service.executeTest({
356
- testFile: "/workspace/test_file.py",
357
- workspacePath: "/workspace",
358
- language: "python",
359
- testType: "smoke",
360
- });
361
- const dockerOptions = mockRun.mock.calls[0][3];
362
- expect(dockerOptions.Env).toContain("SKYRAMP_TEST_SERVICE_URL_BACKEND=http://host1:8000");
363
- expect(dockerOptions.Env).toContain("SKYRAMP_TEST_SERVICE_URL_FRONTEND=http://host2:5173");
364
- });
365
- it("does not include base URL vars when not set in host env", async () => {
366
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
367
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
368
- const service = new TestExecutionService();
369
- await service.executeTest({
370
- testFile: "/workspace/test_file.py",
371
- workspacePath: "/workspace",
372
- language: "python",
373
- testType: "smoke",
374
- });
375
- const dockerOptions = mockRun.mock.calls[0][3];
376
- const envWithBaseUrl = dockerOptions.Env.filter((e) => e.startsWith("SKYRAMP_TEST_BASE_URL=") ||
377
- e.startsWith("SKYRAMP_TEST_SERVICE_URL_"));
378
- expect(envWithBaseUrl).toHaveLength(0);
379
- });
380
- // Approach B: every workspace mount is mirrored at the host-absolute path so
381
- // tests that embed absolute references (storageState, fixtures, snapshots)
382
- // resolve correctly inside the executor regardless of which path-shape the
383
- // codegen happens to emit.
384
- it("mirrors each workspace file mount at both /home/user/<f> and the host-absolute path", async () => {
385
- mockReaddirSync.mockImplementation((_path, options) => {
386
- if (options?.withFileTypes) {
387
- return [
388
- { name: "xdr_dashboard.spec.ts", isFile: () => true, isDirectory: () => false },
389
- { name: "skyramp_session_storage.json", isFile: () => true, isDirectory: () => false },
390
- ];
391
- }
392
- return ["xdr_dashboard.spec.ts", "skyramp_session_storage.json"];
393
- });
394
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
395
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
396
- const service = new TestExecutionService();
397
- await service.executeTest({
398
- testFile: "/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts",
399
- workspacePath: "/Users/pedro/projects/cisco-xdr-tests",
400
- language: "typescript",
401
- testType: "ui",
402
- });
403
- const dockerOptions = mockRun.mock.calls[0][3];
404
- const targets = dockerOptions.HostConfig.Mounts.map((m) => m.Target);
405
- // Canonical /home/user mount
406
- expect(targets).toContain("/home/user/xdr_dashboard.spec.ts");
407
- expect(targets).toContain("/home/user/skyramp_session_storage.json");
408
- // Host-absolute mirror — the fix for absolute paths leaking out of `__dirname`
409
- expect(targets).toContain("/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts");
410
- expect(targets).toContain("/Users/pedro/projects/cisco-xdr-tests/skyramp_session_storage.json");
411
- });
412
- it("does not double-mount when workspacePath equals /home/user", async () => {
413
- mockReaddirSync.mockImplementation((_path, options) => {
414
- if (options?.withFileTypes) {
415
- return [{ name: "test_file.py", isFile: () => true, isDirectory: () => false }];
416
- }
417
- return ["test_file.py"];
418
- });
419
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
420
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
421
- const service = new TestExecutionService();
422
- await service.executeTest({
423
- testFile: "/home/user/test_file.py",
424
- workspacePath: "/home/user",
425
- language: "python",
426
- testType: "smoke",
427
- });
428
- const dockerOptions = mockRun.mock.calls[0][3];
429
- const targetCounts = {};
430
- for (const m of dockerOptions.HostConfig.Mounts) {
431
- targetCounts[m.Target] = (targetCounts[m.Target] ?? 0) + 1;
432
- }
433
- // No mount target should appear twice (no host-absolute mirror when workspace == /home/user)
434
- for (const [t, n] of Object.entries(targetCounts)) {
435
- expect({ target: t, count: n }).toEqual({ target: t, count: 1 });
436
- }
437
- });
438
- });
439
- describe("TestExecutionService.executeTest - Video capture for browser tests", () => {
440
- let TestExecutionService;
441
- let getVideoSubdir;
442
- let EXECUTOR_DOCKER_IMAGE;
443
- beforeAll(async () => {
444
- const mod = await import("./TestExecutionService.js");
445
- TestExecutionService = mod.TestExecutionService;
446
- getVideoSubdir = mod.getVideoSubdir;
447
- EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
448
- });
449
- beforeEach(() => {
450
- jest.clearAllMocks();
451
- mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
452
- mockExistsSync.mockReturnValue(true);
453
- mockReaddirSync.mockImplementation((_path, options) => {
454
- if (options?.withFileTypes) {
455
- return [
456
- { name: "test.spec.ts", isFile: () => true, isDirectory: () => false },
457
- { name: "test.py", isFile: () => true, isDirectory: () => false },
458
- ];
459
- }
460
- return ["test.spec.ts", "test.py"];
461
- });
462
- // Reset writeFileSync mock to default behavior
463
- mockWriteFileSync.mockImplementation(() => { });
464
- });
465
- it("mounts generated Playwright config at /home/user/playwright.config.ts for TypeScript UI test", async () => {
466
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
467
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
468
- const service = new TestExecutionService();
469
- await service.executeTest({
470
- testFile: "/workspace/login.spec.ts",
471
- workspacePath: "/workspace",
472
- language: "typescript",
473
- testType: "ui",
474
- });
475
- const dockerOptions = mockRun.mock.calls[0][3];
476
- const playwrightConfigMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target === "/home/user/playwright.config.ts");
477
- expect(playwrightConfigMount).toBeDefined();
478
- expect(playwrightConfigMount.Type).toBe("bind");
479
- // Source should be a temp file containing the generated config
480
- expect(playwrightConfigMount.Source).toContain("skyramp-playwright-");
481
- expect(playwrightConfigMount.Source).toContain(".config.ts");
482
- });
483
- it("generated Playwright config for TypeScript contains video:on and outputDir", async () => {
484
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
485
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
486
- let capturedConfigContent = "";
487
- mockWriteFileSync.mockImplementation((filePath, content) => {
488
- if (filePath.includes("skyramp-playwright-") && filePath.endsWith(".config.ts")) {
489
- capturedConfigContent = content;
490
- }
491
- });
492
- const service = new TestExecutionService();
493
- await service.executeTest({
494
- testFile: "/workspace/checkout.spec.ts",
495
- workspacePath: "/workspace",
496
- language: "typescript",
497
- testType: "e2e",
498
- });
499
- expect(capturedConfigContent).toContain('video: "on"');
500
- expect(capturedConfigContent).toContain("outputDir:");
501
- expect(capturedConfigContent).toContain(".skyramp/videos/");
502
- });
503
- it("mounts generated pytest.ini at /home/user/pytest.ini for Python UI test", async () => {
504
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
505
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
506
- const service = new TestExecutionService();
507
- await service.executeTest({
508
- testFile: "/workspace/test_login.py",
509
- workspacePath: "/workspace",
510
- language: "python",
511
- testType: "ui",
512
- });
513
- const dockerOptions = mockRun.mock.calls[0][3];
514
- const pytestConfigMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target === "/home/user/pytest.ini");
515
- expect(pytestConfigMount).toBeDefined();
516
- expect(pytestConfigMount.Type).toBe("bind");
517
- expect(pytestConfigMount.Source).toContain("skyramp-pytest-");
518
- expect(pytestConfigMount.Source).toContain(".ini");
519
- });
520
- it("generated pytest.ini for Python contains --video on and --output", async () => {
521
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
522
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
523
- let capturedPytestContent = "";
524
- mockWriteFileSync.mockImplementation((filePath, content) => {
525
- if (filePath.includes("skyramp-pytest-") && filePath.endsWith(".ini")) {
526
- capturedPytestContent = content;
527
- }
528
- });
529
- const service = new TestExecutionService();
530
- await service.executeTest({
531
- testFile: "/workspace/test_checkout.py",
532
- workspacePath: "/workspace",
533
- language: "python",
534
- testType: "e2e",
535
- });
536
- expect(capturedPytestContent).toContain("--video on");
537
- expect(capturedPytestContent).toContain("--output");
538
- expect(capturedPytestContent).toContain(".skyramp/videos/");
539
- });
540
- it("mounts video output directory as writable for UI test", async () => {
541
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
542
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
543
- const service = new TestExecutionService();
544
- await service.executeTest({
545
- testFile: "/workspace/tests/login.spec.ts",
546
- workspacePath: "/workspace",
547
- language: "typescript",
548
- testType: "ui",
549
- });
550
- const dockerOptions = mockRun.mock.calls[0][3];
551
- const videoSubdir = getVideoSubdir("/workspace/tests/login.spec.ts");
552
- // Each execution gets a unique per-run dir: `${videoSubdir}-<timestamp>`,
553
- // so match by prefix rather than exact equality.
554
- const videoMountPrefix = `/home/user/.skyramp/videos/${videoSubdir}`;
555
- const videoMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target.startsWith(videoMountPrefix));
556
- expect(videoMount).toBeDefined();
557
- expect(videoMount.Type).toBe("bind");
558
- expect(videoMount.ReadOnly).toBe(false);
559
- expect(videoMount.Source).toContain(".skyramp/videos/");
560
- expect(videoMount.Source).toContain(videoSubdir);
561
- });
562
- it("does not mount video directory for non-browser tests", async () => {
563
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
564
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
565
- const service = new TestExecutionService();
566
- await service.executeTest({
567
- testFile: "/workspace/contract_test.py",
568
- workspacePath: "/workspace",
569
- language: "python",
570
- testType: "contract",
571
- });
572
- const dockerOptions = mockRun.mock.calls[0][3];
573
- const videoMounts = dockerOptions.HostConfig.Mounts.filter((m) => m.Target.includes(".skyramp/videos"));
574
- expect(videoMounts).toHaveLength(0);
575
- });
576
- it("uses minimal static Playwright config for non-browser TypeScript tests", async () => {
577
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
578
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
579
- let usedGeneratedConfig = false;
580
- mockWriteFileSync.mockImplementation((filePath, content) => {
581
- if (filePath.includes("skyramp-playwright-") && content.includes('video: "on"')) {
582
- usedGeneratedConfig = true;
583
- }
584
- });
585
- const service = new TestExecutionService();
586
- await service.executeTest({
587
- testFile: "/workspace/contract_test.spec.ts",
588
- workspacePath: "/workspace",
589
- language: "typescript",
590
- testType: "contract",
591
- });
592
- // Should NOT generate a video-enabled config for contract test
593
- expect(usedGeneratedConfig).toBe(false);
594
- });
595
- it("populates videoPath when video.webm exists after execution", async () => {
596
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
597
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
598
- const videoSubdir = getVideoSubdir("/workspace/test.spec.ts");
599
- mockReaddirSync.mockImplementation((dir, options) => {
600
- // Mock video directory scan
601
- if (dir.includes(".skyramp/videos") && dir.includes(videoSubdir)) {
602
- if (options?.withFileTypes) {
603
- return [{ name: "video.webm", isFile: () => true, isDirectory: () => false }];
604
- }
605
- return ["video.webm"];
606
- }
607
- // Default workspace files
608
- if (options?.withFileTypes) {
609
- return [{ name: "test.spec.ts", isFile: () => true, isDirectory: () => false }];
610
- }
611
- return ["test.spec.ts"];
612
- });
613
- const service = new TestExecutionService();
614
- const result = await service.executeTest({
615
- testFile: "/workspace/test.spec.ts",
616
- workspacePath: "/workspace",
617
- language: "typescript",
618
- testType: "ui",
619
- });
620
- expect(result.videoPath).toBeDefined();
621
- expect(result.videoPath).toContain(".skyramp/videos/");
622
- expect(result.videoPath).toContain("video.webm");
623
- });
624
- it("leaves videoPath undefined when no video.webm found", async () => {
625
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
626
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
627
- const videoSubdir = getVideoSubdir("/workspace/test.spec.ts");
628
- mockReaddirSync.mockImplementation((dir, options) => {
629
- // Mock empty video directory
630
- if (dir.includes(".skyramp/videos") && dir.includes(videoSubdir)) {
631
- if (options?.withFileTypes) {
632
- return [];
633
- }
634
- return [];
635
- }
636
- // Default workspace files
637
- if (options?.withFileTypes) {
638
- return [{ name: "test.spec.ts", isFile: () => true, isDirectory: () => false }];
639
- }
640
- return ["test.spec.ts"];
641
- });
642
- const service = new TestExecutionService();
643
- const result = await service.executeTest({
644
- testFile: "/workspace/test.spec.ts",
645
- workspacePath: "/workspace",
646
- language: "typescript",
647
- testType: "ui",
648
- });
649
- expect(result.videoPath).toBeUndefined();
650
- });
651
- it("leaves videoPath undefined for non-browser tests", async () => {
652
- const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
653
- mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
654
- const service = new TestExecutionService();
655
- const result = await service.executeTest({
656
- testFile: "/workspace/integration_test.py",
657
- workspacePath: "/workspace",
658
- language: "python",
659
- testType: "integration",
660
- });
661
- expect(result.videoPath).toBeUndefined();
662
- });
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
- });