@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 +0,0 @@
1
- export {};
@@ -1,451 +0,0 @@
1
- import { jest } from "@jest/globals";
2
- jest.unstable_mockModule("../../workspace/workspace.js", () => ({
3
- WorkspaceConfigManager: jest.fn(),
4
- }));
5
- jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
6
- AnalyticsService: { pushMCPToolEvent: jest.fn(() => Promise.resolve()) },
7
- }));
8
- jest.unstable_mockModule("../../playwright/traceRecordingPrompt.js", () => ({
9
- getTraceRecordingPromptText: () => "",
10
- }));
11
- const { getTestbotPrompt, buildWorkspaceRecoveryPrefix, parseRelatedRepositories } = await import("./testbot-prompts.js");
12
- // Minimal args to invoke getTestbotPrompt — only services matter for these tests
13
- const baseArgs = {
14
- prTitle: "Test PR",
15
- prDescription: "desc",
16
- summaryOutputFile: "/tmp/summary.json",
17
- repositoryPath: "/repo",
18
- };
19
- function callWithServices(services) {
20
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
21
- undefined, // maxRecommendations
22
- undefined, // maxGenerate
23
- undefined, // maxCritical
24
- undefined, // prNumber
25
- undefined, // userPrompt
26
- services);
27
- }
28
- describe("buildServiceContext (via getTestbotPrompt)", () => {
29
- it("renders full service with all fields", () => {
30
- const prompt = callWithServices([
31
- {
32
- serviceName: "backend",
33
- language: "python",
34
- framework: "pytest",
35
- testDirectory: "tests/python",
36
- api: { baseUrl: "http://localhost:8000" },
37
- },
38
- ]);
39
- expect(prompt).toContain('<service name="backend">');
40
- expect(prompt).toContain("<language>python</language>");
41
- expect(prompt).toContain("<framework>pytest</framework>");
42
- expect(prompt).toContain("<base_url>http://localhost:8000</base_url>");
43
- expect(prompt).toContain("<test_directory>tests/python</test_directory>");
44
- expect(prompt).toContain("</service>");
45
- expect(prompt).toContain("<services>");
46
- expect(prompt).toContain("</services>");
47
- });
48
- it("omits optional fields when absent", () => {
49
- const prompt = callWithServices([{ serviceName: "minimal" }]);
50
- expect(prompt).toContain('<service name="minimal">');
51
- expect(prompt).not.toContain("<language>");
52
- expect(prompt).not.toContain("<framework>");
53
- expect(prompt).not.toContain("<base_url>");
54
- expect(prompt).not.toContain("<test_directory>");
55
- });
56
- it("renders multiple services", () => {
57
- const prompt = callWithServices([
58
- { serviceName: "api", language: "python" },
59
- { serviceName: "frontend", language: "typescript" },
60
- ]);
61
- expect(prompt).toContain('<service name="api">');
62
- expect(prompt).toContain('<service name="frontend">');
63
- });
64
- it("does not render services block when services array is empty", () => {
65
- const prompt = callWithServices([]);
66
- expect(prompt).not.toContain("<services>");
67
- expect(prompt).not.toContain("<service");
68
- });
69
- it("does not render services block when services is undefined", () => {
70
- const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
71
- expect(prompt).not.toContain("<services>");
72
- });
73
- it("escapes XML special characters in service name", () => {
74
- const prompt = callWithServices([
75
- { serviceName: 'my<service>&"name' },
76
- ]);
77
- expect(prompt).toContain('<service name="my&lt;service&gt;&amp;&quot;name">');
78
- expect(prompt).not.toContain('my<service>&"name">');
79
- });
80
- it("escapes XML special characters in field values", () => {
81
- const prompt = callWithServices([
82
- {
83
- serviceName: "svc",
84
- testDirectory: "tests/a&b",
85
- api: { baseUrl: "http://host?a=1&b=2" },
86
- },
87
- ]);
88
- expect(prompt).toContain("<test_directory>tests/a&amp;b</test_directory>");
89
- expect(prompt).toContain("<base_url>http://host?a=1&amp;b=2</base_url>");
90
- });
91
- it("places services block between REPOSITORY PATH and the Goal section", () => {
92
- const prompt = callWithServices([{ serviceName: "svc" }]);
93
- const repoIdx = prompt.indexOf("<REPOSITORY PATH>");
94
- const servicesIdx = prompt.indexOf("<services>");
95
- const goalIdx = prompt.indexOf("## Goal");
96
- expect(repoIdx).toBeLessThan(servicesIdx);
97
- expect(servicesIdx).toBeLessThan(goalIdx);
98
- });
99
- it("has no extra blank line when services are absent", () => {
100
- const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
101
- // Should go directly from REPOSITORY PATH closing tag to the Goal section
102
- expect(prompt).toContain("</REPOSITORY PATH>\n## Goal");
103
- });
104
- });
105
- describe("uiCredentials in getTestbotPrompt", () => {
106
- function callWithUiCredentials(uiCredentials) {
107
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
108
- undefined, // maxRecommendations
109
- undefined, // maxGenerate
110
- undefined, // maxCritical
111
- undefined, // prNumber
112
- undefined, // userPrompt
113
- undefined, // services
114
- uiCredentials);
115
- }
116
- // The instructions text references `<ui-credentials>` in prose (backticked);
117
- // the injected block puts the opening tag alone on its own line. Match on
118
- // that to distinguish block from reference.
119
- const UI_CREDS_BLOCK_RE = /^<ui-credentials>$/m;
120
- it("does not render ui-credentials block when uiCredentials is undefined", () => {
121
- const prompt = callWithUiCredentials(undefined);
122
- expect(prompt).not.toMatch(UI_CREDS_BLOCK_RE);
123
- });
124
- it("does not render ui-credentials block when uiCredentials is empty string", () => {
125
- const prompt = callWithUiCredentials("");
126
- expect(prompt).not.toMatch(UI_CREDS_BLOCK_RE);
127
- });
128
- it("does not render ui-credentials block when uiCredentials is whitespace only", () => {
129
- const prompt = callWithUiCredentials(" ");
130
- expect(prompt).not.toMatch(UI_CREDS_BLOCK_RE);
131
- });
132
- it("renders ui-credentials block verbatim when credentials are provided", () => {
133
- const creds = "user@example.com:password123";
134
- const prompt = callWithUiCredentials(creds);
135
- expect(prompt).toMatch(UI_CREDS_BLOCK_RE);
136
- expect(prompt).toContain(creds);
137
- expect(prompt).toContain("</ui-credentials>");
138
- });
139
- it("places ui-credentials block before the Goal section", () => {
140
- const prompt = callWithUiCredentials("user@example.com:secret");
141
- const openIdx = prompt.search(UI_CREDS_BLOCK_RE);
142
- const goalIdx = prompt.indexOf("## Goal");
143
- expect(openIdx).toBeGreaterThanOrEqual(0);
144
- expect(openIdx).toBeLessThan(goalIdx);
145
- });
146
- it("passes XML special characters through unchanged (not real XML)", () => {
147
- // The <ui-credentials> block is framing for the agent's prompt context,
148
- // not real XML. Passwords with &, <, >, ", ' are typed verbatim into
149
- // login forms, so we must not escape them.
150
- const creds = 'user&name:<p>assword>"';
151
- const prompt = callWithUiCredentials(creds);
152
- expect(prompt).toContain(creds);
153
- expect(prompt).not.toContain("&amp;");
154
- expect(prompt).not.toContain("&lt;");
155
- });
156
- it("rejects credentials that would break the block framing", () => {
157
- // The only string we cannot safely inject is the closing tag itself,
158
- // since it would terminate the block and let subsequent content inject
159
- // prose into the credential context.
160
- expect(() => callWithUiCredentials("user:pass</ui-credentials>extra"))
161
- .toThrow("</ui-credentials>");
162
- });
163
- });
164
- describe("drift analysis — runtime tool call (step 3)", () => {
165
- // The build-time embed of buildDriftAnalysisPrompt was replaced with a
166
- // runtime instruction: LLM calls skyramp_analyze_test_health then skyramp_actions.
167
- function basePrompt() {
168
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
169
- }
170
- it("step 3 instructs the LLM to call skyramp_analyze_test_health", () => {
171
- const prompt = basePrompt();
172
- expect(prompt).toContain("skyramp_analyze_test_health");
173
- });
174
- it("step 3 instructs the LLM to call skyramp_actions", () => {
175
- const prompt = basePrompt();
176
- expect(prompt).toContain("skyramp_actions");
177
- });
178
- it("skyramp_analyze_test_health is referenced in the prompt", () => {
179
- // After the unified maintenance refactor, analyze_test_health is referenced
180
- // in the testMaintenance submission guidance rather than as an explicit step.
181
- const prompt = basePrompt();
182
- expect(prompt).toContain("skyramp_analyze_test_health");
183
- });
184
- it("does not contain the build-time embedded drift_analysis_rules content (Action Decision Tree)", () => {
185
- // The rules are now fetched at runtime via skyramp_analyze_test_health —
186
- // the <drift_analysis_rules> tag may appear as a reference in prose,
187
- // but the actual rule content (Action Decision Tree) must not be baked in.
188
- const prompt = basePrompt();
189
- expect(prompt).not.toContain("Action Decision Tree\n\nFor each existing test");
190
- expect(prompt).not.toContain("Update Execution Rules\n\nWhen applying UPDATE actions");
191
- });
192
- it("does not contain a persona statement (no nested identity from old embed)", () => {
193
- const prompt = basePrompt();
194
- expect(prompt).not.toContain("You are acting as a Skyramp Integration Architect");
195
- });
196
- });
197
- describe("buildWorkspaceRecoveryPrefix", () => {
198
- it("includes repositoryPath in both init_scan and init_workspace instructions", () => {
199
- const prefix = buildWorkspaceRecoveryPrefix("/home/user/repo");
200
- expect(prefix).toContain('skyramp_init_scan with workspacePath "/home/user/repo"');
201
- expect(prefix).toContain('skyramp_init_workspace with workspacePath "/home/user/repo"');
202
- });
203
- it("includes force: true for both tool calls", () => {
204
- const prefix = buildWorkspaceRecoveryPrefix("/repo");
205
- expect(prefix).toContain("force: true, then call skyramp_init_workspace");
206
- expect(prefix).toContain("force: true to regenerate");
207
- });
208
- it("starts with IMPORTANT", () => {
209
- const prefix = buildWorkspaceRecoveryPrefix("/repo");
210
- expect(prefix).toMatch(/^IMPORTANT:/);
211
- });
212
- });
213
- describe("testsRepoDir in getTestbotPrompt", () => {
214
- function callWithTestsRepoDir(testsRepoDir) {
215
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
216
- undefined, // maxRecommendations
217
- undefined, // maxGenerate
218
- undefined, // maxCritical
219
- undefined, // prNumber
220
- undefined, // userPrompt
221
- undefined, // services
222
- undefined, // uiCredentials
223
- testsRepoDir);
224
- }
225
- function callFollowUpWithTestsRepoDir(testsRepoDir) {
226
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
227
- undefined, // maxRecommendations
228
- undefined, // maxGenerate
229
- undefined, // maxCritical
230
- undefined, // prNumber
231
- "add more tests", // userPrompt — triggers follow-up path
232
- undefined, // services
233
- undefined, // uiCredentials
234
- testsRepoDir);
235
- }
236
- it("includes testsRepoDir in skyramp_analyze_changes call for first-run prompt", () => {
237
- const dir = "/home/runner/work/_temp/skyramp/test-repo";
238
- const prompt = callWithTestsRepoDir(dir);
239
- expect(prompt).toContain(`\`testsRepoDir\`: "${dir}"`);
240
- });
241
- it("includes testsRepoDir in skyramp_analyze_changes call for follow-up prompt", () => {
242
- const dir = "/home/runner/work/_temp/skyramp/test-repo";
243
- const prompt = callFollowUpWithTestsRepoDir(dir);
244
- expect(prompt).toContain(`\`testsRepoDir\`: "${dir}"`);
245
- });
246
- it("omits testsRepoDir when not provided", () => {
247
- const prompt = callWithTestsRepoDir(undefined);
248
- expect(prompt).not.toContain("testsRepoDir");
249
- });
250
- it("omits testsRepoDir from follow-up prompt when not provided", () => {
251
- const prompt = callFollowUpWithTestsRepoDir(undefined);
252
- expect(prompt).not.toContain("testsRepoDir");
253
- });
254
- });
255
- describe("testbot prompt blueprint-grounded recommendations (slice 4)", () => {
256
- it("uses a single skyramp_analyze_changes call (no separate pre-flight tool, no blueprint threading)", () => {
257
- const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
258
- // The standalone pre-flight tool is gone — its work folded into analyze_changes.
259
- expect(prompt).not.toContain("skyramp_ui_analyze_changes");
260
- expect(prompt).toContain("skyramp_analyze_changes");
261
- // Captures stay in tool-result history — the old capturedBlueprints
262
- // threading directive must be gone.
263
- expect(prompt).not.toMatch(/capturedBlueprints/);
264
- });
265
- // Flutter support — both the generalised UI trigger wording and the
266
- // canvas/empty-ARIA issuesFound rule should appear in the prompt.
267
- it("UI test priority defers to the deterministic server signal (uiContext.changedFrontendFiles), not just hard-coded extensions", () => {
268
- const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
269
- // The trigger must reference the server signal (changedFrontendFiles)
270
- // — that's what makes it framework-agnostic. Hard-coded extension lists
271
- // become illustrative, not gating.
272
- expect(prompt).toMatch(/uiContext.*changedFrontendFiles|changedFrontendFiles.*uiContext/);
273
- // .dart should appear in the supported-types example list
274
- expect(prompt).toMatch(/\.dart/);
275
- });
276
- it("frontend-only PR exception lists .dart alongside other frontend extensions", () => {
277
- const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
278
- // The frontend-only-PR exception block (Task 2 backend-fallback skip)
279
- // must include .dart so a Flutter-only PR doesn't emit a backend
280
- // contract test as a fallback.
281
- const exceptionBlock = prompt.slice(prompt.indexOf("Exception — frontend-only PRs"), prompt.indexOf("Exception — frontend-only PRs") + 800);
282
- expect(exceptionBlock).toContain(".dart");
283
- expect(exceptionBlock).toContain(".tsx");
284
- expect(exceptionBlock).toContain(".vue");
285
- });
286
- });
287
- describe("relatedRepositories in getTestbotPrompt (multi-repo mode)", () => {
288
- const PRIMARY_REPO = "letsramp/primary-app";
289
- const related = [
290
- { repo: "letsramp/backend", repositoryPath: "/tmp/skyramp/related-repo-0-backend", baseBranch: "main" },
291
- { repo: "letsramp/shared", repositoryPath: "/tmp/skyramp/related-repo-1-shared", baseBranch: "master" },
292
- ];
293
- function callWithRelated(repos, opts) {
294
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
295
- undefined, // maxRecommendations
296
- undefined, // maxGenerate
297
- undefined, // maxCritical
298
- undefined, // prNumber
299
- undefined, // userPrompt
300
- undefined, // services
301
- undefined, // uiCredentials
302
- opts?.testsRepoDir, // testsRepoDir
303
- repos, opts?.primaryRepo ?? PRIMARY_REPO);
304
- }
305
- it("emits a <related_repositories> XML block with repository + path + base_branch per repo", () => {
306
- const prompt = callWithRelated(related);
307
- expect(prompt).toContain("<related_repositories>");
308
- expect(prompt).toContain("</related_repositories>");
309
- expect(prompt).toContain('<repository repository="letsramp/backend" path="/tmp/skyramp/related-repo-0-backend" base_branch="main" />');
310
- expect(prompt).toContain('<repository repository="letsramp/shared" path="/tmp/skyramp/related-repo-1-shared" base_branch="master" />');
311
- // Pure XML — no free-form "repositoryPath:" / "baseBranch:" colon syntax.
312
- expect(prompt).not.toContain("repositoryPath:");
313
- expect(prompt).not.toMatch(/baseBranch:/);
314
- });
315
- it("emits the primary repo's owner/repo in a <REPOSITORY> block", () => {
316
- const prompt = callWithRelated(related);
317
- expect(prompt).toContain(`<REPOSITORY>${PRIMARY_REPO}</REPOSITORY>`);
318
- });
319
- it("always emits a base_branch attribute, even when no explicit baseBranch is provided", () => {
320
- const prompt = callWithRelated([
321
- { repo: "letsramp/backend", repositoryPath: "/tmp/skyramp/related-repo-0-backend" },
322
- ]);
323
- expect(prompt).toContain("<related_repositories>");
324
- // Absent value → explicit auto-detect marker in the attribute.
325
- expect(prompt).toContain('<repository repository="letsramp/backend" path="/tmp/skyramp/related-repo-0-backend" base_branch="auto-detect" />');
326
- });
327
- it("keeps the unified workspace in the PRIMARY repo even when testsRepoDir is set", () => {
328
- const prompt = callWithRelated(related, { testsRepoDir: "/tmp/skyramp/test-repo" });
329
- // workspace.yml is registered in the primary repo regardless of the test repo.
330
- expect(prompt).toContain(`\`workspacePath\`: "${baseArgs.repositoryPath}"`);
331
- expect(prompt).not.toContain('`workspacePath`: "/tmp/skyramp/test-repo"');
332
- // And it makes the primary-vs-test-repo distinction explicit to the agent.
333
- expect(prompt).toMatch(/NOT in the configured test repo/);
334
- });
335
- it("leaves the single-repo prompt unchanged when no related repos are provided", () => {
336
- const prompt = callWithRelated(undefined);
337
- expect(prompt).not.toContain("<related_repositories>");
338
- expect(prompt).not.toContain("MULTI-REPO CONTEXT");
339
- expect(prompt).not.toContain("MULTI-REPO attribution");
340
- // The cross-repo round-robin / candidate-pooling language must not leak into
341
- // single-repo runs (the as-is GENERATE mandate stays authoritative there).
342
- expect(prompt).not.toMatch(/round-robin/i);
343
- expect(prompt).not.toContain("Multi-repo exception");
344
- expect(prompt).not.toMatch(/candidates, not a fixed selection/i);
345
- });
346
- it("treats an empty array as single-repo", () => {
347
- const prompt = callWithRelated([]);
348
- expect(prompt).not.toContain("<related_repositories>");
349
- });
350
- });
351
- describe("parseRelatedRepositories", () => {
352
- it("parses a valid JSON array", () => {
353
- const parsed = parseRelatedRepositories(JSON.stringify([
354
- { repo: "o/a", repositoryPath: "/a", baseBranch: "main" },
355
- { repo: "o/b", repositoryPath: "/b" },
356
- ]));
357
- expect(parsed).toEqual([
358
- { repo: "o/a", repositoryPath: "/a", baseBranch: "main" },
359
- { repo: "o/b", repositoryPath: "/b", baseBranch: undefined },
360
- ]);
361
- });
362
- it("returns undefined for missing input", () => {
363
- expect(parseRelatedRepositories(undefined)).toBeUndefined();
364
- expect(parseRelatedRepositories("")).toBeUndefined();
365
- });
366
- it("returns undefined for malformed JSON", () => {
367
- expect(parseRelatedRepositories("{not json")).toBeUndefined();
368
- });
369
- it("returns undefined for an empty array", () => {
370
- expect(parseRelatedRepositories("[]")).toBeUndefined();
371
- });
372
- it("drops entries missing a repositoryPath or repo", () => {
373
- const parsed = parseRelatedRepositories(JSON.stringify([
374
- { repo: "o/x", baseBranch: "main" }, // no repositoryPath
375
- { repositoryPath: "/no-repo" }, // no repo
376
- { repo: "o/ok", repositoryPath: "/ok" },
377
- ]));
378
- expect(parsed).toEqual([{ repo: "o/ok", repositoryPath: "/ok", baseBranch: undefined }]);
379
- });
380
- });
381
- describe("plan-only eval mode (via getTestbotPrompt)", () => {
382
- function callWithPlanOnly(planOnly) {
383
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
384
- undefined, // maxRecommendations
385
- undefined, // maxGenerate
386
- undefined, // maxCritical
387
- undefined, // prNumber
388
- undefined, // userPrompt
389
- undefined, // services
390
- undefined, // uiCredentials
391
- undefined, // testsRepoDir
392
- undefined, // relatedRepositories
393
- undefined, // primaryRepo
394
- planOnly);
395
- }
396
- it("branches Task 2 to the plan-only variant when planOnly is true", () => {
397
- const prompt = callWithPlanOnly(true);
398
- expect(prompt).toContain("## Task 2: Commit the Test Plan (plan-only run)");
399
- expect(prompt).not.toContain("## Task 2: Generate New Tests");
400
- // The standard generation/execution mandates must be absent, not overridden.
401
- expect(prompt).not.toContain("skyramp_integration_test_generation");
402
- expect(prompt).not.toContain('phase: "before"');
403
- // Task 3 declares the plan instead of counting generated files.
404
- expect(prompt).toContain("DECLARES your final GENERATE selection");
405
- });
406
- it("renders the standard prompt untouched when planOnly is false or omitted", () => {
407
- const prompt = callWithPlanOnly(false);
408
- expect(prompt).toContain("## Task 2: Generate New Tests");
409
- expect(prompt).not.toContain("plan-only");
410
- expect(prompt).toBe(callWithServices([]));
411
- });
412
- });
413
- describe("report language in getTestbotPrompt", () => {
414
- const callWithLanguage = (language) => getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
415
- undefined, // maxRecommendations
416
- undefined, // maxGenerate
417
- undefined, // maxCritical
418
- undefined, // prNumber
419
- undefined, // userPrompt
420
- undefined, // services
421
- undefined, // uiCredentials
422
- undefined, // testsRepoDir
423
- undefined, // relatedRepositories
424
- undefined, // primaryRepo
425
- false, // planOnly
426
- language);
427
- it("adds the report-language block for ja", () => {
428
- expect(callWithLanguage("ja")).toContain("**Report language: Japanese.**");
429
- });
430
- it("omits the block for en", () => {
431
- expect(callWithLanguage("en")).not.toContain("**Report language:");
432
- });
433
- it("omits the block when language is not set", () => {
434
- expect(callWithLanguage(undefined)).not.toContain("**Report language:");
435
- });
436
- it("resolves the English display name for other language codes", () => {
437
- expect(callWithLanguage("ko")).toContain("**Report language: Korean.**");
438
- });
439
- });
440
- describe("UI code reuse wiring", () => {
441
- it("instructs codeReuse: true for TS/JS Playwright UI generation", () => {
442
- const p = callWithServices([]);
443
- expect(p).toContain("codeReuse: true");
444
- });
445
- it("teaches both reuse outcomes and the raw.bak insurance rule", () => {
446
- const p = callWithServices([]);
447
- expect(p).toContain("No reusable POM layer detected");
448
- expect(p).toContain("verification: passed");
449
- expect(p).toContain(".raw.bak");
450
- });
451
- });
@@ -1,138 +0,0 @@
1
- import { diversityBalancedBudgeter } from "./diversityBalancedBudgeter.js";
2
- import { PriorityTier } from "../../types/TestRecommendation.js";
3
- import { fixedNBudgeter } from "./fixedNBudgeter.js";
4
- import { mkCandidate } from "../testFixtures.js";
5
- function ctx(over = {}) {
6
- return {
7
- maxGenerate: 3,
8
- maxTotal: 20,
9
- isUIOnlyPR: false,
10
- hasFrontendChanges: false,
11
- externalCoverage: new Set(),
12
- ...over,
13
- };
14
- }
15
- const countType = (r, t) => r.generate.filter((c) => c.scenario.testType === t).length;
16
- const typesIn = (r) => new Set(r.generate.map((c) => c.scenario.testType));
17
- describe("diversityBalancedBudgeter", () => {
18
- it("guarantees a contract slot where fixed-n starves it (the SKYR-3879 fix)", () => {
19
- // Protected (CRITICAL) integration scenarios out-rank contract ones and, at N=3,
20
- // fixed-n's protected-first fill consumes every slot with integration → 0 contract.
21
- const ranked = [
22
- mkCandidate("i1", { testType: "integration", priority: PriorityTier.CRITICAL }),
23
- mkCandidate("i2", { testType: "integration", priority: PriorityTier.CRITICAL }),
24
- mkCandidate("i3", { testType: "integration", priority: PriorityTier.CRITICAL }),
25
- mkCandidate("c1", { testType: "contract" }),
26
- mkCandidate("c2", { testType: "contract" }),
27
- ];
28
- const c = ctx({ maxGenerate: 3 });
29
- // Baseline: fixed-n reproduces the skew.
30
- expect(countType(fixedNBudgeter.select(ranked, c), "contract")).toBe(0);
31
- // Fix: diversity-balanced reserves a per-type floor.
32
- const div = diversityBalancedBudgeter.select(ranked, c);
33
- expect(div.generate.length).toBe(3);
34
- expect(countType(div, "contract")).toBeGreaterThanOrEqual(1);
35
- expect(countType(div, "integration")).toBeGreaterThanOrEqual(1);
36
- });
37
- it("gives every present test type at least one slot before any type gets a second", () => {
38
- const ranked = [
39
- mkCandidate("i1", { testType: "integration" }),
40
- mkCandidate("i2", { testType: "integration" }),
41
- mkCandidate("i3", { testType: "integration" }),
42
- mkCandidate("c1", { testType: "contract" }),
43
- mkCandidate("c2", { testType: "contract" }),
44
- mkCandidate("u1", { testType: "ui" }),
45
- ];
46
- const div = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 3 }));
47
- expect(typesIn(div)).toEqual(new Set(["integration", "contract", "ui"]));
48
- });
49
- it("raising maxGenerate increases per-type coverage while keeping the floor", () => {
50
- const ranked = [
51
- mkCandidate("i1", { testType: "integration", priority: PriorityTier.CRITICAL }),
52
- mkCandidate("i2", { testType: "integration", priority: PriorityTier.CRITICAL }),
53
- mkCandidate("i3", { testType: "integration", priority: PriorityTier.CRITICAL }),
54
- mkCandidate("i4", { testType: "integration", priority: PriorityTier.CRITICAL }),
55
- mkCandidate("i5", { testType: "integration", priority: PriorityTier.CRITICAL }),
56
- mkCandidate("c1", { testType: "contract" }),
57
- mkCandidate("c2", { testType: "contract" }),
58
- mkCandidate("u1", { testType: "ui" }),
59
- ];
60
- const atThree = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 3 }));
61
- const atSix = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 6 }));
62
- expect(countType(atThree, "contract")).toBeGreaterThanOrEqual(1);
63
- expect(countType(atThree, "ui")).toBeGreaterThanOrEqual(1);
64
- expect(countType(atSix, "contract")).toBeGreaterThanOrEqual(1);
65
- expect(countType(atSix, "ui")).toBeGreaterThanOrEqual(1);
66
- // more budget → more of the highest-ranked (integration) coverage
67
- expect(countType(atSix, "integration")).toBeGreaterThan(countType(atThree, "integration"));
68
- expect(atSix.generate.length).toBe(6);
69
- });
70
- it("returns everything (rank order) when maxGenerate >= candidate count", () => {
71
- const ranked = [mkCandidate("i1", { testType: "integration" }), mkCandidate("c1", { testType: "contract" })];
72
- const div = diversityBalancedBudgeter.select(ranked, ctx({ maxGenerate: 5 }));
73
- expect(div.generate.map((c) => c.scenario.scenarioName)).toEqual(["i1", "c1"]);
74
- });
75
- });
76
- // Regression: the backend pick leaves UI slots empty (backend count is 0 on a
77
- // UI-only PR, maxGenerate-1 on a mixed PR), and reservedUISlots — the count that
78
- // budgets for them — used to be dropped, so a UI-only PR rendered an empty
79
- // GENERATE list. runBudget now fills the reserved UI slots for BOTH budgeters.
80
- describe("runBudget reserved-UI-slot fill", () => {
81
- it("fills GENERATE with UI candidates on a UI-only PR (was empty)", () => {
82
- const ranked = [
83
- mkCandidate("u1", { testType: "ui" }),
84
- mkCandidate("u2", { testType: "ui" }),
85
- mkCandidate("u3", { testType: "ui" }),
86
- ];
87
- const testCandidates = diversityBalancedBudgeter.select(ranked, ctx({ isUIOnlyPR: true, hasFrontendChanges: true, maxGenerate: 3 }));
88
- expect(testCandidates.generate).toHaveLength(3);
89
- expect(typesIn(testCandidates)).toEqual(new Set(["ui"]));
90
- expect(testCandidates.additional).toHaveLength(0);
91
- });
92
- it("applies to fixedNBudgeter too (shared runBudget pipeline)", () => {
93
- const ranked = [mkCandidate("u1", { testType: "ui" }), mkCandidate("u2", { testType: "ui" })];
94
- const res = fixedNBudgeter.select(ranked, ctx({ isUIOnlyPR: true, hasFrontendChanges: true, maxGenerate: 3 }));
95
- expect(res.generate).toHaveLength(2);
96
- expect(countType(res, "ui")).toBe(2);
97
- });
98
- it("caps promoted UI slots at maxGenerate, leaving the rest in ADDITIONAL", () => {
99
- const ranked = [
100
- mkCandidate("u1", { testType: "ui" }),
101
- mkCandidate("u2", { testType: "ui" }),
102
- mkCandidate("u3", { testType: "ui" }),
103
- mkCandidate("u4", { testType: "ui" }),
104
- ];
105
- const div = diversityBalancedBudgeter.select(ranked, ctx({ isUIOnlyPR: true, hasFrontendChanges: true, maxGenerate: 2 }));
106
- expect(div.generate).toHaveLength(2);
107
- expect(div.additional).toHaveLength(2);
108
- expect(typesIn(div)).toEqual(new Set(["ui"]));
109
- });
110
- it("reserves one UI slot even when the pick would otherwise starve UI (mixed PR)", () => {
111
- // backend = maxGenerate-1 = 2; fixed-n fills both with the protected
112
- // integrations, starving UI — the reserved slot then pulls u1 into GENERATE.
113
- const ranked = [
114
- mkCandidate("i1", { testType: "integration", priority: PriorityTier.CRITICAL }),
115
- mkCandidate("i2", { testType: "integration", priority: PriorityTier.CRITICAL }),
116
- mkCandidate("i3", { testType: "integration", priority: PriorityTier.CRITICAL }),
117
- mkCandidate("u1", { testType: "ui" }),
118
- ];
119
- const res = fixedNBudgeter.select(ranked, ctx({ hasFrontendChanges: true, maxGenerate: 3 }));
120
- expect(res.generate).toHaveLength(3);
121
- expect(countType(res, "ui")).toBe(1);
122
- });
123
- it("fills GENERATE when the top-ranked candidate is UI on a mixed PR", () => {
124
- // Top-ranked (CRITICAL) candidate is UI. Without excluding UI from the backend
125
- // pick it would be consumed there, the UI-promotion loop would find no UI left,
126
- // and GENERATE would be short of maxGenerate. Backend must fill from non-UI.
127
- const ranked = [
128
- mkCandidate("u1", { testType: "ui", priority: PriorityTier.CRITICAL }),
129
- mkCandidate("i1", { testType: "integration" }),
130
- mkCandidate("i2", { testType: "integration" }),
131
- mkCandidate("i3", { testType: "integration" }),
132
- ];
133
- const res = fixedNBudgeter.select(ranked, ctx({ hasFrontendChanges: true, maxGenerate: 3 }));
134
- expect(res.generate).toHaveLength(3);
135
- expect(countType(res, "ui")).toBe(1);
136
- expect(countType(res, "integration")).toBe(2);
137
- });
138
- });