@skyramp/mcp 0.3.8 → 0.4.0-rc.1

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 (229) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -1
  2. package/build/commands/commandLibrary.js +3 -3
  3. package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
  4. package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
  5. package/build/commands/testThisEndpointCommand.js +35 -19
  6. package/build/index.js +9 -3
  7. package/build/playwright/blueprintDigest.d.ts +15 -0
  8. package/build/playwright/blueprintDigest.js +152 -0
  9. package/build/playwright/blueprintDigestStore.d.ts +31 -0
  10. package/build/playwright/blueprintDigestStore.js +117 -0
  11. package/build/playwright/registerPlaywrightTools.js +60 -12
  12. package/build/playwright/traceRecordingPrompt.js +8 -7
  13. package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
  14. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
  15. package/build/prompts/promptAssets.d.ts +20 -0
  16. package/build/prompts/promptAssets.js +55 -0
  17. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
  18. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  19. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
  23. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  24. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
  25. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  26. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  27. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  28. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  29. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  30. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  31. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  33. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  34. package/build/prompts/testbot/planDeclarations.js +9 -0
  35. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  36. package/build/prompts/testbot/testbot-prompts.js +256 -381
  37. package/build/recommendation/answers.d.ts +35 -0
  38. package/build/recommendation/answers.js +96 -0
  39. package/build/recommendation/registerPlan.d.ts +49 -0
  40. package/build/recommendation/registerPlan.js +117 -0
  41. package/build/recommendation/runVerifiers.d.ts +10 -0
  42. package/build/recommendation/runVerifiers.js +49 -0
  43. package/build/recommendation/subjectStep.d.ts +42 -0
  44. package/build/recommendation/subjectStep.js +86 -0
  45. package/build/recommendation/types.d.ts +163 -0
  46. package/build/recommendation/types.js +20 -0
  47. package/build/recommendation/verifierContracts.d.ts +382 -0
  48. package/build/recommendation/verifierContracts.js +263 -0
  49. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  50. package/build/recommendation/verifiers/changedFile.js +82 -0
  51. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  52. package/build/recommendation/verifiers/citedPath.js +35 -0
  53. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  54. package/build/recommendation/verifiers/coverage.js +617 -0
  55. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  56. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  57. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  58. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  59. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  60. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  61. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  62. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  63. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  64. package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
  65. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  66. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  67. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  68. package/build/recommendation/verifiers/screenRoute.js +118 -0
  69. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  70. package/build/recommendation/verifiers/statedDifference.js +140 -0
  71. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  72. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  73. package/build/resources/analysisResources.js +1 -114
  74. package/build/resources/testbotResource.js +23 -13
  75. package/build/services/ModularizationService.js +2 -1
  76. package/build/services/TestDiscoveryService.d.ts +3 -72
  77. package/build/services/TestDiscoveryService.js +10 -303
  78. package/build/services/containerEnv.d.ts +1 -1
  79. package/build/services/containerEnv.js +12 -0
  80. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  81. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  82. package/build/toolNames.d.ts +1 -0
  83. package/build/toolNames.js +1 -0
  84. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  85. package/build/tools/code-refactor/modularizationTool.js +2 -1
  86. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  87. package/build/tools/executeSkyrampTestTool.js +246 -19
  88. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  89. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  90. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  91. package/build/tools/generate-tests/planGuard.js +78 -18
  92. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  93. package/build/tools/one-click/oneClickTool.js +0 -5
  94. package/build/tools/submitReportTool.d.ts +48 -42
  95. package/build/tools/submitReportTool.js +576 -193
  96. package/build/tools/test-management/actionsTool.js +72 -4
  97. package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
  98. package/build/tools/test-management/analyzeChangesTool.js +212 -1219
  99. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  100. package/build/tools/test-management/index.d.ts +1 -0
  101. package/build/tools/test-management/index.js +1 -0
  102. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  103. package/build/tools/test-management/registerTestPlanTool.js +609 -542
  104. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  105. package/build/tools/test-management/resolveScreenTool.js +289 -0
  106. package/build/types/BlueprintDigest.d.ts +34 -0
  107. package/build/types/BlueprintDigest.js +1 -0
  108. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  109. package/build/types/RepositoryAnalysis.js +2 -58
  110. package/build/types/StepMethod.d.ts +40 -0
  111. package/build/types/StepMethod.js +77 -0
  112. package/build/types/TestAnalysis.d.ts +12 -0
  113. package/build/types/TestExecution.d.ts +4 -0
  114. package/build/types/TestRecommendation.d.ts +24 -24
  115. package/build/types/TestRecommendation.js +91 -89
  116. package/build/types/TestbotPromptOptions.d.ts +0 -4
  117. package/build/types/TestbotReport.d.ts +64 -2
  118. package/build/utils/AnalysisStateManager.d.ts +79 -113
  119. package/build/utils/AnalysisStateManager.js +147 -57
  120. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  121. package/build/utils/assertion-verify/metrics.js +85 -36
  122. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  123. package/build/utils/assertion-verify/ui-lints.js +32 -0
  124. package/build/utils/branchDiff.d.ts +63 -31
  125. package/build/utils/branchDiff.js +242 -94
  126. package/build/utils/containedPath.d.ts +18 -0
  127. package/build/utils/containedPath.js +73 -0
  128. package/build/utils/dartRouteExtractor.d.ts +18 -34
  129. package/build/utils/dartRouteExtractor.js +101 -173
  130. package/build/utils/featureFlags.d.ts +12 -0
  131. package/build/utils/featureFlags.js +14 -0
  132. package/build/utils/frontendSelectors.d.ts +48 -27
  133. package/build/utils/frontendSelectors.js +241 -80
  134. package/build/utils/pathMatching.d.ts +2 -4
  135. package/build/utils/pathMatching.js +2 -4
  136. package/build/utils/planMatchKeys.d.ts +38 -47
  137. package/build/utils/planMatchKeys.js +143 -81
  138. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  139. package/build/utils/rebaselineSnapshots.js +65 -0
  140. package/build/utils/removedUiElements.d.ts +22 -0
  141. package/build/utils/removedUiElements.js +106 -0
  142. package/build/utils/reportVerification.d.ts +2 -6
  143. package/build/utils/reportVerification.js +61 -2
  144. package/build/utils/screenRoutes.d.ts +66 -0
  145. package/build/utils/screenRoutes.js +727 -0
  146. package/build/utils/sourceRouteExtractor.js +320 -112
  147. package/build/utils/testFileClassification.d.ts +11 -2
  148. package/build/utils/testFileClassification.js +44 -2
  149. package/build/utils/testFixtures.d.ts +5 -0
  150. package/build/utils/testFixtures.js +13 -0
  151. package/build/utils/utils.d.ts +0 -1
  152. package/build/utils/utils.js +0 -11
  153. package/build/utils/versions.d.ts +3 -3
  154. package/build/utils/versions.js +1 -1
  155. package/build/workspace/workspace.d.ts +12 -12
  156. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  157. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  158. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  160. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  161. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  162. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  168. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  171. package/node_modules/playwright/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +8 -0
  174. package/plugin/plugin.json +6 -0
  175. package/plugin/prompts/declaring-a-plan.md +20 -0
  176. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  177. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  178. package/plugin/prompts/generate-tests/generation.md +108 -0
  179. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  180. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  181. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  182. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  183. package/plugin/prompts/plan-tests.md +42 -0
  184. package/plugin/prompts/testbot-task1.md +82 -0
  185. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  186. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  187. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  188. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  189. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  190. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  191. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  192. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  193. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  194. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  195. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  196. package/build/recommendation/budgeters/shared.d.ts +0 -32
  197. package/build/recommendation/budgeters/shared.js +0 -246
  198. package/build/recommendation/discriminators.d.ts +0 -37
  199. package/build/recommendation/discriminators.js +0 -379
  200. package/build/recommendation/diversity.d.ts +0 -47
  201. package/build/recommendation/diversity.js +0 -101
  202. package/build/recommendation/planRanker.d.ts +0 -65
  203. package/build/recommendation/planRanker.js +0 -83
  204. package/build/recommendation/testFixtures.d.ts +0 -25
  205. package/build/recommendation/testFixtures.js +0 -45
  206. package/build/types/FrontendIntegration.d.ts +0 -28
  207. package/build/types/FrontendIntegration.js +0 -22
  208. package/build/types/Recommendation.d.ts +0 -146
  209. package/build/types/Recommendation.js +0 -74
  210. package/build/utils/changedRoutes.d.ts +0 -29
  211. package/build/utils/changedRoutes.js +0 -87
  212. package/build/utils/frontendIntegration.d.ts +0 -9
  213. package/build/utils/frontendIntegration.js +0 -243
  214. package/build/utils/importerHop.d.ts +0 -135
  215. package/build/utils/importerHop.js +0 -489
  216. package/build/utils/pathAffinityClassification.d.ts +0 -49
  217. package/build/utils/pathAffinityClassification.js +0 -180
  218. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  219. package/build/utils/pythonMountPrefixes.js +0 -347
  220. package/build/utils/repoScanner.d.ts +0 -34
  221. package/build/utils/repoScanner.js +0 -300
  222. package/build/utils/routeParsers.d.ts +0 -95
  223. package/build/utils/routeParsers.js +0 -951
  224. package/build/utils/scenarioDrafting.d.ts +0 -92
  225. package/build/utils/scenarioDrafting.js +0 -951
  226. package/build/utils/subjectEndpoints.d.ts +0 -19
  227. package/build/utils/subjectEndpoints.js +0 -98
  228. package/build/utils/uiPageEnumerator.d.ts +0 -172
  229. package/build/utils/uiPageEnumerator.js +0 -474
@@ -1,474 +0,0 @@
1
- /**
2
- * Programmatic UI page enumeration — translates "which frontend files changed"
3
- * into "which URLs should the agent capture blueprints for."
4
- *
5
- * Replaces the prose-driven Task 0 strategy ladder in the testbot prompt with
6
- * deterministic code so the candidate page list lives in the stateFile and is
7
- * reusable across analyze_changes consumers (testbot, analyze_test_health,
8
- * future blueprint-grounded recommenders).
9
- *
10
- * Strategies, tried in order:
11
- *
12
- * Strategy 1 — Framework route grep
13
- * Translate file-system-routed component paths directly to URLs. Covers
14
- * Next.js (app/ and pages/), Nuxt / Vue Router file-based, and SvelteKit.
15
- * Gated on detection of a framework config file in the repo.
16
- *
17
- * Strategy 1b — Import graph
18
- * For a changed file that is not itself a route (a component, a constants
19
- * module), map the production files that import it through the same
20
- * filesystem-route mapper. Uses the importer list the production-importer integration
21
- * check already computed. Merged with strategy 1 rather than tried after it.
22
- *
23
- * Strategy 2 — Source-grounded routes
24
- * Walks router/App files with the TS Compiler API for code-defined
25
- * route declarations like `<Route path="/cart" element={<Cart/>}>`.
26
- * Matches each route's component file OR declaration file against the
27
- * changed-file list (so router rewrites surface their routes even when
28
- * the component itself is unchanged). Handles React Router string-literal
29
- * paths, cross-file constant resolution, Vue Router object literals,
30
- * and Directus `defineModule` aggregation. See `sourceRouteExtractor.ts`.
31
- *
32
- * Strategy 3 — Root fallback
33
- * Read the workspace's frontend baseUrl and treat it as the single
34
- * candidate page. Always available when a frontend service is configured.
35
- *
36
- * When no frontend baseUrl can be resolved, strategies 1/1b still run and their
37
- * results are returned as bare URL paths (`baseUrlResolved: false`) so the agent
38
- * can prefix the base URL it discovers; strategies 2–3 need a host and are skipped.
39
- */
40
- import * as fs from "fs";
41
- import * as path from "path";
42
- import { extractDartRoutes } from "./dartRouteExtractor.js";
43
- import { extractSourceRoutes } from "./sourceRouteExtractor.js";
44
- import { hasFlutterSdkDep } from "../prompts/test-recommendation/scopeAssessment.js";
45
- import { readWorkspaceConfigRaw } from "./workspaceAuth.js";
46
- /**
47
- * Upper bound on candidate pages. Strategy 1b is bounded by changed files ×
48
- * importers, and every candidate costs the agent a navigate + a capture, so the
49
- * list is cut here — direct route matches first, then import-graph pages that
50
- * render the most changed files.
51
- */
52
- export const MAX_CANDIDATE_PAGES = 10;
53
- // ── Strategy 1: framework route grep ────────────────────────────────────────
54
- /**
55
- * File-system → URL mapping for filesystem-routed frameworks.
56
- *
57
- * Returns the URL path (no host) when the file is a route entrypoint, or
58
- * null when the file isn't a route in any recognized convention.
59
- *
60
- * The `null` cases include component files used by routes (e.g. shared
61
- * widgets in `components/`), API handlers (e.g. Next.js `pages/api/*`),
62
- * and arbitrary `.ts`/`.js` modules under frontend directories. These
63
- * are picked up by strategy 2 (import-graph walk, future) or by strategy
64
- * 3 (root fallback).
65
- */
66
- export function frameworkFileToUrlPath(filePath) {
67
- // Normalize to forward slashes; works on Windows too.
68
- const normalized = filePath.replace(/\\/g, "/");
69
- // Next.js App Router root: app/page.{tsx,jsx} → /
70
- if (/(?:^|\/)app\/page\.(tsx|jsx|ts|js)$/.test(normalized)) {
71
- return "/";
72
- }
73
- // Next.js App Router nested: app/foo/page.tsx → /foo
74
- // app/(group)/foo/page.tsx → /foo (route groups stripped in URL)
75
- // app/foo/[id]/page.tsx → /foo/:id (Next.js bracket → colon)
76
- const nextAppMatch = normalized.match(/(?:^|\/)app\/(.+?)\/page\.(tsx|jsx|ts|js)$/);
77
- if (nextAppMatch) {
78
- return "/" + stripRouteGroups(nextAppMatch[1]).replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
79
- }
80
- // Next.js Pages Router: pages/foo.tsx → /foo, pages/index.tsx → /
81
- // Excludes pages/api/* (API handlers) and the framework files _app, _document,
82
- // _error (they wrap every page and are not routes of their own).
83
- const nextPagesMatch = normalized.match(/(?:^|\/)pages\/(?!api\/)(?!_(?:app|document|error)\.)(.+?)\.(tsx|jsx|ts|js)$/);
84
- if (nextPagesMatch) {
85
- let urlPath = nextPagesMatch[1];
86
- // /index → /, /foo/index → /foo
87
- urlPath = urlPath.replace(/(?:^|\/)index$/, "");
88
- urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
89
- return "/" + urlPath;
90
- }
91
- // Nuxt / Vue Router file-based: pages/foo.vue → /foo
92
- // Vue _id.vue → :id (Nuxt 2) or [id].vue → :id (Nuxt 3)
93
- const nuxtMatch = normalized.match(/(?:^|\/)pages\/(.+?)\.vue$/);
94
- if (nuxtMatch) {
95
- let urlPath = nuxtMatch[1];
96
- urlPath = urlPath.replace(/(?:^|\/)index$/, "");
97
- urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
98
- urlPath = urlPath.replace(/_(\w+)/g, ":$1");
99
- return "/" + urlPath;
100
- }
101
- // SvelteKit: routes/foo/+page.svelte → /foo, routes/+page.svelte → /
102
- const svelteMatch = normalized.match(/(?:^|\/)routes\/(.*?)\+page\.svelte$/);
103
- if (svelteMatch) {
104
- let urlPath = svelteMatch[1].replace(/\/$/, "");
105
- urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
106
- return "/" + urlPath;
107
- }
108
- return null;
109
- }
110
- /**
111
- * Drop Next.js App Router route-group segments — `(marketing)/foo` → `foo`.
112
- * Route groups don't appear in the URL.
113
- */
114
- function stripRouteGroups(routePath) {
115
- return routePath
116
- .split("/")
117
- .filter((seg) => !/^\(.+\)$/.test(seg))
118
- .join("/");
119
- }
120
- /**
121
- * Detect whether the repository is configured for filesystem-based routing.
122
- *
123
- * Strategy 1 maps file paths to URLs based on conventions (Next.js Pages
124
- * Router, Nuxt, SvelteKit). Without a positive signal that the framework
125
- * is actually configured, a repo that just *happens* to have a `pages/`
126
- * directory (e.g. React with conventional layout, Vue with route-config
127
- * code) produces false-positive URLs. This guard suppresses strategy 1
128
- * for those repos so they fall through to root-fallback.
129
- *
130
- * Looks for framework config files in the repo root, common subdirectories,
131
- * and the `frontendFiles[]`-derived candidate frontend roots.
132
- */
133
- export function detectsFilesystemRouting(repositoryPath) {
134
- const FRAMEWORK_CONFIG_FILES = [
135
- // Next.js
136
- "next.config.js", "next.config.mjs", "next.config.ts",
137
- // Nuxt
138
- "nuxt.config.ts", "nuxt.config.js",
139
- // SvelteKit
140
- "svelte.config.js", "svelte.config.ts",
141
- ];
142
- // Search the repo root and one level deep into common app subdirectories.
143
- // Most monorepos place the frontend at apps/web, app/, frontend/, web/, etc.
144
- // Going more than one level adds I/O without meaningful coverage gain.
145
- const ROOTS_TO_CHECK = [".", "frontend", "web", "app", "client", "apps/web"];
146
- for (const root of ROOTS_TO_CHECK) {
147
- for (const cfg of FRAMEWORK_CONFIG_FILES) {
148
- const fullPath = path.join(repositoryPath, root, cfg);
149
- try {
150
- if (fs.existsSync(fullPath))
151
- return true;
152
- }
153
- catch {
154
- // ignore — fs errors don't change the answer
155
- }
156
- }
157
- }
158
- return false;
159
- }
160
- /**
161
- * Strategies 1 + 1b: map changed route files (1) and the route files that import
162
- * changed non-route files (1b) to URL paths. Deduped by path; when both name the
163
- * same path the strategy-1 entry wins and the extra source is appended.
164
- *
165
- * Suppressed when the repo doesn't have a recognized filesystem-routing
166
- * framework config — see `detectsFilesystemRouting`. Without that guard,
167
- * repos with conventional `pages/` directories but code-defined routes
168
- * (e.g. React with React Router) produce false-positive URLs.
169
- */
170
- function collectRoutePaths(frontendFiles, integration, repositoryPath) {
171
- // When repositoryPath is provided, gate on framework detection. When
172
- // omitted (e.g. older callers, unit tests), preserve original behavior.
173
- if (repositoryPath !== undefined && !detectsFilesystemRouting(repositoryPath)) {
174
- return [];
175
- }
176
- const byPath = new Map();
177
- const add = (urlPath, sourceFile, strategy, via) => {
178
- const existing = byPath.get(urlPath);
179
- if (existing) {
180
- // Same path surfaced by multiple files — track all sources.
181
- if (!existing.sourcedFrom.includes(sourceFile))
182
- existing.sourcedFrom.push(sourceFile);
183
- if (via && existing.via && !existing.via.includes(via))
184
- existing.via.push(via);
185
- }
186
- else {
187
- byPath.set(urlPath, { path: urlPath, sourcedFrom: [sourceFile], strategy, ...(via ? { via: [via] } : {}) });
188
- }
189
- };
190
- for (const file of frontendFiles) {
191
- const urlPath = frameworkFileToUrlPath(file);
192
- if (urlPath !== null)
193
- add(urlPath, file, "framework-route-grep");
194
- }
195
- for (const entry of integration ?? []) {
196
- if (!entry.integrated)
197
- continue;
198
- for (const importer of entry.importers) {
199
- const urlPath = frameworkFileToUrlPath(importer);
200
- if (urlPath !== null)
201
- add(urlPath, entry.file, "import-graph", importer);
202
- }
203
- }
204
- const all = Array.from(byPath.values());
205
- const rank = (c) => (c.strategy === "framework-route-grep" ? 1 : 0) * 1000 + c.sourcedFrom.length;
206
- return all.sort((a, b) => rank(b) - rank(a)).slice(0, MAX_CANDIDATE_PAGES);
207
- }
208
- function joinBaseUrl(baseUrl, candidate) {
209
- return {
210
- url: baseUrl.replace(/\/$/, "") + candidate.path,
211
- sourcedFrom: candidate.sourcedFrom,
212
- strategy: candidate.strategy,
213
- ...(candidate.via ? { via: candidate.via } : {}),
214
- };
215
- }
216
- /** Strategy 1 only (no import graph), joined to `baseUrl`. */
217
- export function findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath) {
218
- return collectRoutePaths(frontendFiles, undefined, repositoryPath).map((c) => joinBaseUrl(baseUrl, c));
219
- }
220
- // ── Strategy 2: source-grounded routes ──────────────────────────────────────
221
- /**
222
- * Strategy 2 implementation. Walks router/App files with the TS Compiler API
223
- * to find route declarations (`<Route path="/cart" element={<Cart/>}>`),
224
- * matches each declaration's component file against the changed-file list,
225
- * and emits one CandidateUiPage per matching route.
226
- *
227
- * Slice 3a covers string-literal `path` attributes only — `path={CONST}` and
228
- * Vue Router object literals are deferred to slice 3b.
229
- *
230
- * Returns empty when no route declarations match the changed files (which is
231
- * also the right answer for SPAs without client-side routing — they fall
232
- * through to root-fallback).
233
- */
234
- export function findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath) {
235
- const sourceRoutes = extractSourceRoutes(repositoryPath);
236
- if (sourceRoutes.length === 0)
237
- return [];
238
- // Normalize changed files to absolute paths once.
239
- const changedAbs = new Set(frontendFiles.map((f) => path.resolve(repositoryPath, f)));
240
- const normalizedBase = baseUrl.replace(/\/$/, "");
241
- const byUrl = new Map();
242
- for (const route of sourceRoutes) {
243
- // A route surfaces as a candidate when *either* its component file or its
244
- // declaring file (router.ts, modules/<x>/index.ts) appears in the diff.
245
- // Component-only matching misses route renames in the declaration file
246
- // (the declaration changed; the component is unchanged).
247
- const matchedBy = (route.componentFile && changedAbs.has(route.componentFile))
248
- ? route.componentFile
249
- : (route.declaredIn && changedAbs.has(route.declaredIn))
250
- ? route.declaredIn
251
- : null;
252
- if (matchedBy === null)
253
- continue;
254
- const fullUrl = normalizedBase + (route.path.startsWith("/") ? route.path : "/" + route.path);
255
- const sourceFile = path.relative(repositoryPath, matchedBy);
256
- const existing = byUrl.get(fullUrl);
257
- if (existing) {
258
- if (!existing.sourcedFrom.includes(sourceFile)) {
259
- existing.sourcedFrom.push(sourceFile);
260
- }
261
- }
262
- else {
263
- byUrl.set(fullUrl, {
264
- url: fullUrl,
265
- sourcedFrom: [sourceFile],
266
- strategy: "source-grounded-routes",
267
- });
268
- }
269
- }
270
- return Array.from(byUrl.values());
271
- }
272
- // ── Strategy 2.5: Dart GoRouter routes ─────────────────────────────────────
273
- /**
274
- * Strategy 2.5: walk the repo's Dart files for GoRoute(path: '...') calls.
275
- * Gated on `hasFlutterSdkDep(repoPath)` so non-Flutter Dart projects (e.g.
276
- * shelf_router servers) don't accidentally emit URLs.
277
- *
278
- * Diff-matching: each GoRoute references its screen widget via
279
- * `builder:`/`pageBuilder:` (e.g. `AuthorsScreen`). The extractor resolves
280
- * those identifiers through the route file's imports to absolute screen
281
- * file paths. We surface only routes whose screen file appears in the
282
- * changed-file list. Falls back to all-routes when no route's screen
283
- * file matches — preserves coverage for "router untouched, screen edited"
284
- * cases when the import-resolution heuristic doesn't find a match.
285
- *
286
- * Filters applied:
287
- * 1. Drop redirect-only routes (`redirect:` with no `builder:`/`pageBuilder:`).
288
- * These have no UI to render — navigating there is wasted work.
289
- * 2. Drop nested relative children (paths that don't start with `/`).
290
- * Without parent-path composition the URL is meaningless.
291
- *
292
- * Returns empty when the repo isn't Flutter or no GoRoute calls are
293
- * found — caller falls through to root-fallback.
294
- */
295
- export function findCandidatePagesByDartRoute(repositoryPath, baseUrl, frontendFiles) {
296
- if (!hasFlutterSdkDep(repositoryPath))
297
- return [];
298
- const dartRoutes = extractDartRoutes(repositoryPath);
299
- if (dartRoutes.length === 0)
300
- return [];
301
- // Strip redirect-only and relative routes — they're never valid candidates.
302
- const renderable = dartRoutes.filter((r) => !r.isRedirectOnly && r.path.startsWith("/"));
303
- if (renderable.length === 0)
304
- return [];
305
- // Diff-match each route against frontendFiles via its screenFiles.
306
- const changedAbs = new Set(frontendFiles.map((f) => path.resolve(repositoryPath, f)));
307
- const matched = renderable.filter((r) => r.screenFiles.some((sf) => changedAbs.has(sf)));
308
- // Fall back to all renderable routes when nothing diff-matches. The
309
- // import-resolution heuristic isn't perfect (custom widget naming, multiple
310
- // screens per file, factory functions); a zero-match should not collapse
311
- // the candidate list to empty when the repo clearly has UI changes.
312
- const surfaced = matched.length > 0 ? matched : renderable;
313
- const normalizedBase = baseUrl.replace(/\/$/, "");
314
- const byUrl = new Map();
315
- for (const route of surfaced) {
316
- const fullUrl = normalizedBase + route.path;
317
- const sourceFile = path.relative(repositoryPath, route.declaredIn);
318
- const existing = byUrl.get(fullUrl);
319
- if (existing) {
320
- if (!existing.sourcedFrom.includes(sourceFile)) {
321
- existing.sourcedFrom.push(sourceFile);
322
- }
323
- }
324
- else {
325
- byUrl.set(fullUrl, {
326
- url: fullUrl,
327
- sourcedFrom: [sourceFile],
328
- strategy: "dart-go-router",
329
- });
330
- }
331
- }
332
- return Array.from(byUrl.values());
333
- }
334
- // ── Strategy 3: root fallback ───────────────────────────────────────────────
335
- /**
336
- * Strategy 3: when strategies 1+2 yield nothing, fall back to the workspace's
337
- * frontend baseUrl as the single candidate page. The agent's downstream
338
- * explore-and-discover step can then surface gated UI from the root.
339
- *
340
- * Returns null when the workspace has no recognizable frontend service.
341
- */
342
- export async function findRootFallbackPage(repositoryPath) {
343
- const baseUrl = await pickFrontendBaseUrl(repositoryPath);
344
- return baseUrl ? rootFallbackPage(baseUrl) : null;
345
- }
346
- function rootFallbackPage(baseUrl) {
347
- return { url: baseUrl, sourcedFrom: [], strategy: "root-fallback" };
348
- }
349
- /**
350
- * Mirror of testbot.git src/services.ts exportServiceBaseUrlEnvVars sanitize().
351
- * Keep in sync with that function (separate repo, cannot share code).
352
- *
353
- * Transforms a service name into the corresponding env var suffix:
354
- * `supabase-nextjs` → `SUPABASE_NEXTJS`
355
- * `my.app` → `MY_APP`
356
- * `proto://app` → `PROTO___APP`
357
- */
358
- function sanitizeServiceName(name) {
359
- return name.toUpperCase().replace(/[-.:\/]/g, "_");
360
- }
361
- /**
362
- * Extracted from pickFrontendBaseUrl: returns the chosen frontend service
363
- * object (not the URL). Implements the original heuristics unchanged.
364
- *
365
- * Heuristics, in order:
366
- * 1. Service whose `serviceName` contains a frontend token (`frontend`,
367
- * `ui`, `app`, or `web`) as a `-`/`_`-delimited word
368
- * 2. Service whose `language` is `typescript` / `javascript` AND whose
369
- * `framework` is one of the common UI frameworks (react, vue, svelte,
370
- * next, nuxt, etc.)
371
- * 3. First service with a defined `api.baseUrl`
372
- *
373
- * Returns the service object or null when no service has a usable URL.
374
- */
375
- function pickFrontendService(services) {
376
- // Heuristic 1: serviceName signals frontend.
377
- const byName = services.find((s) => typeof s?.serviceName === "string" &&
378
- /(?:^|[-_])frontend(?:[-_]|$)|(?:^|[-_])ui(?:[-_]|$)|(?:^|[-_])app(?:[-_]|$)|(?:^|[-_])web(?:[-_]|$)/.test(s.serviceName.toLowerCase()) &&
379
- s?.api?.baseUrl);
380
- if (byName)
381
- return byName;
382
- // Heuristic 2: language + framework signal frontend.
383
- const FRONTEND_FRAMEWORKS = new Set([
384
- "react", "vue", "svelte", "next", "nextjs", "nuxt", "sveltekit",
385
- "angular", "solid", "qwik", "remix", "astro",
386
- ]);
387
- const byFramework = services.find((s) => {
388
- const lang = (s?.language ?? "").toLowerCase();
389
- const fw = (s?.framework ?? "").toLowerCase();
390
- return ((lang === "typescript" || lang === "javascript") &&
391
- FRONTEND_FRAMEWORKS.has(fw) &&
392
- s?.api?.baseUrl);
393
- });
394
- if (byFramework)
395
- return byFramework;
396
- // Heuristic 3: first service with a baseUrl.
397
- const anyBase = services.find((s) => s?.api?.baseUrl);
398
- return anyBase ?? null;
399
- }
400
- /**
401
- * Resolves the frontend service's baseUrl from the workspace config.
402
- *
403
- * Returns the baseUrl string with environment variable override precedence:
404
- * 1. Per-service override: `SKYRAMP_TEST_SERVICE_URL_<NAME>` (where <NAME>
405
- * is the picked frontend service's serviceName, sanitized via
406
- * `sanitizeServiceName`)
407
- * 2. Global override: `SKYRAMP_TEST_BASE_URL` (single service or all
408
- * services share one URL). Also honored when the workspace declares no
409
- * services at all — the action can know the URL without a workspace entry.
410
- * 3. Static workspace.yml value: `api.baseUrl` from the picked frontend
411
- * service
412
- *
413
- * Returns null when neither the environment nor any service has a usable URL.
414
- *
415
- * Exported (vs private) so analyze_changes and future tools can reuse the
416
- * same selection without re-implementing it.
417
- */
418
- export async function pickFrontendBaseUrl(repositoryPath) {
419
- const config = await readWorkspaceConfigRaw(repositoryPath);
420
- const global = process.env.SKYRAMP_TEST_BASE_URL?.trim();
421
- if (!config?.services || !Array.isArray(config.services))
422
- return global || null;
423
- const picked = pickFrontendService(config.services);
424
- // 1. Per-service override (multi-service, distinct URLs).
425
- // readWorkspaceConfigRaw may fall back to unvalidated YAML, so serviceName
426
- // is not guaranteed to be a string — guard before sanitizing.
427
- if (typeof picked?.serviceName === "string") {
428
- const perSvc = process.env[`SKYRAMP_TEST_SERVICE_URL_${sanitizeServiceName(picked.serviceName)}`]?.trim();
429
- if (perSvc)
430
- return perSvc;
431
- }
432
- // 2. Global override (single service / all services share a URL).
433
- if (global)
434
- return global;
435
- // 3. Static workspace.yml value.
436
- return picked?.api?.baseUrl ?? null;
437
- }
438
- // ── Composer ────────────────────────────────────────────────────────────────
439
- /**
440
- * Run the strategy ladder: framework route grep + import graph (1/1b) →
441
- * source-grounded routes (2) → Dart GoRouter (2.5) → root fallback (3). Each
442
- * later strategy is consulted only if the earlier ones produced no candidates.
443
- *
444
- * `integration` is the production-importer check for the same files; when given,
445
- * changed non-route files resolve to the pages that import them.
446
- *
447
- * Without a resolvable frontend baseUrl only strategies 1/1b can run; their
448
- * paths are returned with `baseUrlResolved: false` for the agent to prefix.
449
- * Returns empty only when nothing resolves at all.
450
- */
451
- export async function enumerateCandidateUiPages(repositoryPath, frontendFiles, integration) {
452
- if (frontendFiles.length === 0)
453
- return [];
454
- const baseUrl = await pickFrontendBaseUrl(repositoryPath);
455
- const routePaths = collectRoutePaths(frontendFiles, integration, repositoryPath);
456
- if (!baseUrl) {
457
- return routePaths.map((c) => ({
458
- url: c.path,
459
- sourcedFrom: c.sourcedFrom,
460
- strategy: c.strategy,
461
- baseUrlResolved: false,
462
- ...(c.via ? { via: c.via } : {}),
463
- }));
464
- }
465
- if (routePaths.length > 0)
466
- return routePaths.map((c) => joinBaseUrl(baseUrl, c));
467
- const fromSource = findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath);
468
- if (fromSource.length > 0)
469
- return fromSource;
470
- const fromDart = findCandidatePagesByDartRoute(repositoryPath, baseUrl, frontendFiles);
471
- if (fromDart.length > 0)
472
- return fromDart;
473
- return [rootFallbackPage(baseUrl)];
474
- }