@skyramp/mcp 0.2.6 → 0.2.150-rc.mntnc

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 (233) hide show
  1. package/build/commands/commandLibrary.d.ts +12 -0
  2. package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
  3. package/build/commands/testThisEndpointCommand.d.ts +2 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +6 -4
  6. package/build/playwright/PlaywrightTraceService.d.ts +32 -0
  7. package/build/playwright/index.d.ts +3 -0
  8. package/build/playwright/registerPlaywrightTools.d.ts +8 -0
  9. package/build/playwright/registerPlaywrightTools.js +12 -11
  10. package/build/playwright/traceRecordingPrompt.d.ts +13 -0
  11. package/build/playwright/traceRecordingPrompt.js +3 -11
  12. package/build/prompts/code-reuse.d.ts +1 -0
  13. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
  14. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
  15. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
  16. package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
  17. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
  18. package/build/prompts/fix-error-prompt.d.ts +1 -0
  19. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
  20. package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
  21. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  22. package/build/prompts/personas.d.ts +21 -0
  23. package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
  24. package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
  25. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
  26. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
  27. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
  28. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
  29. package/build/prompts/sut-setup/shared.d.ts +39 -0
  30. package/build/prompts/sut-setup/shared.js +132 -0
  31. package/build/prompts/test-maintenance/actionsInstructions.d.ts +35 -0
  32. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
  33. package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
  34. package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
  35. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
  36. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
  37. package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
  38. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
  39. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
  40. package/build/prompts/test-recommendation/diffExecutionPlan.js +7 -16
  41. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
  42. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
  43. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
  44. package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
  45. package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
  46. package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
  47. package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
  48. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
  49. package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
  50. package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
  51. package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
  52. package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
  53. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
  54. package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
  55. package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
  56. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -99
  57. package/build/prompts/testbot/testbot-prompts.d.ts +11 -0
  58. package/build/prompts/testbot/testbot-prompts.js +213 -42
  59. package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
  60. package/build/prompts/testbot/testbot-prompts.test.js +15 -117
  61. package/build/resources/analysisResources.d.ts +17 -0
  62. package/build/resources/progressResource.d.ts +2 -0
  63. package/build/resources/sutSetupResource.d.ts +2 -0
  64. package/build/resources/sutSetupResource.js +45 -0
  65. package/build/resources/testbotResource.d.ts +2 -0
  66. package/build/resources/testbotResource.js +1 -1
  67. package/build/services/AnalyticsService.d.ts +13 -0
  68. package/build/services/AnalyticsService.test.d.ts +1 -0
  69. package/build/services/AnalyticsService.test.js +86 -0
  70. package/build/services/ModularizationService.d.ts +11 -0
  71. package/build/services/ScenarioGenerationService.d.ts +37 -0
  72. package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
  73. package/build/services/ScenarioGenerationService.integration.test.js +4 -0
  74. package/build/services/ScenarioGenerationService.test.d.ts +1 -0
  75. package/build/services/TestDiscoveryService.d.ts +128 -0
  76. package/build/services/TestDiscoveryService.js +22 -12
  77. package/build/services/TestDiscoveryService.test.d.ts +1 -0
  78. package/build/services/TestDiscoveryService.test.js +4 -3
  79. package/build/services/TestExecutionService.d.ts +55 -0
  80. package/build/services/TestExecutionService.js +158 -26
  81. package/build/services/TestExecutionService.test.d.ts +1 -0
  82. package/build/services/TestExecutionService.test.js +339 -25
  83. package/build/services/TestGenerationService.d.ts +56 -0
  84. package/build/services/TestGenerationService.test.d.ts +1 -0
  85. package/build/services/TestGenerationService.test.js +10 -8
  86. package/build/services/containerEnv.d.ts +14 -0
  87. package/build/tool-phase-coverage.test.d.ts +1 -0
  88. package/build/tool-phase-coverage.test.js +2 -0
  89. package/build/tool-phases.d.ts +38 -0
  90. package/build/tools/auth/loginTool.d.ts +2 -0
  91. package/build/tools/auth/logoutTool.d.ts +2 -0
  92. package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
  93. package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
  94. package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
  95. package/build/tools/executeSkyrampTestTool.d.ts +2 -0
  96. package/build/tools/executeSkyrampTestTool.js +17 -4
  97. package/build/tools/fixErrorTool.d.ts +2 -0
  98. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
  99. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
  100. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
  101. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
  102. package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
  103. package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
  104. package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
  105. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
  106. package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
  107. package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
  108. package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
  109. package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
  110. package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
  111. package/build/tools/generate-tests/generateUIRestTool.js +1 -1
  112. package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
  113. package/build/tools/one-click/oneClickTool.d.ts +11 -0
  114. package/build/tools/submitReportTool.d.ts +384 -0
  115. package/build/tools/submitReportTool.js +225 -37
  116. package/build/tools/submitReportTool.test.d.ts +1 -0
  117. package/build/tools/submitReportTool.test.js +479 -55
  118. package/build/tools/test-management/actionsTool.d.ts +10 -0
  119. package/build/tools/test-management/actionsTool.js +10 -0
  120. package/build/tools/test-management/analyzeChangesTool.d.ts +26 -0
  121. package/build/tools/test-management/analyzeChangesTool.js +128 -48
  122. package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
  123. package/build/tools/test-management/analyzeChangesTool.test.js +80 -73
  124. package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
  125. package/build/tools/test-management/analyzeTestHealthTool.js +6 -3
  126. package/build/tools/test-management/index.d.ts +3 -0
  127. package/build/tools/test-management/index.js +0 -1
  128. package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
  129. package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
  130. package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
  131. package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
  132. package/build/tools/trace/sessionState.d.ts +2 -0
  133. package/build/tools/trace/sessionState.test.d.ts +1 -0
  134. package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
  135. package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
  136. package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
  137. package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
  138. package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
  139. package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
  140. package/build/types/OneClickCommands.d.ts +68 -0
  141. package/build/types/RepositoryAnalysis.d.ts +3004 -0
  142. package/build/types/TestAnalysis.d.ts +97 -0
  143. package/build/types/TestExecution.d.ts +63 -0
  144. package/build/types/TestExecution.js +11 -1
  145. package/build/types/TestRecommendation.d.ts +12 -0
  146. package/build/types/TestTypes.d.ts +234 -0
  147. package/build/types/TestbotReport.d.ts +68 -0
  148. package/build/types/TestbotReport.js +1 -0
  149. package/build/types/index.d.ts +4 -0
  150. package/build/types/index.js +3 -0
  151. package/build/utils/AnalysisStateManager.d.ts +182 -0
  152. package/build/utils/AnalysisStateManager.js +10 -1
  153. package/build/utils/AnalysisStateManager.test.d.ts +1 -0
  154. package/build/utils/analyze-openapi.d.ts +1 -0
  155. package/build/utils/branchDiff.d.ts +18 -0
  156. package/build/utils/dartRouteExtractor.d.ts +45 -0
  157. package/build/utils/dartRouteExtractor.test.d.ts +1 -0
  158. package/build/utils/docker.d.ts +19 -0
  159. package/build/utils/docker.test.d.ts +1 -0
  160. package/build/utils/docker.test.js +10 -9
  161. package/build/utils/featureFlags.d.ts +37 -0
  162. package/build/utils/featureFlags.test.d.ts +1 -0
  163. package/build/utils/gitStaging.d.ts +24 -0
  164. package/build/utils/gitStaging.test.d.ts +1 -0
  165. package/build/utils/gitStaging.test.js +13 -7
  166. package/build/utils/httpDefaults.d.ts +10 -0
  167. package/build/utils/httpDefaults.test.d.ts +1 -0
  168. package/build/utils/initAgent.d.ts +36 -0
  169. package/build/utils/language-helper.d.ts +6 -0
  170. package/build/utils/logger.d.ts +11 -0
  171. package/build/utils/normalizeParams.d.ts +14 -0
  172. package/build/utils/normalizeSkyrampImports.d.ts +13 -0
  173. package/build/utils/pr-comment-parser.d.ts +31 -0
  174. package/build/utils/pr-comment-parser.test.d.ts +1 -0
  175. package/build/utils/pr-comment-parser.test.js +6 -5
  176. package/build/utils/projectMetadata.d.ts +15 -0
  177. package/build/utils/projectMetadata.test.d.ts +1 -0
  178. package/build/utils/proxy-terminal.d.ts +2 -0
  179. package/build/utils/repoScanner.d.ts +27 -0
  180. package/build/utils/repoScanner.test.d.ts +1 -0
  181. package/build/utils/routeParsers.d.ts +53 -0
  182. package/build/utils/routeParsers.js +31 -38
  183. package/build/utils/routeParsers.test.d.ts +1 -0
  184. package/build/utils/scenarioDrafting.d.ts +71 -0
  185. package/build/utils/scenarioDrafting.js +23 -1
  186. package/build/utils/scenarioDrafting.test.d.ts +1 -0
  187. package/build/utils/skyrampMdContent.d.ts +5 -0
  188. package/build/utils/sourceRouteExtractor.d.ts +48 -0
  189. package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
  190. package/build/utils/telemetry.d.ts +32 -0
  191. package/build/utils/telemetry.test.d.ts +1 -0
  192. package/build/utils/trace-parser.d.ts +32 -0
  193. package/build/utils/trace-parser.test.d.ts +6 -0
  194. package/build/utils/uiPageEnumerator.d.ts +146 -0
  195. package/build/utils/uiPageEnumerator.js +58 -14
  196. package/build/utils/uiPageEnumerator.test.d.ts +1 -0
  197. package/build/utils/uiPageEnumerator.test.js +223 -43
  198. package/build/utils/utils.d.ts +45 -0
  199. package/build/utils/utils.js +10 -0
  200. package/build/utils/utils.test.d.ts +1 -0
  201. package/build/utils/versions.d.ts +3 -0
  202. package/build/utils/versions.js +1 -1
  203. package/build/utils/workspaceAuth.d.ts +127 -0
  204. package/build/utils/workspaceAuth.test.d.ts +1 -0
  205. package/build/utils/workspaceAuth.test.js +25 -25
  206. package/build/workspace/index.d.ts +1 -0
  207. package/build/workspace/workspace.d.ts +390 -0
  208. package/build/workspace/workspace.test.d.ts +1 -0
  209. package/node_modules/playwright/LICENSE +202 -0
  210. package/node_modules/playwright/NOTICE +5 -0
  211. package/node_modules/playwright/README.md +168 -0
  212. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +13 -9
  213. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  214. package/node_modules/playwright/node_modules/playwright-core/LICENSE +202 -0
  215. package/node_modules/playwright/node_modules/playwright-core/NOTICE +5 -0
  216. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  217. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-B5kqh2EV.js} +1 -1
  218. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CZ9npQ3N.js} +79 -79
  219. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.BCnxj-_b.js} +1 -1
  220. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  221. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.1Ym0Ivn8.js} +1 -1
  222. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  223. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  224. package/node_modules/playwright/package.json +1 -1
  225. package/package.json +16 -4
  226. package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
  227. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
  228. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  229. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  230. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  231. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  232. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  233. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
@@ -0,0 +1,10 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Compute a suggested new filename when an endpoint is renamed.
4
+ */
5
+ export declare function computeRenamedTestFile(testFile: string, renames: Array<{
6
+ oldPath: string;
7
+ newPath: string;
8
+ method: string;
9
+ }>): string | null;
10
+ export declare function registerActionsTool(server: McpServer): void;
@@ -6,6 +6,7 @@ import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import { AnalyticsService } from "../../services/AnalyticsService.js";
8
8
  import { toolError } from "../../utils/utils.js";
9
+ import { stageGeneratedPaths } from "../../utils/gitStaging.js";
9
10
  import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRegenerateStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
10
11
  /**
11
12
  * Compute a suggested new filename when an endpoint is renamed.
@@ -365,6 +366,15 @@ export function registerActionsTool(server) {
365
366
  });
366
367
  llmInstructionsObj.regenerate_strategy = buildRegenerateStrategy();
367
368
  }
369
+ // Stage UPDATE and REGENERATE target paths so testbot's re-stage step
370
+ // commits the agent's edits even when files are outside testDirectory.
371
+ const stagedPaths = [
372
+ ...updateRecommendations.map((r) => r.testFile),
373
+ ...regenerateRecommendations.map((r) => r.testFile),
374
+ ];
375
+ for (const p of stagedPaths) {
376
+ await stageGeneratedPaths(p).catch((e) => logger.warning(`Failed to stage maintained file ${p}: ${e.message}`));
377
+ }
368
378
  const llmInstructions = JSON.stringify(llmInstructionsObj, null, 2);
369
379
  const contentBlocks = [
370
380
  {
@@ -0,0 +1,26 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { CandidateUiPage } from "../../utils/uiPageEnumerator.js";
4
+ import { TraceFile } from "../../types/RepositoryAnalysis.js";
5
+ import { ScannedEndpoint } from "../../utils/repoScanner.js";
6
+ import { TraceParseResult } from "../../utils/trace-parser.js";
7
+ /** Exported for testing: maps a parsed trace result to a TraceFile. */
8
+ export declare function buildTraceFileEntry(tracePath: string, result: TraceParseResult): TraceFile;
9
+ export declare function isSecurityRelevantDiff(diffContent: string): boolean;
10
+ export declare function filterEndpointsBySpec(scannedEndpoints: ScannedEndpoint[], specPaths: Set<string>, specPathItems: Record<string, any>, diffChangedPaths: Set<string>): ScannedEndpoint[];
11
+ export declare function isGraphQLFile(filePath: string, repositoryPath: string): Promise<boolean>;
12
+ export declare function filterUnsupportedGraphQLEndpoints(endpoints: ScannedEndpoint[], repositoryPath: string): Promise<ScannedEndpoint[]>;
13
+ export declare const analyzeChangesInputSchema: {
14
+ repositoryPath: z.ZodString;
15
+ scope: z.ZodOptional<z.ZodDefault<z.ZodEnum<["full_repo", "branch_diff"]>>>;
16
+ baseBranch: z.ZodOptional<z.ZodString>;
17
+ testDirectory: z.ZodOptional<z.ZodString>;
18
+ topN: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
19
+ maxGenerate: z.ZodOptional<z.ZodNumber>;
20
+ prNumber: z.ZodOptional<z.ZodNumber>;
21
+ testsRepoDir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
22
+ };
23
+ export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
24
+ 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.";
25
+ export declare function buildCaptureInstructions(pages: CandidateUiPage[]): string;
26
+ export declare function registerAnalyzeChangesTool(server: McpServer): void;
@@ -24,6 +24,32 @@ import { buildAnalysisOutputText } from "../../prompts/test-recommendation/analy
24
24
  import { parseTraceFile, discoverTraceFiles, discoverPlaywrightZips, } from "../../utils/trace-parser.js";
25
25
  import { TestSource } from "../../types/TestAnalysis.js";
26
26
  import { parsePRComments } from "../../utils/pr-comment-parser.js";
27
+ // Generic module names that appear as imports in virtually every router file.
28
+ // Matching against these would extract ALL prefix= tokens from mount files,
29
+ // flooding changedResources with unrelated resources.
30
+ const GENERIC_MODULE_NAMES = new Set([
31
+ "models", "utils", "helpers", "schemas", "types", "constants",
32
+ "config", "settings", "exceptions", "errors", "base", "common",
33
+ "app", "main", "index", "server", "init", "deps", "dependencies",
34
+ ]);
35
+ function resolveMountPrefixTokens(mountFileContents, changedFiles) {
36
+ const tokens = changedFiles.flatMap((file) => {
37
+ const mod = path.basename(file).replace(/\.[^.]+$/, "").toLowerCase();
38
+ if (GENERIC_MODULE_NAMES.has(mod))
39
+ return [];
40
+ const modRe = new RegExp(`\\b${mod.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
41
+ return mountFileContents.flatMap((content) => {
42
+ if (!modRe.test(content.toLowerCase()))
43
+ return [];
44
+ const matches = [...content.matchAll(/prefix\s*=\s*["'](\/?[^"']+)/g)];
45
+ return matches.flatMap((m) => {
46
+ const token = extractResourceFromPath(m[1]);
47
+ return token !== "unknown" ? [token] : [];
48
+ });
49
+ });
50
+ });
51
+ return [...new Set(tokens)];
52
+ }
27
53
  /** Exported for testing: maps a parsed trace result to a TraceFile. */
28
54
  export function buildTraceFileEntry(tracePath, result) {
29
55
  return {
@@ -339,17 +365,40 @@ export const analyzeChangesInputSchema = {
339
365
  .number()
340
366
  .optional()
341
367
  .describe("GitHub PR number. When provided, fetches previous TestBot comments on this PR and skips re-recommending tests already suggested in earlier commits — reduces duplicate recommendations across multiple pushes to the same PR."),
342
- stateOutputFile: z
343
- .string()
344
- .refine((v) => path.isAbsolute(v), { message: "stateOutputFile must be an absolute path" })
345
- .optional()
346
- .describe("Absolute path where the state file should be written. When provided, overrides the default auto-generated temp path so the caller can locate it without log parsing."),
347
368
  testsRepoDir: z
348
369
  .string()
349
370
  .refine((v) => path.isAbsolute(v), { message: "testsRepoDir must be an absolute path" })
350
371
  .optional()
351
372
  .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."),
352
373
  };
374
+ // ── UI blueprint-capture instructions ──
375
+ // Moved here from the former skyramp_ui_analyze_changes pre-flight tool. These
376
+ // strings + buildCaptureInstructions are appended to the tool output so the
377
+ // agent captures browser_blueprints on the candidate UI pages after this call.
378
+ // The captures stay in its tool-result history and serve as element vocabulary
379
+ // for UI recommendation reasoning (enforced at submit time by the Blueprint
380
+ // Citation Invariant in the testbot prompt).
381
+ export const NO_UI_INSTRUCTIONS = `No UI changes detected — no blueprint capture needed.`;
382
+ export 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.`;
383
+ export function buildCaptureInstructions(pages) {
384
+ const pagesYaml = pages
385
+ .map((p, i) => ` ${i + 1}. ${p.url} (sourcedFrom: ${p.sourcedFrom.join(", ") || "(none)"}, strategy: ${p.strategy})`)
386
+ .join("\n");
387
+ return `Frontend changes detected. **Before writing any UI recommendation \`reasoning\`, capture blueprints on the candidate UI pages below.** Those captures stay in your tool-result history and serve as element vocabulary — the recommendation catalog further down gives you the authoring rules; you bring the observed elements.
388
+
389
+ **Candidate URLs:**
390
+ ${pagesYaml}
391
+
392
+ **For each candidate URL:**
393
+ - \`browser_navigate\` to the URL
394
+ - \`browser_blueprint\` to capture the page
395
+
396
+ You don't need to thread the blueprints back into a tool call — they're in your context once captured.
397
+
398
+ If a candidate URL 404s or redirects unexpectedly, navigate from the workspace baseUrl and explore (admin apps mount routes under base prefixes the source extraction can't see). If the page rendered but lacks the changed feature (gated UI: modal, dropdown, accordion), do NOT iterate further during this step — UI recs will fall back to source-grounded prose for those, and the agent's later trace recording (Task 2) will navigate into the gate via capture-act-capture.
399
+
400
+ If \`browser_blueprint\` fails on every candidate URL (app unreachable, all 404s), proceed and log an \`issuesFound\` info entry. Recommendations will be source-grounded; non-UI work is unaffected.`;
401
+ }
353
402
  export function registerAnalyzeChangesTool(server) {
354
403
  server.registerTool(TOOL_NAME, {
355
404
  annotations: {
@@ -540,6 +589,30 @@ export function registerAnalyzeChangesTool(server) {
540
589
  // testsRepoDir is a cross-repo path override — honour it when set.
541
590
  // Otherwise always scan the full repo root.
542
591
  const testDir = params.testsRepoDir ?? undefined;
592
+ const routerMountContext = grepRouterMountingContext(params.repositoryPath);
593
+ // Read contents once — used for token extraction and LLM prompt injection.
594
+ const ROUTER_INLINE_LIMIT = 4096;
595
+ const routerMountContextContents = routerMountContext.map((f) => {
596
+ try {
597
+ const abs = path.isAbsolute(f) ? f : path.join(params.repositoryPath, f);
598
+ if (fs.statSync(abs).size > ROUTER_INLINE_LIMIT * 10)
599
+ return ""; // skip very large files
600
+ return fs.readFileSync(abs, "utf-8");
601
+ }
602
+ catch {
603
+ return "";
604
+ }
605
+ });
606
+ // Size-capped subset for LLM prompt injection.
607
+ const ROUTER_INLINE_MAX_FILES = 3;
608
+ const routerFileContents = routerMountContext
609
+ .slice(0, ROUTER_INLINE_MAX_FILES)
610
+ .flatMap((f, i) => {
611
+ const content = routerMountContextContents[i] ?? "";
612
+ if (!content || content.length > ROUTER_INLINE_LIMIT)
613
+ return [];
614
+ return [{ file: f, content: content.trimEnd() }];
615
+ });
543
616
  // Compute changedResources from classified endpoints for test discovery filtering.
544
617
  // undefined → full-repo mode (no diff context)
545
618
  // [] → PR mode, no endpoints found → skip external tests
@@ -568,14 +641,22 @@ export function registerAnalyzeChangesTool(server) {
568
641
  }
569
642
  else {
570
643
  // All endpoints resolved to "unknown" (e.g. NestJS relative paths) —
571
- // extract resource names from changed file paths instead.
572
- const candidateFiles = classifiedEndpoints.unmatchedFiles.length > 0
573
- ? classifiedEndpoints.unmatchedFiles
574
- : classifiedEndpoints.changedFiles ?? [];
575
- const fromFiles = candidateFiles
576
- .map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
577
- .filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
578
- changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
644
+ // first try to extract resource names from router mount prefixes.
645
+ const changedSourceFiles = classifiedEndpoints.changedFiles ?? [];
646
+ const mountedResources = resolveMountPrefixTokens(routerMountContextContents, changedSourceFiles);
647
+ if (mountedResources.length > 0) {
648
+ changedResources = mountedResources;
649
+ }
650
+ else {
651
+ // Fall back to extracting resource names from changed file paths.
652
+ const candidateFiles = classifiedEndpoints.unmatchedFiles.length > 0
653
+ ? classifiedEndpoints.unmatchedFiles
654
+ : classifiedEndpoints.changedFiles ?? [];
655
+ const fromFiles = candidateFiles
656
+ .map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
657
+ .filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
658
+ changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
659
+ }
579
660
  }
580
661
  }
581
662
  else if (classifiedEndpoints.unmatchedFiles.length > 0) {
@@ -594,7 +675,15 @@ export function registerAnalyzeChangesTool(server) {
594
675
  changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
595
676
  }
596
677
  else {
597
- changedResources = [];
678
+ // No endpoints and no unmatched files — the diff touches only non-route
679
+ // files (e.g. schema, model, DTO). Extract resource tokens from the
680
+ // changed file paths so tests that import the changed symbol are still
681
+ // surfaced for drift assessment. Fall back to [] only if extraction
682
+ // yields nothing (e.g. a file with a non-resource name like utils.py).
683
+ const fromChangedFiles = (classifiedEndpoints.changedFiles ?? [])
684
+ .map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
685
+ .filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
686
+ changedResources = fromChangedFiles.length > 0 ? fromChangedFiles : [];
598
687
  }
599
688
  }
600
689
  let existingTests = [];
@@ -875,10 +964,15 @@ export function registerAnalyzeChangesTool(server) {
875
964
  sourceFile: m.sourceFile ?? "",
876
965
  })))
877
966
  : [];
967
+ const removedEndpointsForDrafting = classifiedEndpoints?.removedEndpoints.flatMap((ep) => ep.methods.map((m) => ({
968
+ method: m,
969
+ path: ep.path,
970
+ sourceFile: ep.sourceFile,
971
+ }))) ?? [];
878
972
  const codeInferredScenarios = draftScenariosFromEndpoints(skeletonEndpoints, scenarioDraftSeed, wsAuthType, {
879
973
  changedEndpoints: changedEndpointsForSecurityExpansion,
880
974
  securityRelevantDiff,
881
- });
975
+ }, removedEndpointsForDrafting);
882
976
  let allDraftedScenarios = codeInferredScenarios;
883
977
  if (traceResult && traceResult.userFlows.length > 0) {
884
978
  const traceScenarios = traceResult.userFlows
@@ -1081,7 +1175,6 @@ export function registerAnalyzeChangesTool(server) {
1081
1175
  // can be persisted to the state file for downstream tools (health, drift).
1082
1176
  // Without them, analyzeTestHealth would work only off the static catalog
1083
1177
  // which has wrong paths for nested resources and unsupported frameworks.
1084
- const routerMountContext = grepRouterMountingContext(params.repositoryPath);
1085
1178
  const routeLikeUnmatchedFiles = [];
1086
1179
  for (const file of classifiedEndpoints?.unmatchedFiles ?? []) {
1087
1180
  const routeLike = SOURCE_EXTS.test(file) &&
@@ -1110,25 +1203,6 @@ export function registerAnalyzeChangesTool(server) {
1110
1203
  diffFilePath = path.join(os.tmpdir(), `skyramp-diff-${sessionId}.diff`);
1111
1204
  await fs.promises.writeFile(diffFilePath, diffData.diffContent, { encoding: "utf-8", mode: 0o600 });
1112
1205
  }
1113
- // Read router mount files server-side (size-capped) so the LLM has them
1114
- // inline and doesn't need an extra read step when no spec is available.
1115
- const ROUTER_INLINE_LIMIT = 4096; // bytes — skip files larger than ~4 KB
1116
- const ROUTER_INLINE_MAX_FILES = 3;
1117
- const routerFileContents = routerMountContext
1118
- .slice(0, ROUTER_INLINE_MAX_FILES)
1119
- .flatMap((f) => {
1120
- try {
1121
- const absPath = path.isAbsolute(f) ? f : path.join(params.repositoryPath, f);
1122
- const stat = fs.statSync(absPath);
1123
- if (stat.size > ROUTER_INLINE_LIMIT)
1124
- return [];
1125
- const content = fs.readFileSync(absPath, "utf-8").trimEnd();
1126
- return [{ file: f, content }];
1127
- }
1128
- catch {
1129
- return [];
1130
- }
1131
- });
1132
1206
  // Compute UI context from the diff's changed files using the shared
1133
1207
  // `isFrontendFile` classifier. Persisting this in the stateFile lets
1134
1208
  // skyramp_analyze_test_health and the recommendation prompt consume the
@@ -1136,9 +1210,9 @@ export function registerAnalyzeChangesTool(server) {
1136
1210
  //
1137
1211
  // candidateUiPages is enumerated programmatically via the strategy
1138
1212
  // ladder in uiPageEnumerator (framework route grep, source-grounded
1139
- // routes, root fallback). The same enumeration powers the
1140
- // skyramp_ui_analyze_changes pre-flight tool that the testbot prompt
1141
- // calls before this tool, so both code paths see the same candidates.
1213
+ // routes, root fallback). The agent uses these to capture
1214
+ // browser_blueprints see uiInstructions below, which this tool returns
1215
+ // so the agent captures element vocabulary for UI rec reasoning.
1142
1216
  const uiContext = await (async () => {
1143
1217
  const changedFiles = classifiedEndpoints?.changedFiles ?? [];
1144
1218
  if (changedFiles.length === 0)
@@ -1149,9 +1223,8 @@ export function registerAnalyzeChangesTool(server) {
1149
1223
  // enumeration in.
1150
1224
  // Compute hasFlutterSdkDep once and pass it through so .dart files
1151
1225
  // in a Flutter project are recognised as frontend. See Confluence
1152
- // "Flutter support in Testbot" — this is the second budget-driving
1153
- // call site that must thread the flag (the other is
1154
- // uiAnalyzeChangesTool). Both must agree to avoid silent divergence.
1226
+ // "Flutter support in Testbot" — this is the budget-driving call site
1227
+ // that must thread the flag so a Flutter PR enters the UI pipeline.
1155
1228
  const flutterSdk = hasFlutterSdkDep(params.repositoryPath);
1156
1229
  const frontendFiles = changedFiles.filter((f) => isFrontendFile(f, { hasFlutterSdkDep: flutterSdk }) && !isTestFile(f));
1157
1230
  if (frontendFiles.length === 0)
@@ -1162,6 +1235,16 @@ export function registerAnalyzeChangesTool(server) {
1162
1235
  candidateUiPages,
1163
1236
  };
1164
1237
  })();
1238
+ // Derive the UI blueprint-capture instructions the agent acts on after
1239
+ // this call. Mirrors the branching of the former skyramp_ui_analyze_changes
1240
+ // pre-flight tool, now folded in here so the agent makes one analysis call
1241
+ // instead of two. The captures stay in the agent's tool-result history and
1242
+ // serve as element vocabulary when it writes UI recommendation reasoning.
1243
+ const uiInstructions = !uiContext || uiContext.changedFrontendFiles.length === 0
1244
+ ? NO_UI_INSTRUCTIONS
1245
+ : uiContext.candidateUiPages.length === 0
1246
+ ? NO_RESOLVABLE_URLS_INSTRUCTIONS
1247
+ : buildCaptureInstructions(uiContext.candidateUiPages);
1165
1248
  const unifiedState = {
1166
1249
  existingTests,
1167
1250
  newEndpoints: newEndpointsForDrafting,
@@ -1215,17 +1298,14 @@ export function registerAnalyzeChangesTool(server) {
1215
1298
  relevantExternalTestPaths,
1216
1299
  },
1217
1300
  };
1218
- // Clean up old state files (>24 hours) before creating new one
1219
- const stateDir = params.stateOutputFile
1220
- ? path.dirname(path.resolve(params.stateOutputFile))
1221
- : undefined;
1301
+ // Clean up old state files (>24 hours) before creating new one.
1222
1302
  try {
1223
- await StateManager.cleanupOldFiles(24, stateDir);
1303
+ await StateManager.cleanupOldFiles(24);
1224
1304
  }
1225
1305
  catch (error) {
1226
1306
  logger.warning(`Failed to cleanup old state files: ${error.message}`);
1227
1307
  }
1228
- const stateManager = new StateManager("analysis", sessionId, undefined, params.stateOutputFile);
1308
+ const stateManager = new StateManager("analysis", sessionId);
1229
1309
  await stateManager.writeData(unifiedState, {
1230
1310
  repositoryPath: params.repositoryPath,
1231
1311
  step: "analyze_changes",
@@ -1348,7 +1428,7 @@ export function registerAnalyzeChangesTool(server) {
1348
1428
  content: [
1349
1429
  {
1350
1430
  type: "text",
1351
- text: `\`\`\`json\n${structuredSummary}\n\`\`\`\n\n${outputText}\n\n---\n\n## Pre-built Test Catalog — Fill in placeholders from source code, then display verbatim\n⚠️ Do NOT reformat, rename sections, or generate a new catalog. Replace \`<…from source>\` values, then show this output exactly as-is, grouped by test type.\n\n${recommendationPrompt}`,
1431
+ text: `\`\`\`json\n${structuredSummary}\n\`\`\`\n\n## UI Blueprint Capture — do this BEFORE writing UI recommendation reasoning\n${uiInstructions}\n\n${outputText}\n\n---\n\n## Pre-built Test Catalog — Fill in placeholders from source code, then display verbatim\n⚠️ Do NOT reformat, rename sections, or generate a new catalog. Replace \`<…from source>\` values, then show this output exactly as-is, grouped by test type.\n\n${recommendationPrompt}`,
1352
1432
  },
1353
1433
  ],
1354
1434
  isError: false,
@@ -1,70 +1,91 @@
1
- // Mock all heavy dependencies so the module can be loaded in isolation
2
- jest.mock("@skyramp/skyramp", () => ({}));
3
- jest.mock("simple-git", () => ({ simpleGit: jest.fn() }));
4
- jest.mock("../../services/AnalyticsService.js", () => ({
1
+ // @ts-nocheck - Jest ESM type inference issues
2
+ import { jest } from "@jest/globals";
3
+ import { z } from "zod";
4
+ // Mock all heavy dependencies so the module can be loaded in isolation.
5
+ // Under native ESM, jest.mock is not hoisted — every mock the subject's import
6
+ // graph touches must use unstable_mockModule before the dynamic import below.
7
+ jest.unstable_mockModule("simple-git", () => ({ simpleGit: jest.fn() }));
8
+ jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
5
9
  AnalyticsService: { pushMCPToolEvent: jest.fn() },
6
10
  }));
7
- jest.mock("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
11
+ jest.unstable_mockModule("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
8
12
  buildRecommendationPrompt: jest.fn(),
9
13
  }));
10
- jest.mock("../../prompts/test-recommendation/recommendationSections.js", () => ({
14
+ jest.unstable_mockModule("../../prompts/test-recommendation/recommendationSections.js", () => ({
11
15
  MAX_RECOMMENDATIONS: 10,
12
16
  MAX_TESTS_TO_GENERATE: 3,
13
17
  }));
14
- jest.mock("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
18
+ jest.unstable_mockModule("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
15
19
  buildAnalysisOutputText: jest.fn(),
16
20
  }));
17
- jest.mock("../../services/TestDiscoveryService.js", () => ({
21
+ jest.unstable_mockModule("../../services/TestDiscoveryService.js", () => ({
18
22
  TestDiscoveryService: jest.fn(),
19
23
  }));
20
- jest.mock("../../utils/branchDiff.js", () => ({
24
+ jest.unstable_mockModule("../../utils/branchDiff.js", () => ({
21
25
  computeBranchDiff: jest.fn(),
22
26
  }));
23
- jest.mock("../../utils/routeParsers.js", () => ({
27
+ jest.unstable_mockModule("../../utils/routeParsers.js", () => ({
24
28
  classifyEndpointsByChangedFiles: jest.fn(),
25
29
  extractResourceFromPath: jest.fn(),
26
30
  parseFileEndpoints: jest.fn(),
27
31
  }));
28
- jest.mock("../../utils/repoScanner.js", () => ({
32
+ jest.unstable_mockModule("../../utils/repoScanner.js", () => ({
29
33
  scanAllRepoEndpoints: jest.fn(),
30
34
  scanRelatedEndpoints: jest.fn(),
31
35
  grepRouterMountingContext: jest.fn(),
32
36
  findCandidateRouteFiles: jest.fn(),
33
37
  }));
34
- jest.mock("../../utils/projectMetadata.js", () => ({
38
+ jest.unstable_mockModule("../../utils/projectMetadata.js", () => ({
35
39
  detectProjectMetadata: jest.fn(),
36
40
  }));
37
- jest.mock("../../utils/scenarioDrafting.js", () => ({
41
+ jest.unstable_mockModule("../../utils/scenarioDrafting.js", () => ({
38
42
  draftScenariosFromEndpoints: jest.fn(),
39
43
  }));
40
- jest.mock("../../utils/trace-parser.js", () => ({
44
+ jest.unstable_mockModule("../../utils/trace-parser.js", () => ({
41
45
  parseTraceFile: jest.fn(),
42
46
  discoverTraceFiles: jest.fn(),
43
47
  discoverPlaywrightZips: jest.fn(),
44
48
  }));
45
- jest.mock("../../utils/pr-comment-parser.js", () => ({
49
+ jest.unstable_mockModule("../../utils/pr-comment-parser.js", () => ({
46
50
  parsePRComments: jest.fn(),
47
51
  }));
48
- jest.mock("../../utils/AnalysisStateManager.js", () => ({
52
+ jest.unstable_mockModule("../../utils/AnalysisStateManager.js", () => ({
49
53
  StateManager: jest.fn().mockImplementation(() => ({
50
54
  writeData: jest.fn(),
51
55
  })),
52
56
  registerSession: jest.fn(),
53
57
  storeSessionData: jest.fn(),
58
+ setTestsRepoDir: jest.fn(),
54
59
  }));
55
- jest.mock("../../utils/workspaceAuth.js", () => ({
60
+ // Keep every real workspaceAuth export the subject's import graph needs
61
+ // (readWorkspaceConfigRaw, getDefaultAuthHeader, WorkspaceAuthType, …) and
62
+ // override only parseWorkspaceAuthType. requireActual can't load ESM
63
+ // synchronously, so import the real module first (mock is not hoisted).
64
+ const realWorkspaceAuth = await import("../../utils/workspaceAuth.js");
65
+ jest.unstable_mockModule("../../utils/workspaceAuth.js", () => ({
66
+ ...realWorkspaceAuth,
56
67
  parseWorkspaceAuthType: jest.fn(),
57
68
  }));
58
- jest.mock("../../utils/logger.js", () => ({
69
+ jest.unstable_mockModule("../../utils/logger.js", () => ({
59
70
  logger: { info: jest.fn(), debug: jest.fn(), error: jest.fn(), warn: jest.fn(), warning: jest.fn() },
60
71
  }));
61
- jest.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
72
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/server/mcp.js", () => ({
62
73
  McpServer: jest.fn(),
63
74
  }));
64
- jest.mock("@modelcontextprotocol/sdk/types.js", () => ({}));
65
- jest.mock("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
66
- import { z } from "zod";
67
- import { analyzeChangesInputSchema, isSecurityRelevantDiff } from "./analyzeChangesTool.js";
75
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/types.js", () => ({}));
76
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
77
+ // All subject + mocked-module imports must be dynamic and live AFTER the
78
+ // unstable_mockModule calls above static imports hoist above the mocks under
79
+ // native ESM, loading the real (heavy) modules unmocked.
80
+ const { analyzeChangesInputSchema, isSecurityRelevantDiff, filterEndpointsBySpec, filterUnsupportedGraphQLEndpoints, isGraphQLFile, registerAnalyzeChangesTool, buildTraceFileEntry, buildCaptureInstructions, NO_UI_INSTRUCTIONS, NO_RESOLVABLE_URLS_INSTRUCTIONS, } = await import("./analyzeChangesTool.js");
81
+ const { computeBranchDiff } = await import("../../utils/branchDiff.js");
82
+ const { classifyEndpointsByChangedFiles } = await import("../../utils/routeParsers.js");
83
+ const { scanRelatedEndpoints, scanAllRepoEndpoints } = await import("../../utils/repoScanner.js");
84
+ const { StateManager } = await import("../../utils/AnalysisStateManager.js");
85
+ const os = await import("os");
86
+ const path = await import("path");
87
+ const fsSync = await import("fs");
88
+ const fsModule = fsSync;
68
89
  const schema = z.object(analyzeChangesInputSchema);
69
90
  describe("isSecurityRelevantDiff", () => {
70
91
  it("matches auth-specific middleware and admin-key signals", () => {
@@ -78,68 +99,35 @@ describe("isSecurityRelevantDiff", () => {
78
99
  expect(isSecurityRelevantDiff("move compression middleware to server setup")).toBe(false);
79
100
  });
80
101
  });
81
- describe("analyzeChangesInputSchema — stateOutputFile validation", () => {
82
- it("accepts a valid absolute path", () => {
83
- const result = schema.safeParse({
84
- repositoryPath: "/repo",
85
- stateOutputFile: "/tmp/analyze-changes-state.json",
86
- });
87
- expect(result.success).toBe(true);
88
- });
89
- it("rejects a relative path for stateOutputFile", () => {
90
- // stateOutputFile must be absolute so the caller can guarantee the file location.
91
- // Relative paths are silently ambiguous and should be rejected.
92
- const result = schema.safeParse({
93
- repositoryPath: "/repo",
94
- stateOutputFile: "relative/path/state.json",
95
- });
96
- expect(result.success).toBe(false);
97
- });
98
- it("accepts absence of stateOutputFile (optional field)", () => {
99
- const result = schema.safeParse({ repositoryPath: "/repo" });
100
- expect(result.success).toBe(true);
101
- });
102
- });
103
102
  describe("automatic old files cleanup", () => {
104
- let StateManager;
105
103
  let cleanupOldFilesSpy;
106
104
  beforeEach(() => {
107
105
  jest.clearAllMocks();
108
- StateManager = require("../../utils/AnalysisStateManager.js").StateManager;
109
106
  cleanupOldFilesSpy = jest.fn().mockResolvedValue(0);
110
107
  StateManager.cleanupOldFiles = cleanupOldFilesSpy;
111
108
  });
112
- it("calls cleanupOldFiles with default temp dir when no stateOutputFile provided", async () => {
113
- // This test verifies the cleanup call is made without stateDir when using default location
109
+ it("calls cleanupOldFiles with default temp dir", async () => {
114
110
  expect(StateManager.cleanupOldFiles).toBeDefined();
115
111
  await StateManager.cleanupOldFiles(24, undefined);
116
112
  expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined);
117
113
  });
118
- it("calls cleanupOldFiles with custom dir when stateOutputFile is provided", async () => {
119
- // This test verifies the cleanup call is made with the directory of stateOutputFile
114
+ it("calls cleanupOldFiles with custom dir", async () => {
120
115
  const customPath = "/custom/dir";
121
116
  await StateManager.cleanupOldFiles(24, customPath);
122
117
  expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, customPath);
123
118
  });
124
- it("calls cleanupOldFiles with empty stateTypes to restrict to diff files only", async () => {
125
- // analyzeTestHealthTool passes [] so state files (skyramp-analysis-*, skyramp-recommendation-*)
126
- // are never deleted — the caller still needs args.stateFile for skyramp_actions.
119
+ it("calls cleanupOldFiles with empty stateTypes", async () => {
127
120
  await StateManager.cleanupOldFiles(24, undefined, []);
128
121
  expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined, []);
129
122
  });
130
123
  it("continues execution if cleanup fails", async () => {
131
- // Cleanup failures should not crash the analyze flow
132
124
  cleanupOldFilesSpy.mockRejectedValue(new Error("permission denied"));
133
- await expect(StateManager.cleanupOldFiles(24).catch(() => {
134
- // In the real code, this is caught and logged as a warning
135
- return Promise.resolve();
136
- })).resolves.toBeUndefined();
125
+ await expect(StateManager.cleanupOldFiles(24).catch(() => Promise.resolve())).resolves.toBeUndefined();
137
126
  });
138
127
  });
139
128
  // ─────────────────────────────────────────────────────────────────────────────
140
129
  // filterEndpointsBySpec — spec filtering + merge logic (Step 4c / 4c-merge)
141
130
  // ─────────────────────────────────────────────────────────────────────────────
142
- import { filterEndpointsBySpec } from "./analyzeChangesTool.js";
143
131
  describe("filterEndpointsBySpec", () => {
144
132
  const makeEp = (path, methods = ["GET"]) => ({ path, methods, sourceFile: "" });
145
133
  it("removes scanned endpoints that are not in the spec and not diff-changed", () => {
@@ -224,10 +212,6 @@ describe("filterEndpointsBySpec", () => {
224
212
  // ─────────────────────────────────────────────────────────────────────────────
225
213
  // isGraphQLFile — unit tests
226
214
  // ─────────────────────────────────────────────────────────────────────────────
227
- import { filterUnsupportedGraphQLEndpoints, isGraphQLFile } from "./analyzeChangesTool.js";
228
- import * as os from "os";
229
- import * as path from "path";
230
- import * as fsSync from "fs";
231
215
  describe("isGraphQLFile", () => {
232
216
  const tmpDir = os.tmpdir();
233
217
  it("returns true for .graphql extension without reading content", async () => {
@@ -303,14 +287,6 @@ describe("filterUnsupportedGraphQLEndpoints", () => {
303
287
  readSpy.mockRestore();
304
288
  });
305
289
  });
306
- // ─────────────────────────────────────────────────────────────────────────────
307
- // analyzeChangesTool handler — GraphQL-only early return (handler-level)
308
- // ─────────────────────────────────────────────────────────────────────────────
309
- import { registerAnalyzeChangesTool, buildTraceFileEntry } from "./analyzeChangesTool.js";
310
- import { computeBranchDiff } from "../../utils/branchDiff.js";
311
- import { classifyEndpointsByChangedFiles } from "../../utils/routeParsers.js";
312
- import { scanRelatedEndpoints } from "../../utils/repoScanner.js";
313
- import * as fsModule from "fs";
314
290
  /** Register the tool against a minimal mock server and return the captured handler.
315
291
  * The handler takes (params, extra) — extra is pre-filled with a no-op sendNotification. */
316
292
  function captureAnalyzeHandler() {
@@ -399,7 +375,6 @@ describe("analyzeChangesTool handler — GraphQL-only early return", () => {
399
375
  readSpy.mockRestore();
400
376
  });
401
377
  it("does NOT early-return when scope is full_repo (only fires for PR diffs)", async () => {
402
- const { scanAllRepoEndpoints } = require("../../utils/repoScanner.js");
403
378
  scanAllRepoEndpoints.mockReturnValue([]);
404
379
  const handler = captureAnalyzeHandler();
405
380
  const result = await handler({ ...baseParams, scope: "full_repo" });
@@ -437,3 +412,35 @@ describe("buildTraceFileEntry", () => {
437
412
  expect(entry.userFlows).toEqual([]);
438
413
  });
439
414
  });
415
+ describe("UI blueprint-capture instructions (merged from ui_analyze_changes)", () => {
416
+ it("NO_UI_INSTRUCTIONS signals no capture is needed and references no follow-up tool", () => {
417
+ expect(NO_UI_INSTRUCTIONS).toMatch(/no blueprint capture needed/i);
418
+ // The former pre-flight tool told the agent to "proceed to
419
+ // skyramp_analyze_changes" — that's redundant now we're inside it.
420
+ expect(NO_UI_INSTRUCTIONS).not.toMatch(/skyramp_analyze_changes/);
421
+ });
422
+ it("NO_RESOLVABLE_URLS_INSTRUCTIONS falls back to source-grounded", () => {
423
+ expect(NO_RESOLVABLE_URLS_INSTRUCTIONS).toMatch(/source-grounded/i);
424
+ });
425
+ it("buildCaptureInstructions numbers each candidate URL with its provenance", () => {
426
+ const pages = [
427
+ { url: "http://localhost:3000/notifications", sourcedFrom: ["app/routes.tsx"], strategy: "framework-route-grep" },
428
+ { url: "http://localhost:3000/settings", sourcedFrom: [], strategy: "root-fallback" },
429
+ ];
430
+ const text = buildCaptureInstructions(pages);
431
+ expect(text).toContain("1. http://localhost:3000/notifications");
432
+ expect(text).toContain("2. http://localhost:3000/settings");
433
+ expect(text).toContain("sourcedFrom: app/routes.tsx");
434
+ expect(text).toContain("sourcedFrom: (none)");
435
+ expect(text).toContain("browser_navigate");
436
+ expect(text).toContain("browser_blueprint");
437
+ });
438
+ it("buildCaptureInstructions does NOT emit a login step (uiCredentials dropped)", () => {
439
+ const pages = [
440
+ { url: "http://localhost:3000/x", sourcedFrom: [], strategy: "root-fallback" },
441
+ ];
442
+ const text = buildCaptureInstructions(pages);
443
+ expect(text).not.toMatch(/log in first/i);
444
+ expect(text).not.toMatch(/ui-credentials/i);
445
+ });
446
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAnalyzeTestHealthTool(server: McpServer): void;
@@ -15,11 +15,11 @@ export function registerAnalyzeTestHealthTool(server) {
15
15
  idempotentHint: true,
16
16
  openWorldHint: false,
17
17
  },
18
- description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow. Optionally execute tests with skyramp_execute_test before calling skyramp_actions to capture beforeStatus.`,
18
+ description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow.`,
19
19
  inputSchema: {
20
20
  stateFile: z
21
21
  .string()
22
- .describe("Path to state file from skyramp_analyze_changes. Assessment results exist only in the LLM's reasoning context this tool does not write back to the stateFile."),
22
+ .describe("Path to the analyze-changes-state.json file returned by skyramp_analyze_changes (the stateFile field in its output). Do NOT pass summaryOutputFile or testbot-result.txt here."),
23
23
  },
24
24
  outputSchema: {
25
25
  prompt: z.string().describe("LLM drift analysis prompt and assessment instructions."),
@@ -37,7 +37,10 @@ export function registerAnalyzeTestHealthTool(server) {
37
37
  return toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
38
38
  }
39
39
  if (!repositoryPath || typeof repositoryPath !== "string") {
40
- return toolError(`repositoryPath not found in state file metadata. The state file was likely created by an older versionre-run skyramp_analyze_changes to regenerate it.`);
40
+ return toolError(`repositoryPath not found in state file metadata. The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes not summaryOutputFile or testbot-result.txt. Re-run skyramp_analyze_changes to regenerate it.`);
41
+ }
42
+ if (!Array.isArray(stateData.existingTests)) {
43
+ return toolError(`stateFile does not contain test discovery data (existingTests missing). The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not summaryOutputFile or testbot-result.txt.`);
41
44
  }
42
45
  // Skyramp tests: full drift analysis + all actions permitted.
43
46
  // Relevant external tests (user-written, relevant to this PR's endpoints): drift analysis