@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,1011 +0,0 @@
1
- // @ts-ignore
2
- import { nextjsFileToApiPath, parseRouteLine, parseFileEndpoints, extractResourceFromPath, classifyEndpointsByChangedFiles, selectRemovalCandidateFiles, recoverRemovedEndpointsFromBase, parseChangedLinesByFile } from "./routeParsers.js";
3
- describe("nextjsFileToApiPath", () => {
4
- it("converts pages/api route to API path", () => {
5
- expect(nextjsFileToApiPath("pages/api/users/index.ts")).toBe("/api/users");
6
- });
7
- it("converts pages/api nested dynamic route", () => {
8
- expect(nextjsFileToApiPath("pages/api/users/[id].ts")).toBe("/api/users/{id}");
9
- });
10
- it("converts app/api route (App Router)", () => {
11
- expect(nextjsFileToApiPath("app/api/products/route.ts")).toBe("/api/products");
12
- });
13
- it("converts app/api nested dynamic route", () => {
14
- expect(nextjsFileToApiPath("app/api/products/[id]/route.ts")).toBe("/api/products/{id}");
15
- });
16
- it("returns null for non-API paths", () => {
17
- expect(nextjsFileToApiPath("src/components/Button.tsx")).toBeNull();
18
- });
19
- });
20
- describe("parseRouteLine", () => {
21
- it("parses FastAPI decorator", () => {
22
- const result = parseRouteLine('@app.get("/users")', "main.py");
23
- expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "main.py" });
24
- });
25
- it("parses FastAPI with router prefix", () => {
26
- const result = parseRouteLine('@router.post("/items")', "routes.py");
27
- expect(result).toEqual({ method: "POST", path: "/items", sourceFile: "routes.py" });
28
- });
29
- it("parses Flask decorator", () => {
30
- const result = parseRouteLine('@app.route("/health", methods=["GET"])', "app.py");
31
- expect(result).toEqual({ method: "GET", path: "/health", sourceFile: "app.py" });
32
- });
33
- it("parses Express route", () => {
34
- const result = parseRouteLine('router.get("/api/products", handler)', "routes.ts");
35
- expect(result).toEqual({ method: "GET", path: "/api/products", sourceFile: "routes.ts" });
36
- });
37
- it("parses NestJS decorator", () => {
38
- const result = parseRouteLine('@Get("users/:id")', "users.controller.ts");
39
- expect(result).toEqual({ method: "GET", path: "users/:id", sourceFile: "users.controller.ts" });
40
- });
41
- it("parses Spring annotation", () => {
42
- const result = parseRouteLine('@GetMapping("/api/v1/orders")', "OrderController.java");
43
- expect(result).toEqual({ method: "GET", path: "/api/v1/orders", sourceFile: "OrderController.java" });
44
- });
45
- it("parses Next.js App Router export", () => {
46
- const result = parseRouteLine("export async function GET(request: Request) {", "app/api/items/route.ts");
47
- expect(result).not.toBeNull();
48
- expect(result.method).toBe("GET");
49
- });
50
- it("parses Next.js guard clause (req.method !== 'POST')", () => {
51
- const result = parseRouteLine(' if (req.method !== "POST") {', "pages/api/v1/collections/[id]/transfer.ts");
52
- expect(result).not.toBeNull();
53
- expect(result.method).toBe("POST");
54
- expect(result.path).toBe("/api/v1/collections/{id}/transfer");
55
- });
56
- it("parses Next.js guard clause with != operator", () => {
57
- const result = parseRouteLine(" if (req.method != 'DELETE') {", "pages/api/v1/items/[id].ts");
58
- expect(result).not.toBeNull();
59
- expect(result.method).toBe("DELETE");
60
- expect(result.path).toBe("/api/v1/items/{id}");
61
- });
62
- it("parses Gin route", () => {
63
- const result = parseRouteLine('r.GET("/users", getUsers)', "main.go");
64
- expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "main.go" });
65
- });
66
- it("parses Gin group route", () => {
67
- const result = parseRouteLine('api.POST("/products", createProduct)', "router.go");
68
- expect(result).toEqual({ method: "POST", path: "/products", sourceFile: "router.go" });
69
- });
70
- it("parses Rails route", () => {
71
- const result = parseRouteLine('get "/users", to: "users#index"', "routes.rb");
72
- expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "routes.rb" });
73
- });
74
- it("parses Phoenix route in .ex file", () => {
75
- const result = parseRouteLine('get "/users", UserController, :index', "lib/my_app_web/router.ex");
76
- expect(result).toEqual({ method: "GET", path: "/users", sourceFile: "lib/my_app_web/router.ex" });
77
- });
78
- it("parses Phoenix route in .exs file", () => {
79
- const result = parseRouteLine('post "/users", UserController, :create', "lib/my_app_web/router.exs");
80
- expect(result).toEqual({ method: "POST", path: "/users", sourceFile: "lib/my_app_web/router.exs" });
81
- });
82
- it("parses Laravel route", () => {
83
- const result = parseRouteLine("Route::get('/api/users', [UserController::class, 'index'])", "api.php");
84
- expect(result).toEqual({ method: "GET", path: "/api/users", sourceFile: "api.php" });
85
- });
86
- it("parses Actix attribute", () => {
87
- const result = parseRouteLine('#[get("/health")]', "main.rs");
88
- expect(result).toEqual({ method: "GET", path: "/health", sourceFile: "main.rs" });
89
- });
90
- it("parses ASP.NET HttpGet", () => {
91
- const result = parseRouteLine('[HttpGet("users/{id}")]', "UsersController.cs");
92
- expect(result).toEqual({ method: "GET", path: "users/{id}", sourceFile: "UsersController.cs" });
93
- });
94
- it("parses ASP.NET HttpPost without path", () => {
95
- const result = parseRouteLine("[HttpPost]", "OrdersController.cs");
96
- expect(result).toEqual({ method: "POST", path: "/", sourceFile: "OrdersController.cs" });
97
- });
98
- it("parses Hapi route", () => {
99
- const result = parseRouteLine("{ method: 'GET', path: '/api/users' }", "server.js");
100
- expect(result).toEqual({ method: "GET", path: "/api/users", sourceFile: "server.js" });
101
- });
102
- it("returns null for non-route lines", () => {
103
- expect(parseRouteLine("const x = 42;", "app.ts")).toBeNull();
104
- expect(parseRouteLine("// comment", "main.py")).toBeNull();
105
- expect(parseRouteLine("import express from 'express';", "index.ts")).toBeNull();
106
- });
107
- it("ignores Express-style calls in component files (.tsx, .jsx, .vue, .svelte)", () => {
108
- expect(parseRouteLine('router.put("/api/orders", handler)', "components/OrderForm.tsx")).toBeNull();
109
- expect(parseRouteLine('app.get("/products", handler)', "pages/Products.jsx")).toBeNull();
110
- expect(parseRouteLine('router.post("/items", handler)', "views/ItemForm.vue")).toBeNull();
111
- expect(parseRouteLine('app.delete("/cart", handler)', "routes/Cart.svelte")).toBeNull();
112
- });
113
- it("ignores NestJS decorators in component files", () => {
114
- expect(parseRouteLine('@Get("/users")', "components/UserList.tsx")).toBeNull();
115
- expect(parseRouteLine('@Post("/orders")', "forms/OrderForm.jsx")).toBeNull();
116
- });
117
- it("ignores Hapi-style objects in component files", () => {
118
- expect(parseRouteLine("{ method: 'PUT', path: '/api/orders' }", "components/OrderForm.tsx")).toBeNull();
119
- });
120
- it("still parses Express routes from .ts backend files", () => {
121
- const result = parseRouteLine('router.put("/api/orders", handler)', "routes/orders.ts");
122
- expect(result).toEqual({ method: "PUT", path: "/api/orders", sourceFile: "routes/orders.ts" });
123
- });
124
- it("@RequestMapping uses MULTI when no method attr", () => {
125
- const result = parseRouteLine('@RequestMapping("/api/v1/items")', "ItemController.java");
126
- expect(result).toEqual({ method: "MULTI", path: "/api/v1/items", sourceFile: "ItemController.java" });
127
- });
128
- it("@RequestMapping respects explicit RequestMethod", () => {
129
- const result = parseRouteLine('@RequestMapping(value="/orders", method=RequestMethod.POST)', "OrderController.java");
130
- expect(result).toEqual({ method: "POST", path: "/orders", sourceFile: "OrderController.java" });
131
- });
132
- it("@RequestMapping uses MULTI when multiple methods specified", () => {
133
- const result = parseRouteLine('@RequestMapping(value="/bulk", method={RequestMethod.GET, RequestMethod.POST})', "ItemController.java");
134
- expect(result).toEqual({ method: "MULTI", path: "/bulk", sourceFile: "ItemController.java" });
135
- });
136
- });
137
- describe("parseFileEndpoints", () => {
138
- it("Spring: combines @RestController + @RequestMapping class prefix with @GetMapping", () => {
139
- const content = `
140
- @RestController
141
- @RequestMapping("/api/v1/users")
142
- public class UserController {
143
- @GetMapping("/{id}")
144
- public User getUser() {}
145
- @PostMapping("/")
146
- public User createUser() {}
147
- }
148
- `;
149
- const eps = parseFileEndpoints(content, "UserController.java");
150
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/api/v1/users/{id}", sourceFile: "UserController.java" }));
151
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/api/v1/users", sourceFile: "UserController.java" }));
152
- });
153
- it("Spring: class without prefix emits method paths as-is", () => {
154
- const content = `
155
- public class ProductController {
156
- @GetMapping("/products")
157
- public List<Product> list() {}
158
- }
159
- `;
160
- const eps = parseFileEndpoints(content, "ProductController.java");
161
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/products", sourceFile: "ProductController.java" }));
162
- });
163
- it("Spring: second class in file does not inherit first class prefix", () => {
164
- const content = `
165
- @RequestMapping("/api/orders")
166
- public class OrderController {
167
- @GetMapping("/{id}")
168
- public Order get() {}
169
- }
170
- public class HealthController {
171
- @GetMapping("/health")
172
- public String health() {}
173
- }
174
- `;
175
- const eps = parseFileEndpoints(content, "Controllers.java");
176
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/api/orders/{id}", sourceFile: "Controllers.java" }));
177
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/health", sourceFile: "Controllers.java" }));
178
- expect(eps).not.toContainEqual(expect.objectContaining({ method: "GET", path: "/api/orders/health", sourceFile: "Controllers.java" }));
179
- });
180
- it("NestJS: combines @Controller prefix with @Get path", () => {
181
- const content = `
182
- @Controller('users')
183
- export class UsersController {
184
- @Get(':id')
185
- findOne() {}
186
- }
187
- `;
188
- const eps = parseFileEndpoints(content, "users.controller.ts");
189
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/users/:id", sourceFile: "users.controller.ts" }));
190
- });
191
- it("NestJS: @Post() with no path uses controller root", () => {
192
- const content = `
193
- @Controller('users')
194
- export class UsersController {
195
- @Post()
196
- create() {}
197
- @Get()
198
- findAll() {}
199
- }
200
- `;
201
- const eps = parseFileEndpoints(content, "users.controller.ts");
202
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/users", sourceFile: "users.controller.ts" }));
203
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/users", sourceFile: "users.controller.ts" }));
204
- });
205
- it("ASP.NET: combines [Route] prefix with [HttpGet] path", () => {
206
- const content = `
207
- [ApiController]
208
- [Route("api/v1/orders")]
209
- public class OrdersController : ControllerBase {
210
- [HttpGet("{id}")]
211
- public IActionResult Get(int id) {}
212
- [HttpPost]
213
- public IActionResult Create() {}
214
- }
215
- `;
216
- const eps = parseFileEndpoints(content, "OrdersController.cs");
217
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/api/v1/orders/{id}", sourceFile: "OrdersController.cs" }));
218
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/api/v1/orders", sourceFile: "OrdersController.cs" }));
219
- });
220
- it("non-class-based frameworks are unaffected (Express, Flask, Gin)", () => {
221
- const content = `
222
- router.get("/api/products", handler)
223
- @app.post("/items")
224
- r.DELETE("/users/:id", deleteUser)
225
- `;
226
- const eps = parseFileEndpoints(content, "routes.ts");
227
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/api/products", sourceFile: "routes.ts" }));
228
- });
229
- it("FastAPI multi-line APIRouter prefix is combined with method decorators", () => {
230
- const content = `
231
- router = APIRouter(
232
- prefix="/orders",
233
- tags=["orders"],
234
- )
235
-
236
- @router.post("")
237
- async def create_order(): pass
238
-
239
- @router.get("/{order_id}")
240
- async def get_order(): pass
241
-
242
- @router.delete("/{order_id}")
243
- async def delete_order(): pass
244
- `;
245
- const eps = parseFileEndpoints(content, "routers/order.py");
246
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/orders", sourceFile: "routers/order.py" }));
247
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/orders/{order_id}", sourceFile: "routers/order.py" }));
248
- expect(eps).toContainEqual(expect.objectContaining({ method: "DELETE", path: "/orders/{order_id}", sourceFile: "routers/order.py" }));
249
- });
250
- it("FastAPI single-line APIRouter prefix is combined with method decorators", () => {
251
- const content = `
252
- router = APIRouter(prefix="/products", tags=["products"])
253
-
254
- @router.get("/{product_id}")
255
- async def get_product(): pass
256
- `;
257
- const eps = parseFileEndpoints(content, "routers/product.py");
258
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/products/{product_id}", sourceFile: "routers/product.py" }));
259
- });
260
- it("FastAPI multi-line method decorators capture path arguments", () => {
261
- const content = `
262
- router = APIRouter(
263
- prefix="/flows",
264
- tags=["flows"],
265
- )
266
-
267
- @router.delete(
268
- "/{id:uuid}",
269
- status_code=204,
270
- )
271
- async def delete_flow(): pass
272
-
273
- @router.post(
274
- "/bulk_delete",
275
- status_code=204,
276
- )
277
- async def bulk_delete_flows(): pass
278
- `;
279
- const eps = parseFileEndpoints(content, "src/prefect/server/api/flows.py");
280
- expect(eps).toContainEqual(expect.objectContaining({ method: "DELETE", path: "/flows/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }));
281
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/flows/bulk_delete", sourceFile: "src/prefect/server/api/flows.py" }));
282
- });
283
- // ── Multi-line Express route patterns ──────────────────────────────────────
284
- it("Express multi-line: router.post( with path on next line", () => {
285
- const content = `
286
- router.post(
287
- \t'/:collection',
288
- \tasyncHandler(async (req, res, next) => {})
289
- );
290
- `;
291
- const eps = parseFileEndpoints(content, "controllers/collections.ts");
292
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/:collection", sourceFile: "controllers/collections.ts" }));
293
- });
294
- it("Express multi-line: app.get( with path on next line", () => {
295
- const content = `
296
- app.get(
297
- \t'/health',
298
- \t(req, res) => res.json({ status: 'ok' })
299
- );
300
- `;
301
- const eps = parseFileEndpoints(content, "server.ts");
302
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/health", sourceFile: "server.ts" }));
303
- });
304
- it("Express multi-line: does not fire for single-line routes", () => {
305
- const content = `router.get('/api/products', handler)`;
306
- const eps = parseFileEndpoints(content, "routes.ts");
307
- expect(eps).toHaveLength(1);
308
- expect(eps[0]).toEqual(expect.objectContaining({ method: "GET", path: "/api/products", sourceFile: "routes.ts" }));
309
- });
310
- it("Express multi-line: handles multiple consecutive multi-line routes", () => {
311
- const content = `
312
- router.post(
313
- \t'/',
314
- \tasyncHandler(async (req, res) => {})
315
- );
316
-
317
- router.get(
318
- \t'/:pk',
319
- \tasyncHandler(async (req, res) => {})
320
- );
321
-
322
- router.delete(
323
- \t'/:pk',
324
- \tasyncHandler(async (req, res) => {})
325
- );
326
- `;
327
- const eps = parseFileEndpoints(content, "controllers/items.ts");
328
- expect(eps).toHaveLength(3);
329
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/", sourceFile: "controllers/items.ts" }));
330
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/:pk", sourceFile: "controllers/items.ts" }));
331
- expect(eps).toContainEqual(expect.objectContaining({ method: "DELETE", path: "/:pk", sourceFile: "controllers/items.ts" }));
332
- });
333
- it("Express multi-line: ignores component files (.tsx)", () => {
334
- const content = `
335
- router.post(
336
- \t'/:collection',
337
- \thandler
338
- );
339
- `;
340
- const eps = parseFileEndpoints(content, "components/Form.tsx");
341
- expect(eps).toHaveLength(0);
342
- });
343
- it("Express multi-line: abandons pending state on closing paren without path", () => {
344
- const content = `
345
- router.post(
346
- );
347
- router.get('/fallback', handler);
348
- `;
349
- const eps = parseFileEndpoints(content, "routes.ts");
350
- expect(eps).toHaveLength(1);
351
- expect(eps[0]).toEqual(expect.objectContaining({ method: "GET", path: "/fallback", sourceFile: "routes.ts" }));
352
- });
353
- it("Express multi-line: mixes single-line and multi-line routes", () => {
354
- const content = `
355
- router.get('/', validateBatch('read'), readHandler, respond);
356
-
357
- router.get(
358
- \t'/:pk',
359
- \tasyncHandler(async (req, res, next) => {})
360
- );
361
-
362
- router.patch(
363
- \t'/:pk',
364
- \tvalidateBatch('update'),
365
- \tasyncHandler(async (req, res, next) => {})
366
- );
367
- `;
368
- const eps = parseFileEndpoints(content, "controllers/access.ts");
369
- expect(eps).toHaveLength(3);
370
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/", sourceFile: "controllers/access.ts" }));
371
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/:pk", sourceFile: "controllers/access.ts" }));
372
- expect(eps).toContainEqual(expect.objectContaining({ method: "PATCH", path: "/:pk", sourceFile: "controllers/access.ts" }));
373
- });
374
- // ── Fastify server.route({ method, url }) (SKYR-3857) ──────────────────────
375
- it("Fastify: server.route({ method: \"GET\", url }) single method", () => {
376
- const content = `
377
- export const registerSecretRouter = async (server: FastifyZodProvider) => {
378
- server.route({
379
- method: "GET",
380
- url: "/:secretName",
381
- config: {
382
- rateLimit: secretsLimit
383
- },
384
- schema: {
385
- querystring: z.object({})
386
- },
387
- handler: async (req) => {}
388
- });
389
- };
390
- `;
391
- const eps = parseFileEndpoints(content, "backend/src/server/routes/v4/secret-router.ts");
392
- expect(eps).toContainEqual(expect.objectContaining({
393
- method: "GET", path: "/:secretName", sourceFile: "backend/src/server/routes/v4/secret-router.ts",
394
- }));
395
- });
396
- it("Fastify: array method value emits one endpoint per method", () => {
397
- const content = `
398
- server.route({
399
- method: ["GET", "POST"],
400
- url: "/batch",
401
- handler: async (req) => {}
402
- });
403
- `;
404
- const eps = parseFileEndpoints(content, "secret-router.ts");
405
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/batch", sourceFile: "secret-router.ts" }));
406
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/batch", sourceFile: "secret-router.ts" }));
407
- });
408
- it("Fastify: url declared before method still resolves", () => {
409
- const content = `
410
- server.route({
411
- url: "/move",
412
- method: "POST",
413
- handler: async (req) => {}
414
- });
415
- `;
416
- const eps = parseFileEndpoints(content, "secret-router.ts");
417
- expect(eps).toContainEqual(expect.objectContaining({ method: "POST", path: "/move", sourceFile: "secret-router.ts" }));
418
- });
419
- it("Fastify: multiple route() calls in the same file are all parsed", () => {
420
- const content = `
421
- export const registerSecretRouter = async (server: FastifyZodProvider) => {
422
- server.route({
423
- method: "GET",
424
- url: "/",
425
- handler: async () => {}
426
- });
427
-
428
- server.route({
429
- method: "DELETE",
430
- url: "/:secretName",
431
- handler: async () => {}
432
- });
433
- };
434
- `;
435
- const eps = parseFileEndpoints(content, "secret-router.ts");
436
- expect(eps).toHaveLength(2);
437
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/", sourceFile: "secret-router.ts" }));
438
- expect(eps).toContainEqual(expect.objectContaining({ method: "DELETE", path: "/:secretName", sourceFile: "secret-router.ts" }));
439
- });
440
- it("Fastify: method/url declared after a nested multi-line block still resolve", () => {
441
- const content = `
442
- server.route({
443
- config: {
444
- rateLimit: secretsLimit,
445
- },
446
- schema: {
447
- params: z.object({
448
- secretName: z.string(),
449
- }),
450
- },
451
- method: "PATCH",
452
- url: "/:secretName",
453
- handler: async () => {},
454
- });
455
- `;
456
- const eps = parseFileEndpoints(content, "secret-router.ts");
457
- expect(eps).toContainEqual(expect.objectContaining({ method: "PATCH", path: "/:secretName", sourceFile: "secret-router.ts" }));
458
- });
459
- it("Fastify: a url-keyed string literal inside a nested block is not mistaken for the route url", () => {
460
- const content = `
461
- server.route({
462
- method: "GET",
463
- schema: {
464
- examples: [
465
- { url: "/decoy" },
466
- ],
467
- },
468
- url: "/real",
469
- handler: async () => {},
470
- });
471
- `;
472
- const eps = parseFileEndpoints(content, "secret-router.ts");
473
- expect(eps).toHaveLength(1);
474
- expect(eps).toContainEqual(expect.objectContaining({ method: "GET", path: "/real", sourceFile: "secret-router.ts" }));
475
- });
476
- it("Fastify: abandons pending state when the call closes without a url", () => {
477
- const content = `
478
- server.route({
479
- method: "GET"
480
- });
481
- server.get('/fallback', handler);
482
- `;
483
- const eps = parseFileEndpoints(content, "secret-router.ts");
484
- // No Fastify endpoint from the incomplete .route() call, and the bare
485
- // .get() shorthand (not .route()) is out of scope for this parser — so
486
- // nothing is emitted, but crucially nothing throws either.
487
- expect(eps).toHaveLength(0);
488
- });
489
- });
490
- describe("extractResourceFromPath", () => {
491
- it("extracts resource from simple path", () => {
492
- expect(extractResourceFromPath("/orders")).toBe("orders");
493
- });
494
- it("strips api and version prefixes", () => {
495
- expect(extractResourceFromPath("/api/v1/orders")).toBe("orders");
496
- expect(extractResourceFromPath("/api/v2/products")).toBe("products");
497
- expect(extractResourceFromPath("/api/v3/items")).toBe("items");
498
- });
499
- it("strips path parameters", () => {
500
- expect(extractResourceFromPath("/api/v1/orders/{order_id}")).toBe("orders");
501
- });
502
- it("handles nested resources", () => {
503
- expect(extractResourceFromPath("/api/v1/orders/{order_id}/items")).toBe("orders_items");
504
- });
505
- it("handles public prefix", () => {
506
- expect(extractResourceFromPath("/public/products")).toBe("products");
507
- });
508
- it("returns unknown for empty path", () => {
509
- expect(extractResourceFromPath("")).toBe("unknown");
510
- });
511
- it("returns unknown for path with only skippable segments", () => {
512
- expect(extractResourceFromPath("/api/v1/{id}")).toBe("unknown");
513
- });
514
- it("handles path without leading slash", () => {
515
- expect(extractResourceFromPath("api/v1/orders")).toBe("orders");
516
- });
517
- });
518
- describe("classifyEndpointsByChangedFiles", () => {
519
- function makeDiffDataForClassify(opts) {
520
- return {
521
- currentBranch: "feature/test",
522
- baseBranch: "origin/main",
523
- changedFiles: opts.changedFiles,
524
- diffContent: "",
525
- diffStat: "",
526
- newFiles: opts.newFiles ?? [],
527
- deletedFiles: opts.deletedFiles ?? [],
528
- };
529
- }
530
- const scannedEndpoints = [
531
- { path: "/api/v1/orders/{order_id}", methods: ["GET", "PATCH"], sourceFile: "src/routes/orders.py" },
532
- { path: "/api/v1/products/{product_id}", methods: ["GET", "POST"], sourceFile: "src/routes/products.py" },
533
- { path: "/api/v1/users", methods: ["GET"], sourceFile: "src/routes/users.py" },
534
- ];
535
- it("classifies modified endpoints from changed files", () => {
536
- const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/orders.py"] });
537
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
538
- expect(result.changedEndpoints).toHaveLength(1);
539
- expect(result.changedEndpoints[0].path).toBe("/api/v1/orders/{order_id}");
540
- expect(result.newEndpoints).toHaveLength(0);
541
- expect(result.removedEndpoints).toHaveLength(0);
542
- });
543
- it("classifies new endpoints from newly-created files", () => {
544
- const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/products.py"], newFiles: ["src/routes/products.py"] });
545
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
546
- expect(result.newEndpoints).toHaveLength(1);
547
- expect(result.newEndpoints[0].path).toBe("/api/v1/products/{product_id}");
548
- expect(result.changedEndpoints).toHaveLength(0);
549
- });
550
- it("puts files without endpoints into unmatchedFiles", () => {
551
- const diff = makeDiffDataForClassify({ changedFiles: ["src/models/order.py", "src/routes/orders.py"] });
552
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
553
- expect(result.unmatchedFiles).toEqual(["src/models/order.py"]);
554
- expect(result.changedEndpoints).toHaveLength(1);
555
- });
556
- it("skips deleted files from changed classification", () => {
557
- const deletedEndpoints = [{ path: "/api/v1/legacy", methods: ["GET"], sourceFile: "src/routes/legacy.py" }];
558
- const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/legacy.py", "src/routes/orders.py"], deletedFiles: ["src/routes/legacy.py"] });
559
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deletedEndpoints);
560
- expect(result.removedEndpoints).toHaveLength(1);
561
- expect(result.removedEndpoints[0].path).toBe("/api/v1/legacy");
562
- expect(result.changedEndpoints).toHaveLength(1);
563
- expect(result.changedEndpoints[0].path).toBe("/api/v1/orders/{order_id}");
564
- });
565
- it("handles multiple endpoints in the same file", () => {
566
- const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/orders.py"] });
567
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
568
- expect(result.changedEndpoints).toHaveLength(1);
569
- expect(result.changedEndpoints[0].methods).toEqual(["GET", "PATCH"]);
570
- });
571
- it("returns empty results when no changed files match endpoints", () => {
572
- const diff = makeDiffDataForClassify({ changedFiles: ["README.md", "package.json"] });
573
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
574
- expect(result.changedEndpoints).toHaveLength(0);
575
- expect(result.newEndpoints).toHaveLength(0);
576
- expect(result.removedEndpoints).toHaveLength(0);
577
- expect(result.unmatchedFiles).toEqual(["README.md", "package.json"]);
578
- });
579
- it("computes affectedServices from changedFiles", () => {
580
- const diff = makeDiffDataForClassify({ changedFiles: ["services/orders/routes.py", "services/users/models.py"] });
581
- const result = classifyEndpointsByChangedFiles(diff, []);
582
- expect(result.affectedServices).toContain("orders");
583
- expect(result.affectedServices).toContain("users");
584
- });
585
- it("propagates branch metadata from diffData", () => {
586
- const diff = makeDiffDataForClassify({ changedFiles: [] });
587
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
588
- expect(result.currentBranch).toBe("feature/test");
589
- expect(result.baseBranch).toBe("origin/main");
590
- expect(result.changedFiles).toEqual([]);
591
- });
592
- it("handles empty scannedEndpoints (all files unmatched)", () => {
593
- const diff = makeDiffDataForClassify({ changedFiles: ["src/routes/orders.py"] });
594
- const result = classifyEndpointsByChangedFiles(diff, []);
595
- expect(result.changedEndpoints).toHaveLength(0);
596
- expect(result.newEndpoints).toHaveLength(0);
597
- expect(result.removedEndpoints).toHaveLength(0);
598
- expect(result.unmatchedFiles).toEqual(["src/routes/orders.py"]);
599
- });
600
- it("does not double-count a file that is both new and has endpoints", () => {
601
- const diff = makeDiffDataForClassify({
602
- changedFiles: ["src/routes/products.py", "src/routes/orders.py"],
603
- newFiles: ["src/routes/products.py"],
604
- });
605
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
606
- // products.py is new → newEndpoints; orders.py is changed → changedEndpoints
607
- expect(result.newEndpoints).toHaveLength(1);
608
- expect(result.newEndpoints[0].sourceFile).toBe("src/routes/products.py");
609
- expect(result.changedEndpoints).toHaveLength(1);
610
- expect(result.changedEndpoints[0].sourceFile).toBe("src/routes/orders.py");
611
- });
612
- it("includes deleted-file endpoints in removedEndpoints when not in current catalog", () => {
613
- const deleted = [
614
- { path: "/api/v1/removed", methods: ["GET", "POST"], sourceFile: "src/routes/removed.py" },
615
- ];
616
- const diff = makeDiffDataForClassify({
617
- changedFiles: ["src/routes/removed.py"],
618
- deletedFiles: ["src/routes/removed.py"],
619
- });
620
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
621
- expect(result.removedEndpoints).toHaveLength(1);
622
- expect(result.removedEndpoints[0].path).toBe("/api/v1/removed");
623
- expect(result.removedEndpoints[0].methods).toEqual(["GET", "POST"]);
624
- // The deleted file should NOT appear in changedEndpoints or unmatchedFiles
625
- expect(result.changedEndpoints.every(ep => ep.sourceFile !== "src/routes/removed.py")).toBe(true);
626
- expect(result.unmatchedFiles).not.toContain("src/routes/removed.py");
627
- });
628
- it("filters moved endpoints from removedEndpoints when they exist in current catalog", () => {
629
- // Endpoint was in a deleted file but the same METHOD+path exists in scanned catalog
630
- // (moved to another file) — should NOT appear in removedEndpoints
631
- const deleted = [
632
- { path: "/api/v1/orders/{order_id}", methods: ["GET", "DELETE"], sourceFile: "src/routes/old-orders.py" },
633
- ];
634
- const diff = makeDiffDataForClassify({
635
- changedFiles: ["src/routes/old-orders.py"],
636
- deletedFiles: ["src/routes/old-orders.py"],
637
- });
638
- // scannedEndpoints has GET /api/v1/orders/{order_id} (from orders.py) → moved, filter out
639
- // DELETE /api/v1/orders/{order_id} is NOT in scannedEndpoints → truly removed, keep
640
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
641
- expect(result.removedEndpoints).toHaveLength(1);
642
- expect(result.removedEndpoints[0].path).toBe("/api/v1/orders/{order_id}");
643
- expect(result.removedEndpoints[0].methods).toEqual(["DELETE"]);
644
- });
645
- it("filters out entirely moved endpoints (all methods exist in catalog)", () => {
646
- const deleted = [
647
- { path: "/api/v1/orders/{order_id}", methods: ["GET", "PATCH"], sourceFile: "src/routes/old-orders.py" },
648
- ];
649
- const diff = makeDiffDataForClassify({
650
- changedFiles: ["src/routes/old-orders.py"],
651
- deletedFiles: ["src/routes/old-orders.py"],
652
- });
653
- // Both GET and PATCH for /api/v1/orders/{order_id} exist in scannedEndpoints → fully filtered
654
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
655
- expect(result.removedEndpoints).toHaveLength(0);
656
- });
657
- it("treats MULTI sentinel in current catalog as wildcard when filtering moved endpoints", () => {
658
- // Deleted file had explicit GET+POST methods, but current catalog has MULTI for the same path
659
- // (e.g. moved to a Next.js catch-all handler) — should be fully filtered out
660
- const catalogWithMulti = [
661
- ...scannedEndpoints,
662
- { path: "/api/v1/legacy", methods: ["MULTI"], sourceFile: "pages/api/legacy.ts" },
663
- ];
664
- const deleted = [
665
- { path: "/api/v1/legacy", methods: ["GET", "POST"], sourceFile: "src/routes/legacy.py" },
666
- ];
667
- const diff = makeDiffDataForClassify({
668
- changedFiles: ["src/routes/legacy.py"],
669
- deletedFiles: ["src/routes/legacy.py"],
670
- });
671
- const result = classifyEndpointsByChangedFiles(diff, catalogWithMulti, deleted);
672
- expect(result.removedEndpoints).toHaveLength(0);
673
- });
674
- it("treats MULTI sentinel in deleted endpoints as wildcard when path exists in catalog", () => {
675
- // Deleted file was a Next.js catch-all (MULTI), but path now exists with concrete methods
676
- // in the current catalog — should be filtered out as moved, not flagged as removed
677
- const deleted = [
678
- { path: "/api/v1/orders/{order_id}", methods: ["MULTI"], sourceFile: "pages/api/orders/[order_id].ts" },
679
- ];
680
- const diff = makeDiffDataForClassify({
681
- changedFiles: ["pages/api/orders/[order_id].ts"],
682
- deletedFiles: ["pages/api/orders/[order_id].ts"],
683
- });
684
- // scannedEndpoints already has GET+PATCH for /api/v1/orders/{order_id}
685
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints, deleted);
686
- expect(result.removedEndpoints).toHaveLength(0);
687
- });
688
- it("matches POSIX diff paths against Windows-style scanned source files", () => {
689
- const windowsScanned = [
690
- { path: "/api/v1/orders/{order_id}", methods: ["GET"], sourceFile: "src\\routes\\orders.py" },
691
- { path: "/api/v1/products/{product_id}", methods: ["POST"], sourceFile: "src\\routes\\products.py" },
692
- ];
693
- const diff = makeDiffDataForClassify({
694
- changedFiles: ["src/routes/orders.py", "src/routes/products.py", "src/routes/legacy.py"],
695
- newFiles: ["src/routes/products.py"],
696
- deletedFiles: ["src/routes/legacy.py"],
697
- });
698
- const result = classifyEndpointsByChangedFiles(diff, windowsScanned, [
699
- { path: "/api/v1/legacy", methods: ["DELETE"], sourceFile: "src\\routes\\legacy.py" },
700
- ]);
701
- expect(result.changedEndpoints).toEqual([windowsScanned[0]]);
702
- expect(result.newEndpoints).toEqual([windowsScanned[1]]);
703
- expect(result.removedEndpoints).toEqual([
704
- { path: "/api/v1/legacy", methods: ["DELETE"], sourceFile: "src\\routes\\legacy.py" },
705
- ]);
706
- expect(result.unmatchedFiles).toEqual([]);
707
- expect(result.changedFiles).toEqual(diff.changedFiles);
708
- });
709
- it("uses Windows path separators when comparing moved endpoint resources", () => {
710
- const current = [
711
- { path: "/api/v1/orders/{order_id}", methods: ["GET"], sourceFile: "src/routes/orders.py" },
712
- ];
713
- const deleted = [
714
- { path: "/api/v1/orders/{order_id}", methods: ["GET", "DELETE"], sourceFile: "src\\routes\\old-orders.py" },
715
- ];
716
- const diff = makeDiffDataForClassify({
717
- changedFiles: ["src\\routes\\old-orders.py"],
718
- deletedFiles: ["src\\routes\\old-orders.py"],
719
- });
720
- const result = classifyEndpointsByChangedFiles(diff, current, deleted);
721
- expect(result.removedEndpoints).toHaveLength(1);
722
- expect(result.removedEndpoints[0].methods).toEqual(["DELETE"]);
723
- });
724
- it("treats generic same-name route files in the same resource folder as moved", () => {
725
- const current = [
726
- { path: "/{id}", methods: ["GET", "DELETE"], sourceFile: "src/api/flows/server.py" },
727
- ];
728
- const deleted = [
729
- { path: "/{id}", methods: ["GET", "DELETE"], sourceFile: "src/api/flows/old-server.py" },
730
- ];
731
- const diff = makeDiffDataForClassify({
732
- changedFiles: ["src/api/flows/old-server.py"],
733
- deletedFiles: ["src/api/flows/old-server.py"],
734
- });
735
- const result = classifyEndpointsByChangedFiles(diff, current, deleted);
736
- expect(result.removedEndpoints).toHaveLength(0);
737
- });
738
- it("does not treat generic same-name route files in different resource folders as moved", () => {
739
- const current = [
740
- { path: "/{id}", methods: ["GET"], sourceFile: "src/api/deployments/server.py" },
741
- ];
742
- const deleted = [
743
- { path: "/{id}", methods: ["GET", "DELETE"], sourceFile: "src/api/flows/server.py" },
744
- ];
745
- const diff = makeDiffDataForClassify({
746
- changedFiles: ["src/api/flows/server.py"],
747
- deletedFiles: ["src/api/flows/server.py"],
748
- });
749
- const result = classifyEndpointsByChangedFiles(diff, current, deleted);
750
- expect(result.removedEndpoints).toHaveLength(1);
751
- expect(result.removedEndpoints[0].methods).toEqual(["GET", "DELETE"]);
752
- });
753
- it("defaults removedEndpoints to empty when deletedFileEndpoints is omitted", () => {
754
- const diff = makeDiffDataForClassify({
755
- changedFiles: ["src/routes/legacy.py"],
756
- deletedFiles: ["src/routes/legacy.py"],
757
- });
758
- const result = classifyEndpointsByChangedFiles(diff, scannedEndpoints);
759
- expect(result.removedEndpoints).toEqual([]);
760
- });
761
- });
762
- describe("selectRemovalCandidateFiles", () => {
763
- function makeDiff(opts) {
764
- return {
765
- currentBranch: "feature/test",
766
- baseBranch: "origin/main",
767
- changedFiles: opts.changedFiles,
768
- diffContent: "",
769
- diffStat: "",
770
- newFiles: opts.newFiles ?? [],
771
- deletedFiles: opts.deletedFiles ?? [],
772
- };
773
- }
774
- it("includes modified and deleted files but excludes newly-created files", () => {
775
- // A route removed by editing a surviving file lives in a *modified* file, not a
776
- // deleted one — so modified files must be candidates for removed-endpoint recovery.
777
- const diff = makeDiff({
778
- changedFiles: ["src/routes/recipe.py", "src/routes/brand_new.py", "src/routes/legacy.py"],
779
- newFiles: ["src/routes/brand_new.py"],
780
- deletedFiles: ["src/routes/legacy.py"],
781
- });
782
- expect(selectRemovalCandidateFiles(diff).sort()).toEqual(["src/routes/legacy.py", "src/routes/recipe.py"].sort());
783
- });
784
- it("returns empty when the only change is a newly-created file", () => {
785
- const diff = makeDiff({
786
- changedFiles: ["src/routes/brand_new.py"],
787
- newFiles: ["src/routes/brand_new.py"],
788
- });
789
- expect(selectRemovalCandidateFiles(diff)).toEqual([]);
790
- });
791
- });
792
- describe("recoverRemovedEndpointsFromBase", () => {
793
- it("recovers a route removed by editing a surviving file (line-edit removal)", async () => {
794
- // Base version of a MODIFIED router file had two routes; the current branch
795
- // deleted only /suggestions (the /bulk-actions route survives).
796
- const baseContent = `
797
- router = APIRouter(prefix="/api/recipes", tags=["recipes"])
798
-
799
- @router.get("/suggestions")
800
- async def suggest_recipes(): pass
801
-
802
- @router.post("/bulk-actions/categorize")
803
- async def categorize(): pass
804
- `;
805
- const fetchBaseContent = async (file) => file === "mealie/routes/recipe/recipe_crud_routes.py" ? baseContent : "";
806
- const recovered = await recoverRemovedEndpointsFromBase(["mealie/routes/recipe/recipe_crud_routes.py"], fetchBaseContent);
807
- // Both base-branch endpoints are parsed from the modified file.
808
- expect(recovered.map((e) => e.path).sort()).toEqual(["/api/recipes/bulk-actions/categorize", "/api/recipes/suggestions"].sort());
809
- // Feeding these to the classifier with the *current* catalog (which still has the
810
- // surviving route) leaves only the genuinely-removed route in removedEndpoints.
811
- const scanned = [
812
- { path: "/api/recipes/bulk-actions/categorize", methods: ["POST"], sourceFile: "mealie/routes/recipe/recipe_crud_routes.py" },
813
- ];
814
- const diff = {
815
- currentBranch: "feature/test",
816
- baseBranch: "origin/main",
817
- changedFiles: ["mealie/routes/recipe/recipe_crud_routes.py"],
818
- diffContent: "",
819
- diffStat: "",
820
- newFiles: [],
821
- deletedFiles: [],
822
- };
823
- const result = classifyEndpointsByChangedFiles(diff, scanned, recovered);
824
- expect(result.removedEndpoints.map((e) => e.path)).toEqual(["/api/recipes/suggestions"]);
825
- });
826
- it("skips non-route files and tolerates fetch failures", async () => {
827
- const fetchBaseContent = async (file) => {
828
- if (file === "README.md")
829
- throw new Error("should not be fetched");
830
- throw new Error("git show failed");
831
- };
832
- const recovered = await recoverRemovedEndpointsFromBase(["README.md", "src/routes/orders.py"], fetchBaseContent);
833
- expect(recovered).toEqual([]);
834
- });
835
- });
836
- describe("parseFileEndpoints — declaration line numbers", () => {
837
- it("records the 1-based declaration line of each single-line route", () => {
838
- const content = [
839
- "package main", // 1
840
- "", // 2
841
- "func Routes() {", // 3
842
- ' r.GET("/users", h)', // 4
843
- ' r.POST("/items", h)', // 5
844
- "}", // 6
845
- ].join("\n");
846
- const eps = parseFileEndpoints(content, "main.go");
847
- expect(eps.find((e) => e.path === "/users")?.line).toBe(4);
848
- expect(eps.find((e) => e.path === "/items")?.line).toBe(5);
849
- });
850
- it("records the decorator start line for a multi-line FastAPI route", () => {
851
- const content = [
852
- "router = APIRouter()", // 1
853
- "", // 2
854
- "@router.get(", // 3 <- decorator start
855
- ' "/flows",', // 4 <- path resolved here
856
- ")", // 5
857
- "def list_flows():", // 6
858
- " return []", // 7
859
- ].join("\n");
860
- const eps = parseFileEndpoints(content, "api/flows.py");
861
- expect(eps[0].path).toBe("/flows");
862
- expect(eps[0].line).toBe(3);
863
- });
864
- });
865
- describe("parseChangedLinesByFile", () => {
866
- it("collects new-side line numbers of added lines per file", () => {
867
- const diffContent = [
868
- "diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go",
869
- "--- a/routers/api/v1/api.go",
870
- "+++ b/routers/api/v1/api.go",
871
- "@@ -147,5 +147,6 @@ func Routes() {",
872
- " m.Group(\"/misc\", func() {",
873
- " existingA",
874
- " existingB",
875
- "+ m.Post(\"/markdown\", Markdown)",
876
- " existingC",
877
- " })",
878
- ].join("\n");
879
- const map = parseChangedLinesByFile(diffContent);
880
- // new-side numbering from 147: 147 context, 148, 149, 150(+), 151, 152
881
- expect([...(map.get("routers/api/v1/api.go") ?? [])]).toEqual([150]);
882
- });
883
- it("does not advance the new-side cursor on removed lines", () => {
884
- const diffContent = [
885
- "--- a/svc/orders.py",
886
- "+++ b/svc/orders.py",
887
- "@@ -10,4 +10,3 @@",
888
- " keep",
889
- "-gone",
890
- "+added",
891
- " keep2",
892
- ].join("\n");
893
- const map = parseChangedLinesByFile(diffContent);
894
- // 10 context(keep), '-gone' no advance, '+added' at new line 11
895
- expect([...(map.get("svc/orders.py") ?? [])]).toEqual([11]);
896
- });
897
- it("records the new-side position of a deletion-only hunk", () => {
898
- const diffContent = [
899
- "--- a/svc/orders.py",
900
- "+++ b/svc/orders.py",
901
- "@@ -20,3 +20,2 @@",
902
- " keep",
903
- "-gone",
904
- " keep2",
905
- ].join("\n");
906
- const map = parseChangedLinesByFile(diffContent);
907
- // 20 context(keep) -> cursor 21, '-gone' records 21 (no advance), keep2 -> 21
908
- expect([...(map.get("svc/orders.py") ?? [])]).toEqual([21]);
909
- });
910
- });
911
- describe("classifyEndpointsByChangedFiles — hunk-scoped (centralized routing)", () => {
912
- // Gitea-shaped hub: every route registered in one api.go file, each on its own line.
913
- const HUB = "routers/api/v1/api.go";
914
- const hubEndpoints = [
915
- { path: "/repos/{owner}/{repo}/issues", methods: ["GET", "POST"], sourceFile: HUB, line: 100 },
916
- { path: "/markdown", methods: ["POST"], sourceFile: HUB, line: 150 },
917
- { path: "/markup", methods: ["POST"], sourceFile: HUB, line: 151 },
918
- { path: "/user/repos", methods: ["GET"], sourceFile: HUB, line: 200 },
919
- ];
920
- function diffAddingMarkdownRoute() {
921
- // Adds one route line whose new-side line number is 150 (the /markdown decl).
922
- return {
923
- currentBranch: "feature/add-markdown",
924
- baseBranch: "origin/main",
925
- changedFiles: [HUB],
926
- diffContent: [
927
- `diff --git a/${HUB} b/${HUB}`,
928
- `--- a/${HUB}`,
929
- `+++ b/${HUB}`,
930
- "@@ -147,5 +147,6 @@ func Routes() {",
931
- " existingA",
932
- " existingB",
933
- " existingC",
934
- "+ m.Post(\"/markdown\", Markdown)",
935
- " existingD",
936
- ].join("\n"),
937
- diffStat: "",
938
- newFiles: [],
939
- deletedFiles: [],
940
- };
941
- }
942
- it("scopes changed endpoints to the route line actually edited, not the whole hub file", () => {
943
- const result = classifyEndpointsByChangedFiles(diffAddingMarkdownRoute(), hubEndpoints);
944
- expect(result.changedEndpoints.map((e) => e.path)).toEqual(["/markdown"]);
945
- });
946
- it("attributes a handler-body edit to its enclosing route (decentralized, no over-scoping)", () => {
947
- const recipe = [
948
- { path: "/api/recipes", methods: ["GET", "POST"], sourceFile: "mealie/routes/recipe.py", line: 10 },
949
- ];
950
- const diff = {
951
- currentBranch: "f", baseBranch: "origin/main",
952
- changedFiles: ["mealie/routes/recipe.py"],
953
- diffContent: [
954
- "--- a/mealie/routes/recipe.py",
955
- "+++ b/mealie/routes/recipe.py",
956
- "@@ -24,2 +24,3 @@ def get_recipe():",
957
- " x = 1",
958
- "+ y = 2",
959
- " return x",
960
- ].join("\n"),
961
- diffStat: "", newFiles: [], deletedFiles: [],
962
- };
963
- const result = classifyEndpointsByChangedFiles(diff, recipe);
964
- expect(result.changedEndpoints.map((e) => e.path)).toEqual(["/api/recipes"]);
965
- });
966
- it("falls back to file-level scoping when endpoints carry no line info", () => {
967
- const noLines = hubEndpoints.map(({ line: _line, ...rest }) => rest);
968
- const result = classifyEndpointsByChangedFiles(diffAddingMarkdownRoute(), noLines);
969
- expect(result.changedEndpoints).toHaveLength(4);
970
- });
971
- it("falls back to file-level scoping when diffContent is empty", () => {
972
- const diff = { ...diffAddingMarkdownRoute(), diffContent: "" };
973
- const result = classifyEndpointsByChangedFiles(diff, hubEndpoints);
974
- expect(result.changedEndpoints).toHaveLength(4);
975
- });
976
- it("scopes a deletion-only edit to the route that owns the deleted position", () => {
977
- // A hunk that only removes lines (no additions) must still scope — otherwise
978
- // a deletion in a hub file falls back to flagging the whole route table.
979
- const diff = {
980
- currentBranch: "f", baseBranch: "origin/main",
981
- changedFiles: [HUB],
982
- diffContent: [
983
- `+++ b/${HUB}`,
984
- "@@ -158,3 +158,2 @@",
985
- " ctxA",
986
- "- m.Get(\"/markup-legacy\", MarkupLegacy)",
987
- " ctxB",
988
- ].join("\n"),
989
- diffStat: "", newFiles: [], deletedFiles: [],
990
- };
991
- // Deleted position is new-side 159, inside /markup's ownership range [151, 200).
992
- const result = classifyEndpointsByChangedFiles(diff, hubEndpoints);
993
- expect(result.changedEndpoints.map((e) => e.path)).toEqual(["/markup"]);
994
- });
995
- it("falls back to file-level when the change is above the first route (e.g. imports)", () => {
996
- const diff = {
997
- currentBranch: "f", baseBranch: "origin/main",
998
- changedFiles: [HUB],
999
- diffContent: [
1000
- `+++ b/${HUB}`,
1001
- "@@ -1,2 +1,3 @@",
1002
- " package v1",
1003
- "+ import \"fmt\"",
1004
- " import \"net/http\"",
1005
- ].join("\n"),
1006
- diffStat: "", newFiles: [], deletedFiles: [],
1007
- };
1008
- const result = classifyEndpointsByChangedFiles(diff, hubEndpoints);
1009
- expect(result.changedEndpoints).toHaveLength(4);
1010
- });
1011
- });