@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,824 +0,0 @@
1
- // @ts-nocheck - Jest ESM type inference issues
2
- import { jest } from "@jest/globals";
3
- import * as fs from "fs";
4
- import * as os from "os";
5
- import * as path from "path";
6
- // Under native ESM, imported module namespaces are read-only, so jest.spyOn on
7
- // workspaceAuth fails. Partial-mock the module: keep every real export the
8
- // subject's import graph needs and override only readWorkspaceConfigRaw.
9
- // jest.requireActual can't load an ESM module synchronously, so import the real
10
- // module first (unstable_mockModule is not hoisted) and spread its namespace.
11
- const realWorkspaceAuth = await import("./workspaceAuth.js");
12
- const readWorkspaceConfigRaw = jest.fn();
13
- jest.unstable_mockModule("./workspaceAuth.js", () => ({
14
- ...realWorkspaceAuth,
15
- readWorkspaceConfigRaw,
16
- }));
17
- const { frameworkFileToUrlPath, findCandidatePagesByFrameworkRoute, findCandidatePagesBySourceRoute, findRootFallbackPage, enumerateCandidateUiPages, pickFrontendBaseUrl, detectsFilesystemRouting, } = await import("./uiPageEnumerator.js");
18
- // Helper to create a tmp repo dir with optional config files; returns the path.
19
- function makeTmpRepo(configFiles = []) {
20
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-test-repo-"));
21
- for (const cfg of configFiles) {
22
- const fullPath = path.join(dir, cfg);
23
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
24
- fs.writeFileSync(fullPath, "");
25
- }
26
- return dir;
27
- }
28
- // ---------------------------------------------------------------------------
29
- // frameworkFileToUrlPath
30
- // ---------------------------------------------------------------------------
31
- describe("frameworkFileToUrlPath", () => {
32
- it("maps Next.js App Router page.tsx to /path", () => {
33
- expect(frameworkFileToUrlPath("app/orders/page.tsx")).toBe("/orders");
34
- });
35
- it("maps Next.js App Router root page.tsx to /", () => {
36
- expect(frameworkFileToUrlPath("app/page.tsx")).toBe("/");
37
- });
38
- it("maps Next.js App Router dynamic [id] segment to :id", () => {
39
- expect(frameworkFileToUrlPath("app/orders/[id]/page.tsx")).toBe("/orders/:id");
40
- });
41
- it("strips Next.js App Router route groups (parentheses)", () => {
42
- expect(frameworkFileToUrlPath("app/(marketing)/about/page.tsx")).toBe("/about");
43
- });
44
- it("maps Next.js Pages Router /pages/foo.tsx to /foo", () => {
45
- expect(frameworkFileToUrlPath("pages/orders.tsx")).toBe("/orders");
46
- });
47
- it("maps Next.js Pages Router /pages/index.tsx to /", () => {
48
- expect(frameworkFileToUrlPath("pages/index.tsx")).toBe("/");
49
- });
50
- it("maps Next.js Pages Router /pages/foo/index.tsx to /foo", () => {
51
- expect(frameworkFileToUrlPath("pages/orders/index.tsx")).toBe("/orders");
52
- });
53
- it("excludes Next.js Pages Router API handlers (/pages/api/*)", () => {
54
- expect(frameworkFileToUrlPath("pages/api/orders.ts")).toBeNull();
55
- });
56
- it("maps Vue / Nuxt /pages/foo.vue to /foo", () => {
57
- expect(frameworkFileToUrlPath("pages/orders.vue")).toBe("/orders");
58
- });
59
- it("maps Vue / Nuxt 2 /pages/_id.vue to :id", () => {
60
- expect(frameworkFileToUrlPath("pages/_id.vue")).toBe("/:id");
61
- });
62
- it("maps SvelteKit routes/+page.svelte to /", () => {
63
- expect(frameworkFileToUrlPath("src/routes/+page.svelte")).toBe("/");
64
- });
65
- it("maps SvelteKit routes/foo/+page.svelte to /foo", () => {
66
- expect(frameworkFileToUrlPath("src/routes/orders/+page.svelte")).toBe("/orders");
67
- });
68
- it("returns null for a generic component file (not a route)", () => {
69
- expect(frameworkFileToUrlPath("src/components/Button.tsx")).toBeNull();
70
- });
71
- it("returns null for a layout file in app/", () => {
72
- expect(frameworkFileToUrlPath("app/orders/layout.tsx")).toBeNull();
73
- });
74
- it("returns null for a styles file under frontend dir", () => {
75
- expect(frameworkFileToUrlPath("src/styles/global.css")).toBeNull();
76
- });
77
- });
78
- // ---------------------------------------------------------------------------
79
- // findCandidatePagesByFrameworkRoute
80
- // ---------------------------------------------------------------------------
81
- describe("findCandidatePagesByFrameworkRoute", () => {
82
- it("returns empty for empty input", () => {
83
- expect(findCandidatePagesByFrameworkRoute([], "http://localhost:3000")).toEqual([]);
84
- });
85
- it("returns empty when no files map to routes", () => {
86
- expect(findCandidatePagesByFrameworkRoute(["src/components/Button.tsx", "src/utils/format.ts"], "http://localhost:3000")).toEqual([]);
87
- });
88
- it("emits a single page with full URL for a Next.js App Router route", () => {
89
- const pages = findCandidatePagesByFrameworkRoute(["app/orders/page.tsx"], "http://localhost:3000");
90
- expect(pages).toEqual([
91
- {
92
- url: "http://localhost:3000/orders",
93
- sourcedFrom: ["app/orders/page.tsx"],
94
- strategy: "framework-route-grep",
95
- },
96
- ]);
97
- });
98
- it("collapses two files emitting the same URL into one entry with both sources", () => {
99
- const pages = findCandidatePagesByFrameworkRoute(["app/orders/page.tsx", "app/orders/page.tsx"], "http://localhost:3000");
100
- expect(pages).toHaveLength(1);
101
- expect(pages[0].sourcedFrom).toEqual(["app/orders/page.tsx"]);
102
- });
103
- it("strips trailing slash from baseUrl before joining", () => {
104
- const pages = findCandidatePagesByFrameworkRoute(["app/orders/page.tsx"], "http://localhost:3000/");
105
- expect(pages[0].url).toBe("http://localhost:3000/orders");
106
- });
107
- it("ignores files that don't map to routes (mixed input)", () => {
108
- const pages = findCandidatePagesByFrameworkRoute([
109
- "src/components/Button.tsx", // not a route
110
- "app/orders/page.tsx", // route
111
- "src/styles/main.css", // not a route
112
- ], "http://localhost:3000");
113
- expect(pages).toHaveLength(1);
114
- expect(pages[0].url).toBe("http://localhost:3000/orders");
115
- });
116
- });
117
- // ---------------------------------------------------------------------------
118
- // findCandidatePagesBySourceRoute (strategy 2)
119
- // ---------------------------------------------------------------------------
120
- describe("findCandidatePagesBySourceRoute", () => {
121
- function makeRouterRepo() {
122
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-strat2-"));
123
- fs.mkdirSync(path.join(dir, "src", "pages"), { recursive: true });
124
- fs.writeFileSync(path.join(dir, "src", "App.tsx"), `
125
- import { Routes, Route } from 'react-router-dom';
126
- import Cart from './pages/Cart';
127
- import Orders from './pages/Orders';
128
-
129
- export default function App() {
130
- return (
131
- <Routes>
132
- <Route path="/cart" element={<Cart />} />
133
- <Route path="/orders" element={<Orders />} />
134
- </Routes>
135
- );
136
- }
137
- `);
138
- fs.writeFileSync(path.join(dir, "src", "pages", "Cart.tsx"), "export default function Cart() { return <div/>; }");
139
- fs.writeFileSync(path.join(dir, "src", "pages", "Orders.tsx"), "export default function Orders() { return <div/>; }");
140
- return dir;
141
- }
142
- it("emits candidate pages for changed files that match a route's component", () => {
143
- const repo = makeRouterRepo();
144
- const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx"], "http://localhost:3000", repo);
145
- expect(pages).toHaveLength(1);
146
- expect(pages[0]).toMatchObject({
147
- url: "http://localhost:3000/cart",
148
- strategy: "source-grounded-routes",
149
- });
150
- expect(pages[0].sourcedFrom).toEqual(["src/pages/Cart.tsx"]);
151
- fs.rmSync(repo, { recursive: true, force: true });
152
- });
153
- it("emits multiple pages when multiple changed files match routes", () => {
154
- const repo = makeRouterRepo();
155
- const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx", "src/pages/Orders.tsx"], "http://localhost:3000", repo);
156
- expect(pages.map((p) => p.url).sort()).toEqual([
157
- "http://localhost:3000/cart",
158
- "http://localhost:3000/orders",
159
- ]);
160
- fs.rmSync(repo, { recursive: true, force: true });
161
- });
162
- it("returns empty when no changed files match a route's component", () => {
163
- const repo = makeRouterRepo();
164
- const pages = findCandidatePagesBySourceRoute(["src/components/Button.tsx"], "http://localhost:3000", repo);
165
- expect(pages).toEqual([]);
166
- fs.rmSync(repo, { recursive: true, force: true });
167
- });
168
- it("returns empty when the repo has no router files", () => {
169
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-empty-"));
170
- const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx"], "http://localhost:3000", repo);
171
- expect(pages).toEqual([]);
172
- fs.rmSync(repo, { recursive: true, force: true });
173
- });
174
- it("strips trailing slash from baseUrl", () => {
175
- const repo = makeRouterRepo();
176
- const pages = findCandidatePagesBySourceRoute(["src/pages/Cart.tsx"], "http://localhost:3000/", repo);
177
- expect(pages[0].url).toBe("http://localhost:3000/cart");
178
- fs.rmSync(repo, { recursive: true, force: true });
179
- });
180
- it("emits candidate pages when the changed file is the route DECLARATION (router.tsx, not the component)", () => {
181
- // Regression: previously we only matched route.componentFile. A PR that
182
- // renames a route in App.tsx but doesn't touch the component itself would
183
- // surface zero source-grounded candidates. Real example: directus P06
184
- // renamed /settings/data-model in modules/settings/index.ts; the component
185
- // was unchanged, so the agent fell through to root-fallback and missed
186
- // the changed routes entirely.
187
- const repo = makeRouterRepo();
188
- const pages = findCandidatePagesBySourceRoute(["src/App.tsx"], "http://localhost:3000", repo);
189
- expect(pages.map((p) => p.url).sort()).toEqual([
190
- "http://localhost:3000/cart",
191
- "http://localhost:3000/orders",
192
- ]);
193
- expect(pages[0].sourcedFrom).toEqual(["src/App.tsx"]);
194
- fs.rmSync(repo, { recursive: true, force: true });
195
- });
196
- });
197
- // ---------------------------------------------------------------------------
198
- // pickFrontendBaseUrl
199
- // ---------------------------------------------------------------------------
200
- describe("pickFrontendBaseUrl", () => {
201
- afterEach(() => {
202
- readWorkspaceConfigRaw.mockReset();
203
- });
204
- it("returns null when workspace config is missing", async () => {
205
- readWorkspaceConfigRaw.mockResolvedValue(null);
206
- expect(await pickFrontendBaseUrl("/repo")).toBeNull();
207
- });
208
- it("returns null when no services have baseUrl", async () => {
209
- readWorkspaceConfigRaw.mockResolvedValue({
210
- services: [{ serviceName: "backend", language: "python" }],
211
- });
212
- expect(await pickFrontendBaseUrl("/repo")).toBeNull();
213
- });
214
- it("picks the frontend service by name suffix", async () => {
215
- readWorkspaceConfigRaw.mockResolvedValue({
216
- services: [
217
- { serviceName: "demoshop-backend", api: { baseUrl: "http://localhost:8000" } },
218
- { serviceName: "demoshop-frontend", api: { baseUrl: "http://localhost:5173" } },
219
- ],
220
- });
221
- expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:5173");
222
- });
223
- it("picks the frontend service by framework when name doesn't signal it", async () => {
224
- readWorkspaceConfigRaw.mockResolvedValue({
225
- services: [
226
- { serviceName: "api", language: "python", api: { baseUrl: "http://localhost:8000" } },
227
- {
228
- serviceName: "client",
229
- language: "typescript",
230
- framework: "next",
231
- api: { baseUrl: "http://localhost:3000" },
232
- },
233
- ],
234
- });
235
- expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:3000");
236
- });
237
- it("falls back to first service with baseUrl when no frontend signal", async () => {
238
- readWorkspaceConfigRaw.mockResolvedValue({
239
- services: [
240
- { serviceName: "svc-a", language: "go", api: { baseUrl: "http://localhost:8000" } },
241
- { serviceName: "svc-b", language: "go", api: { baseUrl: "http://localhost:8001" } },
242
- ],
243
- });
244
- expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:8000");
245
- });
246
- // Flutter support — the load-bearing assumption from the Confluence plan.
247
- // init_workspace writes a single service with framework: playwright for a
248
- // Flutter web app. Heuristic 1 must match by serviceName suffix
249
- // (e.g. "birdle-frontend") because Heuristic 2 only knows JS/TS frontend
250
- // frameworks. Without this, the agent never reaches the browser regardless
251
- // of the .dart classifier fix.
252
- it("picks the Flutter playwright-framework service by name suffix", async () => {
253
- readWorkspaceConfigRaw.mockResolvedValue({
254
- services: [
255
- {
256
- serviceName: "birdle-frontend",
257
- language: "typescript",
258
- framework: "playwright",
259
- api: { baseUrl: "http://localhost:8080" },
260
- },
261
- ],
262
- });
263
- expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:8080");
264
- });
265
- // Edge case: Flutter service has no "frontend"/"ui"/"app"/"web" suffix
266
- // (so H1 misses) and `framework: playwright` isn't in H2's TS/JS framework
267
- // allow-list. The chosen URL must come from H3 (first service with baseUrl).
268
- // We add a second service to prove H3 picked the first, not H1 by accident.
269
- it("falls back to first service when name+framework don't match H1 or H2", async () => {
270
- readWorkspaceConfigRaw.mockResolvedValue({
271
- services: [
272
- {
273
- serviceName: "birdle",
274
- language: "typescript",
275
- framework: "playwright",
276
- api: { baseUrl: "http://localhost:8080" },
277
- },
278
- {
279
- serviceName: "other-service",
280
- language: "go",
281
- framework: "gin",
282
- api: { baseUrl: "http://localhost:9000" },
283
- },
284
- ],
285
- });
286
- expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:8080");
287
- });
288
- // SKYR-3812: Environment variable override tests
289
- describe("env var overrides (SKYR-3812)", () => {
290
- let originalGlobal;
291
- let originalPerSvc = {};
292
- beforeEach(() => {
293
- // Snapshot env vars this suite may touch so we can restore them exactly.
294
- originalGlobal = process.env.SKYRAMP_TEST_BASE_URL;
295
- originalPerSvc = {};
296
- for (const key of Object.keys(process.env)) {
297
- if (key.startsWith("SKYRAMP_TEST_SERVICE_URL_")) {
298
- originalPerSvc[key] = process.env[key];
299
- }
300
- }
301
- });
302
- afterEach(() => {
303
- // Restore the global override.
304
- if (originalGlobal === undefined) {
305
- delete process.env.SKYRAMP_TEST_BASE_URL;
306
- }
307
- else {
308
- process.env.SKYRAMP_TEST_BASE_URL = originalGlobal;
309
- }
310
- // Restore per-service vars: reset the union of keys that existed before
311
- // the test and keys the test set, so neither pre-existing values nor
312
- // test-set values leak across tests or out of the suite.
313
- const keys = new Set([
314
- ...Object.keys(originalPerSvc),
315
- ...Object.keys(process.env).filter((k) => k.startsWith("SKYRAMP_TEST_SERVICE_URL_")),
316
- ]);
317
- for (const key of keys) {
318
- if (originalPerSvc[key] === undefined) {
319
- delete process.env[key];
320
- }
321
- else {
322
- process.env[key] = originalPerSvc[key];
323
- }
324
- }
325
- readWorkspaceConfigRaw.mockReset();
326
- });
327
- it("SKYRAMP_TEST_BASE_URL (global) overrides workspace.yml baseUrl", async () => {
328
- readWorkspaceConfigRaw.mockResolvedValue({
329
- services: [
330
- {
331
- serviceName: "frontend",
332
- api: { baseUrl: "http://localhost:3000" },
333
- },
334
- ],
335
- });
336
- process.env.SKYRAMP_TEST_BASE_URL = "https://preview.example.com";
337
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://preview.example.com");
338
- });
339
- it("per-service SKYRAMP_TEST_SERVICE_URL_<NAME> overrides for the picked frontend service", async () => {
340
- readWorkspaceConfigRaw.mockResolvedValue({
341
- services: [
342
- { serviceName: "backend", api: { baseUrl: "http://localhost:8000" } },
343
- {
344
- serviceName: "demoshop-frontend",
345
- api: { baseUrl: "http://localhost:5173" },
346
- },
347
- ],
348
- });
349
- process.env.SKYRAMP_TEST_SERVICE_URL_DEMOSHOP_FRONTEND =
350
- "https://frontend-preview.example.com";
351
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://frontend-preview.example.com");
352
- });
353
- it("sanitizes service name: hyphens, dots, colons, slashes to underscores", async () => {
354
- readWorkspaceConfigRaw.mockResolvedValue({
355
- services: [
356
- {
357
- serviceName: "supabase-nextjs",
358
- api: { baseUrl: "http://localhost:3000" },
359
- },
360
- ],
361
- });
362
- process.env.SKYRAMP_TEST_SERVICE_URL_SUPABASE_NEXTJS =
363
- "https://supabase-preview.example.com";
364
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://supabase-preview.example.com");
365
- });
366
- it("sanitizes service name with dot: my.app -> MY_APP", async () => {
367
- readWorkspaceConfigRaw.mockResolvedValue({
368
- services: [
369
- { serviceName: "my.app", api: { baseUrl: "http://localhost:3000" } },
370
- ],
371
- });
372
- process.env.SKYRAMP_TEST_SERVICE_URL_MY_APP =
373
- "https://myapp-preview.example.com";
374
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://myapp-preview.example.com");
375
- });
376
- it("sanitizes service name with colon: proto://app -> PROTO___APP", async () => {
377
- readWorkspaceConfigRaw.mockResolvedValue({
378
- services: [
379
- {
380
- serviceName: "proto://app",
381
- api: { baseUrl: "http://localhost:3000" },
382
- },
383
- ],
384
- });
385
- process.env.SKYRAMP_TEST_SERVICE_URL_PROTO___APP =
386
- "https://proto-preview.example.com";
387
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://proto-preview.example.com");
388
- });
389
- it("per-service takes precedence over global when both are set", async () => {
390
- readWorkspaceConfigRaw.mockResolvedValue({
391
- services: [
392
- {
393
- serviceName: "frontend",
394
- api: { baseUrl: "http://localhost:3000" },
395
- },
396
- ],
397
- });
398
- process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
399
- process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND =
400
- "https://per-service-preview.example.com";
401
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://per-service-preview.example.com");
402
- });
403
- it("empty/whitespace global env var is ignored, falls back to workspace.yml", async () => {
404
- readWorkspaceConfigRaw.mockResolvedValue({
405
- services: [
406
- {
407
- serviceName: "frontend",
408
- api: { baseUrl: "http://localhost:3000" },
409
- },
410
- ],
411
- });
412
- process.env.SKYRAMP_TEST_BASE_URL = " ";
413
- expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:3000");
414
- });
415
- it("empty/whitespace per-service env var is ignored, falls back to global", async () => {
416
- readWorkspaceConfigRaw.mockResolvedValue({
417
- services: [
418
- {
419
- serviceName: "frontend",
420
- api: { baseUrl: "http://localhost:3000" },
421
- },
422
- ],
423
- });
424
- process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
425
- process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND = " ";
426
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://global-preview.example.com");
427
- });
428
- it("global override returned even with no matching service", async () => {
429
- readWorkspaceConfigRaw.mockResolvedValue({
430
- services: [
431
- {
432
- serviceName: "backend",
433
- language: "python",
434
- api: { baseUrl: "http://localhost:8000" },
435
- },
436
- ],
437
- });
438
- process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
439
- // pickFrontendBaseUrl will select the first service with baseUrl (H3), but the override takes precedence
440
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://global-preview.example.com");
441
- });
442
- it("existing no-override behavior still returns workspace.yml value", async () => {
443
- readWorkspaceConfigRaw.mockResolvedValue({
444
- services: [
445
- {
446
- serviceName: "frontend",
447
- api: { baseUrl: "http://localhost:3000" },
448
- },
449
- ],
450
- });
451
- // No env vars set
452
- expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:3000");
453
- });
454
- it("does not throw when picked service has a non-string serviceName (unvalidated YAML)", async () => {
455
- // readWorkspaceConfigRaw can fall back to an unvalidated YAML parse where
456
- // serviceName is e.g. a number. The per-service guard must skip it rather
457
- // than calling sanitizeServiceName on a non-string and throwing.
458
- readWorkspaceConfigRaw.mockResolvedValue({
459
- services: [
460
- {
461
- serviceName: 123,
462
- language: "typescript",
463
- framework: "next",
464
- api: { baseUrl: "http://localhost:3000" },
465
- },
466
- ],
467
- });
468
- process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
469
- // Falls through the per-service branch to the global override.
470
- expect(await pickFrontendBaseUrl("/repo")).toBe("https://global-preview.example.com");
471
- });
472
- });
473
- });
474
- // ---------------------------------------------------------------------------
475
- // findRootFallbackPage
476
- // ---------------------------------------------------------------------------
477
- describe("findRootFallbackPage", () => {
478
- afterEach(() => {
479
- readWorkspaceConfigRaw.mockReset();
480
- });
481
- it("returns null when no frontend baseUrl resolvable", async () => {
482
- readWorkspaceConfigRaw.mockResolvedValue(null);
483
- expect(await findRootFallbackPage("/repo")).toBeNull();
484
- });
485
- it("returns root-fallback page when frontend baseUrl resolves", async () => {
486
- readWorkspaceConfigRaw.mockResolvedValue({
487
- services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
488
- });
489
- const page = await findRootFallbackPage("/repo");
490
- expect(page).toEqual({
491
- url: "http://localhost:3000",
492
- sourcedFrom: [],
493
- strategy: "root-fallback",
494
- });
495
- });
496
- });
497
- // ---------------------------------------------------------------------------
498
- // enumerateCandidateUiPages
499
- // ---------------------------------------------------------------------------
500
- describe("enumerateCandidateUiPages", () => {
501
- afterEach(() => {
502
- readWorkspaceConfigRaw.mockReset();
503
- });
504
- it("returns empty for empty file list", async () => {
505
- expect(await enumerateCandidateUiPages("/repo", [])).toEqual([]);
506
- });
507
- it("returns empty when workspace has no resolvable baseUrl", async () => {
508
- readWorkspaceConfigRaw.mockResolvedValue(null);
509
- expect(await enumerateCandidateUiPages("/repo", ["app/orders/page.tsx"])).toEqual([]);
510
- });
511
- it("uses strategy 1 (framework-route-grep) when route files match", async () => {
512
- readWorkspaceConfigRaw.mockResolvedValue({
513
- services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
514
- });
515
- const repo = makeTmpRepo(["next.config.js"]);
516
- const pages = await enumerateCandidateUiPages(repo, [
517
- "app/orders/page.tsx",
518
- "src/components/Button.tsx", // ignored — not a route
519
- ]);
520
- expect(pages).toHaveLength(1);
521
- expect(pages[0]).toMatchObject({
522
- url: "http://localhost:3000/orders",
523
- strategy: "framework-route-grep",
524
- });
525
- fs.rmSync(repo, { recursive: true, force: true });
526
- });
527
- it("falls back to strategy 3 (root-fallback) when no route files match", async () => {
528
- readWorkspaceConfigRaw.mockResolvedValue({
529
- services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
530
- });
531
- const pages = await enumerateCandidateUiPages("/repo", [
532
- "src/components/Button.tsx", // not a route
533
- "src/state/store.ts", // not a route
534
- ]);
535
- expect(pages).toHaveLength(1);
536
- expect(pages[0]).toMatchObject({
537
- url: "http://localhost:3000",
538
- strategy: "root-fallback",
539
- });
540
- });
541
- it("emits multiple route pages when several match strategy 1", async () => {
542
- readWorkspaceConfigRaw.mockResolvedValue({
543
- services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
544
- });
545
- // Need a real repoPath with framework config so detectsFilesystemRouting passes.
546
- const repo = makeTmpRepo(["next.config.js"]);
547
- const pages = await enumerateCandidateUiPages(repo, [
548
- "app/orders/page.tsx",
549
- "app/products/[id]/page.tsx",
550
- ]);
551
- expect(pages).toHaveLength(2);
552
- const urls = pages.map((p) => p.url).sort();
553
- expect(urls).toEqual([
554
- "http://localhost:3000/orders",
555
- "http://localhost:3000/products/:id",
556
- ]);
557
- fs.rmSync(repo, { recursive: true, force: true });
558
- });
559
- it("uses strategy 2 (source-grounded-routes) when strategy 1 doesn't match but routes are declared in source", async () => {
560
- readWorkspaceConfigRaw.mockResolvedValue({
561
- services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:5173" } }],
562
- });
563
- // Repo with no framework config (so strategy 1 won't fire) but with a
564
- // React Router App.tsx declaring routes.
565
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-ladder-"));
566
- fs.mkdirSync(path.join(repo, "src", "pages"), { recursive: true });
567
- fs.writeFileSync(path.join(repo, "src", "App.tsx"), `
568
- import { Routes, Route } from 'react-router-dom';
569
- import Cart from './pages/Cart';
570
- export default function App() { return <Routes><Route path="/cart" element={<Cart />} /></Routes>; }
571
- `);
572
- fs.writeFileSync(path.join(repo, "src", "pages", "Cart.tsx"), "export default function Cart() { return <div/>; }");
573
- const pages = await enumerateCandidateUiPages(repo, [
574
- "src/pages/Cart.tsx",
575
- ]);
576
- expect(pages).toHaveLength(1);
577
- expect(pages[0]).toMatchObject({
578
- url: "http://localhost:5173/cart",
579
- strategy: "source-grounded-routes",
580
- });
581
- fs.rmSync(repo, { recursive: true, force: true });
582
- });
583
- it("falls back to root when repo lacks a framework config (no false positives)", async () => {
584
- readWorkspaceConfigRaw.mockResolvedValue({
585
- services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
586
- });
587
- // Repo has files matching the Pages Router pattern but no next.config —
588
- // this is the demoshop case (React with conventional layout, no Next.js).
589
- const repo = makeTmpRepo([]);
590
- const pages = await enumerateCandidateUiPages(repo, [
591
- "frontend/src/pages/Cart.tsx",
592
- ]);
593
- expect(pages).toHaveLength(1);
594
- expect(pages[0]).toMatchObject({
595
- url: "http://localhost:3000",
596
- strategy: "root-fallback",
597
- });
598
- fs.rmSync(repo, { recursive: true, force: true });
599
- });
600
- // Flutter end-to-end: pubspec.yaml declares the Flutter SDK, .dart files
601
- // appear in the diff (passed in by callers after isFrontendFile filtering),
602
- // workspace.yml has a baseUrl. Strategies 1, 2, and 2.5 don't match
603
- // (no framework config; TS extractor doesn't see .dart; no GoRoute calls).
604
- // Result: root-fallback with the workspace baseUrl.
605
- it("returns root-fallback for a Flutter repo with no GoRoute declarations", async () => {
606
- readWorkspaceConfigRaw.mockResolvedValue({
607
- services: [
608
- {
609
- serviceName: "birdle-frontend",
610
- framework: "playwright",
611
- api: { baseUrl: "http://localhost:8080" },
612
- },
613
- ],
614
- });
615
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-flutter-enum-"));
616
- fs.writeFileSync(path.join(repo, "pubspec.yaml"), "name: birdle\ndependencies:\n flutter:\n sdk: flutter\n");
617
- fs.mkdirSync(path.join(repo, "lib"), { recursive: true });
618
- fs.writeFileSync(path.join(repo, "lib", "main.dart"), "void main() => runApp(MyApp());");
619
- const pages = await enumerateCandidateUiPages(repo, ["lib/main.dart"]);
620
- expect(pages).toHaveLength(1);
621
- expect(pages[0]).toMatchObject({
622
- url: "http://localhost:8080",
623
- strategy: "root-fallback",
624
- });
625
- fs.rmSync(repo, { recursive: true, force: true });
626
- });
627
- // Strategy 2.5: Flutter repo with GoRouter declarations and no diff-match
628
- // (frontendFiles names a screen file that's not imported by the router)
629
- // surfaces every absolute route as a candidate URL via the all-routes
630
- // fallback. Diff-match narrowing is exercised by the next test.
631
- it("returns dart-go-router URLs for a Flutter repo with GoRoute declarations", async () => {
632
- readWorkspaceConfigRaw.mockResolvedValue({
633
- services: [
634
- {
635
- serviceName: "books-frontend",
636
- framework: "playwright",
637
- api: { baseUrl: "http://localhost:8080" },
638
- },
639
- ],
640
- });
641
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-go-router-"));
642
- fs.writeFileSync(path.join(repo, "pubspec.yaml"), "name: books\ndependencies:\n flutter:\n sdk: flutter\n go_router: ^14.0.0\n");
643
- fs.mkdirSync(path.join(repo, "lib"), { recursive: true });
644
- fs.writeFileSync(path.join(repo, "lib", "main.dart"), `
645
- final router = GoRouter(routes: [
646
- GoRoute(path: '/signin', builder: (c, s) => SignIn()),
647
- GoRoute(path: '/authors', builder: (c, s) => Authors()),
648
- GoRoute(path: '/settings', builder: (c, s) => Settings()),
649
- ]);
650
- `);
651
- const pages = await enumerateCandidateUiPages(repo, ["lib/screens/authors.dart"]);
652
- const urls = pages.map((p) => p.url).sort();
653
- expect(urls).toEqual([
654
- "http://localhost:8080/authors",
655
- "http://localhost:8080/settings",
656
- "http://localhost:8080/signin",
657
- ]);
658
- expect(pages.every((p) => p.strategy === "dart-go-router")).toBe(true);
659
- fs.rmSync(repo, { recursive: true, force: true });
660
- });
661
- // Diff-matching: Strategy 2.5 should narrow candidate URLs to only those
662
- // whose screen file is in frontendFiles. The fixture has 3 routes; only
663
- // /authors's screen file (lib/src/screens/authors.dart) is in the diff.
664
- it("narrows GoRouter candidates to routes whose screen file is in the diff", async () => {
665
- readWorkspaceConfigRaw.mockResolvedValue({
666
- services: [
667
- {
668
- serviceName: "books-frontend",
669
- framework: "playwright",
670
- api: { baseUrl: "http://localhost:8080" },
671
- },
672
- ],
673
- });
674
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-go-router-diff-"));
675
- fs.writeFileSync(path.join(repo, "pubspec.yaml"), "name: books\ndependencies:\n flutter:\n sdk: flutter\n");
676
- fs.mkdirSync(path.join(repo, "lib", "src", "screens"), { recursive: true });
677
- fs.writeFileSync(path.join(repo, "lib", "main.dart"), `
678
- import 'src/screens/authors.dart';
679
- import 'src/screens/settings.dart';
680
- import 'src/screens/sign_in.dart';
681
-
682
- final router = GoRouter(routes: [
683
- GoRoute(path: '/sign_in', builder: (c, s) => SignInScreen()),
684
- GoRoute(path: '/authors', builder: (c, s) => AuthorsScreen()),
685
- GoRoute(path: '/settings', builder: (c, s) => SettingsScreen()),
686
- ]);
687
- `);
688
- fs.writeFileSync(path.join(repo, "lib", "src", "screens", "authors.dart"), "// authors");
689
- fs.writeFileSync(path.join(repo, "lib", "src", "screens", "settings.dart"), "// settings");
690
- fs.writeFileSync(path.join(repo, "lib", "src", "screens", "sign_in.dart"), "// signin");
691
- const pages = await enumerateCandidateUiPages(repo, ["lib/src/screens/authors.dart"]);
692
- const urls = pages.map((p) => p.url).sort();
693
- // Only /authors should surface — its screen file is the only one in the diff.
694
- expect(urls).toEqual(["http://localhost:8080/authors"]);
695
- fs.rmSync(repo, { recursive: true, force: true });
696
- });
697
- // Redirect-only routes (`redirect:` with no builder) must not surface — they
698
- // have no UI to navigate to. Same fixture shape but the only diff-matching
699
- // route is redirect-only, so we should fall back to the non-redirect-only
700
- // routes.
701
- it("filters out redirect-only GoRoutes", async () => {
702
- readWorkspaceConfigRaw.mockResolvedValue({
703
- services: [
704
- {
705
- serviceName: "frontend",
706
- framework: "playwright",
707
- api: { baseUrl: "http://localhost:8080" },
708
- },
709
- ],
710
- });
711
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-go-router-redirect-"));
712
- fs.writeFileSync(path.join(repo, "pubspec.yaml"), "name: app\ndependencies:\n flutter:\n sdk: flutter\n");
713
- fs.mkdirSync(path.join(repo, "lib"), { recursive: true });
714
- fs.writeFileSync(path.join(repo, "lib", "main.dart"), `
715
- final router = GoRouter(routes: [
716
- GoRoute(path: '/', redirect: (_, _) => '/home'),
717
- GoRoute(path: '/home', builder: (c, s) => Home()),
718
- ]);
719
- `);
720
- const pages = await enumerateCandidateUiPages(repo, ["lib/main.dart"]);
721
- const urls = pages.map((p) => p.url).sort();
722
- // / is redirect-only — must not appear. Only /home is renderable.
723
- expect(urls).toEqual(["http://localhost:8080/home"]);
724
- fs.rmSync(repo, { recursive: true, force: true });
725
- });
726
- // Fallback path: when no route's screen file matches the diff (e.g. router
727
- // file edited but our import-resolution heuristic missed the screen), surface
728
- // ALL renderable routes rather than collapsing to empty.
729
- it("falls back to all renderable routes when no screen file matches the diff", async () => {
730
- readWorkspaceConfigRaw.mockResolvedValue({
731
- services: [
732
- {
733
- serviceName: "frontend",
734
- framework: "playwright",
735
- api: { baseUrl: "http://localhost:8080" },
736
- },
737
- ],
738
- });
739
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-go-router-fallback-"));
740
- fs.writeFileSync(path.join(repo, "pubspec.yaml"), "name: app\ndependencies:\n flutter:\n sdk: flutter\n");
741
- fs.mkdirSync(path.join(repo, "lib"), { recursive: true });
742
- fs.writeFileSync(path.join(repo, "lib", "main.dart"), `
743
- final router = GoRouter(routes: [
744
- GoRoute(path: '/foo', builder: (c, s) => FooScreen()),
745
- GoRoute(path: '/bar', builder: (c, s) => BarScreen()),
746
- ]);
747
- `);
748
- // Diff touches only main.dart (the router itself, not any screen). The
749
- // import-resolution heuristic finds zero matches against frontendFiles, so
750
- // we surface all renderable routes.
751
- const pages = await enumerateCandidateUiPages(repo, ["lib/main.dart"]);
752
- const urls = pages.map((p) => p.url).sort();
753
- expect(urls).toEqual([
754
- "http://localhost:8080/bar",
755
- "http://localhost:8080/foo",
756
- ]);
757
- fs.rmSync(repo, { recursive: true, force: true });
758
- });
759
- // Negative gate: pure-Dart server with a `GoRoute(`-shaped string in source
760
- // (e.g. shelf_router uses similar names in tests) must NOT trigger Strategy
761
- // 2.5 — pubspec.yaml lacks `sdk: flutter`, hasFlutterSdkDep returns false.
762
- it("does not invoke Dart extractor when pubspec.yaml lacks sdk: flutter", async () => {
763
- readWorkspaceConfigRaw.mockResolvedValue({
764
- services: [
765
- {
766
- serviceName: "server-frontend",
767
- framework: "playwright",
768
- api: { baseUrl: "http://localhost:8080" },
769
- },
770
- ],
771
- });
772
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-dart-server-"));
773
- fs.writeFileSync(path.join(repo, "pubspec.yaml"), "name: server\ndependencies:\n shelf: ^1.0.0\n");
774
- fs.mkdirSync(path.join(repo, "lib"), { recursive: true });
775
- fs.writeFileSync(path.join(repo, "lib", "routes.dart"), `GoRoute(path: '/api/v1', handler: (req) => Response.ok(''))`);
776
- const pages = await enumerateCandidateUiPages(repo, ["lib/routes.dart"]);
777
- expect(pages.map((p) => p.strategy)).toEqual(["root-fallback"]);
778
- fs.rmSync(repo, { recursive: true, force: true });
779
- });
780
- });
781
- // ---------------------------------------------------------------------------
782
- // detectsFilesystemRouting
783
- // ---------------------------------------------------------------------------
784
- describe("detectsFilesystemRouting", () => {
785
- let repo;
786
- afterEach(() => {
787
- if (repo) {
788
- fs.rmSync(repo, { recursive: true, force: true });
789
- repo = "";
790
- }
791
- });
792
- it("returns false when no framework config exists", () => {
793
- repo = makeTmpRepo([]);
794
- expect(detectsFilesystemRouting(repo)).toBe(false);
795
- });
796
- it("detects Next.js via next.config.js at repo root", () => {
797
- repo = makeTmpRepo(["next.config.js"]);
798
- expect(detectsFilesystemRouting(repo)).toBe(true);
799
- });
800
- it("detects Next.js via next.config.ts", () => {
801
- repo = makeTmpRepo(["next.config.ts"]);
802
- expect(detectsFilesystemRouting(repo)).toBe(true);
803
- });
804
- it("detects Nuxt via nuxt.config.ts", () => {
805
- repo = makeTmpRepo(["nuxt.config.ts"]);
806
- expect(detectsFilesystemRouting(repo)).toBe(true);
807
- });
808
- it("detects SvelteKit via svelte.config.js", () => {
809
- repo = makeTmpRepo(["svelte.config.js"]);
810
- expect(detectsFilesystemRouting(repo)).toBe(true);
811
- });
812
- it("detects framework config one level deep (frontend/)", () => {
813
- repo = makeTmpRepo(["frontend/next.config.js"]);
814
- expect(detectsFilesystemRouting(repo)).toBe(true);
815
- });
816
- it("detects framework config in apps/web/", () => {
817
- repo = makeTmpRepo(["apps/web/next.config.mjs"]);
818
- expect(detectsFilesystemRouting(repo)).toBe(true);
819
- });
820
- it("returns false when only a pages/ directory exists (no config)", () => {
821
- repo = makeTmpRepo(["pages/Cart.tsx"]);
822
- expect(detectsFilesystemRouting(repo)).toBe(false);
823
- });
824
- });