@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,876 +0,0 @@
1
- // @ts-ignore
2
- import { isRealResource, draftScenariosFromEndpoints, draftDiffDirectScenarios, inferResourceRelationships, draftResponseShapeScenarios, } from "./scenarioDrafting.js";
3
- import { WorkspaceAuthType } from "./workspaceAuth.js";
4
- describe("isRealResource", () => {
5
- it("accepts normal resource names", () => {
6
- expect(isRealResource("users")).toBe(true);
7
- expect(isRealResource("products")).toBe(true);
8
- expect(isRealResource("orders")).toBe(true);
9
- });
10
- it("accepts hyphenated resource names (not action verbs)", () => {
11
- expect(isRealResource("saved-searches")).toBe(true);
12
- expect(isRealResource("order-items")).toBe(true);
13
- expect(isRealResource("user-profiles")).toBe(true);
14
- expect(isRealResource("api-keys")).toBe(true);
15
- });
16
- it("rejects infra/monitoring tokens but accepts semantic endpoint names", () => {
17
- expect(isRealResource("health")).toBe(false);
18
- expect(isRealResource("ping")).toBe(false);
19
- expect(isRealResource("dashboard")).toBe(false);
20
- // login, logout, verify, webhook are real resources (phase 6a)
21
- expect(isRealResource("login")).toBe(true);
22
- expect(isRealResource("logout")).toBe(true);
23
- expect(isRealResource("verify")).toBe(true);
24
- expect(isRealResource("webhook")).toBe(true);
25
- });
26
- it("rejects action-verb-hyphenated patterns", () => {
27
- expect(isRealResource("forgot-password")).toBe(false);
28
- expect(isRealResource("reset-password")).toBe(false);
29
- expect(isRealResource("verify-email")).toBe(false);
30
- expect(isRealResource("confirm-order")).toBe(false);
31
- expect(isRealResource("send-notification")).toBe(false);
32
- expect(isRealResource("create-session")).toBe(false);
33
- expect(isRealResource("delete-cache")).toBe(false);
34
- });
35
- it("is case-insensitive", () => {
36
- expect(isRealResource("Login")).toBe(true); // login is now a real resource (phase 6a)
37
- expect(isRealResource("VERIFY")).toBe(true); // verify is now a real resource (phase 6a)
38
- expect(isRealResource("Forgot-Password")).toBe(false); // ACTION_VERB_HYPHEN still filters this
39
- });
40
- });
41
- describe("draftScenariosFromEndpoints — irregular plural singularization", () => {
42
- it("uses 'category' (not 'categorie') in FK field names for -ies plurals", () => {
43
- const endpoints = [
44
- { path: "/api/categories", methods: [{ method: "GET" }, { method: "POST" }] },
45
- { path: "/api/categories/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
46
- { path: "/api/items", methods: [{ method: "GET" }, { method: "POST" }] },
47
- { path: "/api/items/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
48
- ];
49
- const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/categories" }]);
50
- const allText = JSON.stringify(scenarios);
51
- expect(allText).not.toContain("categorie_id");
52
- expect(allText).not.toContain("\"categorie\"");
53
- // The diff-direct integration scenario description should use "category" (singular)
54
- const integrationScenario = scenarios.find((s) => s.scenarioName === "categories-post-lifecycle");
55
- expect(integrationScenario).toBeDefined();
56
- expect(integrationScenario.steps[0].description).toContain("category");
57
- });
58
- it("uses 'status' (not 'statu') in FK field names for -ses plurals", () => {
59
- const endpoints = [
60
- { path: "/api/statuses", methods: [{ method: "GET" }, { method: "POST" }] },
61
- { path: "/api/statuses/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
62
- ];
63
- const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/statuses" }]);
64
- const allText = JSON.stringify(scenarios);
65
- expect(allText).not.toContain("statu_id");
66
- // The diff-direct integration scenario description should use "status" (singular)
67
- const integrationScenario = scenarios.find((s) => s.scenarioName === "statuses-post-lifecycle");
68
- expect(integrationScenario).toBeDefined();
69
- expect(integrationScenario.steps[0].description).toContain("status");
70
- });
71
- });
72
- describe("draftScenariosFromEndpoints", () => {
73
- it("returns empty array for no endpoints", () => {
74
- expect(draftScenariosFromEndpoints([])).toEqual([]);
75
- });
76
- });
77
- describe("draftScenariosFromEndpoints — attack-surface expansion", () => {
78
- const endpoints = [
79
- { path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/flows.py" }] },
80
- { path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/flows.py" }] },
81
- { path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
82
- { path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
83
- ];
84
- it("drafts auth-boundary scenarios for same-file destructive bulk siblings", () => {
85
- const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
86
- securityRelevantDiff: true,
87
- changedEndpoints: [
88
- { method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" },
89
- { method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/deployments.py" },
90
- ],
91
- });
92
- const flowBulkDelete = scenarios.find(s => s.scenarioName === "flows-bulk-delete-post-auth-boundary");
93
- const deploymentBulkDelete = scenarios.find(s => s.scenarioName === "deployments-bulk-delete-post-auth-boundary");
94
- expect(flowBulkDelete).toBeDefined();
95
- expect(flowBulkDelete.category).toBe("security_boundary");
96
- expect(flowBulkDelete.testType).toBe("contract");
97
- expect(flowBulkDelete.steps[0]).toMatchObject({
98
- method: "POST",
99
- path: "/flows/bulk_delete",
100
- expectedStatusCode: 401,
101
- });
102
- expect(flowBulkDelete.description).toContain("destructive sibling");
103
- expect(deploymentBulkDelete).toBeDefined();
104
- expect(deploymentBulkDelete.steps[0]).toMatchObject({
105
- method: "POST",
106
- path: "/deployments/bulk_delete",
107
- expectedStatusCode: 401,
108
- });
109
- });
110
- it("does not expand when the diff lacks security signals", () => {
111
- const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
112
- securityRelevantDiff: false,
113
- changedEndpoints: [{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
114
- });
115
- expect(scenarios.some(s => s.scenarioName.includes("bulk-delete"))).toBe(false);
116
- });
117
- it("uses the source-file resource for router-relative nested destructive endpoints", () => {
118
- const scenarios = draftScenariosFromEndpoints([
119
- { path: "/{id:uuid}/schedules/{schedule_id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
120
- { path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
121
- { path: "/{id:uuid}/schedules/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
122
- ], [], WorkspaceAuthType.ApiKey, {
123
- securityRelevantDiff: true,
124
- changedEndpoints: [
125
- { method: "DELETE", path: "/{id:uuid}/schedules/{schedule_id:uuid}", sourceFile: "src/prefect/server/api/deployments.py" },
126
- ],
127
- });
128
- expect(scenarios.some(s => s.steps.some(step => step.path === "/deployments/bulk_delete"))).toBe(true);
129
- expect(scenarios.some(s => s.steps.some(step => step.path === "/{id:uuid}/schedules/bulk_delete"))).toBe(false);
130
- });
131
- it("preserves Express-style tenant params for attack-surface siblings", () => {
132
- const scenarios = draftScenariosFromEndpoints([
133
- { path: "/:tenant_id/deployments/:id", methods: [{ method: "DELETE", sourceFile: "src/routes/deployments.ts" }] },
134
- { path: "/:tenant_id/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/routes/deployments.ts" }] },
135
- ], [], WorkspaceAuthType.ApiKey, {
136
- securityRelevantDiff: true,
137
- changedEndpoints: [
138
- { method: "DELETE", path: "/:tenant_id/deployments/:id", sourceFile: "src/routes/deployments.ts" },
139
- ],
140
- });
141
- expect(scenarios.some(s => s.steps.some(step => step.path === "/:tenant_id/deployments/bulk_delete"))).toBe(true);
142
- });
143
- it("preserves Next-style tenant params for attack-surface siblings", () => {
144
- const scenarios = draftScenariosFromEndpoints([
145
- { path: "/[tenant_id]/deployments/[id]", methods: [{ method: "DELETE", sourceFile: "src/app/api/deployments/route.ts" }] },
146
- { path: "/[tenant_id]/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/app/api/deployments/route.ts" }] },
147
- ], [], WorkspaceAuthType.ApiKey, {
148
- securityRelevantDiff: true,
149
- changedEndpoints: [
150
- { method: "DELETE", path: "/[tenant_id]/deployments/[id]", sourceFile: "src/app/api/deployments/route.ts" },
151
- ],
152
- });
153
- expect(scenarios.some(s => s.steps.some(step => step.path === "/[tenant_id]/deployments/bulk_delete"))).toBe(true);
154
- });
155
- it("preserves full tenant-scoped paths when the resource segment is explicit", () => {
156
- const scenarios = draftScenariosFromEndpoints([
157
- { path: "/{tenant_id}/deployments/{id}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
158
- { path: "/{tenant_id}/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
159
- ], [], WorkspaceAuthType.ApiKey, {
160
- securityRelevantDiff: true,
161
- changedEndpoints: [
162
- { method: "DELETE", path: "/{tenant_id}/deployments/{id}", sourceFile: "src/prefect/server/api/deployments.py" },
163
- ],
164
- });
165
- expect(scenarios.some(s => s.steps.some(step => step.path === "/{tenant_id}/deployments/bulk_delete"))).toBe(true);
166
- });
167
- it("does not invent a bulk sibling when no destructive sibling endpoint exists", () => {
168
- const scenarios = draftScenariosFromEndpoints([{ path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/flows.py" }] }], [], WorkspaceAuthType.ApiKey, {
169
- securityRelevantDiff: true,
170
- changedEndpoints: [{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
171
- });
172
- expect(scenarios).toEqual([]);
173
- });
174
- it("does not expand ordinary changed GET endpoints", () => {
175
- const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
176
- securityRelevantDiff: true,
177
- changedEndpoints: [{ method: "GET", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
178
- });
179
- expect(scenarios.some(s => s.scenarioName.includes("bulk-delete"))).toBe(false);
180
- });
181
- it("does not promote unrelated destructive routes from the same source file", () => {
182
- const scenarios = draftScenariosFromEndpoints([
183
- { path: "/flows/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/routes/api.py" }] },
184
- { path: "/admin/bulk_delete", methods: [{ method: "POST", sourceFile: "src/routes/api.py" }] },
185
- ], [], WorkspaceAuthType.ApiKey, {
186
- securityRelevantDiff: true,
187
- changedEndpoints: [{ method: "DELETE", path: "/flows/{id:uuid}", sourceFile: "src/routes/api.py" }],
188
- });
189
- expect(scenarios).toEqual([]);
190
- });
191
- });
192
- describe("inferResourceRelationships", () => {
193
- it("returns empty map for endpoints with no nesting or FK fields", () => {
194
- const endpoints = [
195
- { path: "/api/users", methods: [{ method: "GET" }] },
196
- { path: "/api/products", methods: [{ method: "GET" }] },
197
- ];
198
- expect(inferResourceRelationships(endpoints).size).toBe(0);
199
- });
200
- it("detects parent→child relationship from path nesting (/parent/{id}/child)", () => {
201
- const endpoints = [
202
- { path: "/api/collections/{id}/links", methods: [{ method: "GET" }] },
203
- ];
204
- const rel = inferResourceRelationships(endpoints);
205
- expect(rel.has("links")).toBe(true);
206
- expect(rel.get("links").has("collections")).toBe(true);
207
- });
208
- it("detects FK relationship from request body field ({resource}_id)", () => {
209
- const endpoints = [
210
- { path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { product_id: "abc" } } }] }] },
211
- { path: "/api/products", methods: [{ method: "GET" }] },
212
- ];
213
- const rel = inferResourceRelationships(endpoints);
214
- expect(rel.has("orders")).toBe(true);
215
- expect(rel.get("orders").has("products")).toBe(true);
216
- });
217
- it("does not create self-reference", () => {
218
- const endpoints = [
219
- { path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { order_id: "abc" } } }] }] },
220
- ];
221
- const rel = inferResourceRelationships(endpoints);
222
- // orders should not reference itself
223
- expect(rel.get("orders")?.has("orders")).toBeFalsy();
224
- });
225
- it("ignores FK fields that point to unknown resources", () => {
226
- const endpoints = [
227
- { path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { nonexistent_resource_id: "abc" } } }] }] },
228
- ];
229
- const rel = inferResourceRelationships(endpoints);
230
- // nonexistent_resource is not a known endpoint resource, should be ignored
231
- expect(rel.get("orders")?.size ?? 0).toBe(0);
232
- });
233
- });
234
- describe("draftDiffDirectScenarios", () => {
235
- const makeGroups = (defs) => new Map(Object.entries(defs).map(([k, v]) => [k, { ...v, methods: new Set(v.methods) }]));
236
- it("returns empty array when newEndpoints is empty", () => {
237
- const groups = makeGroups({ orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{id}" } });
238
- expect(draftDiffDirectScenarios([], groups)).toEqual([]);
239
- });
240
- it("generates new_endpoint-category scenarios for a new PUT endpoint", () => {
241
- const groups = makeGroups({
242
- orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
243
- });
244
- const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
245
- // PUT generates: mutation-recalc, contract, not-found (3 scenarios)
246
- // Note: we intentionally skip the redundant "happy-path" integration for PUT/PATCH
247
- // because mutation-recalc already provides comprehensive happy-path coverage
248
- expect(scenarios.length).toBeGreaterThanOrEqual(3);
249
- const newEndpointScenarios = scenarios.filter(s => s.category === "new_endpoint");
250
- expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(2);
251
- // Mutation-recalculation scenario is drafted first for PUT/PATCH
252
- const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
253
- expect(mutationRecalc).toBeDefined();
254
- expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
255
- expect(mutationRecalc.description).toContain("Mutation test");
256
- // Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
257
- // Contract test
258
- const contract = scenarios.find(s => s.testType === "contract" && s.steps[0].expectedStatusCode === 200);
259
- expect(contract).toBeDefined();
260
- // Not-found edge case
261
- const notFound = scenarios.find(s => s.steps.some(st => st.expectedStatusCode === 404));
262
- expect(notFound).toBeDefined();
263
- });
264
- it("generates mutation-recalc scenario for a new PATCH endpoint", () => {
265
- const groups = makeGroups({
266
- orders: { basePath: "/api/orders", methods: ["GET", "POST", "PATCH"], paramPath: "/api/orders/{order_id}" },
267
- });
268
- const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/orders/{order_id}" }], groups);
269
- // Mutation-recalculation scenario is drafted for PATCH
270
- const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
271
- expect(mutationRecalc).toBeDefined();
272
- expect(mutationRecalc.testType).toBe("integration");
273
- expect(mutationRecalc.category).toBe("new_endpoint");
274
- expect(mutationRecalc.priority).toBe("high");
275
- expect(mutationRecalc.description).toContain("Mutation test");
276
- expect(mutationRecalc.description).toContain("derived calculations");
277
- // Should have 3 steps: POST (create) → PATCH (add items) → GET (verify)
278
- expect(mutationRecalc.steps).toHaveLength(3);
279
- expect(mutationRecalc.steps[0].method).toBe("POST");
280
- expect(mutationRecalc.steps[1].method).toBe("PATCH");
281
- expect(mutationRecalc.steps[2].method).toBe("GET");
282
- // Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
283
- });
284
- it("integration scenario minimum steps: POST resource then PUT — LLM discovers prereqs from source code", () => {
285
- const groups = makeGroups({
286
- orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
287
- });
288
- const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
289
- // Mutation-recalculation scenario comes first
290
- const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
291
- expect(mutationRecalc).toBeDefined();
292
- expect(mutationRecalc.steps.some(st => st.method === "POST" && st.path === "/api/orders")).toBe(true);
293
- expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
294
- // Note: happy-path integration removed for PUT/PATCH
295
- // mutation-recalc already provides comprehensive happy-path coverage with POST → PUT → GET
296
- // The minimum steps (POST + PUT) are already verified in the mutationRecalc check above
297
- });
298
- it("generates 3 scenarios for a new POST endpoint", () => {
299
- const groups = makeGroups({
300
- products: { basePath: "/api/products", methods: ["GET", "POST"], paramPath: "/api/products/{id}" },
301
- });
302
- const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/products" }], groups);
303
- expect(scenarios.length).toBe(3); // integration, contract, validation
304
- const types = scenarios.map(s => s.testType);
305
- expect(types).toContain("integration");
306
- expect(types).toContain("contract");
307
- // validation error scenario (422)
308
- expect(scenarios.some(s => s.steps.some(st => st.expectedStatusCode === 422))).toBe(true);
309
- });
310
- it("ignores unknown paths and skips action-verb segments", () => {
311
- const groups = makeGroups({
312
- orders: { basePath: "/api/orders", methods: ["POST", "DELETE"], paramPath: "/api/orders/{id}" },
313
- });
314
- const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/health" }], // 'health' is not a real resource
315
- groups);
316
- expect(scenarios).toEqual([]);
317
- });
318
- it("draftScenariosFromEndpoints passes newEndpoints through to diff-direct scenarios", () => {
319
- const endpoints = [
320
- { path: "/api/orders", methods: ["GET", "POST", "PUT"] },
321
- { path: "/api/orders/{order_id}", methods: ["GET", "PUT", "DELETE"] },
322
- ];
323
- const newEndpoints = [{ method: "PUT", path: "/api/orders/{order_id}" }];
324
- const scenarios = draftScenariosFromEndpoints(endpoints, newEndpoints);
325
- const diffDirect = scenarios.filter(s => s.category === "new_endpoint");
326
- expect(diffDirect.length).toBeGreaterThan(0);
327
- expect(diffDirect.some(s => s.steps.some(st => st.method === "PUT"))).toBe(true);
328
- });
329
- it("resolves router-relative param paths (e.g. /{order_id}) against known resourceGroups", () => {
330
- const groups = makeGroups({
331
- orders: { basePath: "/api/v1/orders", methods: ["GET", "POST", "PATCH", "DELETE"], paramPath: "/api/v1/orders/{order_id}" },
332
- products: { basePath: "/api/v1/products", methods: ["GET", "POST"], paramPath: "/api/v1/products/{id}" },
333
- });
334
- // Diff scanner reports router-relative path, not the full API path
335
- const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/{order_id}" }], groups);
336
- expect(scenarios.length).toBeGreaterThanOrEqual(3);
337
- // Mutation-recalculation scenario is drafted
338
- const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
339
- expect(mutationRecalc).toBeDefined();
340
- expect(mutationRecalc.testType).toBe("integration");
341
- expect(mutationRecalc.category).toBe("new_endpoint");
342
- expect(mutationRecalc.steps[1].method).toBe("PATCH");
343
- // Contract and not-found scenarios use the resolved full path
344
- const contract = scenarios.find(s => s.scenarioName.includes("contract"));
345
- expect(contract).toBeDefined();
346
- expect(contract.steps[0].path).toBe("/api/v1/orders/{order_id}");
347
- const notFound = scenarios.find(s => s.scenarioName.includes("not-found"));
348
- expect(notFound).toBeDefined();
349
- expect(notFound.steps[0].path).toBe("/api/v1/orders/{order_id}");
350
- });
351
- it("resolves router-relative base paths (e.g. /orders) against known resourceGroups", () => {
352
- const groups = makeGroups({
353
- orders: { basePath: "/api/v1/orders", methods: ["GET", "POST"], paramPath: "/api/v1/orders/{order_id}" },
354
- });
355
- const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/orders" }], groups);
356
- expect(scenarios.length).toBeGreaterThanOrEqual(2);
357
- const integration = scenarios.find(s => s.testType === "integration");
358
- expect(integration).toBeDefined();
359
- expect(integration.category).toBe("new_endpoint");
360
- });
361
- it("skips unresolvable router-relative paths", () => {
362
- const groups = makeGroups({
363
- products: { basePath: "/api/v1/products", methods: ["GET", "POST"], paramPath: "/api/v1/products/{id}" },
364
- });
365
- // /{nonexistent_id} doesn't match any known resource
366
- const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/{nonexistent_id}" }], groups);
367
- expect(scenarios).toEqual([]);
368
- });
369
- it("PR #195 regression: PATCH /{order_id} produces mutation-recalculate scenario in GENERATE slots", () => {
370
- // Simulates the exact data from PR #195:
371
- // - Full endpoint list with orders, products, reviews
372
- // - newEndpoints has PATCH /{order_id} (router-relative)
373
- const endpoints = [
374
- { path: "/api/v1/orders", methods: ["GET", "POST"] },
375
- { path: "/api/v1/orders/{order_id}", methods: ["GET", "PATCH", "DELETE"] },
376
- { path: "/api/v1/products", methods: ["GET", "POST"] },
377
- { path: "/api/v1/products/{product_id}", methods: ["GET", "PUT", "DELETE"] },
378
- { path: "/api/v1/reviews", methods: ["GET", "POST"] },
379
- { path: "/api/v1/reset", methods: ["POST"] },
380
- ];
381
- const newEndpoints = [{ method: "PATCH", path: "/{order_id}" }];
382
- const allScenarios = draftScenariosFromEndpoints(endpoints, newEndpoints);
383
- // The diff-direct scenarios should include orders-patch-mutation-verify
384
- const mutationRecalc = allScenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
385
- expect(mutationRecalc).toBeDefined();
386
- expect(mutationRecalc.category).toBe("new_endpoint"); // → CRITICAL priority tier
387
- expect(mutationRecalc.steps).toHaveLength(3);
388
- expect(mutationRecalc.steps[0].method).toBe("POST");
389
- expect(mutationRecalc.steps[1].method).toBe("PATCH");
390
- expect(mutationRecalc.steps[2].method).toBe("GET");
391
- expect(mutationRecalc.description).toContain("derived calculations");
392
- // Verify it outranks all non-new_endpoint scenarios (cascade-delete, unique-constraint, etc.)
393
- const newEndpointScenarios = allScenarios.filter(s => s.category === "new_endpoint");
394
- const otherScenarios = allScenarios.filter(s => s.category !== "new_endpoint");
395
- expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(3);
396
- // All new_endpoint scenarios should include our mutation-recalculate
397
- expect(newEndpointScenarios.some(s => s.scenarioName === "orders-patch-mutation-verify")).toBe(true);
398
- // Non-new_endpoint scenarios should NOT include mutation-recalculate
399
- expect(otherScenarios.every(s => s.scenarioName !== "orders-patch-mutation-verify")).toBe(true);
400
- });
401
- });
402
- describe("diffDirectMutationRecalc — diverse app types (no hardcoded field names)", () => {
403
- const makeGroups = (defs) => new Map(Object.entries(defs).map(([k, v]) => [k, { ...v, methods: new Set(v.methods) }]));
404
- const testCases = [
405
- {
406
- name: "E-commerce: PATCH /orders/{order_id}",
407
- resource: "orders",
408
- group: { basePath: "/api/v1/orders", methods: ["GET", "POST", "PATCH"], paramPath: "/api/v1/orders/{order_id}" },
409
- method: "PATCH",
410
- expectedParamName: "order_id",
411
- },
412
- {
413
- name: "Project management: PUT /projects/{id}",
414
- resource: "projects",
415
- group: { basePath: "/api/projects", methods: ["GET", "POST", "PUT"], paramPath: "/api/projects/{id}" },
416
- method: "PUT",
417
- expectedParamName: "id",
418
- },
419
- {
420
- name: "Invoicing: PATCH /invoices/{invoice_id}",
421
- resource: "invoices",
422
- group: { basePath: "/v2/invoices", methods: ["GET", "POST", "PATCH"], paramPath: "/v2/invoices/{invoice_id}" },
423
- method: "PATCH",
424
- expectedParamName: "invoice_id",
425
- },
426
- {
427
- name: "Education LMS: PUT /courses/{course_id}",
428
- resource: "courses",
429
- group: { basePath: "/api/courses", methods: ["GET", "POST", "PUT"], paramPath: "/api/courses/{course_id}" },
430
- method: "PUT",
431
- expectedParamName: "course_id",
432
- },
433
- {
434
- name: "Healthcare: PATCH /prescriptions/{prescription_id}",
435
- resource: "prescriptions",
436
- group: { basePath: "/api/prescriptions", methods: ["GET", "POST", "PATCH"], paramPath: "/api/prescriptions/{prescription_id}" },
437
- method: "PATCH",
438
- expectedParamName: "prescription_id",
439
- },
440
- {
441
- name: "Logistics: PUT /shipments/{shipment_id}",
442
- resource: "shipments",
443
- group: { basePath: "/api/shipments", methods: ["GET", "POST", "PUT"], paramPath: "/api/shipments/{shipment_id}" },
444
- method: "PUT",
445
- expectedParamName: "shipment_id",
446
- },
447
- {
448
- name: "Restaurant: PATCH /menus/{menuId}",
449
- resource: "menus",
450
- group: { basePath: "/api/menus", methods: ["GET", "POST", "PATCH"], paramPath: "/api/menus/{menuId}" },
451
- method: "PATCH",
452
- expectedParamName: "menuId",
453
- },
454
- {
455
- name: "Cloud billing: PUT /subscriptions/{subscription_id}",
456
- resource: "subscriptions",
457
- group: { basePath: "/billing/subscriptions", methods: ["GET", "POST", "PUT"], paramPath: "/billing/subscriptions/{subscription_id}" },
458
- method: "PUT",
459
- expectedParamName: "subscription_id",
460
- },
461
- {
462
- name: "Warehouse: PATCH /inventories/{uuid}",
463
- resource: "inventories",
464
- group: { basePath: "/api/inventories", methods: ["GET", "POST", "PATCH"], paramPath: "/api/inventories/{uuid}" },
465
- method: "PATCH",
466
- expectedParamName: "uuid",
467
- },
468
- {
469
- name: "Minimal: PATCH /items (no paramPath)",
470
- resource: "items",
471
- group: { basePath: "/items", methods: ["GET", "POST", "PATCH"] },
472
- method: "PATCH",
473
- expectedParamName: "item_id",
474
- },
475
- ];
476
- it.each(testCases)("$name — produces valid mutation-recalc scenario", ({ resource, group, method, expectedParamName }) => {
477
- const groups = makeGroups({ [resource]: group });
478
- const scenarios = draftDiffDirectScenarios([{ method, path: group.paramPath ?? group.basePath }], groups);
479
- const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
480
- expect(mutationRecalc).toBeDefined();
481
- expect(mutationRecalc.category).toBe("new_endpoint");
482
- expect(mutationRecalc.testType).toBe("integration");
483
- expect(mutationRecalc.priority).toBe("high");
484
- // Step 1: POST to create resource (all test cases have POST)
485
- expect(mutationRecalc.steps[0].method).toBe("POST");
486
- expect(mutationRecalc.steps[0].path).toBe(group.basePath);
487
- // Step 2: PUT/PATCH with mutation
488
- const mutationStep = mutationRecalc.steps[1];
489
- expect(mutationStep.method).toBe(method);
490
- // Note: bodyMustInclude removed in intent-based refactor - LLM discovers fields from source
491
- // Verify NO hardcoded domain-specific field names
492
- // Verify hints are descriptive (contain "e.g." or similar guidance)
493
- // Chaining uses the actual path param name (only when paramPath exists)
494
- if (group.paramPath) {
495
- expect(mutationStep.chainsFrom).toBeDefined();
496
- const chaining = mutationStep.chainsFrom;
497
- expect(chaining.targetParam).toBe(expectedParamName);
498
- expect(chaining.sourceStep).toBe(1);
499
- expect(chaining.sourceField).toBe("<id-field>");
500
- }
501
- else {
502
- expect(mutationStep.chainsFrom).toBeUndefined();
503
- }
504
- // Step 3: GET verification (if paramPath exists)
505
- if (group.paramPath) {
506
- expect(mutationRecalc.steps).toHaveLength(3);
507
- const getStep = mutationRecalc.steps[2];
508
- expect(getStep.method).toBe("GET");
509
- // Note: expectedResponseFields removed in intent-based refactor
510
- }
511
- // chainingKeys uses the actual path param name, not hardcoded singular_id
512
- expect(mutationRecalc.chainingKeys).toContain("id");
513
- expect(mutationRecalc.chainingKeys).toContain(expectedParamName);
514
- // Description is domain-agnostic
515
- expect(mutationRecalc.description).toContain("Mutation test");
516
- expect(mutationRecalc.description).toContain("derived calculations");
517
- });
518
- it("no paramPath: PATCH step targets basePath, no GET verification, fallback param name", () => {
519
- const groups = makeGroups({
520
- items: { basePath: "/items", methods: ["GET", "POST", "PATCH"] },
521
- });
522
- const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/items" }], groups);
523
- const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
524
- expect(mutationRecalc).toBeDefined();
525
- // No paramPath → PATCH targets basePath, no GET step
526
- expect(mutationRecalc.steps).toHaveLength(2);
527
- expect(mutationRecalc.steps[1].path).toBe("/items");
528
- // Fallback param name: singular + _id
529
- expect(mutationRecalc.chainingKeys).toContain("item_id");
530
- });
531
- it("resource without POST: no create step, no chaining", () => {
532
- const groups = makeGroups({
533
- configs: { basePath: "/api/configs", methods: ["GET", "PATCH"], paramPath: "/api/configs/{config_id}" },
534
- });
535
- const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/configs/{config_id}" }], groups);
536
- const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
537
- expect(mutationRecalc).toBeDefined();
538
- // No POST → starts with PATCH directly, no chaining
539
- expect(mutationRecalc.steps[0].method).toBe("PATCH");
540
- expect(mutationRecalc.steps[0].chainsFrom).toBeUndefined();
541
- // Still has GET verification
542
- expect(mutationRecalc.steps[1].method).toBe("GET");
543
- expect(mutationRecalc.steps[1].chainsFrom).toBeUndefined();
544
- });
545
- it("generates auth-boundary scenario for each new endpoint (security_boundary, not new_endpoint)", () => {
546
- const groups = makeGroups({
547
- orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
548
- });
549
- const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
550
- const authBoundary = scenarios.find(s => s.scenarioName === "orders-put-auth-boundary");
551
- expect(authBoundary).toBeDefined();
552
- expect(authBoundary.category).toBe("security_boundary");
553
- expect(authBoundary.testType).toBe("contract");
554
- expect(authBoundary.steps[0].method).toBe("PUT");
555
- expect(authBoundary.steps[0].path).toBe("/api/orders/{order_id}");
556
- expect(authBoundary.steps[0].expectedStatusCode).toBe(401);
557
- expect(authBoundary.requiresAuth).toBe(false);
558
- });
559
- it("auth-boundary scenario is not duplicated for same method/path", () => {
560
- const groups = makeGroups({
561
- orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT", "DELETE"], paramPath: "/api/orders/{order_id}" },
562
- });
563
- const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }, { method: "DELETE", path: "/api/orders/{order_id}" }], groups);
564
- const authBoundaries = scenarios.filter(s => s.scenarioName.includes("auth-boundary"));
565
- // One for PUT, one for DELETE (not GET, not POST)
566
- expect(authBoundaries).toHaveLength(2);
567
- expect(authBoundaries.map(s => s.scenarioName).sort()).toEqual([
568
- "orders-delete-auth-boundary",
569
- "orders-put-auth-boundary",
570
- ]);
571
- });
572
- it("does NOT generate auth-boundary for GET endpoints (often public)", () => {
573
- const groups = makeGroups({
574
- orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{order_id}" },
575
- });
576
- const scenarios = draftDiffDirectScenarios([{ method: "GET", path: "/api/orders/{order_id}" }], groups);
577
- const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
578
- expect(authBoundary).toBeUndefined();
579
- });
580
- it("does NOT generate auth-boundary for POST", () => {
581
- const groups = makeGroups({
582
- products: { basePath: "/api/products", methods: ["GET", "POST"], paramPath: "/api/products/{id}" },
583
- });
584
- const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/products" }], groups);
585
- const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
586
- expect(authBoundary).toBeUndefined();
587
- });
588
- });
589
- describe("draftDiffDirectScenarios — GET without path param", () => {
590
- const resourceGroups = new Map([
591
- ["products", {
592
- basePath: "/api/v1/products",
593
- methods: new Set(["GET", "POST"]),
594
- paramPath: "/api/v1/products/{product_id}",
595
- }],
596
- ]);
597
- it("GET no-param: produces contract + collection scenario (LLM determines if query params apply)", () => {
598
- const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
599
- const names = result.map(s => s.scenarioName);
600
- expect(names).toContain("products-get-new-endpoint-contract");
601
- expect(names).toContain("products-search-list-query");
602
- const scenario = result.find(s => s.scenarioName === "products-search-list-query");
603
- expect(scenario.category).toBe("new_endpoint");
604
- // Description instructs LLM to read source for query param logic
605
- expect(scenario.description).toMatch(/read diff source|query param/i);
606
- });
607
- it("GET no-param: produces zero cascade-delete or unique-constraint scenarios", () => {
608
- const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
609
- const names = result.map(s => s.scenarioName);
610
- expect(names.some(n => n.includes("cascade"))).toBe(false);
611
- expect(names.some(n => n.includes("unique"))).toBe(false);
612
- expect(names.some(n => n.includes("delete-blocked"))).toBe(false);
613
- });
614
- it("collection GET with POST: produces integration scenario (POST then GET)", () => {
615
- const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products" }], resourceGroups);
616
- const scenario = result.find(s => s.scenarioName === "products-list-query");
617
- expect(scenario).toBeDefined();
618
- expect(scenario.testType).toBe("integration");
619
- expect(scenario.steps[0].method).toBe("POST");
620
- expect(scenario.steps[1].method).toBe("GET");
621
- });
622
- it("collection GET without POST: produces contract scenario (single GET step)", () => {
623
- const readOnlyGroups = new Map([
624
- ["reports", {
625
- basePath: "/api/v1/reports",
626
- methods: new Set(["GET"]),
627
- paramPath: undefined,
628
- }],
629
- ]);
630
- const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/reports" }], readOnlyGroups);
631
- const scenario = result.find(s => s.scenarioName === "reports-list-query");
632
- expect(scenario).toBeDefined();
633
- expect(scenario.testType).toBe("contract");
634
- expect(scenario.steps).toHaveLength(1);
635
- });
636
- });
637
- // ---------------------------------------------------------------------------
638
- // capScenarios — enforced via draftScenariosFromEndpoints
639
- // ---------------------------------------------------------------------------
640
- describe("draftScenariosFromEndpoints — capScenarios hard limit", () => {
641
- // Each PUT/PATCH new endpoint produces ~5 scenarios (mutation-verify, boundary-values,
642
- // contract, not-found, auth-boundary). Seven resources × 2 methods each = 70 new
643
- // endpoints → well above the 30-scenario cap.
644
- function makeEndpointsAndNewEps(resourceNames) {
645
- const endpoints = resourceNames.flatMap(r => [
646
- { path: `/api/${r}`, methods: [{ method: "GET" }, { method: "POST" }] },
647
- { path: `/api/${r}/{id}`, methods: [{ method: "GET" }, { method: "PUT" }, { method: "PATCH" }, { method: "DELETE" }] },
648
- ]);
649
- const newEndpoints = resourceNames.flatMap(r => [
650
- { method: "PUT", path: `/api/${r}/{id}` },
651
- { method: "PATCH", path: `/api/${r}/{id}` },
652
- ]);
653
- return { endpoints, newEndpoints };
654
- }
655
- it("caps output at 30 scenarios when many new endpoints are present", () => {
656
- const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
657
- const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
658
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
659
- expect(result.length).toBeLessThanOrEqual(30);
660
- });
661
- it("always keeps CRITICAL (new_endpoint) scenarios under the cap", () => {
662
- const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
663
- const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
664
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
665
- // new_endpoint is CRITICAL — verify at least some survive
666
- const criticalOnes = result.filter((s) => s.category === "new_endpoint");
667
- expect(criticalOnes.length).toBeGreaterThan(0);
668
- });
669
- it("CRITICAL items (new_endpoint) fill the cap first; security_boundary may be dropped", () => {
670
- // With many new endpoints, diffDirectContract/NotFound/MutationVerify/BoundaryValues all
671
- // produce category "new_endpoint" (CRITICAL). When CRITICAL items alone exceed 30, the hard
672
- // cap slices them first — breadth picks for non-CRITICAL categories are not guaranteed.
673
- const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
674
- const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
675
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
676
- // All 30 surviving scenarios should be CRITICAL (new_endpoint)
677
- const nonCritical = result.filter((s) => s.category !== "new_endpoint");
678
- // Non-critical (security_boundary) may be 0 — this is the documented cap behavior
679
- expect(nonCritical.length).toBeLessThanOrEqual(result.length);
680
- // And the cap is enforced
681
- expect(result.length).toBeLessThanOrEqual(30);
682
- });
683
- it("does not cap when total scenarios are within the limit", () => {
684
- const endpoints = [
685
- { path: "/api/users", methods: [{ method: "GET" }, { method: "POST" }] },
686
- { path: "/api/users/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
687
- ];
688
- const newEndpoints = [
689
- { method: "PUT", path: "/api/users/{id}" },
690
- ];
691
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
692
- // A single PUT endpoint produces 5 scenarios — well within the 30-scenario cap
693
- expect(result.length).toBeLessThanOrEqual(30);
694
- expect(result.length).toBeGreaterThanOrEqual(4);
695
- });
696
- });
697
- describe("path collision fix — resourceGroups disambiguation", () => {
698
- it("unrelated paths sharing a last segment do not overwrite each other", () => {
699
- // /api/v1/orders/search and /api/v1/products/search share 'search' as last segment.
700
- // Before the fix, the second overwrote the first in resourceGroups.
701
- // The scenario must reference the orders/search path specifically, not products/search.
702
- const endpoints = [
703
- { path: "/api/v1/orders/search", methods: ["GET"] },
704
- { path: "/api/v1/products/search", methods: ["GET"] },
705
- ];
706
- const newEndpoints = [{ method: "GET", path: "/api/v1/orders/search" }];
707
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
708
- expect(result.length).toBeGreaterThanOrEqual(1);
709
- const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
710
- // Must contain orders/search
711
- expect(allPaths.some((p) => p.includes("orders/search"))).toBe(true);
712
- // Must NOT contain products/search — wrong path from a collision would appear here
713
- expect(allPaths.some((p) => p.includes("products/search"))).toBe(false);
714
- });
715
- it("related paths (nested resources) still merge correctly", () => {
716
- // /api/v1/users and /api/v1/users/{id} are related — should still merge
717
- const endpoints = [
718
- { path: "/api/v1/users", methods: ["GET", "POST"] },
719
- { path: "/api/v1/users/{id}", methods: ["GET", "PUT", "DELETE"] },
720
- ];
721
- const newEndpoints = [{ method: "POST", path: "/api/v1/users" }];
722
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
723
- expect(result.length).toBeGreaterThanOrEqual(1);
724
- });
725
- it("new endpoint under a disambiguated group is not silently dropped", () => {
726
- // When two paths share a last segment, both are stored under disambiguated keys
727
- // (e.g. "search::/api/v1/orders/search"). The fallback in draftDiffDirectScenarios
728
- // must use the full key for resourceGroups.get(), not the stripped "search" name
729
- // (which has no entry), otherwise the scenario is silently skipped.
730
- const endpoints = [
731
- { path: "/api/v1/orders/search", methods: ["GET"] },
732
- { path: "/api/v1/products/search", methods: ["GET"] },
733
- { path: "/api/v1/orders", methods: ["GET", "POST"] },
734
- { path: "/api/v1/products", methods: ["GET", "POST"] },
735
- ];
736
- const newEndpoints = [
737
- { method: "GET", path: "/api/v1/orders/search" },
738
- { method: "GET", path: "/api/v1/products/search" },
739
- ];
740
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
741
- // Both new endpoints must produce at least one scenario each — neither should be dropped
742
- const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
743
- expect(allPaths.some((p) => p.includes("orders/search") || p.includes("orders"))).toBe(true);
744
- expect(allPaths.some((p) => p.includes("products/search") || p.includes("products"))).toBe(true);
745
- });
746
- it("paths sharing a string prefix but different resource names are not merged", () => {
747
- // /api/v1/orders and /api/v1/orders-archive start with the same string
748
- // but are different resources. startsWith alone would incorrectly merge them.
749
- const endpoints = [
750
- { path: "/api/v1/orders", methods: ["GET", "POST"] },
751
- { path: "/api/v1/orders-archive", methods: ["GET"] },
752
- ];
753
- const newEndpoints = [{ method: "POST", path: "/api/v1/orders" }];
754
- const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
755
- expect(result.length).toBeGreaterThanOrEqual(1);
756
- const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
757
- // orders scenario must be present
758
- expect(allPaths.some((p) => p.includes("/orders"))).toBe(true);
759
- // orders-archive must not appear in scenarios targeting /orders
760
- expect(allPaths.some((p) => p.includes("orders-archive"))).toBe(false);
761
- });
762
- });
763
- describe("draftDiffDirectScenarios — execution endpoints", () => {
764
- it("produces execution templates for /execute-style endpoints (non-GET)", () => {
765
- const newEndpoints = [{ method: "POST", path: "/api/jobs/execute" }];
766
- const resourceGroups = new Map([
767
- ["jobs", { basePath: "/api/jobs", methods: new Set(["GET", "POST"]), paramPath: "/api/jobs/{id}" }],
768
- ]);
769
- const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
770
- const names = result.map((s) => s.scenarioName);
771
- expect(names).toContain("jobs-post-execute-happy");
772
- expect(names).toContain("jobs-post-execute-invalid");
773
- expect(names).toContain("jobs-post-auth-boundary");
774
- // Should NOT produce CRUD lifecycle templates
775
- expect(names.some((n) => n.includes("integration") || n.includes("contract"))).toBe(false);
776
- });
777
- it("does not produce execution templates for GET on execution endpoints", () => {
778
- const newEndpoints = [{ method: "GET", path: "/api/jobs/execute" }];
779
- const resourceGroups = new Map([
780
- ["jobs", { basePath: "/api/jobs", methods: new Set(["GET"]), paramPath: "/api/jobs/{id}" }],
781
- ]);
782
- const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
783
- const names = result.map((s) => s.scenarioName);
784
- // GET should fall through to normal GET handling, not execution templates
785
- expect(names.some((n) => n.includes("execute-happy"))).toBe(false);
786
- expect(names.some((n) => n.includes("execute-invalid"))).toBe(false);
787
- });
788
- });
789
- describe("draftDiffDirectScenarios — auth boundary status codes", () => {
790
- const newEndpoints = [{ method: "DELETE", path: "/api/orders/{id}" }];
791
- const resourceGroups = new Map([
792
- ["orders", { basePath: "/api/orders", methods: new Set(["GET", "POST", "DELETE"]), paramPath: "/api/orders/{id}" }],
793
- ]);
794
- it("expects 403 for Bearer auth type", () => {
795
- const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.Bearer);
796
- const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
797
- expect(authScenario).toBeDefined();
798
- expect(authScenario.steps[0].expectedStatusCode).toBe(403);
799
- });
800
- it("expects 403 for OAuth auth type (uses Bearer scheme)", () => {
801
- const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.OAuth);
802
- const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
803
- expect(authScenario).toBeDefined();
804
- expect(authScenario.steps[0].expectedStatusCode).toBe(403);
805
- });
806
- it("expects 401 for other auth types", () => {
807
- const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.ApiKey);
808
- const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
809
- expect(authScenario).toBeDefined();
810
- expect(authScenario.steps[0].expectedStatusCode).toBe(401);
811
- });
812
- it("expects 401 when no auth type specified", () => {
813
- const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
814
- const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
815
- expect(authScenario).toBeDefined();
816
- expect(authScenario.steps[0].expectedStatusCode).toBe(401);
817
- });
818
- });
819
- describe("draftResponseShapeScenarios", () => {
820
- it("drafts a contract scenario with expectedResponseFields when field hints are known", () => {
821
- const hopEndpoints = [
822
- { path: "/users/me", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
823
- ];
824
- const byFile = { "src/dtos/user.dto.ts": ["/users/me"] };
825
- const addedFieldsByFile = { "src/dtos/user.dto.ts": ["initials"] };
826
- const scenarios = draftResponseShapeScenarios(hopEndpoints, byFile, addedFieldsByFile);
827
- expect(scenarios).toHaveLength(1);
828
- const s = scenarios[0];
829
- expect(s.category).toBe("breaking_change");
830
- expect(s.priority).toBe("high");
831
- expect(s.testType).toBe("contract");
832
- expect(s.steps).toHaveLength(1);
833
- expect(s.steps[0].method).toBe("GET");
834
- expect(s.steps[0].path).toBe("/users/me");
835
- expect(s.steps[0].expectedStatusCode).toBe(200);
836
- expect(s.steps[0].expectedResponseFields).toEqual(["initials"]);
837
- expect(s.scenarioName).toBe("user-response-shape-initials");
838
- expect(s.bugCatchingTarget).toContain("initials");
839
- expect(s.bugCatchingTarget).toContain("user.dto.ts");
840
- expect(s.description).toContain("initials");
841
- });
842
- it("still drafts a scenario without expectedResponseFields when no field hints are known", () => {
843
- const hopEndpoints = [
844
- { path: "/orders/{id}", methods: ["GET"], sourceFile: "src/controllers/order.controller.ts" },
845
- ];
846
- const byFile = { "src/services/order.service.ts": ["/orders/{id}"] };
847
- const scenarios = draftResponseShapeScenarios(hopEndpoints, byFile, {});
848
- expect(scenarios).toHaveLength(1);
849
- expect(scenarios[0].steps[0].expectedResponseFields).toBeUndefined();
850
- expect(scenarios[0].scenarioName).toBe("order-response-shape-response-change");
851
- });
852
- it("caps drafted scenarios at 2 per file, preferring detail GETs over collection GETs", () => {
853
- const hopEndpoints = [
854
- { path: "/users", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
855
- { path: "/users/{id}", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
856
- { path: "/users/me", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
857
- ];
858
- const byFile = { "src/dtos/user.dto.ts": ["/users", "/users/{id}", "/users/me"] };
859
- const scenarios = draftResponseShapeScenarios(hopEndpoints, byFile, {});
860
- expect(scenarios).toHaveLength(2);
861
- const paths = scenarios.map((s) => s.steps[0].path);
862
- // Both detail-style GETs (path param or literal /me) win over the plain collection GET.
863
- expect(paths).toEqual(expect.arrayContaining(["/users/{id}", "/users/me"]));
864
- expect(paths).not.toContain("/users");
865
- });
866
- it("skips a file whose only hop-mapped endpoints have no GET method", () => {
867
- const hopEndpoints = [
868
- { path: "/orders/{id}", methods: ["DELETE"], sourceFile: "src/controllers/order.controller.ts" },
869
- ];
870
- const byFile = { "src/services/order.service.ts": ["/orders/{id}"] };
871
- expect(draftResponseShapeScenarios(hopEndpoints, byFile, {})).toEqual([]);
872
- });
873
- it("returns an empty array when byFile is empty", () => {
874
- expect(draftResponseShapeScenarios([], {}, {})).toEqual([]);
875
- });
876
- });