@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,1925 +0,0 @@
1
- import { jest } from "@jest/globals";
2
- import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
3
- import { TestType } from "../../types/TestTypes.js";
4
- jest.unstable_mockModule("@skyramp/skyramp", () => ({
5
- WorkspaceConfigManager: { create: jest.fn() },
6
- }));
7
- const { buildRecommendationPrompt, buildExternalCoverageSet, externalDedupKey } = await import("./test-recommendation-prompt.js");
8
- const { buildExecutionPlan, EXEC_STEP_REGISTER } = await import("./diffExecutionPlan.js");
9
- const { PATH_PARAM_UUID_GUIDANCE, MAX_TESTS_TO_GENERATE, buildTestQualityCriteria, buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildFewShotExamples, buildVerificationChecklist, } = await import("./recommendationSections.js");
10
- import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
11
- // ---------------------------------------------------------------------------
12
- // Minimal fixtures
13
- // ---------------------------------------------------------------------------
14
- function minimalAnalysis(overrides = {}) {
15
- return {
16
- metadata: { repositoryName: "test-repo", analysisDate: "2026-01-01", scanDepth: "quick" },
17
- projectClassification: {
18
- projectType: "rest-api",
19
- primaryLanguage: "TypeScript",
20
- primaryFramework: "Express",
21
- deploymentPattern: "traditional",
22
- },
23
- technologyStack: { languages: ["TypeScript"], frameworks: ["Express"], runtime: "node", keyDependencies: [] },
24
- businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
25
- artifacts: { openApiSpecs: [], playwrightRecordings: [], traceFiles: [], notFound: [] },
26
- apiEndpoints: {
27
- totalCount: 1,
28
- baseUrl: "http://localhost:3000",
29
- endpoints: [{
30
- path: "/api/items",
31
- resourceGroup: "items",
32
- pathParams: [],
33
- methods: [{
34
- method: "GET",
35
- description: "List items",
36
- queryParams: [],
37
- authRequired: false,
38
- sourceFile: "routes/items.ts",
39
- interactions: [{ description: "success", type: "success", request: {}, response: { statusCode: 200, description: "OK" } }],
40
- }],
41
- }],
42
- },
43
- authentication: { method: "bearer", configLocation: "middleware/auth.ts", envVarsRequired: [], setupExample: "" },
44
- infrastructure: { isContainerized: false, hasDockerCompose: false, hasKubernetes: false, hasCiCd: false },
45
- existingTests: { frameworks: ["jest"], coverage: { unit: 0, integration: 0, e2e: 0, ui: 0, load: 0, contract: 0, smoke: 0 }, testLocations: {}, hasCoverageReports: false },
46
- ...overrides,
47
- };
48
- }
49
- function makePRContext(overrides = {}) {
50
- return {
51
- prNumber: 42,
52
- previousRecommendations: [],
53
- implementedTestFiles: [],
54
- executionResults: [],
55
- ...overrides,
56
- };
57
- }
58
- // ---------------------------------------------------------------------------
59
- // Tests — PR History & Stability in prompt output
60
- // ---------------------------------------------------------------------------
61
- describe("buildRecommendationPrompt — PR History section", () => {
62
- it("omits PR History section when no prContext is provided", () => {
63
- const prompt = buildRecommendationPrompt(minimalAnalysis());
64
- expect(prompt).not.toContain("PR History");
65
- expect(prompt).not.toContain("Stability rule");
66
- });
67
- it("omits PR History section when prContext has no recommendations", () => {
68
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, makePRContext());
69
- expect(prompt).not.toContain("PR History");
70
- });
71
- it("includes PR History section with prNumber when recommendations exist", () => {
72
- const ctx = makePRContext({
73
- prNumber: 99,
74
- previousRecommendations: [
75
- { testType: "integration", endpoint: "POST /api/items", status: "implemented", commentId: "1" },
76
- ],
77
- });
78
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
79
- expect(prompt).toContain("PR History (PR #99)");
80
- });
81
- it("renders Previously Generated Tests for implemented recommendations", () => {
82
- const ctx = makePRContext({
83
- previousRecommendations: [
84
- { testType: "contract", endpoint: "GET /api/items", status: "implemented", commentId: "1" },
85
- ],
86
- implementedTestFiles: ["test_items_contract.py"],
87
- });
88
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
89
- expect(prompt).toContain("Previously Generated Tests");
90
- expect(prompt).toContain("contract — GET /api/items");
91
- expect(prompt).toContain("test_items_contract.py");
92
- });
93
- it("does NOT include stability rule when only implemented tests exist (no recommended)", () => {
94
- const ctx = makePRContext({
95
- previousRecommendations: [
96
- { testType: "contract", endpoint: "GET /api/items", status: "implemented", commentId: "1" },
97
- ],
98
- });
99
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
100
- expect(prompt).not.toContain("Stability rule");
101
- expect(prompt).not.toContain("Previously Recommended");
102
- });
103
- it("includes stability rule when recommended-but-not-generated tests exist", () => {
104
- const ctx = makePRContext({
105
- previousRecommendations: [
106
- { testType: "integration", endpoint: "POST /api/orders", scenarioName: "order-lifecycle", status: "recommended", commentId: "1" },
107
- ],
108
- });
109
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
110
- expect(prompt).toContain("Previously Recommended (not generated)");
111
- expect(prompt).toContain("Stability rule");
112
- expect(prompt).toContain("integration — POST /api/orders (scenario: order-lifecycle)");
113
- });
114
- it("stability rule uses scenarioName for multi-step and endpoint for single-endpoint matching", () => {
115
- const ctx = makePRContext({
116
- previousRecommendations: [
117
- { testType: "integration", endpoint: "POST /api/orders", status: "recommended", commentId: "1" },
118
- ],
119
- });
120
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
121
- const stabilityBlock = prompt.slice(prompt.indexOf("**Stability rule**"), prompt.indexOf("Only add NEW recommendations"));
122
- expect(stabilityBlock).toContain("scenarioName");
123
- expect(stabilityBlock).toContain("endpoint");
124
- expect(stabilityBlock).toContain("Re-derive category and priority");
125
- expect(stabilityBlock).not.toMatch(/same.*category/);
126
- expect(stabilityBlock).not.toMatch(/same.*priority/);
127
- });
128
- it("includes top-level stability guidance about not churning recommendations", () => {
129
- const ctx = makePRContext({
130
- previousRecommendations: [
131
- { testType: "integration", endpoint: "POST /api/orders", status: "recommended", commentId: "1" },
132
- ],
133
- });
134
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
135
- expect(prompt).toContain("Do not churn recommendations without cause");
136
- expect(prompt).toContain("Carry forward");
137
- });
138
- it("includes both implemented and recommended sections when both exist", () => {
139
- const ctx = makePRContext({
140
- previousRecommendations: [
141
- { testType: "contract", endpoint: "GET /api/items", status: "implemented", commentId: "1" },
142
- { testType: "integration", endpoint: "POST /api/orders", scenarioName: "order-flow", status: "recommended", commentId: "1" },
143
- ],
144
- implementedTestFiles: ["test_items_contract.py"],
145
- });
146
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
147
- expect(prompt).toContain("Previously Generated Tests");
148
- expect(prompt).toContain("Previously Recommended (not generated)");
149
- expect(prompt).toContain("Stability rule");
150
- });
151
- it("includes execution results when present", () => {
152
- const ctx = makePRContext({
153
- previousRecommendations: [
154
- { testType: "contract", endpoint: "GET /api/items", status: "implemented", commentId: "1" },
155
- ],
156
- executionResults: [
157
- { testFile: "test_items_contract.py", status: "fail", timestamp: "2026-01-01T00:00:00Z" },
158
- ],
159
- });
160
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
161
- expect(prompt).toContain("Execution Results from Prior Run");
162
- expect(prompt).toContain("test_items_contract.py");
163
- expect(prompt).toContain("fail");
164
- });
165
- it("does not render scenarioName parenthetical when scenarioName is undefined", () => {
166
- const ctx = makePRContext({
167
- previousRecommendations: [
168
- { testType: "contract", endpoint: "GET /api/items", status: "recommended", commentId: "1" },
169
- ],
170
- });
171
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
172
- expect(prompt).toContain("contract — GET /api/items");
173
- expect(prompt).not.toContain("(scenario:");
174
- });
175
- it("lists ALL recommended scenarios when multiple exist", () => {
176
- const ctx = makePRContext({
177
- previousRecommendations: [
178
- { testType: "integration", endpoint: "POST /api/orders", scenarioName: "order-lifecycle", status: "recommended", commentId: "1" },
179
- { testType: "contract", endpoint: "GET /api/items/{id}", scenarioName: "item-schema-check", status: "recommended", commentId: "1" },
180
- { testType: "integration", endpoint: "DELETE /api/users/{id}", scenarioName: "cascade-delete-user", status: "recommended", commentId: "1" },
181
- ],
182
- });
183
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
184
- expect(prompt).toContain("order-lifecycle");
185
- expect(prompt).toContain("item-schema-check");
186
- expect(prompt).toContain("cascade-delete-user");
187
- expect(prompt).toContain("Stability rule");
188
- });
189
- it("excludes dismissed recommendations from Previously Generated and Previously Recommended", () => {
190
- const ctx = makePRContext({
191
- previousRecommendations: [
192
- { testType: "integration", endpoint: "POST /api/orders", scenarioName: "order-lifecycle", status: "dismissed", commentId: "1" },
193
- { testType: "contract", endpoint: "GET /api/items", status: "recommended", commentId: "1" },
194
- ],
195
- });
196
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
197
- expect(prompt).not.toContain("order-lifecycle");
198
- expect(prompt).toContain("contract — GET /api/items");
199
- });
200
- it("shows promotion instruction alongside carry-forward when recommended tests exist", () => {
201
- const ctx = makePRContext({
202
- previousRecommendations: [
203
- { testType: "integration", endpoint: "POST /api/orders", status: "recommended", commentId: "1" },
204
- ],
205
- });
206
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
207
- expect(prompt).toContain("Promote the highest-priority ones");
208
- expect(prompt).toContain("into generation slots if capacity allows");
209
- });
210
- it("includes do-not-re-recommend instruction for implemented tests", () => {
211
- const ctx = makePRContext({
212
- previousRecommendations: [
213
- { testType: "contract", endpoint: "GET /api/items", status: "implemented", commentId: "1" },
214
- ],
215
- });
216
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
217
- expect(prompt).toContain("Do NOT re-recommend");
218
- expect(prompt).toContain("Previously Generated Tests");
219
- });
220
- it("de-duplicates multi-step scenario entries to one line per scenario", () => {
221
- const ctx = makePRContext({
222
- previousRecommendations: [
223
- { testType: "integration", endpoint: "POST /api/items", scenarioName: "order-lifecycle", status: "recommended", commentId: "1" },
224
- { testType: "integration", endpoint: "POST /api/orders", scenarioName: "order-lifecycle", status: "recommended", commentId: "1" },
225
- { testType: "integration", endpoint: "GET /api/orders/{id}", scenarioName: "order-lifecycle", status: "recommended", commentId: "1" },
226
- ],
227
- });
228
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
229
- const recSection = prompt.slice(prompt.indexOf("Previously Recommended"), prompt.indexOf("**Stability rule**"));
230
- const scenarioOccurrences = (recSection.match(/order-lifecycle/g) || []).length;
231
- expect(scenarioOccurrences).toBe(1);
232
- expect(recSection).toContain("integration — POST /api/items (scenario: order-lifecycle)");
233
- expect(recSection).not.toContain("POST /api/orders");
234
- expect(recSection).not.toContain("GET /api/orders/{id}");
235
- });
236
- it("keeps entries with no scenarioName as separate lines (de-dup by testType+endpoint)", () => {
237
- const ctx = makePRContext({
238
- previousRecommendations: [
239
- { testType: "contract", endpoint: "GET /api/items", status: "recommended", commentId: "1" },
240
- { testType: "contract", endpoint: "GET /api/items", status: "recommended", commentId: "1" },
241
- { testType: "contract", endpoint: "POST /api/items", status: "recommended", commentId: "1" },
242
- ],
243
- });
244
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
245
- const recSection = prompt.slice(prompt.indexOf("Previously Recommended"), prompt.indexOf("**Stability rule**"));
246
- const getOccurrences = (recSection.match(/contract — GET \/api\/items/g) || []).length;
247
- expect(getOccurrences).toBe(1);
248
- expect(recSection).toContain("contract — POST /api/items");
249
- });
250
- // Ensure de-duplicates across different scenarios independently
251
- it("de-duplicates across different scenarios independently", () => {
252
- const ctx = makePRContext({
253
- previousRecommendations: [
254
- { testType: "integration", endpoint: "POST /api/items", scenarioName: "order-lifecycle", status: "recommended", commentId: "1" },
255
- { testType: "integration", endpoint: "POST /api/orders", scenarioName: "order-lifecycle", status: "recommended", commentId: "1" },
256
- { testType: "integration", endpoint: "POST /api/items", scenarioName: "cascade-delete", status: "recommended", commentId: "1" },
257
- { testType: "integration", endpoint: "DELETE /api/items/{id}", scenarioName: "cascade-delete", status: "recommended", commentId: "1" },
258
- ],
259
- });
260
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
261
- const recSection = prompt.slice(prompt.indexOf("Previously Recommended"), prompt.indexOf("**Stability rule**"));
262
- const orderOccurrences = (recSection.match(/order-lifecycle/g) || []).length;
263
- const cascadeOccurrences = (recSection.match(/cascade-delete/g) || []).length;
264
- expect(orderOccurrences).toBe(1);
265
- expect(cascadeOccurrences).toBe(1);
266
- });
267
- });
268
- // ---------------------------------------------------------------------------
269
- // Tests — Stability and supplement in buildRecommendationPrompt
270
- // ---------------------------------------------------------------------------
271
- function minimalScenario(overrides = {}) {
272
- return {
273
- scenarioName: "item-create-delete",
274
- description: "Create and delete an item to verify cascade",
275
- category: "data_integrity",
276
- priority: "high",
277
- steps: [
278
- { order: 1, method: "POST", path: "/api/items", description: "Create item", interactionType: "success", expectedStatusCode: 201 },
279
- { order: 2, method: "DELETE", path: "/api/items/{id}", description: "Delete item", interactionType: "success", expectedStatusCode: 204 },
280
- ],
281
- chainingKeys: ["id"],
282
- requiresAuth: false,
283
- estimatedComplexity: "simple",
284
- testType: TestType.INTEGRATION,
285
- ...overrides,
286
- };
287
- }
288
- describe("buildRecommendationPrompt — Stability and supplement section", () => {
289
- it("includes Recommendation Stability section in output when scenarios exist (PR mode)", () => {
290
- const analysis = minimalAnalysis({
291
- businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [minimalScenario()] },
292
- });
293
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
294
- expect(prompt).toContain("## Recommendation Stability");
295
- });
296
- it("stability section uses scenarioName/endpoint matching strategy (PR mode)", () => {
297
- const analysis = minimalAnalysis({
298
- businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [minimalScenario()] },
299
- });
300
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
301
- const stabilityStart = prompt.indexOf("## Recommendation Stability");
302
- const stabilityBlock = prompt.slice(stabilityStart, stabilityStart + 500);
303
- expect(stabilityBlock).toContain("scenarioName");
304
- expect(stabilityBlock).toContain("endpoint");
305
- expect(stabilityBlock).toContain("Re-derive category and priority");
306
- });
307
- it("stability section specifies when to drop a recommendation (PR mode)", () => {
308
- const analysis = minimalAnalysis({
309
- businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [minimalScenario()] },
310
- });
311
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
312
- expect(prompt).toContain("target endpoint was removed");
313
- expect(prompt).toContain("business logic changed");
314
- expect(prompt).toContain("covered by a generated test");
315
- });
316
- it("instructs to supplement beyond pre-ranked list to reach topN", () => {
317
- // 1 scenario, topN=10 → supplementCount=9 → supplement note should appear
318
- const analysis = minimalAnalysis({
319
- businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [minimalScenario()] },
320
- });
321
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.FullRepo, 10);
322
- expect(prompt).toContain("<supplement_guidance>");
323
- expect(prompt).toContain("5-dimension rubric");
324
- });
325
- // Verify MAX_TESTS_TO_GENERATE is still exported and equals 3
326
- it("MAX_TESTS_TO_GENERATE is 3", () => {
327
- expect(MAX_TESTS_TO_GENERATE).toBe(3);
328
- });
329
- it("uses MAX_CRITICAL_TESTS in category-aware selection rules (PR mode)", () => {
330
- const analysis = minimalAnalysis({
331
- businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [minimalScenario()] },
332
- });
333
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
334
- // MAX_CRITICAL_TESTS applies to PR mode (GENERATE items) — full_repo mode only presents, does not execute
335
- expect(prompt).toContain("GENERATE items should be from HIGH-priority categories");
336
- });
337
- });
338
- // ---------------------------------------------------------------------------
339
- // Tests — PATH_PARAM_UUID_GUIDANCE (no hardcoded UUID anchor)
340
- // ---------------------------------------------------------------------------
341
- const UUID_V4_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i;
342
- describe("PATH_PARAM_UUID_GUIDANCE — no hardcoded UUID anchor", () => {
343
- it("contains no hardcoded UUID that the LLM could anchor to", () => {
344
- expect(PATH_PARAM_UUID_GUIDANCE).not.toMatch(UUID_V4_REGEX);
345
- });
346
- it("uses <random-uuid-v4> as the placeholder in the pathParams example", () => {
347
- expect(PATH_PARAM_UUID_GUIDANCE).toContain("<random-uuid-v4>");
348
- });
349
- it("instructs the LLM to generate a fresh UUID v4", () => {
350
- expect(PATH_PARAM_UUID_GUIDANCE).toContain("generate a fresh random UUID v4");
351
- });
352
- it("is included in the recommendation prompt for an endpoint with a UUID path param", () => {
353
- const analysis = minimalAnalysis({
354
- apiEndpoints: {
355
- totalCount: 1,
356
- baseUrl: "http://localhost:3000",
357
- endpoints: [{
358
- path: "/api/items/{item_id}",
359
- resourceGroup: "items",
360
- pathParams: [{ name: "item_id", type: "string", required: true }],
361
- methods: [{
362
- method: "GET",
363
- description: "Get item by ID",
364
- queryParams: [],
365
- authRequired: false,
366
- sourceFile: "routes/items.ts",
367
- interactions: [{ description: "success", type: "success", request: {}, response: { statusCode: 200, description: "OK" } }],
368
- }],
369
- }],
370
- },
371
- });
372
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
373
- expect(prompt).toContain("<random-uuid-v4>");
374
- expect(prompt).not.toMatch(UUID_V4_REGEX);
375
- });
376
- });
377
- // ---------------------------------------------------------------------------
378
- // Tests — maxGenerateOverride parameter in buildRecommendationPrompt
379
- // ---------------------------------------------------------------------------
380
- describe("buildRecommendationPrompt — maxGenerateOverride", () => {
381
- const scenariosForOverride = [
382
- // One single-step scenario (auto-classified as contract in full_repo mode) to verify contract tool calls
383
- minimalScenario({
384
- scenarioName: "scenario-0",
385
- description: "Test scenario 0",
386
- category: "security_boundary",
387
- priority: "high",
388
- testType: TestType.CONTRACT,
389
- steps: [{ order: 1, method: "GET", path: "/api/items", description: "Get items", interactionType: "success", expectedStatusCode: 200 }],
390
- }),
391
- // Five multi-step integration scenarios
392
- ...Array.from({ length: 5 }, (_, i) => minimalScenario({
393
- scenarioName: `scenario-${i + 1}`,
394
- description: `Test scenario ${i + 1}`,
395
- category: i < 1 ? "security_boundary" : "crud",
396
- priority: i < 1 ? "high" : "low",
397
- })),
398
- ];
399
- const analysisWithScenarios = minimalAnalysis({
400
- businessContext: {
401
- mainPurpose: "Test API",
402
- userFlows: [],
403
- dataFlows: [],
404
- integrationPatterns: [],
405
- draftedScenarios: scenariosForOverride,
406
- },
407
- });
408
- it("uses MAX_TESTS_TO_GENERATE as default when maxGenerateOverride is undefined", () => {
409
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.CurrentBranchDiff, 10);
410
- // buildExecutionPlan seed line: "Max: N generate + up to M additional"
411
- expect(prompt).toContain(`Max: ${MAX_TESTS_TO_GENERATE} generate`);
412
- });
413
- it("respects maxGenerateOverride when provided", () => {
414
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 5);
415
- expect(prompt).toContain("Max: 5 generate");
416
- expect(prompt).toContain("up to 5 additional");
417
- });
418
- it("clamps maxGenerateOverride to topN when override exceeds topN", () => {
419
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.CurrentBranchDiff, 4, undefined, undefined, undefined, undefined, 10);
420
- expect(prompt).toContain("Max: 4 generate");
421
- });
422
- it("clamps maxGenerateOverride to 0 when negative", () => {
423
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, -5);
424
- expect(prompt).toContain("Max: 0 generate");
425
- });
426
- it("allows maxGenerateOverride of 0 to produce no generate items", () => {
427
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 0);
428
- expect(prompt).toContain("Max: 0 generate");
429
- // The few-shot examples section contains "#1 — GENERATE" as an illustration,
430
- // so check the execution plan's GENERATE section is empty instead.
431
- expect(prompt).toContain("(no pre-ranked generate items");
432
- });
433
- it("uses topN as default maxGen in full_repo scope — Repo mode output format", () => {
434
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.FullRepo, 6);
435
- expect(prompt).toContain("Test Recommendations — 6 total (grouped by test type)");
436
- expect(prompt).toContain("Repo mode");
437
- expect(prompt).not.toContain("Budget:");
438
- // Full-repo mode: no execution, just presentation
439
- expect(prompt).toContain("no tests are executed");
440
- expect(prompt).toContain("Present up to 6 recommendations.");
441
- // Full-repo items must include tool calls for on-demand generation
442
- expect(prompt).toContain("skyramp_contract_test_generation");
443
- });
444
- it("full_repo mode ignores maxGenerateOverride — always presents all", () => {
445
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.FullRepo, 6, undefined, undefined, undefined, undefined, 2);
446
- expect(prompt).toContain("Test Recommendations — 6 total (grouped by test type)");
447
- expect(prompt).toContain("Repo mode");
448
- expect(prompt).not.toContain("Budget:");
449
- expect(prompt).toContain("no tests are executed");
450
- });
451
- it("full_repo mode includes tool calls in each recommendation item", () => {
452
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.FullRepo, 6);
453
- // Contract items should have tool calls
454
- expect(prompt).toContain("skyramp_contract_test_generation");
455
- // Integration items should use the batch tool
456
- expect(prompt).toContain("skyramp_batch_scenario_test_generation");
457
- // MANDATORY language for test type mix and item counts
458
- expect(prompt).toContain("Test type mix");
459
- });
460
- it("full_repo mode uses MANDATORY language for item count and test type mix", () => {
461
- const prompt = buildRecommendationPrompt(analysisWithScenarios, AnalysisScope.FullRepo, 6);
462
- expect(prompt).toContain("Test type mix — MANDATORY");
463
- expect(prompt).toContain("Present up to 6 recommendations.");
464
- });
465
- it("full_repo mode pre-allocates E2E and UI sections for full-stack repos", () => {
466
- const fullStackAnalysis = minimalAnalysis({
467
- projectClassification: {
468
- projectType: "full-stack",
469
- primaryLanguage: "TypeScript",
470
- primaryFramework: "Express",
471
- deploymentPattern: "traditional",
472
- },
473
- businessContext: {
474
- mainPurpose: "Test API",
475
- userFlows: [],
476
- dataFlows: [],
477
- integrationPatterns: [],
478
- draftedScenarios: scenariosForOverride,
479
- },
480
- });
481
- const prompt = buildRecommendationPrompt(fullStackAnalysis, AnalysisScope.FullRepo, 10);
482
- // E2E and UI sections must be present even though scenarioDrafting only produces backend types
483
- expect(prompt).toContain("### E2E");
484
- expect(prompt).toContain("### UI");
485
- expect(prompt).toContain("skyramp_e2e_test_generation");
486
- expect(prompt).toContain("skyramp_ui_test_generation");
487
- // Backend sections should still be present
488
- expect(prompt).toContain("### Integration");
489
- });
490
- });
491
- // ---------------------------------------------------------------------------
492
- // Tests — Additional recommendation dedup (Fix 1) and E2E slot guard (Fix 2)
493
- // ---------------------------------------------------------------------------
494
- describe("buildRecommendationPrompt — additional recommendation dedup", () => {
495
- function patchOrdersScenario(name, overrides = {}) {
496
- return {
497
- scenarioName: name,
498
- description: `Test ${name}`,
499
- category: "new_endpoint",
500
- priority: "high",
501
- steps: [
502
- { order: 1, method: "POST", path: "/api/v1/products", description: "Create product", interactionType: "success", expectedStatusCode: 201 },
503
- { order: 2, method: "POST", path: "/api/v1/orders", description: "Create order", interactionType: "success", expectedStatusCode: 201 },
504
- { order: 3, method: "PATCH", path: "/api/v1/orders/{order_id}", description: "Patch order", interactionType: "success", expectedStatusCode: 200 },
505
- ],
506
- chainingKeys: ["id"],
507
- requiresAuth: false,
508
- estimatedComplexity: "complex",
509
- testType: TestType.INTEGRATION,
510
- ...overrides,
511
- };
512
- }
513
- function analysisWithPatchScenarios(scenarios) {
514
- return minimalAnalysis({
515
- businessContext: {
516
- mainPurpose: "Order API",
517
- userFlows: [],
518
- dataFlows: [],
519
- integrationPatterns: [],
520
- draftedScenarios: scenarios,
521
- },
522
- branchDiffContext: {
523
- baseBranch: "main",
524
- currentBranch: "feature/patch-orders",
525
- changedFiles: ["backend/src/api/orders.py", "src/frontend/components/OrderDetail.tsx"],
526
- newEndpoints: [{ path: "/api/v1/orders/{order_id}", methods: [{ method: "PATCH", sourceFile: "routes.py", interactionCount: 0 }] }],
527
- modifiedEndpoints: [],
528
- affectedServices: [],
529
- },
530
- apiEndpoints: {
531
- totalCount: 3,
532
- baseUrl: "http://localhost:8000",
533
- endpoints: [
534
- { path: "/api/v1/products", resourceGroup: "products", pathParams: [], methods: [{ method: "POST", description: "Create product", queryParams: [], authRequired: false, sourceFile: "routes.py", interactions: [] }] },
535
- { path: "/api/v1/orders", resourceGroup: "orders", pathParams: [], methods: [{ method: "POST", description: "Create order", queryParams: [], authRequired: false, sourceFile: "routes.py", interactions: [] }] },
536
- { path: "/api/v1/orders/{order_id}", resourceGroup: "orders", pathParams: [{ name: "order_id", type: "string", required: true }], methods: [{ method: "PATCH", description: "Update order", queryParams: [], authRequired: false, sourceFile: "routes.py", interactions: [] }] },
537
- ],
538
- },
539
- });
540
- }
541
- it("filters additional items that share resource and test type with GENERATE items", () => {
542
- const scenarios = [
543
- patchOrdersScenario("orders-patch-add-items-recalculate"),
544
- patchOrdersScenario("orders-patch-new-endpoint-happy-path"),
545
- patchOrdersScenario("orders-patch-items-cleanup-verification"),
546
- patchOrdersScenario("orders-patch-discount-fixed"),
547
- patchOrdersScenario("orders-patch-another-variant"),
548
- ];
549
- const analysis = analysisWithPatchScenarios(scenarios);
550
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 2);
551
- // First 2 become GENERATE, the remaining share orders::integration → should be filtered
552
- const ordersPatchAdditional = (prompt.match(/\[ADDITIONAL\].*orders-patch/g) || []);
553
- // Same-resource same-type scenarios should NOT appear in ADDITIONAL
554
- expect(ordersPatchAdditional.length).toBe(0);
555
- });
556
- it("preserves additional items with different test type for same endpoint", () => {
557
- const scenarios = [
558
- patchOrdersScenario("orders-patch-add-items-recalculate"),
559
- patchOrdersScenario("orders-patch-new-endpoint-happy-path"),
560
- // Contract test for same endpoint — different test type, should survive dedup
561
- {
562
- ...patchOrdersScenario("orders-patch-contract"),
563
- testType: TestType.CONTRACT,
564
- steps: [{ order: 1, method: "PATCH", path: "/api/v1/orders/{order_id}", description: "Contract test", interactionType: "success", expectedStatusCode: 200 }],
565
- },
566
- ];
567
- const analysis = analysisWithPatchScenarios(scenarios);
568
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 2);
569
- // Contract test targets orders but is a different type → should be in ADDITIONAL
570
- expect(prompt).toContain("orders-patch-contract");
571
- });
572
- it("preserves additional items targeting a different resource", () => {
573
- const scenarios = [
574
- patchOrdersScenario("orders-patch-add-items-recalculate"),
575
- patchOrdersScenario("orders-patch-new-endpoint-happy-path"),
576
- // Different resource entirely
577
- {
578
- ...patchOrdersScenario("products-unique-constraint"),
579
- steps: [
580
- { order: 1, method: "POST", path: "/api/v1/products", description: "Create product", interactionType: "success", expectedStatusCode: 201 },
581
- { order: 2, method: "POST", path: "/api/v1/products", description: "Create duplicate", interactionType: "error", expectedStatusCode: 409 },
582
- ],
583
- },
584
- ];
585
- const analysis = analysisWithPatchScenarios(scenarios);
586
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 2);
587
- expect(prompt).toContain("products-unique-constraint");
588
- });
589
- });
590
- describe("buildRecommendationPrompt — LLM scope assessment for UI/E2E split", () => {
591
- it("embeds scope assessment section in diff-scope prompt for mixed PR", () => {
592
- // The LLM now determines UI vs backend split via the scope assessment — verify it's present.
593
- const scenarios = [
594
- minimalScenario({ scenarioName: "integration-test-1", category: "new_endpoint" }),
595
- ];
596
- const analysis = minimalAnalysis({
597
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: scenarios },
598
- branchDiffContext: {
599
- baseBranch: "main",
600
- currentBranch: "feature/test",
601
- changedFiles: ["backend/routes.py", "src/frontend/components/App.tsx"],
602
- newEndpoints: [{ path: "/api/items/{id}", methods: [{ method: "PATCH", sourceFile: "routes.py", interactionCount: 0 }] }],
603
- modifiedEndpoints: [],
604
- affectedServices: [],
605
- },
606
- });
607
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 3);
608
- expect(prompt).toContain("PR Scope Assessment");
609
- expect(prompt).toContain("Budget Plan:");
610
- expect(prompt).toContain("UI/E2E tests (add per your Budget Plan)");
611
- expect(prompt).toContain("Honor your Budget Plan");
612
- });
613
- it("includes UI/E2E tool guidance in the ADDITIONAL section for mixed PRs", () => {
614
- const analysis = minimalAnalysis({
615
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
616
- branchDiffContext: {
617
- baseBranch: "main",
618
- currentBranch: "feature/test",
619
- changedFiles: ["backend/routes.py", "src/frontend/components/App.tsx"],
620
- newEndpoints: [{ path: "/api/items", methods: [{ method: "POST", sourceFile: "routes.py", interactionCount: 0 }] }],
621
- modifiedEndpoints: [],
622
- affectedServices: [],
623
- },
624
- });
625
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 1);
626
- expect(prompt).toContain("skyramp_e2e_test_generation");
627
- expect(prompt).toContain("skyramp_ui_test_generation");
628
- });
629
- it("omits UI/E2E guidance for backend-only PRs (no frontend files)", () => {
630
- const analysis = minimalAnalysis({
631
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
632
- branchDiffContext: {
633
- baseBranch: "main",
634
- currentBranch: "feature/test",
635
- changedFiles: ["backend/routes.py", "services/order_service.py"],
636
- newEndpoints: [{ path: "/api/items", methods: [{ method: "POST", sourceFile: "routes.py", interactionCount: 0 }] }],
637
- modifiedEndpoints: [],
638
- affectedServices: [],
639
- },
640
- });
641
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10, undefined, undefined, undefined, undefined, 3);
642
- // No UI guidance injected for backend-only PR — scope assessment covers it
643
- expect(prompt).not.toContain("UI/E2E tests (add per your Budget Plan)");
644
- });
645
- it("embeds scope assessment in UI-only PR (frontend files, no API changes)", () => {
646
- // Previously the UI-only path used a hard-coded Budget: N total mandate.
647
- // Now it goes through scope assessment so the LLM can scale down for CSS tweaks.
648
- const analysis = minimalAnalysis({
649
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
650
- branchDiffContext: {
651
- baseBranch: "main",
652
- currentBranch: "feature/test",
653
- changedFiles: ["src/components/Button.tsx", "styles/global.css"],
654
- newEndpoints: [],
655
- modifiedEndpoints: [],
656
- affectedServices: [],
657
- },
658
- });
659
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
660
- expect(prompt).toContain("PR Scope Assessment");
661
- expect(prompt).toContain("Budget Plan:");
662
- expect(prompt).toContain("100% UI/E2E");
663
- expect(prompt).toContain("Honor your Budget Plan");
664
- // Hard-coded exact mandate should be gone — LLM decides the count
665
- expect(prompt).not.toContain("You MUST produce EXACTLY");
666
- });
667
- it("embeds scope assessment in no-scenarios path (backend PR, no drafted scenarios)", () => {
668
- // Previously the no-scenarios path used a hard-coded Budget mandate.
669
- // Now it uses scope assessment so a trivial PR doesn't get told to produce 20 tests.
670
- const analysis = minimalAnalysis({
671
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
672
- branchDiffContext: {
673
- baseBranch: "main",
674
- currentBranch: "feature/test",
675
- changedFiles: ["backend/routes.py"],
676
- newEndpoints: [{ path: "/api/items", methods: [{ method: "POST", sourceFile: "routes.py", interactionCount: 0 }] }],
677
- modifiedEndpoints: [],
678
- affectedServices: [],
679
- },
680
- });
681
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
682
- expect(prompt).toContain("PR Scope Assessment");
683
- expect(prompt).toContain("Budget Plan:");
684
- expect(prompt).toContain("Honor your Budget Plan");
685
- expect(prompt).not.toContain("You MUST produce EXACTLY");
686
- });
687
- });
688
- // ---------------------------------------------------------------------------
689
- // Tests — zero-classified diff: budget defaults to 0 (SKYR-3820)
690
- // ---------------------------------------------------------------------------
691
- describe("buildRecommendationPrompt — zero-classified diff (SKYR-3820)", () => {
692
- function zeroClassifiedAnalysis(changedFiles) {
693
- return minimalAnalysis({
694
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
695
- branchDiffContext: {
696
- baseBranch: "main",
697
- currentBranch: "skyramp/testbot-setup",
698
- changedFiles,
699
- newEndpoints: [],
700
- modifiedEndpoints: [],
701
- affectedServices: [],
702
- },
703
- });
704
- }
705
- it("config-only PR (netbox shape): Budget Plan defaults to 0 total, fixed budget mandate absent", () => {
706
- const prompt = buildRecommendationPrompt(zeroClassifiedAnalysis([
707
- ".github/workflows/skyramp-testbot.yml",
708
- "deployment-testbot/Dockerfile",
709
- "deployment-testbot/configuration.py",
710
- "deployment-testbot/setup.sh",
711
- ]), AnalysisScope.CurrentBranchDiff, 20, undefined, undefined, undefined, undefined, 3);
712
- expect(prompt).toContain("Budget Plan: 0 total");
713
- expect(prompt).not.toContain("Budget Plan: 20 total (3 generate + 17 additional)");
714
- expect(prompt).not.toContain("Use these exact numbers throughout the rest of the prompt.");
715
- // The "draft your own" pressure valves must not fire on a zero-classified diff
716
- expect(prompt).not.toContain("draft your own based on endpoint analysis");
717
- expect(prompt).not.toContain("If your Budget Plan total exceeds the pre-ranked items listed above");
718
- });
719
- it("backend source change with 0 classified endpoints keeps the conditional ceiling (SKYR-3855 guard)", () => {
720
- // immich user.dto.ts shape — classification blind to a real behavior change.
721
- // The agent must still be able to claim the budget from its own code review.
722
- const prompt = buildRecommendationPrompt(zeroClassifiedAnalysis(["server/src/dtos/user.dto.ts"]), AnalysisScope.CurrentBranchDiff, 20, undefined, undefined, undefined, undefined, 3);
723
- expect(prompt).toContain("Budget Plan: 0 total");
724
- expect(prompt).toContain("up to 20 total (3 generate + 17 additional)");
725
- });
726
- it("classified backend PR is unchanged: fixed budget line still rendered", () => {
727
- const analysis = minimalAnalysis({
728
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
729
- branchDiffContext: {
730
- baseBranch: "main",
731
- currentBranch: "feature/test",
732
- changedFiles: ["backend/routes.py"],
733
- newEndpoints: [{ path: "/api/items", methods: [{ method: "POST", sourceFile: "routes.py", interactionCount: 0 }] }],
734
- modifiedEndpoints: [],
735
- affectedServices: [],
736
- },
737
- });
738
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 20, undefined, undefined, undefined, undefined, 3);
739
- expect(prompt).toContain("Budget Plan: 20 total (3 generate + 17 additional), 0% UI/E2E");
740
- expect(prompt).toContain("Use these exact numbers throughout the rest of the prompt.");
741
- expect(prompt).not.toContain("Budget Plan: 0 total");
742
- });
743
- it("buildExecutionPlan omitting hasApiChanges renders the fixed budget (back-compat)", () => {
744
- const plan = buildExecutionPlan([], 3, 20, "http://localhost:3000", "", "", "", "seed", 100, false, false);
745
- expect(plan).toContain("Budget Plan: 20 total (3 generate + 17 additional), 0% UI/E2E");
746
- expect(plan).not.toContain("Budget Plan: 0 total");
747
- });
748
- });
749
- // ---------------------------------------------------------------------------
750
- // Tests — REGISTER step (SKYR-3879 Path B checkpoint)
751
- // ---------------------------------------------------------------------------
752
- describe("buildRecommendationPrompt — REGISTER step (SKYR-3879 Path B)", () => {
753
- it("diff-scope execution plan includes the REGISTER step and its skyramp_register_test_plan instruction", () => {
754
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
755
- expect(prompt).toContain(`Step ${EXEC_STEP_REGISTER} — Register your test plan`);
756
- expect(prompt).toContain("skyramp_register_test_plan");
757
- });
758
- it("diff-scope execution plan's GENERATE header references the REGISTER step", () => {
759
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
760
- expect(prompt).toContain(`registering via Step ${EXEC_STEP_REGISTER}`);
761
- });
762
- it("full-repo mode (no execution plan) never mentions skyramp_register_test_plan", () => {
763
- const scenariosForFullRepo = [
764
- minimalScenario({
765
- scenarioName: "scenario-0",
766
- category: "crud",
767
- priority: "low",
768
- testType: TestType.CONTRACT,
769
- steps: [{ order: 1, method: "GET", path: "/api/items", description: "Get items", interactionType: "success", expectedStatusCode: 200 }],
770
- }),
771
- ];
772
- const analysis = minimalAnalysis({
773
- businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: scenariosForFullRepo },
774
- });
775
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.FullRepo, 6);
776
- expect(prompt).not.toContain("skyramp_register_test_plan");
777
- });
778
- it("diff-scope 'draft your own' fallback (no pre-drafted scenarios) still includes the REGISTER step — the execution plan is always used in diff scope", () => {
779
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
780
- // minimalAnalysis() has no draftedScenarios and no branchDiffContext, so scored=[]
781
- // but buildExecutionPlan still renders (see buildRecommendationPrompt's isDiffScope branch).
782
- expect(prompt).toContain("skyramp_register_test_plan");
783
- });
784
- });
785
- // ---------------------------------------------------------------------------
786
- // Tests — GENERATE slot allocation (UI vs backend slots)
787
- // ---------------------------------------------------------------------------
788
- describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
789
- function makeScenario(name) {
790
- return minimalScenario({ scenarioName: name, category: "new_endpoint" });
791
- }
792
- it("UI-only PR: all GENERATE slots are UI placeholders (no backend)", () => {
793
- const analysis = minimalAnalysis({
794
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
795
- branchDiffContext: {
796
- baseBranch: "main",
797
- currentBranch: "feature/ui",
798
- changedFiles: ["src/components/OrderForm.tsx", "src/pages/Checkout.tsx"],
799
- newEndpoints: [],
800
- modifiedEndpoints: [],
801
- affectedServices: [],
802
- },
803
- });
804
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
805
- // The GENERATE slots are the UI placeholder blocks — check their specific scenario names
806
- expect(prompt).toContain("#1 — GENERATE** | ui | workflow | new");
807
- expect(prompt).toContain("ui-test-for-changed-component-1");
808
- expect(prompt).toContain("ui_test_1_trace.zip");
809
- expect(prompt).toContain("skyramp_ui_test_generation");
810
- // Each slot targets a distinct changed component/flow
811
- expect(prompt).toContain("distinct changed component or user flow");
812
- });
813
- it("mixed PR: last GENERATE slot is UI, preceding slots are backend scenarios", () => {
814
- const scenarios = [
815
- makeScenario("orders-create"),
816
- makeScenario("orders-update"),
817
- makeScenario("orders-delete"),
818
- ];
819
- const analysis = minimalAnalysis({
820
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: scenarios },
821
- branchDiffContext: {
822
- baseBranch: "main",
823
- currentBranch: "feature/mixed",
824
- changedFiles: ["src/routes/orders.ts", "src/components/OrderForm.tsx"],
825
- newEndpoints: [{ path: "/api/orders", methods: [{ method: "POST", sourceFile: "orders.ts", interactionCount: 0 }] }],
826
- modifiedEndpoints: [],
827
- affectedServices: [],
828
- },
829
- });
830
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
831
- // Last GENERATE slot is UI (not E2E)
832
- expect(prompt).toContain("— GENERATE** | ui");
833
- expect(prompt).toContain("skyramp_ui_test_generation");
834
- expect(prompt).not.toContain("— GENERATE** | e2e");
835
- // At least one backend scenario in GENERATE (#1 or #2)
836
- expect(prompt).toContain("#1 — GENERATE** | integration");
837
- // Scenario name from the pre-ranked list (orders-create or orders-update)
838
- expect(prompt).toContain("orders-create");
839
- });
840
- it("backend-only PR: all GENERATE slots are backend scenarios (no E2E injection)", () => {
841
- const scenarios = [makeScenario("items-create"), makeScenario("items-get"), makeScenario("items-delete")];
842
- const analysis = minimalAnalysis({
843
- businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: scenarios },
844
- branchDiffContext: {
845
- baseBranch: "main",
846
- currentBranch: "feature/backend",
847
- changedFiles: ["backend/routes.py"],
848
- newEndpoints: [{ path: "/api/items", methods: [{ method: "POST", sourceFile: "routes.py", interactionCount: 0 }] }],
849
- modifiedEndpoints: [],
850
- affectedServices: [],
851
- },
852
- });
853
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
854
- // All 3 GENERATE slots are backend
855
- expect(prompt).toContain("#1 — GENERATE** | integration");
856
- // No UI placeholder injected for backend-only PR
857
- expect(prompt).not.toContain("ui-test-for-changed-components");
858
- expect(prompt).not.toContain("ui_mixed_pr_trace.zip");
859
- });
860
- it("promotes attack-surface security boundaries into generated slots", () => {
861
- const attackSurface = minimalScenario({
862
- scenarioName: "flows-bulk-delete-post-auth-boundary",
863
- description: "Attack-surface auth boundary: /api/flows/bulk_delete is a destructive sibling of changed DELETE /api/flows/{id}; verify it rejects missing authentication",
864
- category: "security_boundary",
865
- priority: "high",
866
- testType: TestType.CONTRACT,
867
- steps: [{
868
- order: 1,
869
- method: "POST",
870
- path: "/api/flows/bulk_delete",
871
- description: "POST /api/flows/bulk_delete without auth",
872
- interactionType: "error",
873
- expectedStatusCode: 401,
874
- }],
875
- });
876
- const directDelete = minimalScenario({
877
- scenarioName: "flows-delete-auth-boundary",
878
- description: "Auth boundary: DELETE /api/flows/{id} rejects missing authentication",
879
- category: "security_boundary",
880
- priority: "high",
881
- testType: TestType.CONTRACT,
882
- steps: [{
883
- order: 1,
884
- method: "DELETE",
885
- path: "/api/flows/{id}",
886
- description: "DELETE /api/flows/{id} without auth",
887
- interactionType: "error",
888
- expectedStatusCode: 401,
889
- }],
890
- });
891
- const validation = minimalScenario({
892
- scenarioName: "flows-delete-invalid-id",
893
- description: "Validate malformed IDs",
894
- category: "data_validation",
895
- priority: "medium",
896
- testType: TestType.CONTRACT,
897
- steps: [{
898
- order: 1,
899
- method: "DELETE",
900
- path: "/api/flows/not-a-uuid",
901
- description: "DELETE /api/flows/not-a-uuid",
902
- interactionType: "error",
903
- expectedStatusCode: 422,
904
- }],
905
- });
906
- const analysis = minimalAnalysis({
907
- businessContext: {
908
- mainPurpose: "Test",
909
- userFlows: [],
910
- dataFlows: [],
911
- integrationPatterns: [],
912
- draftedScenarios: [directDelete, validation, attackSurface],
913
- },
914
- branchDiffContext: {
915
- baseBranch: "main",
916
- currentBranch: "feature/admin-key",
917
- changedFiles: ["src/prefect/server/api/flows.py"],
918
- newEndpoints: [],
919
- modifiedEndpoints: [{
920
- path: "/api/flows/{id}",
921
- methods: [{ method: "DELETE", sourceFile: "flows.py", changeType: "modified" }],
922
- }],
923
- affectedServices: [],
924
- },
925
- });
926
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 5, undefined, undefined, undefined, undefined, 2);
927
- const attackIdx = prompt.indexOf("POST /api/flows/bulk_delete → 401");
928
- const directIdx = prompt.indexOf("DELETE /api/flows/{id} → 401");
929
- expect(attackIdx).toBeGreaterThanOrEqual(0);
930
- expect(directIdx).toBeGreaterThanOrEqual(0);
931
- expect(attackIdx).toBeLessThan(directIdx);
932
- expect(prompt).toContain("#1 — GENERATE** | contract | security_boundary");
933
- expect(prompt).toContain("preserve attack-surface `security_boundary` items");
934
- });
935
- it("does not external-dedup protected bug-catching or attack-surface scenarios", () => {
936
- const attackSurface = minimalScenario({
937
- scenarioName: "flows-bulk-delete-post-auth-boundary",
938
- description: "Verify destructive sibling rejects missing authentication",
939
- category: "security_boundary",
940
- isAttackSurfaceSecurityBoundary: true,
941
- testType: TestType.CONTRACT,
942
- steps: [{ order: 1, method: "POST", path: "/api/flows/bulk_delete", description: "POST /api/flows/bulk_delete without auth", interactionType: "error", expectedStatusCode: 401 }],
943
- });
944
- const bugCaught = minimalScenario({
945
- scenarioName: "orders-discount-bug-caught",
946
- description: "Bug-catching test: discount should subtract from total",
947
- category: "bug_caught",
948
- testType: TestType.CONTRACT,
949
- steps: [{ order: 1, method: "POST", path: "/api/orders", description: "POST /api/orders exposes discount bug", interactionType: "success", expectedStatusCode: 201 }],
950
- });
951
- const ordinary = minimalScenario({
952
- scenarioName: "customers-create-contract",
953
- description: "Create customer contract",
954
- category: "crud",
955
- testType: TestType.CONTRACT,
956
- steps: [{ order: 1, method: "POST", path: "/api/customers", description: "POST /api/customers", interactionType: "success", expectedStatusCode: 201 }],
957
- });
958
- const prompt = buildExecutionPlan([attackSurface, bugCaught, ordinary].map((scenario) => ({
959
- scenario,
960
- priority: scenario.category === "crud" ? PriorityTier.LOW : PriorityTier.CRITICAL,
961
- novelty: Novelty.MODIFIED,
962
- })), 3, 3, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 3, false, false, false, new Set(["POST::flows::contract", "POST::orders::contract", "POST::customers::contract"]));
963
- expect(prompt).toContain("POST /api/flows/bulk_delete → 401");
964
- expect(prompt).toContain("POST /api/orders → 201");
965
- expect(prompt).not.toContain("POST /api/customers → 201");
966
- expect(prompt).toContain("`bug_caught` and attack-surface `security_boundary` items get the strictest reading");
967
- });
968
- it("keeps symmetric attack-surface siblings together before ordinary auth boundaries", () => {
969
- const directWidgets = minimalScenario({
970
- scenarioName: "widgets-delete-auth-boundary",
971
- description: "Auth boundary: DELETE /api/widgets/{id} rejects missing authentication",
972
- category: "security_boundary",
973
- testType: TestType.CONTRACT,
974
- steps: [{ order: 1, method: "DELETE", path: "/api/widgets/{id}", description: "DELETE /api/widgets/{id} without auth", interactionType: "error", expectedStatusCode: 401 }],
975
- });
976
- const directGadgets = minimalScenario({
977
- scenarioName: "gadgets-delete-auth-boundary",
978
- description: "Auth boundary: DELETE /api/gadgets/{id} rejects missing authentication",
979
- category: "security_boundary",
980
- testType: TestType.CONTRACT,
981
- steps: [{ order: 1, method: "DELETE", path: "/api/gadgets/{id}", description: "DELETE /api/gadgets/{id} without auth", interactionType: "error", expectedStatusCode: 401 }],
982
- });
983
- const widgetsBulk = minimalScenario({
984
- scenarioName: "widgets-bulk-delete-auth-boundary",
985
- description: "Attack-surface auth boundary: /api/widgets/bulk_delete is a destructive sibling of changed DELETE /api/widgets/{id}; verify it rejects missing authentication",
986
- category: "security_boundary",
987
- testType: TestType.CONTRACT,
988
- steps: [{ order: 1, method: "POST", path: "/api/widgets/bulk_delete", description: "POST /api/widgets/bulk_delete without auth", interactionType: "error", expectedStatusCode: 401 }],
989
- });
990
- const gadgetsBulk = minimalScenario({
991
- scenarioName: "gadgets-bulk-delete-auth-boundary",
992
- description: "Attack-surface auth boundary: /api/gadgets/bulk_delete is a destructive sibling of changed DELETE /api/gadgets/{id}; verify it rejects missing authentication",
993
- category: "security_boundary",
994
- testType: TestType.CONTRACT,
995
- steps: [{ order: 1, method: "POST", path: "/api/gadgets/bulk_delete", description: "POST /api/gadgets/bulk_delete without auth", interactionType: "error", expectedStatusCode: 401 }],
996
- });
997
- const prompt = buildExecutionPlan([directWidgets, directGadgets, widgetsBulk, gadgetsBulk].map((scenario) => ({
998
- scenario,
999
- priority: PriorityTier.CRITICAL,
1000
- novelty: Novelty.MODIFIED,
1001
- })), 3, 4, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 4, false);
1002
- const generatedBlock = prompt.slice(0, prompt.indexOf("#4 [ADDITIONAL]"));
1003
- const additionalBlock = prompt.slice(prompt.indexOf("#4 [ADDITIONAL]"));
1004
- expect(generatedBlock).toContain("POST /api/widgets/bulk_delete → 401");
1005
- expect(generatedBlock).toContain("POST /api/gadgets/bulk_delete → 401");
1006
- expect(additionalBlock).not.toContain("POST /api/widgets/bulk_delete → 401");
1007
- expect(additionalBlock).not.toContain("POST /api/gadgets/bulk_delete → 401");
1008
- expect(additionalBlock).toContain("DELETE /api/gadgets/{id} → 401");
1009
- });
1010
- });
1011
- describe("buildExecutionPlan — even-with-spillover test-type distribution", () => {
1012
- // Backend-only PR (hasFrontendChanges=false) so every GENERATE slot is a backend
1013
- // scenario drawn from `scored` — this is where contract-vs-integration distribution
1014
- // applies. baseUrl/auth args mirror the existing buildExecutionPlan tests above.
1015
- const contract = (name, path, priority = PriorityTier.HIGH) => ({
1016
- scenario: minimalScenario({
1017
- scenarioName: name,
1018
- category: "crud",
1019
- testType: TestType.CONTRACT,
1020
- steps: [{ order: 1, method: "POST", path, description: `POST ${path}`, interactionType: "success", expectedStatusCode: 201 }],
1021
- }),
1022
- priority: priority,
1023
- novelty: Novelty.NEW,
1024
- });
1025
- const integration = (name, p1, p2, priority = PriorityTier.HIGH) => ({
1026
- scenario: minimalScenario({
1027
- scenarioName: name,
1028
- category: "data_integrity",
1029
- testType: TestType.INTEGRATION,
1030
- steps: [
1031
- { order: 1, method: "POST", path: p1, description: `POST ${p1}`, interactionType: "success", expectedStatusCode: 201 },
1032
- { order: 2, method: "PATCH", path: p2, description: `PATCH ${p2}`, interactionType: "success", expectedStatusCode: 200 },
1033
- ],
1034
- }),
1035
- priority: priority,
1036
- novelty: Novelty.NEW,
1037
- });
1038
- const plan = (items, maxGen, topN) => buildExecutionPlan(items, maxGen, topN, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", items.length, false);
1039
- it("distributes GENERATE slots across contract AND integration when budget < candidates", () => {
1040
- // 4 contract out-rank 4 integration; pure top-3 rank slice would be all contract.
1041
- const items = [
1042
- contract("c1", "/api/a"), contract("c2", "/api/b"), contract("c3", "/api/c"), contract("c4", "/api/d"),
1043
- integration("i1", "/api/x", "/api/x/{id}"), integration("i2", "/api/y", "/api/y/{id}"),
1044
- ];
1045
- const prompt = plan(items, 3, 6);
1046
- const generated = prompt.slice(0, prompt.indexOf("[ADDITIONAL]") >= 0 ? prompt.indexOf("[ADDITIONAL]") : prompt.length);
1047
- expect(generated).toMatch(/GENERATE\*\* \| contract/);
1048
- expect(generated).toMatch(/GENERATE\*\* \| integration/);
1049
- });
1050
- it("single backend type → unchanged top-N rank slice (no regression)", () => {
1051
- const items = [contract("c1", "/api/a"), contract("c2", "/api/b"), contract("c3", "/api/c"), contract("c4", "/api/d")];
1052
- const prompt = plan(items, 3, 6);
1053
- const generated = prompt.slice(0, prompt.indexOf("[ADDITIONAL]") >= 0 ? prompt.indexOf("[ADDITIONAL]") : prompt.length);
1054
- // Top 3 contract by rank are generated; the 4th is not.
1055
- expect(generated).toContain("POST /api/a → 201");
1056
- expect(generated).toContain("POST /api/b → 201");
1057
- expect(generated).toContain("POST /api/c → 201");
1058
- expect(generated).not.toContain("POST /api/d → 201");
1059
- expect(generated).not.toMatch(/GENERATE\*\* \| integration/);
1060
- });
1061
- it("keeps a CRITICAL item in GENERATE even when lower-ranked than other-type candidates", () => {
1062
- // A CRITICAL integration sits after several HIGH contracts in rank order.
1063
- const items = [
1064
- contract("c1", "/api/a"), contract("c2", "/api/b"), contract("c3", "/api/c"),
1065
- integration("crit", "/api/orders", "/api/orders/{id}", PriorityTier.CRITICAL),
1066
- ];
1067
- const prompt = plan(items, 3, 6);
1068
- const generated = prompt.slice(0, prompt.indexOf("[ADDITIONAL]") >= 0 ? prompt.indexOf("[ADDITIONAL]") : prompt.length);
1069
- // The CRITICAL integration must occupy a GENERATE slot (protected-first).
1070
- expect(generated).toContain("PATCH /api/orders/{id}");
1071
- });
1072
- it("spills freed slots to the remaining type when one type runs dry", () => {
1073
- // 1 integration + 4 contract, budget 3: round 1 = 1 integration + 1 contract,
1074
- // integration bucket now empty → remaining 1 slot spills to contract → 2 contract total.
1075
- const items = [
1076
- integration("i1", "/api/x", "/api/x/{id}"),
1077
- contract("c1", "/api/a"), contract("c2", "/api/b"), contract("c3", "/api/c"), contract("c4", "/api/d"),
1078
- ];
1079
- const prompt = plan(items, 3, 6);
1080
- const generated = prompt.slice(0, prompt.indexOf("[ADDITIONAL]") >= 0 ? prompt.indexOf("[ADDITIONAL]") : prompt.length);
1081
- const contractSlots = (generated.match(/GENERATE\*\* \| contract/g) || []).length;
1082
- const integrationSlots = (generated.match(/GENERATE\*\* \| integration/g) || []).length;
1083
- expect(integrationSlots).toBe(1);
1084
- expect(contractSlots).toBe(2);
1085
- });
1086
- it("never lists a GENERATE item again under ADDITIONAL (set-difference recompute)", () => {
1087
- const items = [
1088
- contract("c1", "/api/a"), contract("c2", "/api/b"), contract("c3", "/api/c"), contract("c4", "/api/d"),
1089
- integration("i1", "/api/x", "/api/x/{id}"), integration("i2", "/api/y", "/api/y/{id}"),
1090
- ];
1091
- const prompt = plan(items, 3, 6);
1092
- const splitIdx = prompt.indexOf("[ADDITIONAL]");
1093
- expect(splitIdx).toBeGreaterThan(0);
1094
- const generated = prompt.slice(0, splitIdx);
1095
- const additional = prompt.slice(splitIdx);
1096
- // Each generated endpoint must not reappear in the ADDITIONAL section.
1097
- for (const m of generated.matchAll(/(POST|PATCH) (\/api\/[^\s]+)/g)) {
1098
- expect(additional).not.toContain(`${m[1]} ${m[2]}`);
1099
- }
1100
- });
1101
- });
1102
- // ---------------------------------------------------------------------------
1103
- // Tests — buildTestQualityCriteria contract-test guidance (regression guard)
1104
- // ---------------------------------------------------------------------------
1105
- describe("buildTestQualityCriteria — contract test guidance for error-handling", () => {
1106
- it("includes guidance to use contract tests for single-endpoint error-handling scenarios", () => {
1107
- const criteria = buildTestQualityCriteria();
1108
- expect(criteria).toContain("Contract tests");
1109
- expect(criteria).toContain("error-handling scenarios on a single");
1110
- expect(criteria).toContain("Do NOT add setup steps just to avoid hardcoding an ID");
1111
- });
1112
- it("instructs to use a hardcoded nonexistent ID to keep it a single-step test", () => {
1113
- const criteria = buildTestQualityCriteria();
1114
- expect(criteria).toContain("99999");
1115
- expect(criteria).toContain("single-step contract test");
1116
- });
1117
- it("is included in the recommendation prompt when scored scenarios exist", () => {
1118
- const analysis = minimalAnalysis({
1119
- businessContext: {
1120
- mainPurpose: "Test API",
1121
- userFlows: [],
1122
- dataFlows: [],
1123
- integrationPatterns: [],
1124
- draftedScenarios: [minimalScenario()],
1125
- },
1126
- });
1127
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.FullRepo, 10);
1128
- expect(prompt).toContain("Do NOT add setup steps just to avoid hardcoding an ID");
1129
- });
1130
- });
1131
- // ---------------------------------------------------------------------------
1132
- // Tests — Architect Persona Preamble
1133
- // ---------------------------------------------------------------------------
1134
- describe("buildRecommendationPrompt — Architect Persona Preamble", () => {
1135
- it("includes Skyramp Integration Architect persona at the beginning of the prompt", () => {
1136
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1137
- expect(prompt).toContain("Skyramp Integration Architect");
1138
- });
1139
- it("persona appears before the mode preamble", () => {
1140
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1141
- const architectIdx = prompt.indexOf("Skyramp Integration Architect");
1142
- const modeIdx = prompt.indexOf("Repo mode");
1143
- expect(architectIdx).toBeLessThan(modeIdx);
1144
- expect(architectIdx).toBeGreaterThan(-1);
1145
- });
1146
- it("persona instructs not to guess or fabricate values", () => {
1147
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1148
- expect(prompt).toContain("No guessing");
1149
- expect(prompt).toContain("derive all parameters");
1150
- });
1151
- it("buildArchitectPreamble(diff) returns the diff persona", () => {
1152
- const preamble = buildArchitectPreamble(true);
1153
- expect(preamble).toContain("Skyramp Integration Architect");
1154
- expect(preamble).toContain("generate tests for this PR");
1155
- expect(preamble).toContain("duplicate coverage");
1156
- });
1157
- it("buildArchitectPreamble(full_repo) returns the full-repo persona", () => {
1158
- const preamble = buildArchitectPreamble(false);
1159
- expect(preamble).toContain("Skyramp Integration Architect");
1160
- expect(preamble).toContain("comprehensive test recommendation catalog");
1161
- expect(preamble).toContain("Do not call any generation tools");
1162
- });
1163
- });
1164
- // ---------------------------------------------------------------------------
1165
- // Tests — Mandatory Reasoning Protocol
1166
- // ---------------------------------------------------------------------------
1167
- describe("buildRecommendationPrompt — Mandatory Reasoning Protocol", () => {
1168
- it("includes reasoning protocol section in the prompt", () => {
1169
- const analysis = minimalAnalysis({
1170
- businessContext: {
1171
- mainPurpose: "Test API",
1172
- userFlows: [],
1173
- dataFlows: [],
1174
- integrationPatterns: [],
1175
- draftedScenarios: [minimalScenario()],
1176
- },
1177
- });
1178
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.FullRepo, 10);
1179
- expect(prompt).toContain("Parameter Grounding Rule");
1180
- });
1181
- it("reasoning protocol covers the key parameter sources", () => {
1182
- const protocol = buildReasoningProtocol();
1183
- expect(protocol).toContain("requestBody");
1184
- expect(protocol).toContain("endpointURL");
1185
- expect(protocol).toContain("authHeader");
1186
- expect(protocol).toContain("Foreign-key path params");
1187
- });
1188
- it("reasoning protocol instructs to read source file when value cannot be sourced", () => {
1189
- const protocol = buildReasoningProtocol();
1190
- expect(protocol).toContain("read the relevant source file");
1191
- });
1192
- });
1193
- // ---------------------------------------------------------------------------
1194
- // Tests — Context Fetching Guidance
1195
- // ---------------------------------------------------------------------------
1196
- describe("buildRecommendationPrompt — Context Fetching Guidance", () => {
1197
- it("includes execution plan context when sessionId is provided", () => {
1198
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.FullRepo, 10, undefined, undefined, undefined, undefined, undefined, "test-session-123");
1199
- expect(prompt).toContain("Execution Plan Context");
1200
- expect(prompt).toContain("relevant source file");
1201
- });
1202
- it("omits execution plan context when sessionId is not provided", () => {
1203
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1204
- expect(prompt).not.toContain("Execution Plan Context");
1205
- });
1206
- it("buildContextFetchingGuidance returns empty string when no sessionId", () => {
1207
- expect(buildContextFetchingGuidance()).toBe("");
1208
- expect(buildContextFetchingGuidance(undefined)).toBe("");
1209
- });
1210
- it("buildContextFetchingGuidance references enriched scenarios and source files", () => {
1211
- const guidance = buildContextFetchingGuidance("session-1");
1212
- expect(guidance).toContain("placeholder");
1213
- expect(guidance).toContain("relevant source file");
1214
- });
1215
- });
1216
- // ---------------------------------------------------------------------------
1217
- // Tests — Tool Contract Framing
1218
- // ---------------------------------------------------------------------------
1219
- describe("buildRecommendationPrompt — Tool Contract Framing", () => {
1220
- it("includes contract language in tool workflows section (PR mode only)", () => {
1221
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff);
1222
- expect(prompt).toContain("strict technical contracts");
1223
- });
1224
- it("includes parameter grounding reminder in tool workflows (PR mode only)", () => {
1225
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff);
1226
- expect(prompt).toContain("Confirm WHERE each key value comes from");
1227
- });
1228
- });
1229
- // ---------------------------------------------------------------------------
1230
- // Tests — Multi-method endpoint partitioning
1231
- // ---------------------------------------------------------------------------
1232
- describe("buildRecommendationPrompt — multi-method endpoint partitioning", () => {
1233
- it("classifies all methods of a changed endpoint as changed", () => {
1234
- // When classifyEndpointsByChangedFiles identifies a file as changed,
1235
- // all methods from that endpoint's scanned catalog entry are included
1236
- // with concrete methods (no MULTI sentinels).
1237
- const analysis = minimalAnalysis({
1238
- apiEndpoints: {
1239
- totalCount: 2,
1240
- baseUrl: "http://localhost:3000",
1241
- endpoints: [
1242
- {
1243
- path: "/api/products",
1244
- resourceGroup: "products",
1245
- pathParams: [],
1246
- methods: [
1247
- { method: "GET", description: "List products", queryParams: [], authRequired: false, sourceFile: "app/api/products/route.ts", interactions: [] },
1248
- { method: "POST", description: "Create product", queryParams: [], authRequired: false, sourceFile: "app/api/products/route.ts", interactions: [] },
1249
- ],
1250
- },
1251
- {
1252
- path: "/api/items",
1253
- resourceGroup: "items",
1254
- pathParams: [],
1255
- methods: [
1256
- { method: "GET", description: "List items", queryParams: [], authRequired: false, sourceFile: "routes/items.ts", interactions: [] },
1257
- ],
1258
- },
1259
- ],
1260
- },
1261
- branchDiffContext: {
1262
- baseBranch: "main",
1263
- currentBranch: "feature/products",
1264
- changedFiles: ["app/api/products/route.ts"],
1265
- newEndpoints: [{
1266
- path: "/api/products",
1267
- methods: [
1268
- { method: "GET", sourceFile: "app/api/products/route.ts", interactionCount: 0 },
1269
- { method: "POST", sourceFile: "app/api/products/route.ts", interactionCount: 0 },
1270
- ],
1271
- }],
1272
- modifiedEndpoints: [],
1273
- affectedServices: [],
1274
- },
1275
- });
1276
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1277
- // Both GET and POST for /api/products should be in "Changed in this PR"
1278
- expect(prompt).toContain("Likely changed in this PR");
1279
- expect(prompt).toMatch(/Likely changed in this PR[\s\S]*GET \/api\/products/);
1280
- expect(prompt).toMatch(/Likely changed in this PR[\s\S]*POST \/api\/products/);
1281
- // /api/items should NOT be in changed section
1282
- expect(prompt).toMatch(/Other endpoints[\s\S]*GET \/api\/items/);
1283
- });
1284
- it("handles mix of new and modified endpoints with concrete methods", () => {
1285
- const analysis = minimalAnalysis({
1286
- apiEndpoints: {
1287
- totalCount: 2,
1288
- baseUrl: "http://localhost:3000",
1289
- endpoints: [
1290
- {
1291
- path: "/api/products",
1292
- resourceGroup: "products",
1293
- pathParams: [],
1294
- methods: [
1295
- { method: "GET", description: "List", queryParams: [], authRequired: false, sourceFile: "routes.ts", interactions: [] },
1296
- { method: "POST", description: "Create", queryParams: [], authRequired: false, sourceFile: "routes.ts", interactions: [] },
1297
- ],
1298
- },
1299
- {
1300
- path: "/api/orders",
1301
- resourceGroup: "orders",
1302
- pathParams: [],
1303
- methods: [
1304
- { method: "POST", description: "Create order", queryParams: [], authRequired: false, sourceFile: "routes.ts", interactions: [] },
1305
- ],
1306
- },
1307
- ],
1308
- },
1309
- branchDiffContext: {
1310
- baseBranch: "main",
1311
- currentBranch: "feature/mix",
1312
- changedFiles: ["routes.ts"],
1313
- newEndpoints: [
1314
- { path: "/api/products", methods: [
1315
- { method: "GET", sourceFile: "routes.ts", interactionCount: 0 },
1316
- { method: "POST", sourceFile: "routes.ts", interactionCount: 0 },
1317
- ] },
1318
- ],
1319
- modifiedEndpoints: [
1320
- { path: "/api/orders", methods: [{ method: "POST", sourceFile: "routes.ts", changeType: "modified" }] },
1321
- ],
1322
- affectedServices: [],
1323
- },
1324
- });
1325
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1326
- // Both products and orders should be in changed section
1327
- expect(prompt).toMatch(/Likely changed in this PR[\s\S]*GET \/api\/products/);
1328
- expect(prompt).toMatch(/Likely changed in this PR[\s\S]*POST \/api\/orders/);
1329
- });
1330
- });
1331
- // ---------------------------------------------------------------------------
1332
- // Tests — Removed endpoint [removed] marker in prompt (Fix 3 verification)
1333
- // ---------------------------------------------------------------------------
1334
- describe("buildRecommendationPrompt — removed endpoint listing", () => {
1335
- it("appends [removed] marker for removed endpoints not in current catalog", () => {
1336
- const analysis = minimalAnalysis({
1337
- apiEndpoints: {
1338
- totalCount: 1,
1339
- baseUrl: "http://localhost:3000",
1340
- endpoints: [{
1341
- path: "/api/items",
1342
- resourceGroup: "items",
1343
- pathParams: [],
1344
- methods: [{
1345
- method: "GET", description: "List items", queryParams: [],
1346
- authRequired: false, sourceFile: "routes.ts", interactions: [],
1347
- }],
1348
- }],
1349
- },
1350
- branchDiffContext: {
1351
- baseBranch: "main",
1352
- currentBranch: "feature/remove",
1353
- changedFiles: ["routes.ts"],
1354
- newEndpoints: [],
1355
- modifiedEndpoints: [],
1356
- removedEndpoints: [{
1357
- path: "/api/legacy",
1358
- methods: [{ method: "DELETE", sourceFile: "routes.ts", changeType: "removed" }],
1359
- }],
1360
- affectedServices: [],
1361
- },
1362
- });
1363
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1364
- expect(prompt).toContain("DELETE /api/legacy [removed]");
1365
- expect(prompt).toContain("Likely changed in this PR");
1366
- });
1367
- });
1368
- // ---------------------------------------------------------------------------
1369
- // Tests — Long-context best practices: XML tags structure
1370
- // ---------------------------------------------------------------------------
1371
- describe("buildRecommendationPrompt — XML tag structure (long-context best practice)", () => {
1372
- it("wraps repository context in <repository_context> tags", () => {
1373
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1374
- expect(prompt).toContain("<repository_context>");
1375
- expect(prompt).toContain("</repository_context>");
1376
- });
1377
- it("wraps endpoint interactions in <endpoint_interactions> tags", () => {
1378
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1379
- expect(prompt).toContain("<endpoint_interactions>");
1380
- expect(prompt).toContain("</endpoint_interactions>");
1381
- });
1382
- it("wraps existing tests in <existing_tests> tags", () => {
1383
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1384
- expect(prompt).toContain("<existing_tests>");
1385
- expect(prompt).toContain("</existing_tests>");
1386
- });
1387
- it("wraps instructions in <instructions> tags", () => {
1388
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1389
- expect(prompt).toContain("<instructions>");
1390
- expect(prompt).toContain("</instructions>");
1391
- });
1392
- it("wraps branch diff in <branch_diff> tags when diff context is present", () => {
1393
- const analysis = minimalAnalysis({
1394
- branchDiffContext: {
1395
- baseBranch: "main",
1396
- currentBranch: "feature/test",
1397
- changedFiles: ["src/routes.ts"],
1398
- newEndpoints: [{ path: "/api/new", methods: [{ method: "POST", sourceFile: "routes.ts", interactionCount: 0 }] }],
1399
- modifiedEndpoints: [],
1400
- affectedServices: [],
1401
- },
1402
- });
1403
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1404
- expect(prompt).toContain("<branch_diff>");
1405
- expect(prompt).toContain("</branch_diff>");
1406
- });
1407
- it("wraps PR history in <pr_history> tags when present", () => {
1408
- const ctx = makePRContext({
1409
- prNumber: 42,
1410
- previousRecommendations: [
1411
- { testType: "contract", endpoint: "GET /api/items", status: "implemented", commentId: "1" },
1412
- ],
1413
- });
1414
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 10, ctx);
1415
- expect(prompt).toContain("<pr_history>");
1416
- expect(prompt).toContain("</pr_history>");
1417
- });
1418
- });
1419
- // ---------------------------------------------------------------------------
1420
- // Tests — Long-context best practice: data before instructions
1421
- // ---------------------------------------------------------------------------
1422
- describe("buildRecommendationPrompt — data-before-instructions ordering", () => {
1423
- it("places repository_context before instructions", () => {
1424
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1425
- const dataIdx = prompt.indexOf("<repository_context>");
1426
- const instructionsIdx = prompt.indexOf("<instructions>");
1427
- expect(dataIdx).toBeLessThan(instructionsIdx);
1428
- });
1429
- it("places existing_tests before instructions", () => {
1430
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1431
- const testsIdx = prompt.indexOf("<existing_tests>");
1432
- const instructionsIdx = prompt.indexOf("<instructions>");
1433
- expect(testsIdx).toBeLessThan(instructionsIdx);
1434
- });
1435
- it("places endpoint_interactions before instructions", () => {
1436
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1437
- const interactionsIdx = prompt.indexOf("<endpoint_interactions>");
1438
- const instructionsIdx = prompt.indexOf("<instructions>");
1439
- expect(interactionsIdx).toBeLessThan(instructionsIdx);
1440
- });
1441
- });
1442
- // ---------------------------------------------------------------------------
1443
- // Tests — Few-shot examples
1444
- // ---------------------------------------------------------------------------
1445
- describe("buildFewShotExamples — concrete few-shot examples with thinking", () => {
1446
- it("returns examples wrapped in <examples> tags", () => {
1447
- const examples = buildFewShotExamples();
1448
- expect(examples).toContain("<examples>");
1449
- expect(examples).toContain("</examples>");
1450
- });
1451
- it("contains at least 2 example blocks", () => {
1452
- const examples = buildFewShotExamples();
1453
- const count = (examples.match(/<example /g) || []).length;
1454
- expect(count).toBeGreaterThanOrEqual(2);
1455
- });
1456
- it("includes <thinking> blocks inside examples to demonstrate reasoning", () => {
1457
- const examples = buildFewShotExamples();
1458
- expect(examples).toContain("<thinking>");
1459
- expect(examples).toContain("</thinking>");
1460
- });
1461
- it("includes an integration test example with tool calls", () => {
1462
- const examples = buildFewShotExamples();
1463
- expect(examples).toContain("skyramp_batch_scenario_test_generation");
1464
- expect(examples).toContain("skyramp_integration_test_generation");
1465
- });
1466
- it("includes a contract test example", () => {
1467
- const examples = buildFewShotExamples();
1468
- expect(examples).toContain("skyramp_contract_test_generation");
1469
- });
1470
- it("includes an ADDITIONAL example", () => {
1471
- const examples = buildFewShotExamples();
1472
- expect(examples).toContain("[ADDITIONAL]");
1473
- });
1474
- it("thinking blocks demonstrate parameter grounding", () => {
1475
- const examples = buildFewShotExamples();
1476
- expect(examples).toContain("**Parameter grounding**:");
1477
- expect(examples).toContain("workspace");
1478
- expect(examples).toContain("chained from");
1479
- });
1480
- it("integration example uses filePath from batch response, not a guessed filename", () => {
1481
- const examples = buildFewShotExamples();
1482
- expect(examples).toContain("<filePath returned by skyramp_batch_scenario_test_generation");
1483
- expect(examples).not.toContain('scenarioFile: "scenario_');
1484
- });
1485
- it("integration example includes bugCatchingTarget", () => {
1486
- const examples = buildFewShotExamples();
1487
- expect(examples).toContain("bugCatchingTarget:");
1488
- });
1489
- it("is included in the recommendation prompt (PR mode only)", () => {
1490
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff);
1491
- expect(prompt).toContain("<examples>");
1492
- });
1493
- });
1494
- // ---------------------------------------------------------------------------
1495
- // Tests — Verification checklist
1496
- // ---------------------------------------------------------------------------
1497
- describe("buildVerificationChecklist — self-check at end of prompt", () => {
1498
- it("returns checklist wrapped in <verification> tags", () => {
1499
- const checklist = buildVerificationChecklist(10, 3);
1500
- expect(checklist).toContain("<verification>");
1501
- expect(checklist).toContain("</verification>");
1502
- });
1503
- it("includes total count check aligned with Budget Plan range", () => {
1504
- const checklist = buildVerificationChecklist(10, 3);
1505
- // minTotal = min(3+1, 10) = 4 — range is 4–10
1506
- expect(checklist).toContain("between 4 and 10");
1507
- expect(checklist).toContain("Budget Plan");
1508
- expect(checklist).not.toContain("equals exactly 10");
1509
- });
1510
- it("includes scenarioFile source check", () => {
1511
- const checklist = buildVerificationChecklist(10, 3);
1512
- expect(checklist).toContain("filePath");
1513
- expect(checklist).toContain("skyramp_batch_scenario_test_generation");
1514
- });
1515
- it("includes bugCatchingTarget check", () => {
1516
- const checklist = buildVerificationChecklist(10, 3);
1517
- expect(checklist).toContain("bugCatchingTarget");
1518
- });
1519
- it("includes issue coverage check with promotion cap", () => {
1520
- const checklist = buildVerificationChecklist(10, 3);
1521
- expect(checklist).toContain("Issue coverage");
1522
- expect(checklist).toContain("highest-severity flaw");
1523
- expect(checklist).toContain("At most one promotion per run");
1524
- });
1525
- it("includes distinct code path check", () => {
1526
- const checklist = buildVerificationChecklist(10, 3);
1527
- expect(checklist).toContain("distinct code path");
1528
- });
1529
- it("includes auth consistency check", () => {
1530
- const checklist = buildVerificationChecklist(10, 3);
1531
- expect(checklist).toContain("Auth parameters are consistent");
1532
- });
1533
- it("includes endpointURL format check", () => {
1534
- const checklist = buildVerificationChecklist(10, 3);
1535
- expect(checklist).toContain("base URL and the path");
1536
- });
1537
- it("includes placeholder removal check", () => {
1538
- const checklist = buildVerificationChecklist(10, 3);
1539
- expect(checklist).toContain("from source");
1540
- });
1541
- it("is included in the recommendation prompt (PR mode only)", () => {
1542
- const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff);
1543
- expect(prompt).toContain("<verification>");
1544
- });
1545
- });
1546
- // ---------------------------------------------------------------------------
1547
- // Tests — Source evidence grounding
1548
- // ---------------------------------------------------------------------------
1549
- describe("buildRecommendationPrompt — source evidence grounding", () => {
1550
- it("includes source_evidence instruction in repo-mode enrichment", () => {
1551
- const analysis = minimalAnalysis({
1552
- businessContext: {
1553
- mainPurpose: "Test API",
1554
- userFlows: [],
1555
- dataFlows: [],
1556
- integrationPatterns: [],
1557
- draftedScenarios: [minimalScenario()],
1558
- },
1559
- });
1560
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.FullRepo, 10);
1561
- expect(prompt).toContain("Test Recommendations");
1562
- });
1563
- it("includes source_evidence instruction in PR-mode enrichment", () => {
1564
- const analysis = minimalAnalysis({
1565
- businessContext: {
1566
- mainPurpose: "Test API",
1567
- userFlows: [],
1568
- dataFlows: [],
1569
- integrationPatterns: [],
1570
- draftedScenarios: [minimalScenario()],
1571
- },
1572
- });
1573
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1574
- expect(prompt).toContain("Execution Plan");
1575
- });
1576
- });
1577
- // ---------------------------------------------------------------------------
1578
- // Tests — Over-prompting reduction
1579
- // ---------------------------------------------------------------------------
1580
- describe("buildRecommendationPrompt — reduced over-prompting", () => {
1581
- it("uses softer language for step labels (no MANDATORY)", () => {
1582
- const analysis = minimalAnalysis({
1583
- businessContext: {
1584
- mainPurpose: "Test API",
1585
- userFlows: [],
1586
- dataFlows: [],
1587
- integrationPatterns: [],
1588
- draftedScenarios: [minimalScenario()],
1589
- },
1590
- });
1591
- const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1592
- expect(prompt).not.toContain("(MANDATORY before executing anything)");
1593
- expect(prompt).toContain("(before anything else)");
1594
- });
1595
- it("uses XML tags in context fetching guidance", () => {
1596
- const guidance = buildContextFetchingGuidance("session-1");
1597
- expect(guidance).toContain("<context_fetching_protocol>");
1598
- expect(guidance).toContain("</context_fetching_protocol>");
1599
- });
1600
- it("uses XML tags in reasoning protocol", () => {
1601
- const protocol = buildReasoningProtocol();
1602
- expect(protocol).toContain("<reasoning_protocol>");
1603
- expect(protocol).toContain("</reasoning_protocol>");
1604
- });
1605
- });
1606
- // ---------------------------------------------------------------------------
1607
- // testFingerprint — compact existing-test summary in repoContext
1608
- // ---------------------------------------------------------------------------
1609
- describe("buildRecommendationPrompt — testFingerprint", () => {
1610
- it("omits fingerprint when no existing test locations", () => {
1611
- const prompt = buildRecommendationPrompt(minimalAnalysis());
1612
- expect(prompt).not.toContain("Tests already covering endpoints in this repo");
1613
- });
1614
- it("includes fingerprint with file count and endpoints when testLocations present", () => {
1615
- const analysis = minimalAnalysis({
1616
- existingTests: {
1617
- frameworks: ["pytest"],
1618
- coverage: { unit: 0, integration: 1, e2e: 0, ui: 0, load: 0, contract: 1, smoke: 0 },
1619
- testLocations: {
1620
- contract: "test_items_contract.py (covers: GET /api/items, POST /api/items)",
1621
- integration: "test_orders_integration.py (covers: POST /api/orders)",
1622
- },
1623
- hasCoverageReports: false,
1624
- },
1625
- });
1626
- const prompt = buildRecommendationPrompt(analysis);
1627
- expect(prompt).toContain("Tests already covering endpoints in this repo (2 files)");
1628
- expect(prompt).toContain("contract: GET /api/items, POST /api/items");
1629
- expect(prompt).toContain("integration: POST /api/orders");
1630
- });
1631
- it("counts files without covers: clause correctly (e.g. UI tests)", () => {
1632
- const analysis = minimalAnalysis({
1633
- existingTests: {
1634
- frameworks: ["playwright"],
1635
- coverage: { unit: 0, integration: 0, e2e: 0, ui: 2, load: 0, contract: 0, smoke: 0 },
1636
- testLocations: {
1637
- ui: "test_ui_login.py, test_ui_dashboard.py",
1638
- },
1639
- hasCoverageReports: false,
1640
- },
1641
- });
1642
- const prompt = buildRecommendationPrompt(analysis);
1643
- // File count should be 2, not 1
1644
- expect(prompt).toContain("Tests already covering endpoints in this repo (2 files)");
1645
- });
1646
- it("omits types with no endpoint coverage from fingerprint lines (no trailing 'ui: ' line)", () => {
1647
- const analysis = minimalAnalysis({
1648
- existingTests: {
1649
- frameworks: ["pytest", "playwright"],
1650
- coverage: { unit: 0, integration: 1, e2e: 0, ui: 1, load: 0, contract: 0, smoke: 0 },
1651
- testLocations: {
1652
- integration: "test_orders.py (covers: POST /api/orders)",
1653
- ui: "test_ui_login.py", // no covers: clause
1654
- },
1655
- hasCoverageReports: false,
1656
- },
1657
- });
1658
- const prompt = buildRecommendationPrompt(analysis);
1659
- expect(prompt).toContain("Tests already covering endpoints in this repo (2 files)");
1660
- expect(prompt).toContain("integration: POST /api/orders");
1661
- // UI type has no endpoints — must not emit a blank "ui: " line
1662
- expect(prompt).not.toMatch(/^\s*ui:\s*$/m);
1663
- });
1664
- it("distinguishes external tests from Skyramp tests in fingerprint", () => {
1665
- const analysis = minimalAnalysis({
1666
- existingTests: {
1667
- frameworks: ["pytest"],
1668
- coverage: { unit: 0, integration: 1, e2e: 0, ui: 0, load: 0, contract: 1, smoke: 0 },
1669
- testLocations: {
1670
- contract: "test_items_contract.py (covers: GET /api/items)",
1671
- integration: "tests/test_api.py [external] (covers: POST /api/orders)",
1672
- },
1673
- hasCoverageReports: false,
1674
- },
1675
- });
1676
- const prompt = buildRecommendationPrompt(analysis);
1677
- expect(prompt).toContain("1 Skyramp + 1 external");
1678
- expect(prompt).toContain("cannot be updated");
1679
- });
1680
- it("uses inclusive header for test coverage table", () => {
1681
- const analysis = minimalAnalysis({
1682
- existingTests: {
1683
- frameworks: ["pytest"],
1684
- coverage: { unit: 0, integration: 0, e2e: 0, ui: 0, load: 0, contract: 1, smoke: 0 },
1685
- testLocations: {
1686
- contract: "test_items_contract.py (covers: GET /api/items)",
1687
- },
1688
- hasCoverageReports: false,
1689
- },
1690
- });
1691
- const prompt = buildRecommendationPrompt(analysis);
1692
- expect(prompt).toContain("Existing test coverage (Skyramp + external)");
1693
- expect(prompt).not.toContain("Existing Skyramp test coverage");
1694
- });
1695
- it("includes external test dedup rule that blocks CREATE", () => {
1696
- const analysis = minimalAnalysis({
1697
- existingTests: {
1698
- frameworks: ["pytest"],
1699
- coverage: { unit: 0, integration: 1, e2e: 0, ui: 0, load: 0, contract: 0, smoke: 0 },
1700
- testLocations: {
1701
- integration: "tests/test_api.py [external] (covers: POST /api/orders)",
1702
- },
1703
- hasCoverageReports: false,
1704
- },
1705
- });
1706
- const prompt = buildRecommendationPrompt(analysis);
1707
- expect(prompt).toContain("[external]");
1708
- expect(prompt).toContain("do NOT create a new parallel test");
1709
- expect(prompt).toContain("Task 1 maintenance applies to them the same as Skyramp tests");
1710
- });
1711
- });
1712
- // ---------------------------------------------------------------------------
1713
- // Tests — External test dedup primitives
1714
- // ---------------------------------------------------------------------------
1715
- describe("buildExternalCoverageSet", () => {
1716
- it("parses single external test with one endpoint", () => {
1717
- const set = buildExternalCoverageSet({
1718
- integration: 'tests/test_api.py [external] (covers: GET /api/v1/orders)',
1719
- });
1720
- expect(set.has("GET::orders::integration")).toBe(true);
1721
- expect(set.size).toBe(1);
1722
- });
1723
- it("parses multiple endpoints in one covers clause", () => {
1724
- const set = buildExternalCoverageSet({
1725
- integration: 'tests/test_api.py [external] (covers: GET /api/v1/orders, POST /api/v1/orders, DELETE /api/v1/orders/{id})',
1726
- });
1727
- expect(set.has("GET::orders::integration")).toBe(true);
1728
- expect(set.has("POST::orders::integration")).toBe(true);
1729
- expect(set.has("DELETE::orders::integration")).toBe(true);
1730
- expect(set.size).toBe(3);
1731
- });
1732
- it("parses multiple external files in one test type", () => {
1733
- const set = buildExternalCoverageSet({
1734
- integration: 'tests/test_orders.py [external] (covers: GET /api/orders), tests/test_products.py [external] (covers: POST /api/products)',
1735
- });
1736
- expect(set.has("GET::orders::integration")).toBe(true);
1737
- expect(set.has("POST::products::integration")).toBe(true);
1738
- expect(set.size).toBe(2);
1739
- });
1740
- it("handles multiple test types", () => {
1741
- const set = buildExternalCoverageSet({
1742
- integration: 'tests/test_api.py [external] (covers: GET /api/orders)',
1743
- contract: 'tests/test_contract.py [external] (covers: GET /api/orders)',
1744
- });
1745
- expect(set.has("GET::orders::integration")).toBe(true);
1746
- expect(set.has("GET::orders::contract")).toBe(true);
1747
- expect(set.size).toBe(2);
1748
- });
1749
- it("emits both integration and contract keys for unknown test type", () => {
1750
- const set = buildExternalCoverageSet({
1751
- unknown: 'tests/test_misc.py [external] (covers: GET /api/items)',
1752
- });
1753
- expect(set.has("GET::items::integration")).toBe(true);
1754
- expect(set.has("GET::items::contract")).toBe(true);
1755
- expect(set.size).toBe(2);
1756
- });
1757
- it("ignores Skyramp tests (no [external] tag)", () => {
1758
- const set = buildExternalCoverageSet({
1759
- contract: 'test_items_contract.py (covers: GET /api/items)',
1760
- });
1761
- expect(set.size).toBe(0);
1762
- });
1763
- it("ignores external tests without covers clause", () => {
1764
- const set = buildExternalCoverageSet({
1765
- integration: 'tests/test_api.py [external]',
1766
- });
1767
- expect(set.size).toBe(0);
1768
- });
1769
- it("returns empty set for empty testLocations", () => {
1770
- const set = buildExternalCoverageSet({});
1771
- expect(set.size).toBe(0);
1772
- });
1773
- it("skips endpoints with unparseable paths", () => {
1774
- const set = buildExternalCoverageSet({
1775
- integration: 'tests/test_api.py [external] (covers: GET )',
1776
- });
1777
- // "GET " → method="GET", path="" → resource="unknown" → skipped
1778
- expect(set.size).toBe(0);
1779
- });
1780
- it("strips path parameters from resource extraction", () => {
1781
- const set = buildExternalCoverageSet({
1782
- integration: 'tests/test_api.py [external] (covers: PUT /api/v1/orders/{order_id})',
1783
- });
1784
- // {order_id} is a path param → skipped, resource is "orders"
1785
- expect(set.has("PUT::orders::integration")).toBe(true);
1786
- expect(set.size).toBe(1);
1787
- });
1788
- it("normalizes method to uppercase", () => {
1789
- const set = buildExternalCoverageSet({
1790
- integration: 'tests/test_api.py [external] (covers: get /api/orders)',
1791
- });
1792
- expect(set.has("GET::orders::integration")).toBe(true);
1793
- });
1794
- });
1795
- describe("externalDedupKey", () => {
1796
- it("builds key from single-step contract scenario", () => {
1797
- const scenario = {
1798
- scenarioName: "get_orders",
1799
- description: "Get orders",
1800
- category: "crud",
1801
- priority: "high",
1802
- steps: [{ order: 1, method: "GET", path: "/api/v1/orders", description: "list orders", interactionType: "success", expectedStatusCode: 200 }],
1803
- chainingKeys: [],
1804
- requiresAuth: false,
1805
- estimatedComplexity: "simple",
1806
- };
1807
- expect(externalDedupKey(scenario)).toBe("GET::orders::contract");
1808
- });
1809
- it("builds key from multi-step integration scenario using last mutating step", () => {
1810
- const scenario = {
1811
- scenarioName: "create_and_update_order",
1812
- description: "Create then update order",
1813
- category: "workflow",
1814
- priority: "high",
1815
- steps: [
1816
- { order: 1, method: "POST", path: "/api/v1/orders", description: "create order", interactionType: "success", expectedStatusCode: 201 },
1817
- { order: 2, method: "PUT", path: "/api/v1/orders/{order_id}", description: "update order", interactionType: "success", expectedStatusCode: 200 },
1818
- { order: 3, method: "GET", path: "/api/v1/orders/{order_id}", description: "verify", interactionType: "success", expectedStatusCode: 200 },
1819
- ],
1820
- chainingKeys: [],
1821
- requiresAuth: false,
1822
- estimatedComplexity: "moderate",
1823
- };
1824
- // Last mutating step is PUT /orders/{order_id} → resource "orders"
1825
- expect(externalDedupKey(scenario)).toBe("PUT::orders::integration");
1826
- });
1827
- it("falls back to last step when no mutating methods present", () => {
1828
- const scenario = {
1829
- scenarioName: "get_items",
1830
- description: "List and get items",
1831
- category: "crud",
1832
- priority: "medium",
1833
- steps: [
1834
- { order: 1, method: "GET", path: "/api/v1/items", description: "list items", interactionType: "success", expectedStatusCode: 200 },
1835
- { order: 2, method: "GET", path: "/api/v1/items/{id}", description: "get item", interactionType: "success", expectedStatusCode: 200 },
1836
- ],
1837
- chainingKeys: [],
1838
- requiresAuth: false,
1839
- estimatedComplexity: "simple",
1840
- };
1841
- // No mutating steps → falls back to last step → GET /items/{id} → resource "items"
1842
- expect(externalDedupKey(scenario)).toBe("GET::items::integration");
1843
- });
1844
- it("uses explicit testType when provided", () => {
1845
- const scenario = {
1846
- scenarioName: "get_orders_contract",
1847
- description: "Contract test for orders",
1848
- category: "crud",
1849
- priority: "high",
1850
- steps: [
1851
- { order: 1, method: "GET", path: "/api/v1/orders", description: "list orders", interactionType: "success", expectedStatusCode: 200 },
1852
- { order: 1, method: "POST", path: "/api/v1/orders", description: "create order", interactionType: "success", expectedStatusCode: 201 },
1853
- ],
1854
- chainingKeys: [],
1855
- requiresAuth: false,
1856
- estimatedComplexity: "simple",
1857
- testType: "contract",
1858
- };
1859
- expect(externalDedupKey(scenario)).toBe("POST::orders::contract");
1860
- });
1861
- });
1862
- // ---------------------------------------------------------------------------
1863
- // Tests — UI recommendation authoring rules
1864
- // ---------------------------------------------------------------------------
1865
- //
1866
- // The recommendation prompt always emits a "UI Recommendation Authoring Rules"
1867
- // section that anchors UI rec reasoning. Earlier iterations of this code
1868
- // accepted a `capturedBlueprints` parameter and rendered the captures inline,
1869
- // but that was redundant: the agent has the captures in its own tool-result
1870
- // history. The prompt now ships the rules; the agent supplies the vocabulary.
1871
- describe("buildRecommendationPrompt UI authoring rules", () => {
1872
- function minimalDiffAnalysis() {
1873
- return minimalAnalysis({
1874
- branchDiffContext: {
1875
- baseBranch: "main",
1876
- currentBranch: "feature/test",
1877
- changedFiles: ["src/components/OrderForm.tsx"],
1878
- newEndpoints: [],
1879
- modifiedEndpoints: [],
1880
- affectedServices: [],
1881
- },
1882
- });
1883
- }
1884
- it("emits UI authoring rules wrapped in an XML tag, unconditionally", () => {
1885
- const analysis = minimalDiffAnalysis();
1886
- const prompt = buildRecommendationPrompt(analysis);
1887
- expect(prompt).toContain("<ui_recommendation_authoring_rules>");
1888
- expect(prompt).toContain("</ui_recommendation_authoring_rules>");
1889
- expect(prompt).toMatch(/do NOT mention "blueprint"/i);
1890
- expect(prompt).toMatch(/do not invent element names/i);
1891
- });
1892
- it("does not render a 'Captured Blueprints' data section (param removed)", () => {
1893
- // Regression on the previous design: capturedBlueprints used to be
1894
- // threaded through the call and rendered as a "## Captured Blueprints"
1895
- // data section. That path is gone — the agent's own browser_blueprint
1896
- // tool-result history is the source of truth for element vocabulary.
1897
- const analysis = minimalDiffAnalysis();
1898
- const prompt = buildRecommendationPrompt(analysis);
1899
- expect(prompt).not.toContain("## Captured Blueprints");
1900
- });
1901
- it("instructs the LLM to ground UI recommendations in elements observed via earlier browser_blueprint calls", () => {
1902
- const analysis = minimalDiffAnalysis();
1903
- const prompt = buildRecommendationPrompt(analysis);
1904
- expect(prompt).toMatch(/ground the [`]?reasoning[`]?\s*field in elements you have actually observed/i);
1905
- expect(prompt).toMatch(/inform.*how.*describe.*not.*which/i);
1906
- });
1907
- it("instructs the LLM not to leak internal MCP terminology into reasoning", () => {
1908
- const analysis = minimalDiffAnalysis();
1909
- const prompt = buildRecommendationPrompt(analysis);
1910
- expect(prompt).toMatch(/do NOT mention "blueprint"/i);
1911
- expect(prompt).toMatch(/leak builder internals/i);
1912
- });
1913
- it("does not abbreviate 'recommendation' to 'rec' in the rules section", () => {
1914
- // Per PR review: shortform 'rec' invites the LLM to hallucinate the term.
1915
- // Spell it out to keep the language consistent with the rest of the prompt.
1916
- const analysis = minimalDiffAnalysis();
1917
- const prompt = buildRecommendationPrompt(analysis);
1918
- const rulesStart = prompt.indexOf("<ui_recommendation_authoring_rules>");
1919
- const rulesEnd = prompt.indexOf("</ui_recommendation_authoring_rules>");
1920
- const rulesBlock = prompt.slice(rulesStart, rulesEnd);
1921
- // Allow "recs" as a substring within "recommendations" — match only the
1922
- // standalone word.
1923
- expect(rulesBlock).not.toMatch(/\b(rec|recs)\b/);
1924
- });
1925
- });