@skyramp/mcp 0.3.8 → 0.4.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -1
  2. package/build/commands/commandLibrary.js +3 -3
  3. package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
  4. package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
  5. package/build/commands/testThisEndpointCommand.js +35 -19
  6. package/build/index.js +9 -3
  7. package/build/playwright/blueprintDigest.d.ts +15 -0
  8. package/build/playwright/blueprintDigest.js +152 -0
  9. package/build/playwright/blueprintDigestStore.d.ts +31 -0
  10. package/build/playwright/blueprintDigestStore.js +117 -0
  11. package/build/playwright/registerPlaywrightTools.js +60 -12
  12. package/build/playwright/traceRecordingPrompt.js +8 -7
  13. package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
  14. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
  15. package/build/prompts/promptAssets.d.ts +20 -0
  16. package/build/prompts/promptAssets.js +55 -0
  17. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
  18. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  19. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
  23. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  24. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
  25. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  26. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  27. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  28. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  29. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  30. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  31. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  33. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  34. package/build/prompts/testbot/planDeclarations.js +9 -0
  35. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  36. package/build/prompts/testbot/testbot-prompts.js +256 -381
  37. package/build/recommendation/answers.d.ts +35 -0
  38. package/build/recommendation/answers.js +96 -0
  39. package/build/recommendation/registerPlan.d.ts +49 -0
  40. package/build/recommendation/registerPlan.js +117 -0
  41. package/build/recommendation/runVerifiers.d.ts +10 -0
  42. package/build/recommendation/runVerifiers.js +49 -0
  43. package/build/recommendation/subjectStep.d.ts +42 -0
  44. package/build/recommendation/subjectStep.js +86 -0
  45. package/build/recommendation/types.d.ts +163 -0
  46. package/build/recommendation/types.js +20 -0
  47. package/build/recommendation/verifierContracts.d.ts +382 -0
  48. package/build/recommendation/verifierContracts.js +263 -0
  49. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  50. package/build/recommendation/verifiers/changedFile.js +82 -0
  51. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  52. package/build/recommendation/verifiers/citedPath.js +35 -0
  53. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  54. package/build/recommendation/verifiers/coverage.js +617 -0
  55. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  56. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  57. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  58. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  59. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  60. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  61. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  62. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  63. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  64. package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
  65. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  66. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  67. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  68. package/build/recommendation/verifiers/screenRoute.js +118 -0
  69. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  70. package/build/recommendation/verifiers/statedDifference.js +140 -0
  71. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  72. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  73. package/build/resources/analysisResources.js +1 -114
  74. package/build/resources/testbotResource.js +23 -13
  75. package/build/services/ModularizationService.js +2 -1
  76. package/build/services/TestDiscoveryService.d.ts +3 -72
  77. package/build/services/TestDiscoveryService.js +10 -303
  78. package/build/services/containerEnv.d.ts +1 -1
  79. package/build/services/containerEnv.js +12 -0
  80. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  81. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  82. package/build/toolNames.d.ts +1 -0
  83. package/build/toolNames.js +1 -0
  84. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  85. package/build/tools/code-refactor/modularizationTool.js +2 -1
  86. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  87. package/build/tools/executeSkyrampTestTool.js +246 -19
  88. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  89. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  90. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  91. package/build/tools/generate-tests/planGuard.js +78 -18
  92. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  93. package/build/tools/one-click/oneClickTool.js +0 -5
  94. package/build/tools/submitReportTool.d.ts +48 -42
  95. package/build/tools/submitReportTool.js +576 -193
  96. package/build/tools/test-management/actionsTool.js +72 -4
  97. package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
  98. package/build/tools/test-management/analyzeChangesTool.js +212 -1219
  99. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  100. package/build/tools/test-management/index.d.ts +1 -0
  101. package/build/tools/test-management/index.js +1 -0
  102. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  103. package/build/tools/test-management/registerTestPlanTool.js +609 -542
  104. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  105. package/build/tools/test-management/resolveScreenTool.js +289 -0
  106. package/build/types/BlueprintDigest.d.ts +34 -0
  107. package/build/types/BlueprintDigest.js +1 -0
  108. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  109. package/build/types/RepositoryAnalysis.js +2 -58
  110. package/build/types/StepMethod.d.ts +40 -0
  111. package/build/types/StepMethod.js +77 -0
  112. package/build/types/TestAnalysis.d.ts +12 -0
  113. package/build/types/TestExecution.d.ts +4 -0
  114. package/build/types/TestRecommendation.d.ts +24 -24
  115. package/build/types/TestRecommendation.js +91 -89
  116. package/build/types/TestbotPromptOptions.d.ts +0 -4
  117. package/build/types/TestbotReport.d.ts +64 -2
  118. package/build/utils/AnalysisStateManager.d.ts +79 -113
  119. package/build/utils/AnalysisStateManager.js +147 -57
  120. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  121. package/build/utils/assertion-verify/metrics.js +85 -36
  122. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  123. package/build/utils/assertion-verify/ui-lints.js +32 -0
  124. package/build/utils/branchDiff.d.ts +63 -31
  125. package/build/utils/branchDiff.js +242 -94
  126. package/build/utils/containedPath.d.ts +18 -0
  127. package/build/utils/containedPath.js +73 -0
  128. package/build/utils/dartRouteExtractor.d.ts +18 -34
  129. package/build/utils/dartRouteExtractor.js +101 -173
  130. package/build/utils/featureFlags.d.ts +12 -0
  131. package/build/utils/featureFlags.js +14 -0
  132. package/build/utils/frontendSelectors.d.ts +48 -27
  133. package/build/utils/frontendSelectors.js +241 -80
  134. package/build/utils/pathMatching.d.ts +2 -4
  135. package/build/utils/pathMatching.js +2 -4
  136. package/build/utils/planMatchKeys.d.ts +38 -47
  137. package/build/utils/planMatchKeys.js +143 -81
  138. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  139. package/build/utils/rebaselineSnapshots.js +65 -0
  140. package/build/utils/removedUiElements.d.ts +22 -0
  141. package/build/utils/removedUiElements.js +106 -0
  142. package/build/utils/reportVerification.d.ts +2 -6
  143. package/build/utils/reportVerification.js +61 -2
  144. package/build/utils/screenRoutes.d.ts +66 -0
  145. package/build/utils/screenRoutes.js +727 -0
  146. package/build/utils/sourceRouteExtractor.js +320 -112
  147. package/build/utils/testFileClassification.d.ts +11 -2
  148. package/build/utils/testFileClassification.js +44 -2
  149. package/build/utils/testFixtures.d.ts +5 -0
  150. package/build/utils/testFixtures.js +13 -0
  151. package/build/utils/utils.d.ts +0 -1
  152. package/build/utils/utils.js +0 -11
  153. package/build/utils/versions.d.ts +3 -3
  154. package/build/utils/versions.js +1 -1
  155. package/build/workspace/workspace.d.ts +12 -12
  156. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  157. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  158. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  160. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  161. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  162. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  168. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  171. package/node_modules/playwright/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +8 -0
  174. package/plugin/plugin.json +6 -0
  175. package/plugin/prompts/declaring-a-plan.md +20 -0
  176. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  177. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  178. package/plugin/prompts/generate-tests/generation.md +108 -0
  179. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  180. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  181. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  182. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  183. package/plugin/prompts/plan-tests.md +42 -0
  184. package/plugin/prompts/testbot-task1.md +82 -0
  185. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  186. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  187. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  188. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  189. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  190. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  191. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  192. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  193. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  194. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  195. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  196. package/build/recommendation/budgeters/shared.d.ts +0 -32
  197. package/build/recommendation/budgeters/shared.js +0 -246
  198. package/build/recommendation/discriminators.d.ts +0 -37
  199. package/build/recommendation/discriminators.js +0 -379
  200. package/build/recommendation/diversity.d.ts +0 -47
  201. package/build/recommendation/diversity.js +0 -101
  202. package/build/recommendation/planRanker.d.ts +0 -65
  203. package/build/recommendation/planRanker.js +0 -83
  204. package/build/recommendation/testFixtures.d.ts +0 -25
  205. package/build/recommendation/testFixtures.js +0 -45
  206. package/build/types/FrontendIntegration.d.ts +0 -28
  207. package/build/types/FrontendIntegration.js +0 -22
  208. package/build/types/Recommendation.d.ts +0 -146
  209. package/build/types/Recommendation.js +0 -74
  210. package/build/utils/changedRoutes.d.ts +0 -29
  211. package/build/utils/changedRoutes.js +0 -87
  212. package/build/utils/frontendIntegration.d.ts +0 -9
  213. package/build/utils/frontendIntegration.js +0 -243
  214. package/build/utils/importerHop.d.ts +0 -135
  215. package/build/utils/importerHop.js +0 -489
  216. package/build/utils/pathAffinityClassification.d.ts +0 -49
  217. package/build/utils/pathAffinityClassification.js +0 -180
  218. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  219. package/build/utils/pythonMountPrefixes.js +0 -347
  220. package/build/utils/repoScanner.d.ts +0 -34
  221. package/build/utils/repoScanner.js +0 -300
  222. package/build/utils/routeParsers.d.ts +0 -95
  223. package/build/utils/routeParsers.js +0 -951
  224. package/build/utils/scenarioDrafting.d.ts +0 -92
  225. package/build/utils/scenarioDrafting.js +0 -951
  226. package/build/utils/subjectEndpoints.d.ts +0 -19
  227. package/build/utils/subjectEndpoints.js +0 -98
  228. package/build/utils/uiPageEnumerator.d.ts +0 -172
  229. package/build/utils/uiPageEnumerator.js +0 -474
@@ -1,16 +1,12 @@
1
- import * as crypto from "crypto";
2
1
  import { AnalysisScope, isDiff, } from "../../types/RepositoryAnalysis.js";
3
2
  import { WorkspaceAuthType, getDefaultAuthHeader } from "../../utils/workspaceAuth.js";
4
3
  import { logger } from "../../utils/logger.js";
5
- import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildFewShotExamples, buildVerificationChecklist, getAuthSnippets, MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, } from "./recommendationSections.js";
6
- import { CATEGORY_PRIORITY, Novelty, PriorityTier } from "../../types/TestRecommendation.js";
7
- import { buildScopeAssessmentSection, isFrontendFile } from "./scopeAssessment.js";
8
- import { buildExecutionPlan, EXEC_STEP_CODE_REVIEW, EXEC_STEP_ENRICH } from "./diffExecutionPlan.js";
9
- import { buildFullRepoRecommendations } from "./fullRepoCatalog.js";
10
- import { ANALYSIS_STEP_EXTRACT } from "./analysisOutputPrompt.js";
11
- import { TASK_GENERATE, buildExternalCoverageSet, externalDedupKeys, isAttackSurfaceSecurityBoundary, taskRef, } from "./recommendationShared.js";
12
- // Re-export for backward compatibility (tests and external callers import these from this module)
13
- export { buildExternalCoverageSet, externalDedupKeys };
4
+ import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildVerificationChecklist, } from "./recommendationSections.js";
5
+ import { buildExecutionPlan, EXEC_STEP_ENRICH } from "./diffExecutionPlan.js";
6
+ import { readPromptAsset } from "../promptAssets.js";
7
+ import { renderPlanDeclarationGuidance } from "../testbot/planDeclarations.js";
8
+ import { TASK_GENERATE, taskRef, } from "./recommendationShared.js";
9
+ // Re-export for backward compatibility (tests and external callers import this from this module)
14
10
  function formatTestLocations(locs) {
15
11
  const entries = Object.entries(locs || {});
16
12
  if (entries.length === 0)
@@ -19,218 +15,28 @@ function formatTestLocations(locs) {
19
15
  return ("\n**Existing test coverage (Skyramp + external):**\n" +
20
16
  "| Test type | File (covers: endpoints) |\n" +
21
17
  "|--------------|---------------------------------------------------------|\n" +
22
- rows + "\n\n" +
23
- "**Deduplication rule (apply this table before generating anything):**\n" +
24
- "- `[external]` tests: if a resource is covered by an `[external]` test, do NOT create a new parallel test for the same HTTP method + resource + test type. These tests still break when the API changes — Task 1 maintenance applies to them the same as Skyramp tests (in-place UPDATE only; do not regenerate or delete).\n" +
25
- "- `[skyramp]` contract test: if the HTTP method + path already appears in a `[skyramp]` `covers:` entry of type `contract` → UPDATE that file, do NOT create a new one.\n" +
26
- "- `[skyramp]` integration test: use the endpoints this PR changed. A setup step or a cleanup step is not an endpoint under test. A scenario can test more than one changed endpoint. UPDATE an existing `[skyramp]` `covers:` entry of type `integration` only when it already covers EVERY changed endpoint the scenario tests. If it covers some but not all of them, the scenario is not a duplicate — create it.\n" +
27
- "- UI/E2E test: always create a new file — traces are distinct recordings.\n" +
28
- "For `[skyramp]` contract and integration tests: if in doubt, prefer UPDATE over creating a duplicate.");
29
- }
30
- // ── Priority-tier ordering (replaces numeric CATEGORY_WEIGHTS) ──
31
- // Categories map to HIGH / MEDIUM / LOW tiers.
32
- // Within a tier, novelty (new > modified > existing) breaks ties,
33
- // then cross-resource, step count, and finally the deterministic SHA-256 seed.
34
- // CATEGORY_PRIORITY and PriorityTier imported from ../../types/TestRecommendation.js
35
- const PRIORITY_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
36
- const NOVELTY_ORDER = { new: 3, modified: 2, existing: 1 };
37
- function classifyNovelty(scenario, diffContext) {
38
- if (!diffContext)
39
- return Novelty.EXISTING;
40
- const paths = scenario.steps.map(s => s.path);
41
- const newPaths = new Set((diffContext.newEndpoints || []).map(ep => ep.path));
42
- const modPaths = new Set((diffContext.modifiedEndpoints || []).map(ep => ep.path));
43
- const removedPaths = new Set((diffContext.removedEndpoints || []).map(ep => ep.path));
44
- if (paths.some(p => newPaths.has(p)))
45
- return Novelty.NEW;
46
- if (paths.some(p => modPaths.has(p) || removedPaths.has(p)))
47
- return Novelty.MODIFIED;
48
- return Novelty.EXISTING;
49
- }
50
- function prioritiseCandidate(scenario, diffContext) {
51
- const priority = isAttackSurfaceSecurityBoundary(scenario)
52
- ? PriorityTier.CRITICAL
53
- : CATEGORY_PRIORITY[scenario.category] ?? PriorityTier.LOW;
54
- const novelty = classifyNovelty(scenario, diffContext);
55
- return { priority, novelty };
56
- }
57
- function computeTiebreakerSeed(endpoints, diffFiles) {
58
- const canonical = [...endpoints].sort().join("|") + "::" + [...diffFiles].sort().join("|");
59
- return crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 8);
18
+ rows + "\n");
60
19
  }
20
+ // ── Execution Plan (replaces pre-ranked + scenarios + heuristic sections) ──
61
21
  // Prevents bot-committed test files from being treated as application changes
62
22
  // on subsequent testbot runs on the same PR.
63
23
  const SKYRAMP_TEST_FILE_PATTERN = /(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$|scenario_[^/]+\.json$/;
64
- /**
65
- * Compute the pre-ranked scenario scoring, the UI/API classification flags,
66
- * and the effective GENERATE cap extracted from buildRecommendationPrompt
67
- * so skyramp_analyze_changes (SKYR-3879 Path B pre-seed) can derive the exact
68
- * same candidate set the Execution Plan prompt is built from, without
69
- * duplicating (and risking drift from) this scoring logic.
70
- */
71
- export function computeScoredCandidates(analysis, analysisScope = AnalysisScope.FullRepo, topN = MAX_RECOMMENDATIONS, maxGenerateOverride) {
24
+ /** How this prompt tells the agent to plan: the whole procedure, every time.
25
+ * Rendered from `plugin/prompts/plan-tests.md` and `declaring-a-plan.md` rather
26
+ * than retyped, so there is one text and nothing to drift from. `isDiffScope` is
27
+ * unused here the procedure reads the same for a diff and for a repository. */
28
+ function planningInstruction(_isDiffScope) {
29
+ return `${readPromptAsset("plan-tests.md").trim()}\n\n${renderPlanDeclarationGuidance()}`;
30
+ }
31
+ export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScope.FullRepo, prContext, workspaceAuthHeader, workspaceAuthType, workspaceAuthScheme, sessionId) {
72
32
  const isDiffScope = isDiff(analysisScope);
73
33
  const diffContext = analysis.branchDiffContext;
74
- // ── Filter out bot-generated test files from changedFiles ──
34
+ const openApiSpec = analysis.artifacts?.openApiSpecs?.[0];
75
35
  const filteredChangedFiles = diffContext
76
36
  ? diffContext.changedFiles.filter(f => !SKYRAMP_TEST_FILE_PATTERN.test(f))
77
37
  : [];
78
- // ── Frontend / UI change detection ──
79
- // Used for isUIOnlyPR detection and mode preamble text.
80
- // The actual UI vs backend test split is determined by LLM judgment via the
81
- // scope assessment embedded in buildExecutionPlan (see scopeAssessment.ts).
82
- const hasFrontendChanges = isDiffScope && diffContext
83
- ? filteredChangedFiles.some(f => isFrontendFile(f))
84
- : false;
85
- const hasApiChanges = isDiffScope && diffContext
86
- ? (diffContext.newEndpoints.length > 0 || diffContext.modifiedEndpoints.length > 0 || (diffContext.removedEndpoints?.length ?? 0) > 0)
87
- : false;
88
- const isUIOnlyPR = hasFrontendChanges && !hasApiChanges;
89
- // ── Scoring ──
90
- const baseMaxGen = Math.min(Math.max(maxGenerateOverride ?? (isDiffScope ? MAX_TESTS_TO_GENERATE : topN), 0), topN);
91
- const maxGen = isUIOnlyPR ? Math.max(baseMaxGen, 1) : baseMaxGen;
92
- const scenarios = analysis.businessContext.draftedScenarios;
93
- let scored = [];
94
- let seed = "";
95
- if (!isUIOnlyPR && scenarios.length > 0) {
96
- const diffFiles = filteredChangedFiles; // use filtered list so bot-committed test files don't shift the seed
97
- const endpointPaths = analysis.apiEndpoints.endpoints.map(ep => ep.path);
98
- seed = computeTiebreakerSeed(endpointPaths, diffFiles);
99
- scored = scenarios.map(s => {
100
- const result = prioritiseCandidate(s, diffContext ?? undefined);
101
- return { scenario: s, ...result };
102
- });
103
- scored.sort((a, b) => {
104
- const pa = PRIORITY_ORDER[a.priority], pb = PRIORITY_ORDER[b.priority];
105
- if (pb !== pa)
106
- return pb - pa;
107
- const na = NOVELTY_ORDER[a.novelty], nb = NOVELTY_ORDER[b.novelty];
108
- if (nb !== na)
109
- return nb - na;
110
- const crossA = a.scenario.steps.length > 2 ? 1 : 0;
111
- const crossB = b.scenario.steps.length > 2 ? 1 : 0;
112
- if (crossB !== crossA)
113
- return crossB - crossA;
114
- if (b.scenario.steps.length !== a.scenario.steps.length)
115
- return b.scenario.steps.length - a.scenario.steps.length;
116
- const errorA = a.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
117
- const errorB = b.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
118
- if (errorB !== errorA)
119
- return errorB - errorA;
120
- // Use locale-independent comparison to avoid runtime-locale non-determinism
121
- const nameA = a.scenario.scenarioName;
122
- const nameB = b.scenario.scenarioName;
123
- if (nameA < nameB)
124
- return -1;
125
- if (nameA > nameB)
126
- return 1;
127
- const hashA = parseInt(crypto.createHash("sha256").update(seed + a.scenario.scenarioName).digest("hex").slice(0, 8), 16);
128
- const hashB = parseInt(crypto.createHash("sha256").update(seed + b.scenario.scenarioName).digest("hex").slice(0, 8), 16);
129
- return hashA - hashB;
130
- });
131
- }
132
- return { scored, filteredChangedFiles, isUIOnlyPR, hasFrontendChanges, hasApiChanges, maxGen, seed };
133
- }
134
- export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScope.FullRepo, topN = MAX_RECOMMENDATIONS, prContext, workspaceAuthHeader, workspaceAuthType, workspaceAuthScheme, maxGenerateOverride, sessionId) {
135
- const isDiffScope = isDiff(analysisScope);
136
- const diffContext = analysis.branchDiffContext;
137
- const openApiSpec = analysis.artifacts?.openApiSpecs?.[0];
138
- const { scored, filteredChangedFiles, isUIOnlyPR, hasFrontendChanges, hasApiChanges, maxGen, seed, } = computeScoredCandidates(analysis, analysisScope, topN, maxGenerateOverride);
139
38
  const hasTraces = (analysis.artifacts?.traceFiles?.length ?? 0) > 0 ||
140
39
  (analysis.artifacts?.playwrightRecordings?.length ?? 0) > 0;
141
- // ── Mode preamble ──
142
- const modePreamble = isDiffScope
143
- ? `You are in **PR mode**. Maximize test coverage for the branch changes.
144
- Focus on tests that validate the changed fields, endpoints, and their interactions.
145
- ${isUIOnlyPR ? `\n**UI-only PR** — no backend changes. UI and E2E tests are most relevant.`
146
- : hasFrontendChanges ? `\n**Mixed PR** — both frontend and backend changes detected. Backend and E2E/UI tests are both required.`
147
- : ``}
148
- Output should be concise and immediately actionable.`
149
- : `You are in **Repo mode**. Comprehensive test strategy across all endpoints.`;
150
- // ── UI rec authoring rules ──
151
- // Anchors every UI recommendation regardless of whether the agent captured
152
- // browser_blueprints (from the UI Blueprint Capture section this tool returns).
153
- // When the agent has prior captures in its own tool-result history, those serve
154
- // as grounding; when it doesn't, recommendations fall back to source-grounded
155
- // prose. Either way, inventing elements / leaking jargon / duplicating UI
156
- // tests are wrong, so the rules fire unconditionally.
157
- const uiRecRulesSection = `
158
-
159
- <ui_recommendation_authoring_rules>
160
- **Blueprints inform *how* you describe UI tests, not *which* tests to recommend.** The recommendation catalog derives from the same change-signals listed elsewhere in this prompt (new endpoints, schema/field changes, security boundaries, business-logic modifications, frontend route or component additions, layout additions, etc.) — refer to those signals for the source-of-truth list, not this section.
161
-
162
- **Do not generate near-duplicate UI tests of the same surface;** one well-targeted UI test per surface is enough.
163
-
164
- For UI recommendations you *do* emit, ground the \`reasoning\` field in elements you have actually observed via \`browser_blueprint\` calls earlier in this session. If a recommendation's target element is not in any blueprint you have observed, either rephrase the recommendation around an element that IS observed, or describe the test target in higher-level terms. Do not invent element names from the PR description, source diff, or component name. If you have not captured any blueprints yet (e.g. backend-only PR, or pre-flight skipped), UI recommendations fall back to source-grounded prose drawn from the diff alone — that is a legitimate outcome, not a reason to invent.
165
-
166
- **That rule governs element *identification*, not what you may assert about an identified element.** It does NOT forbid a description-derived **negative** assertion. When the PR description says an element should be gone and a captured page still renders it, the name is blueprint-grounded — you read it off the live page, not off the description — so a failing "is not present" assertion on that page is the correct output, and the requirement conflict is what it documents. Ground \`targetElements\` against the captured page where the element DOES render (the same grounding the negative-test guidance elsewhere in this prompt requires), and cite the test as a \`requirement_conflict\`. What stays forbidden is naming an element **no** blueprint contains because the description mentioned it.
167
-
168
- Write UI recommendation \`reasoning\` fields in **natural prose** that names elements as a human would describe them (e.g. "the Notifications heading", "the disabled Mark all as read button"). **Do NOT mention "blueprint", "captured blueprint", "DOM analyzer", or any other internal MCP terminology in the reasoning text.** The reader of the report is a developer reviewing test recommendations on a PR; they don't know what a blueprint is and shouldn't have to. Phrases like "the captured blueprint shows X" or "visible from the captured blueprint" leak builder internals — instead just describe what the test verifies in plain product terms ("the disabled mark-all-read button in the empty state"). Likewise do NOT use internal-identifier syntax like \`role=button, accessibleName='X', logicalName=...\`.
169
- </ui_recommendation_authoring_rules>
170
- `;
171
- // ── Endpoint listing ──
172
- const allEndpoints = analysis.apiEndpoints.endpoints;
173
- // In PR mode, identify which endpoints were changed so we can partition the listing.
174
- const changedEndpointKeys = new Set();
175
- if (isDiffScope && diffContext) {
176
- for (const ep of [...(diffContext.newEndpoints || []), ...(diffContext.modifiedEndpoints || []), ...(diffContext.removedEndpoints || [])]) {
177
- for (const m of (ep.methods ?? [])) {
178
- changedEndpointKeys.add(`${m.method} ${ep.path}`);
179
- }
180
- }
181
- }
182
- const fmtEndpoint = (m, ep) => ` ${m.method} ${ep.path}${m.authRequired ? " [auth]" : ""} (${(m.interactions ?? []).length} interactions)`;
183
- // In diff scope, cap the reference endpoint list to prevent context overflow.
184
- // Changed endpoints are always shown in full; only the "other" list is capped.
185
- const DIFF_SCOPE_OTHER_ENDPOINT_CAP = 20;
186
- let endpointLines;
187
- if (isDiffScope && changedEndpointKeys.size > 0) {
188
- const changedLines = [];
189
- const otherLines = [];
190
- for (const ep of allEndpoints) {
191
- for (const m of (ep.methods ?? [])) {
192
- const line = fmtEndpoint(m, ep);
193
- if (changedEndpointKeys.has(`${m.method} ${ep.path}`)) {
194
- changedLines.push(line);
195
- }
196
- else {
197
- otherLines.push(line);
198
- }
199
- }
200
- }
201
- // Removed endpoints no longer exist in allEndpoints (current catalog), so they
202
- // would be silently absent from changedLines. Append them explicitly with a
203
- // [removed] marker so the LLM knows to generate verify-404/deprecation tests.
204
- for (const ep of (diffContext?.removedEndpoints || [])) {
205
- for (const m of (ep.methods ?? [])) {
206
- changedLines.push(` ${m.method} ${ep.path} [removed]`);
207
- }
208
- }
209
- const cappedOther = otherLines.slice(0, DIFF_SCOPE_OTHER_ENDPOINT_CAP);
210
- const hiddenOtherCount = otherLines.length - cappedOther.length;
211
- const otherSuffix = hiddenOtherCount > 0
212
- ? `\n ... and ${hiddenOtherCount} more (full endpoint list in state file)`
213
- : "";
214
- const otherLabel = hiddenOtherCount > 0
215
- ? `Other endpoints (reference only, ${cappedOther.length} of ${otherLines.length} shown)`
216
- : "Other endpoints (reference only)";
217
- endpointLines = `**Likely changed in this PR (from static file→endpoint mapping — verify against diff in Step ${ANALYSIS_STEP_EXTRACT}):**\n${changedLines.join("\n") || " none"}\n\n**${otherLabel}:**\n${cappedOther.join("\n") || " none"}${otherSuffix}`;
218
- }
219
- else if (isDiffScope) {
220
- // Diff scope but no changed endpoints detected — cap to avoid dumping the full catalog.
221
- const allMethodLines = allEndpoints.flatMap((ep) => (ep.methods ?? []).map((m) => fmtEndpoint(m, ep)));
222
- const cappedLines = allMethodLines.slice(0, DIFF_SCOPE_OTHER_ENDPOINT_CAP);
223
- const hiddenCount = allMethodLines.length - cappedLines.length;
224
- const suffix = hiddenCount > 0
225
- ? `\n ... and ${hiddenCount} more (full endpoint list in state file — trace changed files directly from the diff to find affected endpoints)`
226
- : "";
227
- endpointLines = `${cappedLines.join("\n") || " none"}${suffix}`;
228
- }
229
- else {
230
- endpointLines = allEndpoints
231
- .flatMap((ep) => (ep.methods ?? []).map((m) => fmtEndpoint(m, ep)))
232
- .join("\n");
233
- }
234
40
  const authMethod = analysis.authentication.method || "unknown";
235
41
  const authTypeValue = workspaceAuthType ?? "";
236
42
  let authHeaderValue;
@@ -252,192 +58,24 @@ Write UI recommendation \`reasoning\` fields in **natural prose** that names ele
252
58
  "Add authHeader: <X-Your-Key-Header> to .skyramp/workspace.yml.");
253
59
  }
254
60
  }
255
- const { authSchemeSnippet } = getAuthSnippets(authHeaderValue, authTypeValue, workspaceAuthScheme);
256
- const routeDiscovery = analysis.routeDiscovery;
257
- const routeDiscoverySection = routeDiscovery
258
- ? `
259
- ## LLM Route Discovery Inputs
260
- Static endpoint data below is a best-effort hint, not a complete parser for every framework.
261
- Authoritative endpoint extraction must come from reading the changed source files, router/module context, OpenAPI paths when available, and the diff.
262
- Candidate files to inspect: ${routeDiscovery.candidateFiles.length > 0 ? routeDiscovery.candidateFiles.join(", ") : "none"}
263
- Router/module context files: ${routeDiscovery.routerMountContext.length > 0 ? routeDiscovery.routerMountContext.join(", ") : "none"}
264
- OpenAPI paths available: ${routeDiscovery.openApiPaths.length}
265
- Static hints available: ${routeDiscovery.staticHints.length}
266
- ${routeDiscovery.diffFilePath ? `Diff file: ${routeDiscovery.diffFilePath}` : ""}
267
- `.trim()
268
- : "";
269
- // ── Source Priority ──
270
- // The ladder ranks sources that describe what the system DOES. The PR
271
- // title/description states what the change is SUPPOSED to do, so it is not a
272
- // rung — resolving a description-vs-code disagreement by rung produced tests
273
- // that asserted the code and buried the contradiction in prose (SKYR-4291).
274
- // Only the ladder-placement rule lives here; the Execution Plan's Code Review
275
- // step is the canonical home for how a conflict is handled, so this document
276
- // states it once. Diff-scoped: repo mode has no PR to compare against.
277
- const requirementConflictRules = isDiffScope ? `
278
- That ladder ranks sources that describe what the system **does**. The **PR title and description** — plus any requirements file they reference — state what this change is **supposed to do**. They are the *intent* side; the ladder is the *implementation* side. Never settle a disagreement between the two by picking the higher rung: a requirement the implemented behavior contradicts is a \`requirement_conflict\`, NOT a case of "the description loses". The Code Review step of the Execution Plan below specifies how to detect, check, and handle one — follow it there.
279
- ` : "";
280
- const sourcePriority = `
281
- ## Source Priority
282
- When information conflicts, prefer: **Traces** (actual behavior) > **Source code read by the LLM** (implemented behavior) > **OpenAPI spec/docs** (documented behavior) > **Static parser hints** (best-effort, may be incomplete or framework-blind).
283
- ${requirementConflictRules}`;
284
- // Compact fingerprint of tests already covering endpoints in this repo (Skyramp + external).
285
- // Re-derived fresh each run from test files on disk — no separate persistence needed.
286
- const testLocations = analysis.existingTests?.testLocations ?? {};
287
- const testFingerprint = (() => {
288
- const entries = Object.entries(testLocations);
289
- if (entries.length === 0)
290
- return "";
291
- // Each value is a comma-joined list of "file (covers: ep1, ep2)" entries — one per file.
292
- // Count files by splitting on "), " boundaries (each entry ends with ")").
293
- let totalFiles = 0;
294
- let externalCount = 0;
295
- const byType = new Map();
296
- for (const [type, fileList] of entries) {
297
- // Use matchAll to extract covers from ALL files of this type, not just the first.
298
- const allEndpoints = [...fileList.matchAll(/covers:\s*([^)]+)/g)].map(m => m[1].trim());
299
- // Count files: strip "[external]", "[skyramp]", and "(covers: ...)" clauses then split on ", " to count entries,
300
- // correctly handling both files-with-covers and files-without-covers (e.g. UI tests).
301
- const strippedList = fileList.replace(/\s*\[(?:external|skyramp)\]/g, "").replace(/\s*\(covers:[^)]*\)/g, "");
302
- totalFiles += strippedList.split(", ").filter(s => s.trim().length > 0).length;
303
- // Count external files from [external] annotations
304
- externalCount += (fileList.match(/\[external\]/g) || []).length;
305
- if (!byType.has(type))
306
- byType.set(type, []);
307
- byType.get(type).push(...allEndpoints);
308
- }
309
- const lines = [...byType.entries()]
310
- .filter(([, eps]) => eps.length > 0)
311
- .map(([type, eps]) => ` ${type}: ${[...new Set(eps)].join(", ")}`);
312
- const skyrampCount = totalFiles - externalCount;
313
- const breakdown = externalCount > 0
314
- ? `${skyrampCount} Skyramp + ${externalCount} external`
315
- : `${totalFiles} files`;
316
- return `\nTests already covering endpoints in this repo (${breakdown}):\n${lines.join("\n")}\n(Use this to focus on coverage gaps. External tests block new recommendations but cannot be updated.)`;
317
- })();
318
61
  const repoContext = `
319
62
  Repository: ${analysis.metadata.repositoryName}
320
63
  Framework: ${analysis.projectClassification.primaryFramework} (${analysis.projectClassification.primaryLanguage})
321
64
  Project type: ${analysis.projectClassification.projectType}
322
65
  Auth: ${authMethod} (header: ${authHeaderValue}${authTypeValue ? `, type: ${authTypeValue}` : ""})
323
- Base URL: ${analysis.apiEndpoints.baseUrl}
324
- Candidate endpoint hints from static scan — unverified and non-exhaustive; confirm paths by reading source/router context before use (${analysis.apiEndpoints.totalCount}):
325
- ${endpointLines}${testFingerprint}
326
- ${routeDiscoverySection ? `
327
-
328
- ${routeDiscoverySection}` : ""}
66
+ Base URL: ${analysis.workspace.baseUrl}
329
67
  `.trim();
330
68
  // ── Branch diff ──
331
69
  let diffSection = "";
332
70
  if (isDiffScope && diffContext) {
333
- const fmtEps = (eps, detail) => eps.flatMap((ep) => (ep.methods ?? []).map((m) => ` ${m.method} ${ep.path} (${detail(m)})`)).join("\n") || " none";
334
71
  diffSection = `
335
72
  ## Branch Diff Context
336
73
  Branch: \`${diffContext.currentBranch}\` → base: \`${diffContext.baseBranch}\`
337
74
  Changed files: ${filteredChangedFiles.join(", ")}
338
- New endpoints:
339
- ${fmtEps(diffContext.newEndpoints, (m) => `${m.sourceFile}, ${m.interactionCount} interactions`)}
340
- Modified endpoints:
341
- ${fmtEps(diffContext.modifiedEndpoints, (m) => `${m.sourceFile}, ${m.changeType}`)}
342
- Removed endpoints:
343
- ${fmtEps(diffContext.removedEndpoints ?? [], (m) => `${m.sourceFile}, removed`)}
344
- Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
345
-
346
- Focus on tests that validate these changes and how they interact with existing resources.
347
- For removed endpoints: verify they now return 404 or the appropriate deprecation status code.
348
- Treat the endpoint lists above as static hints. If source/diff inspection finds a different changed endpoint set, prefer the source-grounded set and use other endpoints only as setup steps.
349
- `;
350
- }
351
- // ── Interactions ──
352
- let interactionSection = "";
353
- {
354
- const diffEndpointKeys = new Set();
355
- if (isDiffScope && diffContext) {
356
- for (const ep of [...(diffContext.newEndpoints || []), ...(diffContext.modifiedEndpoints || [])]) {
357
- for (const m of (ep.methods ?? []))
358
- diffEndpointKeys.add(`${m.method} ${ep.path}`);
359
- }
360
- }
361
- // SKYR-4188: `description` held exactly "<METHOD> <path>" in 11,273 of 11,273
362
- // interaction lines on eval run 32227152533 — a copy of the start of its own
363
- // line. Emit it only when it says something the line does not already say.
364
- const interactionLabel = (i, method, path) => i.description && i.description !== `${method} ${path}` ? `: ${i.description}` : "";
365
- // detailBlocks repeats every summary line and adds the bodies. The two cover
366
- // the same endpoints unless the scope is a diff, so outside a diff the summary
367
- // is pure repetition.
368
- const summaryLines = !isDiffScope ? "" : allEndpoints
369
- .flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}`)))
370
- .join("\n");
371
- const detailEndpoints = isDiffScope
372
- ? allEndpoints.filter((ep) => (ep.methods ?? []).some((m) => diffEndpointKeys.has(`${m.method} ${ep.path}`)))
373
- : allEndpoints;
374
- const detailBlocks = detailEndpoints
375
- .flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => {
376
- const reqBody = i.request.body ? `\n requestBody: ${JSON.stringify(i.request.body)} (pass as JSON string when calling tools)` : "";
377
- const qParams = i.request.queryParams ? `\n queryParams: ${JSON.stringify(i.request.queryParams)}` : "";
378
- const resBody = i.response.body ? `\n responseBody: ${JSON.stringify(i.response.body)}` : "";
379
- const headers = i.request.headers ? `\n headers: ${JSON.stringify(i.request.headers)}` : "";
380
- return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}${reqBody}${qParams}${resBody}${headers}`;
381
- })))
382
- .join("\n");
383
- interactionSection = `
384
- ## Endpoint Interactions
385
- ${summaryLines ? `${summaryLines}\n` : ""}
386
- ### Detailed (request/response bodies)
387
- ${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies and query parameters.
388
- ${detailBlocks}
389
- `;
390
- }
391
- const endpointCount = allEndpoints.reduce((acc, ep) => acc + (ep.methods ?? []).length, 0);
392
- // ── Main section: execution plan, UI-only guidance, or draft-your-own ──
393
- let mainSection;
394
- if (!isDiffScope && scored.length > 0) {
395
- const projectType = analysis.projectClassification.projectType;
396
- const isFrontendProject = projectType === "full-stack" || projectType === "frontend";
397
- const isFrontendOnlyProject = projectType === "frontend";
398
- const externalCoverageFullRepo = buildExternalCoverageSet(testLocations);
399
- if (externalCoverageFullRepo.size > 0) {
400
- logger.info(`External test coverage keys (full-repo): ${[...externalCoverageFullRepo].join(", ")}`);
401
- }
402
- mainSection = buildFullRepoRecommendations(scored, topN, analysis.apiEndpoints.baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, isFrontendProject, isFrontendOnlyProject, externalCoverageFullRepo);
403
- }
404
- else if (isDiffScope) {
405
- // Always use the full execution plan in diff scope — even when scored.length === 0
406
- // (no pre-ranked scenarios). The execution plan includes the dynamic Code Review step,
407
- // bug-catching insertion, and the dynamic bug-coverage gate
408
- // that are critical for catching seeded bugs in new endpoints.
409
- const externalCoverage = buildExternalCoverageSet(testLocations);
410
- if (externalCoverage.size > 0) {
411
- logger.info(`External test coverage keys: ${[...externalCoverage].join(", ")}`);
412
- }
413
- mainSection = buildExecutionPlan(scored, maxGen, topN, analysis.apiEndpoints.baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges, hasTraces, externalCoverage, analysis.existingTests.relevantExternalTestPaths ?? [],
414
- // Zero-classified gating (SKYR-3820) requires an actual diff that classified
415
- // nothing — a missing diffContext is absence of evidence, not an empty
416
- // change surface, so it keeps the legacy fixed-budget rendering.
417
- diffContext ? hasApiChanges : true);
418
- }
419
- else {
420
- // Full-repo scope with no scored items — rare fallback
421
- mainSection = `
422
- ## Draft Your Execution Plan
423
-
424
- No pre-drafted scenarios available.
425
-
426
- ${buildScopeAssessmentSection(topN, maxGen)}
427
-
428
- Draft tests from the endpoint interactions and source code above, following the same tool pipeline described in Tool Workflows below. Prioritize critical categories: security_boundary > data_integrity > business_rule > workflow > crud.
429
-
430
- For each test: pick the highest-impact endpoint(s), draft a realistic scenario with actual request/response shapes from source code, and execute the generation tool pipeline.
431
-
432
- **Honor your Budget Plan: produce exactly the total you committed to (GENERATE + ADDITIONAL). No fewer, no padding with low-value tests.**
433
-
434
- ## Recommendation Stability
435
- - **Carry forward** previous additionalRecommendations that still apply — match by scenarioName (multi-step) or endpoint (single-endpoint). Re-derive category and priority from test content.
436
- - **Only drop** a previous recommendation if its target endpoint was removed, its business logic changed, or it is now covered by a generated test.
437
- - **Only add** new recommendations for code paths introduced since the last run.
438
- - Do not churn recommendations without cause.
439
75
  `;
440
76
  }
77
+ // ── Main section: how to fill a generation tool call, in both scopes ──
78
+ const mainSection = buildExecutionPlan(hasTraces, isDiffScope);
441
79
  // ── OpenAPI spec note ──
442
80
  const specNote = openApiSpec
443
81
  ? `\n**OpenAPI Spec available**: \`${openApiSpec.path}\`
@@ -448,7 +86,6 @@ Use it for contract tests (\`apiSchema: "${openApiSpec.path}"\`) and to extract
448
86
  let prHistorySection = "";
449
87
  if (prContext && prContext.previousRecommendations.length > 0) {
450
88
  const implemented = prContext.previousRecommendations.filter(r => r.status === "implemented");
451
- const recommended = prContext.previousRecommendations.filter(r => r.status === "recommended");
452
89
  let historyBody = "";
453
90
  if (implemented.length > 0) {
454
91
  const implLines = implemented
@@ -466,73 +103,31 @@ ${implLines}${fileLines}`;
466
103
  .join("\n");
467
104
  historyBody += `### Execution Results from Prior Run
468
105
  ${resultLines}
469
- If a test failed previously, check whether the failure was environmental or a real bug,
470
- and adjust the test approach if needed.
471
- `;
472
- }
473
- if (recommended.length > 0) {
474
- // De-duplicate: the parser creates one entry per step-endpoint for
475
- // multi-step scenarios. Collapse them so the LLM sees one line per
476
- // unique (testType, scenarioName) — or (testType, endpoint) when no
477
- // scenarioName exists.
478
- const seen = new Set();
479
- const dedupedRecs = recommended.filter((r) => {
480
- const key = r.scenarioName
481
- ? `${r.testType}::${r.scenarioName}`
482
- : `${r.testType}::${r.endpoint}`;
483
- if (seen.has(key))
484
- return false;
485
- seen.add(key);
486
- return true;
487
- });
488
- const recLines = dedupedRecs
489
- .map((r) => ` - ${r.testType} — ${r.endpoint}${r.scenarioName ? ` (scenario: ${r.scenarioName})` : ""}`)
490
- .join("\n");
491
- historyBody += `
492
- ### Previously Recommended (not generated)
493
- ${recLines}
494
- **Stability rule**: If a previously recommended test still applies to the current code
495
- (the endpoint exists, the business logic hasn't changed), carry it forward in your
496
- additionalRecommendations — match by scenarioName (for multi-step scenarios) or by
497
- endpoint (for single-endpoint tests). Re-derive category and priority from the test
498
- content. Do NOT drop a previous recommendation unless the underlying code was removed
499
- or the test is now covered by a generated test.
500
- Only add NEW recommendations for code paths introduced in the latest commit.
501
106
  `;
502
107
  }
503
108
  prHistorySection = `
504
109
  ## PR History (PR #${prContext.prNumber})
505
- Tests from prior bot runs are still in the working tree the maintenance pipeline
506
- (${taskRef(TASK_GENERATE)}) keeps them up to date. Use the history below to **avoid duplicating** existing
507
- coverage and to fill gaps:
508
- - **Do NOT re-recommend** tests listed under "Previously Generated Tests" — they already
509
- exist and are maintained automatically.
510
- - **Carry forward** previously recommended-but-not-generated tests unchanged in
511
- additionalRecommendations if they still apply. Promote the highest-priority ones
512
- into generation slots if capacity allows.
513
- - **Add new** recommendations only for endpoints or code paths introduced in the latest
514
- commit that aren't covered by existing tests or previous recommendations.
515
- - **Stability**: When the code diff between commits is small, the recommendation set
516
- should be mostly stable. Do not churn recommendations without cause.
517
- - If prior execution results show failures, note the issue but do not re-recommend
518
- the test — ${taskRef(TASK_GENERATE)} handles fixes for existing tests.
110
+ Tests from prior bot runs are still in the working tree, and ${taskRef(TASK_GENERATE)} keeps them
111
+ up to date.
519
112
  ${historyBody}`;
520
113
  }
521
114
  // ── Compose all sections ──
522
115
  // Long-context best practice: all data at top, all instructions + query at bottom.
523
116
  // See: https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#long-context-prompting
117
+ //
118
+ // What to test, and how to declare and submit it, is written ONCE — in
119
+ // `plugin/prompts/plan-tests.md`. This prompt carries the run's data and the
120
+ // generation mechanics, and renders that file for the rest.
524
121
  const scopeNote = isDiffScope
525
122
  ? "Scoped to current branch changes."
526
123
  : "Covers the full repository.";
527
124
  return `
528
125
  ${buildArchitectPreamble(isDiffScope)}
529
126
 
530
- ${modePreamble}
531
-
532
127
  Scope: ${scopeNote}
533
128
 
534
- ${sourcePriority}
535
- ${uiRecRulesSection}
129
+ ${planningInstruction(isDiffScope)}
130
+
536
131
  <repository_context>
537
132
  ## Repository Context
538
133
 
@@ -540,16 +135,11 @@ ${repoContext}
540
135
  ${specNote}
541
136
  </repository_context>
542
137
  ${diffSection ? `<branch_diff>\n${diffSection}\n</branch_diff>` : ""}
543
- <endpoint_interactions>
544
- ${interactionSection}
545
- </endpoint_interactions>
546
-
547
138
  <existing_tests>
548
139
  ## Existing Tests
549
140
 
550
- **Two categories of test files (identified by tag):**
551
- - \`[skyramp]\` generated by Skyramp tools. You may UPDATE these when the covered endpoint changes.
552
- - \`[external]\` — user-written tests (pytest, jest, junit, etc.). Do not generate a new parallel test file for an endpoint already covered by an external test. These tests still break when the API changes — Task 1 maintenance applies to them the same as to Skyramp tests (in-place UPDATE only; do not regenerate or delete).
141
+ Two categories of test file, by tag: \`[skyramp]\` was generated by Skyramp tools, and
142
+ \`[external]\` was written by a user. Both break when the API changes.
553
143
 
554
144
  - Frameworks: ${analysis.existingTests.frameworks.join(", ") || "none"}
555
145
  ${formatTestLocations(analysis.existingTests.testLocations)}
@@ -562,26 +152,11 @@ ${buildContextFetchingGuidance(sessionId)}
562
152
 
563
153
  ${buildReasoningProtocol()}
564
154
 
565
- ${isDiffScope ? buildFewShotExamples() : ""}
566
-
567
- ${isDiffScope
568
- ? (isUIOnlyPR
569
- ? `## How to Generate Tests — Tool Workflows
570
-
571
- **For UI tests (AI-driven recording):**
572
- 1. \`browser_navigate({ url: "<frontend_url>" })\`
573
- 2. Interact with changed components (\`browser_click\`, \`browser_type\`, \`browser_fill_form\`, etc.)
574
- 3. \`browser_snapshot()\` after each interaction
575
- 4. \`skyramp_export_zip({ outputPath: "<repositoryPath>/.skyramp/<component>_trace.zip" })\` — absolute path
576
- 5. \`skyramp_ui_test_generation({ playwrightInput: "<absolute_path_to_zip>" })\`
577
-
578
- **For E2E tests:** Same browser recording flow, then call \`skyramp_e2e_test_generation\` with the zip.`
579
- : buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme, EXEC_STEP_ENRICH))
580
- : ""}
155
+ ${buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme, EXEC_STEP_ENRICH)}
581
156
 
582
157
  ${mainSection}
583
158
 
584
- ${isDiffScope ? buildVerificationChecklist(topN, maxGen, EXEC_STEP_CODE_REVIEW) : ""}
159
+ ${isDiffScope ? buildVerificationChecklist() : ""}
585
160
  </instructions>
586
161
  `;
587
162
  }
@@ -0,0 +1,6 @@
1
+ /** Plan-time guidance for the v2 recommendation path. The text lives in
2
+ * `plugin/prompts/declaring-a-plan.md`. It carries no
3
+ * `{placeholder}`: what each field is for and what its check reads is stated in
4
+ * the plan tool's schema, and the judgment a check cannot make is stated in that
5
+ * check's own objection. */
6
+ export declare function renderPlanDeclarationGuidance(): string;
@@ -0,0 +1,9 @@
1
+ import { readPromptAsset } from "../promptAssets.js";
2
+ /** Plan-time guidance for the v2 recommendation path. The text lives in
3
+ * `plugin/prompts/declaring-a-plan.md`. It carries no
4
+ * `{placeholder}`: what each field is for and what its check reads is stated in
5
+ * the plan tool's schema, and the judgment a check cannot make is stated in that
6
+ * check's own objection. */
7
+ export function renderPlanDeclarationGuidance() {
8
+ return readPromptAsset("declaring-a-plan.md").trim();
9
+ }
@@ -1,5 +1,13 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { RelatedRepository, TestbotPromptOptions } from "../../types/TestbotPromptOptions.js";
3
+ /** "After generation, complete these steps" list at the end of Task 2. Exported
4
+ * for `referenceIntegrity.test.ts`, which compares it against the ordinals the
5
+ * prompt renders. */
6
+ export declare const POSTGEN: Record<"INTEGRATION_ENHANCE" | "CONTRACT_ENHANCE" | "UI_CODE_REUSE" | "UI_ENHANCE" | "WAIT", string>;
7
+ /** The capture-act-capture pattern list inside the UI recording section. */
8
+ export declare const CAPTURE: Record<"BEFORE" | "CUSTOM_WIDGET" | "ACT" | "AFTER" | "POSSIBLE_ASSERTIONS", string>;
9
+ /** The "test execution fails" recovery list under Failure Recovery. */
10
+ export declare const EXEC_FAIL: Record<"DIAGNOSE" | "EXPECTED_FAILURE" | "INFRA_RETRY" | "REPORT_FAIL", string>;
3
11
  /**
4
12
  * Parse the JSON-encoded `relatedRepositories` argument passed via the testbot
5
13
  * prompt/resource. Returns undefined for missing/blank input or malformed JSON so the