@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
@@ -12,6 +12,7 @@ import { buildPlaywrightArgs, parsePlaywrightJson, } from "../adapters/playwrigh
12
12
  import { buildPytestArgs, parsePytestJson } from "../adapters/pytestAdapter.js";
13
13
  import { buildJestArgs, buildVitestArgs, parseJestJson } from "../adapters/jestAdapter.js";
14
14
  import { buildMochaArgs, parseMochaJson } from "../adapters/mochaAdapter.js";
15
+ import picomatch from "picomatch";
15
16
  import { AnalyticsService } from "../services/AnalyticsService.js";
16
17
  import { logger } from "../utils/logger.js";
17
18
  import { toolError } from "../utils/utils.js";
@@ -128,6 +129,81 @@ export function resolveRunConfig(config, opts = {}) {
128
129
  }
129
130
  return { runs: runnable.map(toResolvedRunConfig), skips };
130
131
  }
132
+ /** The file part of a selector — `file::test title` keeps only `file`. */
133
+ function selectorFile(selector) {
134
+ const idx = selector.indexOf("::");
135
+ return (idx >= 0 ? selector.slice(0, idx) : selector).trim();
136
+ }
137
+ /**
138
+ * Resolves a selector's file to a repo-relative path, the form `pathGlobs` are
139
+ * written in.
140
+ *
141
+ * Selectors arrive in whatever form the caller used — repo-relative, absolute, or
142
+ * a shorter tail. Discovery (analyze_changes) records the ABSOLUTE path of every
143
+ * test it found, so those paths are the authority: when a selector does not
144
+ * resolve to one of them directly, match it by tail to recover the real file.
145
+ */
146
+ function toRepoRelative(file, workspacePath, knownTestFiles) {
147
+ let abs = path.isAbsolute(file) ? file : path.resolve(workspacePath, file);
148
+ if (knownTestFiles?.length && !knownTestFiles.includes(abs)) {
149
+ const match = knownTestFiles.find((known) => known.endsWith(`/${file}`));
150
+ if (match)
151
+ abs = match;
152
+ }
153
+ return path.relative(workspacePath, abs);
154
+ }
155
+ /**
156
+ * Partitions `selectors` into the ones this suite owns and the ones it does not,
157
+ * using the suite's own `pathGlobs` from workspace.yml.
158
+ *
159
+ * One repo commonly hosts several runners (mocha for a core package, vitest for
160
+ * app packages, Playwright for e2e), and handing a runner another runner's spec is
161
+ * not a test failure — it is a loader crash that yields no report at all. Which
162
+ * suite owns which paths is a fact about the repo, so it is recorded as config the
163
+ * user can read and correct, rather than guessed at run time. Run 30297865488 is
164
+ * the cautionary case: routing on a guessed framework tag sent Ghost's Playwright
165
+ * specs away from the Playwright suite, and nothing ran.
166
+ *
167
+ * A suite with no globs owns everything. That is deliberate: absent globs means
168
+ * the suite is unscoped, not that it owns nothing, and narrowing on no
169
+ * information would silently shrink the run.
170
+ */
171
+ export function routeSelectorsToSuite(input) {
172
+ const globs = input.pathGlobs ?? [];
173
+ if (globs.length === 0)
174
+ return { kept: [...input.selectors], dropped: [] };
175
+ const kept = [];
176
+ const dropped = [];
177
+ for (const selector of input.selectors) {
178
+ const rel = toRepoRelative(selectorFile(selector), input.workspacePath, input.knownTestFiles);
179
+ if (picomatch.isMatch(rel, globs))
180
+ kept.push(selector);
181
+ else
182
+ dropped.push(selector);
183
+ }
184
+ return { kept, dropped };
185
+ }
186
+ /**
187
+ * The absolute test-file paths analyze_changes recorded, used to canonicalize a
188
+ * selector before glob matching. Best-effort: with no state file, an unreadable
189
+ * one, or a repo section that was never written, routing just resolves selectors
190
+ * against the repo root.
191
+ */
192
+ async function loadKnownTestFiles(stateFile, repository) {
193
+ if (!stateFile)
194
+ return [];
195
+ try {
196
+ const sm = StateManager.fromStatePath(stateFile);
197
+ const state = await sm.readRepoData(repository);
198
+ return (state?.existingTests ?? [])
199
+ .map((t) => t.testFile)
200
+ .filter((f) => !!f);
201
+ }
202
+ catch (err) {
203
+ logger.warning(`${TOOL_NAME}: could not read existingTests for selector routing: ${err.message}`);
204
+ return [];
205
+ }
206
+ }
131
207
  /** Maps a NormalizedSuite to the run-time config the runner consumes. */
132
208
  function toResolvedRunConfig(s) {
133
209
  return {
@@ -135,6 +211,7 @@ function toResolvedRunConfig(s) {
135
211
  framework: s.framework,
136
212
  testRunCommand: s.testRunCommand,
137
213
  testEnv: s.testEnv,
214
+ pathGlobs: s.pathGlobs,
138
215
  };
139
216
  }
140
217
  /** Combines per-suite runs into one ParsedExternalRun for the tool's summary
@@ -166,14 +243,57 @@ export function aggregateRuns(parsed) {
166
243
  }
167
244
  }
168
245
  }
246
+ // Only when EVERY suite was skipped is the whole run a skip — otherwise the
247
+ // suites that did run own the verdict. Surfacing it matters: the tool's caller
248
+ // branches on `skipped` to say why nothing ran instead of reporting a bare
249
+ // "ran 0 tests", which reads as a clean pass.
250
+ const allSkipped = parsed.length > 0 && parsed.every((p) => p.skipped);
251
+ const skipReasons = parsed.map((p) => p.skipReason).filter(Boolean);
169
252
  return {
170
253
  results,
171
254
  summary,
172
255
  environmentHealthy,
173
256
  healthDetail: details.length ? details.join(" | ") : undefined,
174
257
  diagnostics,
258
+ ...(allSkipped
259
+ ? { skipped: true, skipReason: skipReasons.join(" | ") || undefined }
260
+ : {}),
175
261
  };
176
262
  }
263
+ /**
264
+ * How much of a failed run's stderr to carry into `healthDetail`/`skipReason`.
265
+ *
266
+ * This is often the ONLY record of the failure: a run command that shells out to
267
+ * a container writes to the MCP server's child process, not to the CI job log, so
268
+ * nothing else captures it. Run 30314973126 lost the real error to the previous
269
+ * 1200-char tail — it cut off mid-line, one line after the failing command.
270
+ */
271
+ const STDERR_TAIL_CHARS = 4000;
272
+ /**
273
+ * How much of a failed run's stdout to carry alongside the stderr tail.
274
+ *
275
+ * Every framework EXCEPT mocha reports via a file (`outputVia: "file"`), and those
276
+ * runners print their human-readable failure text — a Playwright global-setup stack
277
+ * trace, a pytest collection error — on STDOUT, which used to be dropped unread to
278
+ * avoid backpressure. So the stderr-only tail left exactly the failures it was added
279
+ * for with no cause attached: run 30402703836 reported `global.setup.ts` as failed
280
+ * and carried nothing about why, because the reason went to stdout.
281
+ */
282
+ const STDOUT_TAIL_CHARS = 4000;
283
+ /**
284
+ * Label and join whichever of the child's output streams carried something, so a
285
+ * failure path reports the cause regardless of which stream the runner chose.
286
+ */
287
+ function buildOutputDiagnostic(stderrTail, stdoutTail) {
288
+ const parts = [];
289
+ const err = stripVTControlCharacters(stderrTail).trim();
290
+ const out = stripVTControlCharacters(stdoutTail).trim();
291
+ if (err)
292
+ parts.push(`stderr: ${err}`);
293
+ if (out)
294
+ parts.push(`stdout: ${out}`);
295
+ return parts.join(" | ");
296
+ }
177
297
  /** POSIX single-quote a shell argument. */
178
298
  function shellQuote(s) {
179
299
  return `'${s.replace(/'/g, `'\\''`)}'`;
@@ -200,7 +320,7 @@ export async function runExternalSuite(spec, cfg, opts) {
200
320
  ...(cfg.testEnv ?? {}),
201
321
  ...(spec.reportEnvVar ? { [spec.reportEnvVar]: reportPath } : {}),
202
322
  };
203
- const { code, stdout, stderrTail } = await new Promise((resolve) => {
323
+ const { code, stdout, stderrTail, stdoutTail } = await new Promise((resolve) => {
204
324
  const child = spawn("bash", ["-c", cmd], {
205
325
  cwd: opts.workspacePath,
206
326
  env,
@@ -208,20 +328,33 @@ export async function runExternalSuite(spec, cfg, opts) {
208
328
  });
209
329
  let stderr = "";
210
330
  let out = "";
331
+ let outTail = "";
211
332
  child.stderr?.on("data", (d) => {
212
333
  stderr += d.toString();
213
334
  if (stderr.length > 16000)
214
335
  stderr = stderr.slice(-16000);
215
336
  });
216
337
  child.stdout?.on("data", (d) => {
338
+ const chunk = d.toString();
339
+ // Always keep a bounded tail: for `outputVia: "file"` frameworks this is the
340
+ // ONLY record of why the run failed. Accumulating the full stream stays gated
341
+ // on "stdout" readers, which parse their JSON report out of it.
342
+ outTail += chunk;
343
+ if (outTail.length > STDOUT_TAIL_CHARS)
344
+ outTail = outTail.slice(-STDOUT_TAIL_CHARS);
217
345
  if (spec.outputVia !== "stdout")
218
346
  return; // drain to avoid backpressure
219
- out += d.toString();
347
+ out += chunk;
220
348
  if (out.length > 8_000_000)
221
349
  out = out.slice(-8_000_000);
222
350
  });
223
- child.on("close", (c) => resolve({ code: c, stdout: out, stderrTail: stderr.slice(-1200) }));
224
- child.on("error", (e) => resolve({ code: -1, stdout: out, stderrTail: String(e) }));
351
+ child.on("close", (c) => resolve({
352
+ code: c,
353
+ stdout: out,
354
+ stderrTail: stderr.slice(-STDERR_TAIL_CHARS),
355
+ stdoutTail: outTail,
356
+ }));
357
+ child.on("error", (e) => resolve({ code: -1, stdout: out, stderrTail: String(e), stdoutTail: outTail }));
225
358
  });
226
359
  let report;
227
360
  try {
@@ -233,11 +366,40 @@ export async function runExternalSuite(spec, cfg, opts) {
233
366
  catch {
234
367
  if (spec.outputVia === "file")
235
368
  await fs.unlink(reportPath).catch(() => { });
369
+ const emptySummary = {
370
+ ran: 0,
371
+ failed: 0,
372
+ errored: 0,
373
+ skipped: 0,
374
+ truncated: opts.truncated,
375
+ };
376
+ const outputDetail = buildOutputDiagnostic(stderrTail, stdoutTail) || "(no output)";
377
+ // A SCOPED run that produced no report is not evidence the environment is
378
+ // down: the usual causes are a spec this runner cannot load (the wrong suite
379
+ // for the file) or this suite's own setup failing. Calling it unhealthy is
380
+ // what let a mocha loader crash read as a dead SUT and suppress PR findings,
381
+ // so report a skip and let the other suites speak for the environment.
382
+ //
383
+ // Do NOT guess between those causes here — run 30314973126 reported "they
384
+ // probably belong to another suite" about selectors that provably belonged to
385
+ // this one, while the real fault was the suite's containerised run command
386
+ // never emitting a report. State both and hand over the stderr.
387
+ if (opts.selectors.length > 0) {
388
+ return {
389
+ results: [],
390
+ summary: emptySummary,
391
+ environmentHealthy: true,
392
+ skipped: true,
393
+ skipReason: `${spec.framework} produced no parseable JSON report (exit ${code}) for the ${opts.selectors.length} selector(s) it was given, so this suite reported nothing either way. Either the selectors are not this suite's to run (check its testSuites pathGlobs) or its own run command failed before emitting a report — the output below says which. ${outputDetail}`,
394
+ };
395
+ }
396
+ // Nothing was narrowed, so a suite that cannot even produce a report does
397
+ // point at the environment.
236
398
  return {
237
399
  results: [],
238
- summary: { ran: 0, failed: 0, errored: 0, skipped: 0, truncated: opts.truncated },
400
+ summary: emptySummary,
239
401
  environmentHealthy: false,
240
- healthDetail: `${spec.framework} produced no parseable JSON report (exit ${code}); the suite likely failed to install/collect or the environment never came up. stderr: ${stripVTControlCharacters(stderrTail).trim() || "(empty)"}`,
402
+ healthDetail: `${spec.framework} produced no parseable JSON report (exit ${code}); the suite likely failed to install/collect or the environment never came up. ${outputDetail}`,
241
403
  };
242
404
  }
243
405
  if (spec.outputVia === "file")
@@ -246,13 +408,19 @@ export async function runExternalSuite(spec, cfg, opts) {
246
408
  // The parser builds healthDetail from the report contents only. When it flags
247
409
  // an unhealthy env (e.g. a Playwright global-setup spec failed), the report is
248
410
  // parseable but the actual cause — a stack trace, "container not found", a
249
- // dependency download that hung — lives on the runner's stderr, which the
250
- // report never carries. Append the stderr tail so a maintenance run's env
251
- // failure is diagnosable from healthDetail alone, not only by re-running blind.
411
+ // dependency download that hung — lives on the runner's output, which the
412
+ // report never carries. Append it so a maintenance run's env failure is
413
+ // diagnosable from healthDetail alone, not only by re-running blind.
414
+ //
415
+ // Both streams, not just stderr: the runners that hit this path all report via a
416
+ // file and print their failure text on stdout, so a stderr-only tail named the
417
+ // failing setup spec and dropped the reason (run 30402703836). A dead end here
418
+ // costs the whole maintenance loop — with no baseline, testbot's step (f) skips
419
+ // the `mode: "verify"` re-run, so the agent never confirms its own fix.
252
420
  if (!parsed.environmentHealthy) {
253
- const tail = stripVTControlCharacters(stderrTail).trim();
254
- if (tail) {
255
- parsed.healthDetail = `${parsed.healthDetail ?? `${spec.framework} environment unhealthy (exit ${code}).`} stderr: ${tail}`;
421
+ const detail = buildOutputDiagnostic(stderrTail, stdoutTail);
422
+ if (detail) {
423
+ parsed.healthDetail = `${parsed.healthDetail ?? `${spec.framework} environment unhealthy (exit ${code}).`} ${detail}`;
256
424
  }
257
425
  }
258
426
  return parsed;
@@ -325,11 +493,44 @@ export async function runExternalTests(params, deps = defaultDeps) {
325
493
  }
326
494
  return skippedRun;
327
495
  }
496
+ const knownTestFiles = await loadKnownTestFiles(params.stateFile, params.repository);
328
497
  const perSuite = [];
329
498
  for (const cfg of resolution.runs) {
499
+ const routing = routeSelectorsToSuite({
500
+ selectors: effective,
501
+ pathGlobs: cfg.pathGlobs,
502
+ knownTestFiles,
503
+ workspacePath: params.workspacePath,
504
+ });
505
+ // An empty selector list means "run the whole configured suite", so routing
506
+ // every selector away must NOT collapse into that: it would silently widen a
507
+ // scoped run into the entire suite. Skip the suite instead, loudly.
508
+ if (effective.length > 0 && routing.kept.length === 0) {
509
+ const owned = (cfg.pathGlobs ?? []).join(", ");
510
+ const routedOut = {
511
+ results: [],
512
+ summary: { ran: 0, failed: 0, errored: 0, skipped: 0, truncated },
513
+ environmentHealthy: true,
514
+ skipped: true,
515
+ skipReason: `suite "${cfg.serviceName}" owns ${owned}, and none of the ${routing.dropped.length} requested selector(s) fall under it — nothing for this suite to run. Widen its testSuites pathGlobs, or add a suite that owns those paths.`,
516
+ };
517
+ perSuite.push(routedOut);
518
+ if (params.stateFile) {
519
+ await persistExternalRun(params.stateFile, params.repository, {
520
+ ...routedOut,
521
+ mode,
522
+ ranAt: deps.now(),
523
+ repository: params.repository,
524
+ });
525
+ }
526
+ continue;
527
+ }
528
+ if (routing.dropped.length > 0) {
529
+ logger.warning(`${TOOL_NAME}: dropped ${routing.dropped.length} selector(s) outside ${cfg.serviceName}'s ${cfg.framework} suite paths (${(cfg.pathGlobs ?? []).join(", ")}): ${routing.dropped.join(", ")}`);
530
+ }
330
531
  const parsed = await deps.runner(cfg, {
331
532
  workspacePath: params.workspacePath,
332
- selectors: effective,
533
+ selectors: routing.kept,
333
534
  mode,
334
535
  project: params.project,
335
536
  timeoutMs: params.timeoutMs,
@@ -12,6 +12,7 @@ import { toolError, testFileMatches } from "../utils/utils.js";
12
12
  import { matchesApprovedPlan } from "../utils/planMatchKeys.js";
13
13
  import { isTestbotEnabled } from "../utils/featureFlags.js";
14
14
  import { findUnbackedClaims, listChangedFiles } from "../utils/reportVerification.js";
15
+ import { rederiveReuseOutcome } from "./code-refactor/reuse-state.js";
15
16
  // SKYR-3879 Path B: which testTypes the register-plan checkpoint gates. Mirrors
16
17
  // the generation tools actually wired to planGuard (batch-scenario/integration,
17
18
  // contract) — UI and E2E are on a separate blueprint-grounded pipeline and are
@@ -337,6 +338,34 @@ function computeReportMetrics(params) {
337
338
  issuesLow: String(countBy(params.issuesFound, (i) => i.severity === "low")),
338
339
  };
339
340
  }
341
+ /**
342
+ * Attach the POM code-reuse outcome for a UI test, matched by the report's
343
+ * `fileName` (the basename skyramp_reuse_code keyed run state on).
344
+ *
345
+ * Server-derived, never supplied by the LLM — the same line this file already
346
+ * draws for testMaintenance's beforeStatus/afterStatus. `skyramp_reuse_code`
347
+ * computes every value in-process (tier-1 detection by selectScopedPoms, the call
348
+ * counts and violations by verifyReuse, the declines from the `// kept inline:`
349
+ * comments in the spec), so asking the agent to read them out of tool text and
350
+ * retype them later would leave the signal prose-mediated — droppable and
351
+ * fabricable, which is the failure this field exists to fix.
352
+ *
353
+ * The counts are RE-DERIVED from the delivered spec rather than read back from
354
+ * state: the execution fix-up can restore `<testFile>.raw.bak` over it afterwards
355
+ * by plain `cp`, which this tool never sees. See rederiveReuseOutcome.
356
+ *
357
+ * Non-UI tests, tests the reuse tool never ran for, and specs whose outcome cannot
358
+ * be re-derived all get nothing — which consumers already treat as "no reuse
359
+ * summary". */
360
+ async function attachReuseOutcome(test, outcomes) {
361
+ if (test.testType !== TestType.UI || !outcomes)
362
+ return test;
363
+ const found = outcomes[path.basename(test.fileName)];
364
+ if (!found)
365
+ return test;
366
+ const reuse = await rederiveReuseOutcome(found);
367
+ return reuse ? { ...test, reuse } : test;
368
+ }
340
369
  function deduplicateById(items) {
341
370
  const seen = new Set();
342
371
  const result = [];
@@ -558,9 +587,15 @@ export function registerSubmitReportTool(server) {
558
587
  primaryRepository: fullState?.metadata?.repository,
559
588
  });
560
589
  if (unbacked.length > 0) {
590
+ // Offer the attribution-correction path explicitly and show what DID change:
591
+ // without it, the cheapest compliant move is a token no-op edit to the
592
+ // mis-claimed file, which pollutes the delivered branch and leaves the report
593
+ // crediting the wrong file (observed in the Ghost rc.pom-3 tester run, D-04).
594
+ const changedList = changedFiles.slice(0, 20).map((f) => ` - ${f}`).join("\n");
561
595
  errorResult = toolError(`${unbacked.length} report claim(s) are not backed by any change in the working tree — ` +
562
- `Testbot will not report file work that hasn't actually been made. Make the edit (or remove the claim), then resubmit:\n` +
563
- unbacked.map((u) => ` - ${u}`).join("\n"));
596
+ `Testbot will not report file work that hasn't actually been made. If the work WAS done but in a different file, correct the claim's fileName/description to the file you actually edited (see the changed files below) — do NOT make a token edit to the claimed file just to satisfy this check. Otherwise make the edit, or remove the claim. Then resubmit.\n` +
597
+ `Unbacked claims:\n${unbacked.map((u) => ` - ${u}`).join("\n")}\n` +
598
+ `Files with actual working-tree changes:\n${changedList || " (none)"}`);
564
599
  return errorResult;
565
600
  }
566
601
  }
@@ -576,7 +611,7 @@ export function registerSubmitReportTool(server) {
576
611
  // generation — downstream scoring scripts don't expect them and fail if
577
612
  // they encounter these string fields while traversing the object.
578
613
  // Also normalize each item's `repository` (blank → undefined).
579
- const sanitizedNewTests = dedupedNewTests.map(({ scenarioFile: _sf, traceFile: _tf, frontendTrace: _ft, ...rest }) => normalizeRepository(rest));
614
+ const sanitizedNewTests = await Promise.all(dedupedNewTests.map(({ scenarioFile: _sf, traceFile: _tf, frontendTrace: _ft, ...rest }) => attachReuseOutcome(normalizeRepository(rest), stateData.reuseOutcomes)));
580
615
  const report = {
581
616
  businessCaseAnalysis: params.businessCaseAnalysis,
582
617
  newTestsCreated: sanitizedNewTests,
@@ -16,11 +16,12 @@ export declare const analyzeChangesInputSchema: {
16
16
  scope: z.ZodOptional<z.ZodDefault<z.ZodEnum<["full_repo", "branch_diff"]>>>;
17
17
  baseBranch: z.ZodOptional<z.ZodString>;
18
18
  testDirectory: z.ZodOptional<z.ZodString>;
19
- topN: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
19
+ topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
20
20
  maxGenerate: z.ZodOptional<z.ZodNumber>;
21
21
  prNumber: z.ZodOptional<z.ZodNumber>;
22
22
  repository: z.ZodOptional<z.ZodString>;
23
23
  testsRepoDir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
24
+ includeUncommitted: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
24
25
  };
25
26
  export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
26
27
  export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.";
@@ -287,8 +287,8 @@ export const analyzeChangesInputSchema = {
287
287
  .describe("Directory containing existing tests (auto-detected if omitted)"),
288
288
  topN: z
289
289
  .number()
290
- .default(MAX_RECOMMENDATIONS)
291
290
  .optional()
291
+ .default(MAX_RECOMMENDATIONS)
292
292
  .describe(`Number of ranked test recommendations to generate. Defaults to ${MAX_RECOMMENDATIONS}.`),
293
293
  maxGenerate: z
294
294
  .number()
@@ -309,6 +309,11 @@ export const analyzeChangesInputSchema = {
309
309
  .refine((v) => path.isAbsolute(v), { message: "testsRepoDir must be an absolute path" })
310
310
  .optional()
311
311
  .describe("Absolute path to a separate test repository clone. When set, existing test discovery scans this directory instead of repositoryPath. Used in cross-repo test delivery mode where tests live in a separate repo."),
312
+ includeUncommitted: z
313
+ .boolean()
314
+ .optional()
315
+ .default(false)
316
+ .describe("When true, diffs the base ref against the working tree (captures uncommitted and unstaged changes). Use for local-dev workflows. Defaults to false (CI mode — committed changes only)."),
312
317
  };
313
318
  // ── UI blueprint-capture instructions ──
314
319
  // Moved here from the former skyramp_ui_analyze_changes pre-flight tool. These
@@ -391,7 +396,7 @@ export function registerAnalyzeChangesTool(server) {
391
396
  if (analysisScope === AnalysisScope.CurrentBranchDiff) {
392
397
  await sendProgress(10, 100, "Computing branch diff...");
393
398
  try {
394
- diffData = await computeBranchDiff(params.repositoryPath, params.baseBranch);
399
+ diffData = await computeBranchDiff(params.repositoryPath, params.baseBranch, params.includeUncommitted ?? false);
395
400
  logger.info("Branch diff computed", {
396
401
  currentBranch: diffData.currentBranch,
397
402
  baseBranch: diffData.baseBranch,
@@ -489,7 +494,7 @@ export function registerAnalyzeChangesTool(server) {
489
494
  const removalCandidateFiles = selectRemovalCandidateFiles(diffData);
490
495
  const git = simpleGit(params.repositoryPath);
491
496
  const recoveredBaseEndpoints = removalCandidateFiles.length > 0
492
- ? await recoverRemovedEndpointsFromBase(removalCandidateFiles, (file) => git.show([`${diffData.baseBranch}:${file}`]))
497
+ ? await recoverRemovedEndpointsFromBase(removalCandidateFiles, (file) => git.show([`${diffData.baseBranch}:${file}`]), params.repositoryPath, diffData.baseBranch)
493
498
  : [];
494
499
  classifiedEndpoints = classifyEndpointsByChangedFiles(diffData, scannedEndpoints, recoveredBaseEndpoints);
495
500
  classifiedEndpoints = {
@@ -1181,6 +1186,56 @@ export function registerAnalyzeChangesTool(server) {
1181
1186
  affectedServices: classifiedEndpoints.affectedServices,
1182
1187
  summary: "",
1183
1188
  } : undefined;
1189
+ // ── Route discovery context for LLM grounding and state persistence ──
1190
+ // fullAnalysis lives only in inMemorySessionStore (for MCP resources
1191
+ // and registerRecommendTestsPrompt). The disk state carries only the
1192
+ // slim fields that downstream tools (health, execute, actions) need.
1193
+ // routerMountContext and candidateRouteFiles are computed here so they
1194
+ // can be persisted to the state file for downstream tools (health, drift).
1195
+ // Without them, analyzeTestHealth would work only off the static catalog
1196
+ // which has wrong paths for nested resources and unsupported frameworks.
1197
+ const routeLikeUnmatchedFiles = [];
1198
+ for (const file of classifiedEndpoints?.unmatchedFiles ?? []) {
1199
+ const routeLike = SOURCE_EXTS.test(file) &&
1200
+ (ROUTE_FILE_PATTERN.test(file) || ROUTE_FILE_BASENAME_PATTERN.test(path.basename(file)));
1201
+ if (routeLike && !(await isGraphQLFile(file, params.repositoryPath))) {
1202
+ routeLikeUnmatchedFiles.push(file);
1203
+ }
1204
+ }
1205
+ const shouldIncludeCandidateRouteFiles = analysisScope !== AnalysisScope.CurrentBranchDiff ||
1206
+ rawRelatedEndpointCount === 0 ||
1207
+ scannedEndpoints.length === 0 ||
1208
+ routeLikeUnmatchedFiles.length > 0;
1209
+ let candidateRouteFiles;
1210
+ if (shouldIncludeCandidateRouteFiles) {
1211
+ candidateRouteFiles = [];
1212
+ for (const file of findCandidateRouteFiles(params.repositoryPath)) {
1213
+ if (!(await isGraphQLFile(file, params.repositoryPath))) {
1214
+ candidateRouteFiles.push(file);
1215
+ }
1216
+ }
1217
+ }
1218
+ // Write the full diff to a temp file before building state so the path
1219
+ // can be persisted and read by analyzeTestHealthTool for per-line detection.
1220
+ let diffFilePath;
1221
+ if (diffData?.diffContent) {
1222
+ diffFilePath = path.join(os.tmpdir(), `skyramp-diff-${sessionId}.diff`);
1223
+ await fs.promises.writeFile(diffFilePath, diffData.diffContent, { encoding: "utf-8", mode: 0o600 });
1224
+ }
1225
+ const routeDiscovery = {
1226
+ candidateFiles: [
1227
+ ...(diffData?.changedFiles ?? []),
1228
+ ...(candidateRouteFiles ?? []),
1229
+ ].filter((file, index, files) => files.indexOf(file) === index),
1230
+ staticHints: scannedEndpoints.map((ep) => ({
1231
+ path: ep.path,
1232
+ methods: ep.methods,
1233
+ sourceFile: ep.sourceFile,
1234
+ })),
1235
+ openApiPaths: specPaths ? [...specPaths] : [],
1236
+ routerMountContext,
1237
+ ...(diffFilePath ? { diffFilePath } : {}),
1238
+ };
1184
1239
  const fullAnalysis = {
1185
1240
  metadata: {
1186
1241
  repositoryName: path.basename(params.repositoryPath),
@@ -1237,6 +1292,7 @@ export function registerAnalyzeChangesTool(server) {
1237
1292
  hasCoverageReports: false,
1238
1293
  relevantExternalTestPaths,
1239
1294
  },
1295
+ routeDiscovery,
1240
1296
  ...(diffContext ? { branchDiffContext: diffContext } : {}),
1241
1297
  };
1242
1298
  // Store RecommendationState in memory so it's compatible with skyramp_recommend_tests if needed
@@ -1246,42 +1302,6 @@ export function registerAnalyzeChangesTool(server) {
1246
1302
  analysis: fullAnalysis,
1247
1303
  };
1248
1304
  storeSessionData(sessionId, recommendationState);
1249
- // ── Step 11: Build UnifiedAnalysisState and save ──
1250
- // fullAnalysis lives only in inMemorySessionStore (for MCP resources
1251
- // and registerRecommendTestsPrompt). The disk state carries only the
1252
- // slim fields that downstream tools (health, execute, actions) need.
1253
- // routerMountContext and candidateRouteFiles are computed here so they
1254
- // can be persisted to the state file for downstream tools (health, drift).
1255
- // Without them, analyzeTestHealth would work only off the static catalog
1256
- // which has wrong paths for nested resources and unsupported frameworks.
1257
- const routeLikeUnmatchedFiles = [];
1258
- for (const file of classifiedEndpoints?.unmatchedFiles ?? []) {
1259
- const routeLike = SOURCE_EXTS.test(file) &&
1260
- (ROUTE_FILE_PATTERN.test(file) || ROUTE_FILE_BASENAME_PATTERN.test(path.basename(file)));
1261
- if (routeLike && !(await isGraphQLFile(file, params.repositoryPath))) {
1262
- routeLikeUnmatchedFiles.push(file);
1263
- }
1264
- }
1265
- const shouldIncludeCandidateRouteFiles = analysisScope !== AnalysisScope.CurrentBranchDiff ||
1266
- rawRelatedEndpointCount === 0 ||
1267
- scannedEndpoints.length === 0 ||
1268
- routeLikeUnmatchedFiles.length > 0;
1269
- let candidateRouteFiles;
1270
- if (shouldIncludeCandidateRouteFiles) {
1271
- candidateRouteFiles = [];
1272
- for (const file of findCandidateRouteFiles(params.repositoryPath)) {
1273
- if (!(await isGraphQLFile(file, params.repositoryPath))) {
1274
- candidateRouteFiles.push(file);
1275
- }
1276
- }
1277
- }
1278
- // Write the full diff to a temp file before building state so the path
1279
- // can be persisted and read by analyzeTestHealthTool for per-line detection.
1280
- let diffFilePath;
1281
- if (diffData?.diffContent) {
1282
- diffFilePath = path.join(os.tmpdir(), `skyramp-diff-${sessionId}.diff`);
1283
- await fs.promises.writeFile(diffFilePath, diffData.diffContent, { encoding: "utf-8", mode: 0o600 });
1284
- }
1285
1305
  // SKYR-3879 Path B: size-capped raw diff text, persisted (not just the
1286
1306
  // file path above) so skyramp_register_test_plan can verify a
1287
1307
  // discriminator's changedCodeAnchor occurs verbatim in the diff without
@@ -1367,7 +1387,7 @@ export function registerAnalyzeChangesTool(server) {
1367
1387
  source: CandidateSource.SERVER,
1368
1388
  candidateId: computeCandidateId(scenario),
1369
1389
  }));
1370
- const plan = selectPlan(serverCandidates, budgetCtx);
1390
+ const plan = selectPlan(serverCandidates, { ...budgetCtx, diffText });
1371
1391
  approvedPlan = {
1372
1392
  planId: crypto.randomUUID(),
1373
1393
  createdAt: new Date().toISOString(),
@@ -1429,6 +1449,7 @@ export function registerAnalyzeChangesTool(server) {
1429
1449
  sessionId,
1430
1450
  routerMountContext,
1431
1451
  candidateRouteFiles,
1452
+ routeDiscovery,
1432
1453
  relevantExternalTestPaths,
1433
1454
  },
1434
1455
  };
@@ -1526,6 +1547,8 @@ export function registerAnalyzeChangesTool(server) {
1526
1547
  modifiedEndpointCount: classifiedEndpoints?.changedEndpoints.length ?? 0,
1527
1548
  removedEndpointCount: classifiedEndpoints?.removedEndpoints.length ?? 0,
1528
1549
  endpointCount: skeletonEndpoints.reduce((acc, ep) => acc + ep.methods.length, 0),
1550
+ newEndpoints: (classifiedEndpoints?.newEndpoints ?? []).flatMap((ep) => ep.methods.map((m) => `${m} ${ep.path}`)),
1551
+ modifiedEndpoints: (classifiedEndpoints?.changedEndpoints ?? []).flatMap((ep) => ep.methods.map((m) => `${m} ${ep.path}`)),
1529
1552
  },
1530
1553
  // Surface uiContext inline so the testbot prompt can iterate
1531
1554
  // candidateUiPages and inspect changedFrontendFiles without