@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-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 (177) hide show
  1. package/build/index.js +2 -1
  2. package/build/prompts/code-reuse.d.ts +7 -1
  3. package/build/prompts/code-reuse.js +8 -5
  4. package/build/prompts/code-reuse.test.d.ts +1 -0
  5. package/build/prompts/code-reuse.test.js +62 -0
  6. package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
  7. package/build/prompts/pom-aware-code-reuse.js +98 -52
  8. package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
  9. package/build/prompts/pom-aware-code-reuse.test.js +11 -0
  10. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
  12. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
  13. package/build/prompts/test-recommendation/recommendationSections.js +5 -2
  14. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  15. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
  16. package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
  17. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
  18. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  19. package/build/prompts/testbot/testbot-prompts.js +67 -21
  20. package/build/prompts/testbot/testbot-prompts.test.js +44 -0
  21. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
  22. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
  23. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
  24. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
  25. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
  26. package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
  27. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
  28. package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
  29. package/build/recommendation/budgeters/shared.d.ts +19 -0
  30. package/build/recommendation/budgeters/shared.js +66 -0
  31. package/build/recommendation/discriminators.d.ts +31 -0
  32. package/build/recommendation/discriminators.js +355 -0
  33. package/build/recommendation/discriminators.test.d.ts +1 -0
  34. package/build/recommendation/discriminators.test.js +324 -0
  35. package/build/recommendation/diversity.d.ts +47 -0
  36. package/build/recommendation/diversity.js +101 -0
  37. package/build/recommendation/diversity.test.d.ts +1 -0
  38. package/build/recommendation/diversity.test.js +77 -0
  39. package/build/recommendation/planRanker.d.ts +50 -0
  40. package/build/recommendation/planRanker.js +67 -0
  41. package/build/recommendation/planRanker.test.d.ts +1 -0
  42. package/build/recommendation/planRanker.test.js +110 -0
  43. package/build/recommendation/testFixtures.d.ts +25 -0
  44. package/build/recommendation/testFixtures.js +45 -0
  45. package/build/resources/testbotResource.js +4 -1
  46. package/build/services/ScenarioGenerationService.d.ts +5 -0
  47. package/build/services/ScenarioGenerationService.js +16 -1
  48. package/build/services/ScenarioGenerationService.test.js +44 -0
  49. package/build/services/TestExecutionService.d.ts +15 -1
  50. package/build/services/TestExecutionService.js +210 -55
  51. package/build/services/TestExecutionService.test.js +397 -0
  52. package/build/services/TestGenerationService.js +19 -1
  53. package/build/services/TestGenerationService.test.js +58 -0
  54. package/build/tool-phases.js +1 -0
  55. package/build/toolNames.d.ts +19 -0
  56. package/build/toolNames.js +19 -0
  57. package/build/tools/code-refactor/codeReuseTool.d.ts +3 -0
  58. package/build/tools/code-refactor/codeReuseTool.js +76 -4
  59. package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
  60. package/build/tools/code-refactor/codeReuseTool.test.js +51 -0
  61. package/build/tools/executeSkyrampTestTool.js +8 -2
  62. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
  65. package/build/tools/generate-tests/generateContractRestTool.js +11 -1
  66. package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
  67. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
  68. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
  69. package/build/tools/generate-tests/planGuard.d.ts +13 -0
  70. package/build/tools/generate-tests/planGuard.js +78 -0
  71. package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
  72. package/build/tools/generate-tests/planGuard.test.js +185 -0
  73. package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
  74. package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
  75. package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
  76. package/build/tools/generate-tests/scenarioLint.js +150 -0
  77. package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
  78. package/build/tools/generate-tests/scenarioLint.test.js +100 -0
  79. package/build/tools/submitReportTool.js +78 -0
  80. package/build/tools/submitReportTool.test.js +255 -0
  81. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  82. package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
  83. package/build/tools/test-management/index.d.ts +1 -0
  84. package/build/tools/test-management/index.js +1 -0
  85. package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
  86. package/build/tools/test-management/registerTestPlanTool.js +329 -0
  87. package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
  88. package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
  89. package/build/types/Recommendation.d.ts +97 -0
  90. package/build/types/Recommendation.js +48 -0
  91. package/build/types/RepositoryAnalysis.d.ts +14 -14
  92. package/build/types/TestExecution.d.ts +2 -0
  93. package/build/types/TestRecommendation.d.ts +12 -1
  94. package/build/types/TestRecommendation.js +26 -11
  95. package/build/types/TestTypes.js +1 -1
  96. package/build/utils/AnalysisStateManager.d.ts +47 -0
  97. package/build/utils/docker.test.js +1 -1
  98. package/build/utils/planMatchKeys.d.ts +61 -0
  99. package/build/utils/planMatchKeys.js +125 -0
  100. package/build/utils/pom-scope/import-expansion.d.ts +5 -0
  101. package/build/utils/pom-scope/import-expansion.js +32 -0
  102. package/build/utils/pom-scope/index.d.ts +39 -0
  103. package/build/utils/pom-scope/index.js +120 -0
  104. package/build/utils/pom-scope/index.test.d.ts +1 -0
  105. package/build/utils/pom-scope/index.test.js +239 -0
  106. package/build/utils/pom-scope/pom-files.d.ts +3 -0
  107. package/build/utils/pom-scope/pom-files.js +48 -0
  108. package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
  109. package/build/utils/pom-scope/pom-files.test.js +29 -0
  110. package/build/utils/pom-scope/scoring.d.ts +20 -0
  111. package/build/utils/pom-scope/scoring.js +45 -0
  112. package/build/utils/pom-scope/scoring.test.d.ts +1 -0
  113. package/build/utils/pom-scope/scoring.test.js +39 -0
  114. package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
  115. package/build/utils/pom-scope/selector-extractor.js +57 -0
  116. package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
  117. package/build/utils/pom-scope/selector-extractor.test.js +67 -0
  118. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
  119. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
  120. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
  121. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
  122. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
  123. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
  124. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
  125. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
  126. package/build/utils/pom-verify/bindings.d.ts +19 -0
  127. package/build/utils/pom-verify/bindings.js +161 -0
  128. package/build/utils/pom-verify/bindings.test.d.ts +1 -0
  129. package/build/utils/pom-verify/bindings.test.js +164 -0
  130. package/build/utils/pom-verify/calls.d.ts +16 -0
  131. package/build/utils/pom-verify/calls.js +42 -0
  132. package/build/utils/pom-verify/calls.test.d.ts +1 -0
  133. package/build/utils/pom-verify/calls.test.js +61 -0
  134. package/build/utils/pom-verify/index.d.ts +4 -0
  135. package/build/utils/pom-verify/index.js +4 -0
  136. package/build/utils/pom-verify/resolve.d.ts +7 -0
  137. package/build/utils/pom-verify/resolve.js +27 -0
  138. package/build/utils/pom-verify/resolve.test.d.ts +1 -0
  139. package/build/utils/pom-verify/resolve.test.js +68 -0
  140. package/build/utils/pom-verify/strip.d.ts +9 -0
  141. package/build/utils/pom-verify/strip.js +89 -0
  142. package/build/utils/pom-verify/verify.d.ts +14 -0
  143. package/build/utils/pom-verify/verify.js +158 -0
  144. package/build/utils/pom-verify/verify.test.d.ts +1 -0
  145. package/build/utils/pom-verify/verify.test.js +325 -0
  146. package/build/utils/reportVerification.d.ts +61 -0
  147. package/build/utils/reportVerification.js +104 -0
  148. package/build/utils/reportVerification.test.d.ts +1 -0
  149. package/build/utils/reportVerification.test.js +185 -0
  150. package/build/utils/scenarioDrafting.js +5 -5
  151. package/build/utils/versions.d.ts +3 -3
  152. package/build/utils/versions.js +1 -1
  153. package/build/utils/workspaceAuth.d.ts +9 -1
  154. package/build/utils/workspaceAuth.js +25 -5
  155. package/build/utils/workspaceAuth.test.js +48 -0
  156. package/build/workspace/workspace.d.ts +20 -0
  157. package/build/workspace/workspace.js +4 -0
  158. package/build/workspace/workspace.test.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
  160. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  161. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  170. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  171. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  172. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  173. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +3 -3
  177. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -1,7 +1,7 @@
1
1
  import { DraftedScenario } from "../../types/RepositoryAnalysis.js";
2
- import { PriorityTier } from "../../types/TestRecommendation.js";
2
+ import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
3
3
  export declare function buildFullRepoRecommendations(scored: Array<{
4
4
  scenario: DraftedScenario;
5
5
  priority: PriorityTier;
6
- novelty: string;
6
+ novelty: Novelty;
7
7
  }>, topN: number, baseUrl: string, authHeaderValue: string, authSchemeSnippet: string, authTypeValue: string, isFrontendProject?: boolean, isFrontendOnlyProject?: boolean, externalCoverage?: Set<string>): string;
@@ -51,7 +51,7 @@ Before each GENERATE tool call, confirm WHERE each key value comes from:
51
51
  - **endpointURL** → workspace \`baseUrl\` + endpoint path (both required — never path alone)
52
52
  - **authHeader / authScheme** → workspace config or OpenAPI \`securitySchemes\`
53
53
  - **Foreign-key path params** → chained from a prior step's response — never invented or hardcoded. Common field names: \`id\`, \`uuid\`, \`_id\`, \`*_id\`; use whatever identifier field the server returns for this resource. The chaining source can be a response body (POST or GET), a response header (e.g. \`Location\`), or a cookie.
54
- - **Names / string values** → realistic; append timestamp suffix to avoid re-run conflicts
54
+ - **Names / string values** → realistic. Do NOT hardcode a timestamp/uuid suffix. Instead, for create fields that carry a UNIQUE constraint (e.g. \`name\`, \`slug\`, \`email\` — confirm from the source schema: \`unique=True\`, SQL \`UNIQUE\`, DTO), list their field paths in the step's \`uniqueFields\` (gjson notation) — the generator injects a run-unique value so re-runs don't 409.
55
55
 
56
56
  ## Ranking Rule
57
57
  For each GENERATE item, include one sentence in your output (before the tool calls) stating the specific bug or failure it targets — derived from \`bugCatchingTarget\` or your source-code reading. Example: "Targets: order total miscalculation — total_amount = sum(item.price × item.quantity) should recompute when items array changes."
@@ -332,7 +332,10 @@ ${authGuidance}
332
332
  collection array (e.g. \`"items": [{"product_id": <chained from prior POST>, "quantity": 2}]\`).
333
333
  Never send a PATCH that only modifies metadata (discount, status) without also including the
334
334
  items/products collection — such a test will not catch collection-level or total-recalculation bugs.
335
- Use unique names with timestamp suffix to avoid conflicts on re-runs.
335
+ For UNIQUE-constrained create fields, set the step's \`uniqueFields\` (gjson paths) rather than
336
+ hardcoding timestamp suffixes — the generator makes them run-unique so re-runs don't 409.
337
+ For every resource-creating step (POST/PUT), also add a matching DELETE step for the created
338
+ resource (path ID chained from the create response) so the scenario cleans up after itself.
336
339
  For GET/PUT/DELETE with path IDs, use a placeholder — chaining resolves the real ID.
337
340
  2. Produces a \`scenario_<name>.json\` in the same \`outputDir\` as the test files (not \`.skyramp/\`).
338
341
  3. Call \`skyramp_integration_test_generation\` with \`scenarioFile\`: ${integrationAuthNote}
@@ -200,7 +200,7 @@ Budget Plan: 0 total — no new, modified, or removed endpoints were classified
200
200
 
201
201
  With a 0-total Budget Plan: generate zero tests, recommend zero tests, and follow the zero-test report path. Do NOT draft baseline or generic tests for unchanged endpoints to fill a budget — an empty diff surface is a valid, expected outcome.
202
202
 
203
- **Exception — claim the ceiling only with evidence:** if your code review of the changed files shows an observable API behavior change the classifier missed (e.g. a DTO/serializer/service change that alters a response shape, or a deployment/config change that newly exposes or removes endpoints), raise your Budget Plan to cover exactly those affected endpoints, up to ${maxTotal} total (${effectiveGenerate} generate + ${additional} additional), 0% UI/E2E. Every test must name the changed file that justifies it. State your raised plan now in the canonical format — \`Budget Plan: <total> total (<generate> generate + <additional> additional), 0% UI/E2E\` — and use those exact numbers throughout the rest of the prompt; the raised generate count is your committed generate count.`;
203
+ **Exception — claim the ceiling only with evidence:** if your code review of the changed files shows an observable API behavior change the classifier missed (e.g. a DTO/serializer/service change that alters a response shape, a deployment/config change that newly exposes or removes endpoints, or a schema-defined API contract change — a CRD type/kubebuilder validation marker, GraphQL schema, or gRPC proto edit that adds, removes, or re-validates what the server accepts or returns), raise your Budget Plan to cover exactly those affected endpoints, up to ${maxTotal} total (${effectiveGenerate} generate + ${additional} additional), 0% UI/E2E. Note: repositories whose entire API surface is schema-defined (e.g. a Kubernetes operator serving CRDs through the kube-apiserver) ALWAYS classify zero endpoints — for these, a schema change in the diff IS the endpoint change; evaluate this exception against the schema files instead of concluding there is nothing to test. Every test must name the changed file that justifies it. State your raised plan now in the canonical format — \`Budget Plan: <total> total (<generate> generate + <additional> additional), 0% UI/E2E\` — and use those exact numbers throughout the rest of the prompt; the raised generate count is your committed generate count.`;
204
204
  }
205
205
  // Unambiguous backend-only or UI-only: emit a single Budget Plan line — no LLM counting needed.
206
206
  if (precomputedUIPct !== undefined) {
@@ -1,6 +1,31 @@
1
- import { RepositoryAnalysis, AnalysisScope } from "../../types/RepositoryAnalysis.js";
1
+ import { RepositoryAnalysis, AnalysisScope, DraftedScenario } from "../../types/RepositoryAnalysis.js";
2
2
  import { WorkspaceAuthType } from "../../utils/workspaceAuth.js";
3
3
  import { PRTestContext } from "../../utils/pr-comment-parser.js";
4
+ import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
4
5
  import { buildExternalCoverageSet, externalDedupKey } from "./recommendationShared.js";
5
6
  export { buildExternalCoverageSet, externalDedupKey };
7
+ /** Result of {@link computeScoredCandidates} — the scoring/classification
8
+ * inputs shared between prompt rendering and the SKYR-3879 register-plan
9
+ * pre-seed (analyzeChangesTool.ts), so both derive the exact same numbers. */
10
+ export interface ScoredCandidatesResult {
11
+ scored: Array<{
12
+ scenario: DraftedScenario;
13
+ priority: PriorityTier;
14
+ novelty: Novelty;
15
+ }>;
16
+ filteredChangedFiles: string[];
17
+ isUIOnlyPR: boolean;
18
+ hasFrontendChanges: boolean;
19
+ hasApiChanges: boolean;
20
+ maxGen: number;
21
+ seed: string;
22
+ }
23
+ /**
24
+ * Compute the pre-ranked scenario scoring, the UI/API classification flags,
25
+ * and the effective GENERATE cap — extracted from buildRecommendationPrompt
26
+ * so skyramp_analyze_changes (SKYR-3879 Path B pre-seed) can derive the exact
27
+ * same candidate set the Execution Plan prompt is built from, without
28
+ * duplicating (and risking drift from) this scoring logic.
29
+ */
30
+ export declare function computeScoredCandidates(analysis: RepositoryAnalysis, analysisScope?: AnalysisScope, topN?: number, maxGenerateOverride?: number): ScoredCandidatesResult;
6
31
  export declare function buildRecommendationPrompt(analysis: RepositoryAnalysis, analysisScope?: AnalysisScope, topN?: number, prContext?: PRTestContext, workspaceAuthHeader?: string, workspaceAuthType?: WorkspaceAuthType, workspaceAuthScheme?: string, maxGenerateOverride?: number, sessionId?: string): string;
@@ -3,7 +3,7 @@ import { AnalysisScope, isDiff, } from "../../types/RepositoryAnalysis.js";
3
3
  import { WorkspaceAuthType, getDefaultAuthHeader } from "../../utils/workspaceAuth.js";
4
4
  import { logger } from "../../utils/logger.js";
5
5
  import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildFewShotExamples, buildVerificationChecklist, getAuthSnippets, MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, } from "./recommendationSections.js";
6
- import { CATEGORY_PRIORITY } from "../../types/TestRecommendation.js";
6
+ import { CATEGORY_PRIORITY, Novelty, PriorityTier } from "../../types/TestRecommendation.js";
7
7
  import { buildScopeAssessmentSection, isFrontendFile } from "./scopeAssessment.js";
8
8
  import { buildExecutionPlan, EXEC_STEP_CODE_REVIEW } from "./diffExecutionPlan.js";
9
9
  import { buildFullRepoRecommendations } from "./fullRepoCatalog.js";
@@ -36,21 +36,21 @@ const PRIORITY_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
36
36
  const NOVELTY_ORDER = { new: 3, modified: 2, existing: 1 };
37
37
  function classifyNovelty(scenario, diffContext) {
38
38
  if (!diffContext)
39
- return "existing";
39
+ return Novelty.EXISTING;
40
40
  const paths = scenario.steps.map(s => s.path);
41
41
  const newPaths = new Set((diffContext.newEndpoints || []).map(ep => ep.path));
42
42
  const modPaths = new Set((diffContext.modifiedEndpoints || []).map(ep => ep.path));
43
43
  const removedPaths = new Set((diffContext.removedEndpoints || []).map(ep => ep.path));
44
44
  if (paths.some(p => newPaths.has(p)))
45
- return "new";
45
+ return Novelty.NEW;
46
46
  if (paths.some(p => modPaths.has(p) || removedPaths.has(p)))
47
- return "modified";
48
- return "existing";
47
+ return Novelty.MODIFIED;
48
+ return Novelty.EXISTING;
49
49
  }
50
50
  function prioritiseCandidate(scenario, diffContext) {
51
51
  const priority = isAttackSurfaceSecurityBoundary(scenario)
52
- ? "CRITICAL"
53
- : CATEGORY_PRIORITY[scenario.category] ?? "LOW";
52
+ ? PriorityTier.CRITICAL
53
+ : CATEGORY_PRIORITY[scenario.category] ?? PriorityTier.LOW;
54
54
  const novelty = classifyNovelty(scenario, diffContext);
55
55
  return { priority, novelty };
56
56
  }
@@ -58,15 +58,20 @@ function computeTiebreakerSeed(endpoints, diffFiles) {
58
58
  const canonical = [...endpoints].sort().join("|") + "::" + [...diffFiles].sort().join("|");
59
59
  return crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 8);
60
60
  }
61
- // ── Execution Plan (replaces pre-ranked + scenarios + heuristic sections) ──
62
- export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScope.FullRepo, topN = MAX_RECOMMENDATIONS, prContext, workspaceAuthHeader, workspaceAuthType, workspaceAuthScheme, maxGenerateOverride, sessionId) {
61
+ // Prevents bot-committed test files from being treated as application changes
62
+ // on subsequent testbot runs on the same PR.
63
+ 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) {
63
72
  const isDiffScope = isDiff(analysisScope);
64
73
  const diffContext = analysis.branchDiffContext;
65
- const openApiSpec = analysis.artifacts?.openApiSpecs?.[0];
66
74
  // ── Filter out bot-generated test files from changedFiles ──
67
- // Prevents bot-committed test files from being treated as application changes
68
- // on subsequent testbot runs on the same PR.
69
- const SKYRAMP_TEST_FILE_PATTERN = /(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$|scenario_[^/]+\.json$/;
70
75
  const filteredChangedFiles = diffContext
71
76
  ? diffContext.changedFiles.filter(f => !SKYRAMP_TEST_FILE_PATTERN.test(f))
72
77
  : [];
@@ -81,6 +86,56 @@ export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScop
81
86
  ? (diffContext.newEndpoints.length > 0 || diffContext.modifiedEndpoints.length > 0 || (diffContext.removedEndpoints?.length ?? 0) > 0)
82
87
  : false;
83
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);
84
139
  const hasTraces = (analysis.artifacts?.traceFiles?.length ?? 0) > 0 ||
85
140
  (analysis.artifacts?.playwrightRecordings?.length ?? 0) > 0;
86
141
  // ── Mode preamble ──
@@ -277,50 +332,7 @@ ${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pyd
277
332
  ${detailBlocks}
278
333
  `;
279
334
  }
280
- // ── Scoring ──
281
335
  const endpointCount = allEndpoints.reduce((acc, ep) => acc + (ep.methods ?? []).length, 0);
282
- const baseMaxGen = Math.min(Math.max(maxGenerateOverride ?? (isDiffScope ? MAX_TESTS_TO_GENERATE : topN), 0), topN);
283
- const maxGen = isUIOnlyPR ? Math.max(baseMaxGen, 1) : baseMaxGen;
284
- const scenarios = analysis.businessContext.draftedScenarios;
285
- let scored = [];
286
- let seed = "";
287
- if (!isUIOnlyPR && scenarios.length > 0) {
288
- const diffFiles = filteredChangedFiles; // use filtered list so bot-committed test files don't shift the seed
289
- const endpointPaths = allEndpoints.map(ep => ep.path);
290
- seed = computeTiebreakerSeed(endpointPaths, diffFiles);
291
- scored = scenarios.map(s => {
292
- const result = prioritiseCandidate(s, diffContext ?? undefined);
293
- return { scenario: s, ...result };
294
- });
295
- scored.sort((a, b) => {
296
- const pa = PRIORITY_ORDER[a.priority], pb = PRIORITY_ORDER[b.priority];
297
- if (pb !== pa)
298
- return pb - pa;
299
- const na = NOVELTY_ORDER[a.novelty], nb = NOVELTY_ORDER[b.novelty];
300
- if (nb !== na)
301
- return nb - na;
302
- const crossA = a.scenario.steps.length > 2 ? 1 : 0;
303
- const crossB = b.scenario.steps.length > 2 ? 1 : 0;
304
- if (crossB !== crossA)
305
- return crossB - crossA;
306
- if (b.scenario.steps.length !== a.scenario.steps.length)
307
- return b.scenario.steps.length - a.scenario.steps.length;
308
- const errorA = a.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
309
- const errorB = b.scenario.steps.some(s => s.interactionType === "error" || s.interactionType === "edge-case") ? 1 : 0;
310
- if (errorB !== errorA)
311
- return errorB - errorA;
312
- // Use locale-independent comparison to avoid runtime-locale non-determinism
313
- const nameA = a.scenario.scenarioName;
314
- const nameB = b.scenario.scenarioName;
315
- if (nameA < nameB)
316
- return -1;
317
- if (nameA > nameB)
318
- return 1;
319
- const hashA = parseInt(crypto.createHash("sha256").update(seed + a.scenario.scenarioName).digest("hex").slice(0, 8), 16);
320
- const hashB = parseInt(crypto.createHash("sha256").update(seed + b.scenario.scenarioName).digest("hex").slice(0, 8), 16);
321
- return hashA - hashB;
322
- });
323
- }
324
336
  // ── Main section: execution plan, UI-only guidance, or draft-your-own ──
325
337
  let mainSection;
326
338
  if (!isDiffScope && scored.length > 0) {
@@ -1,10 +1,11 @@
1
1
  import { jest } from "@jest/globals";
2
+ import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
2
3
  import { TestType } from "../../types/TestTypes.js";
3
4
  jest.unstable_mockModule("@skyramp/skyramp", () => ({
4
5
  WorkspaceConfigManager: { create: jest.fn() },
5
6
  }));
6
7
  const { buildRecommendationPrompt, buildExternalCoverageSet, externalDedupKey } = await import("./test-recommendation-prompt.js");
7
- const { buildExecutionPlan } = await import("./diffExecutionPlan.js");
8
+ const { buildExecutionPlan, EXEC_STEP_REGISTER } = await import("./diffExecutionPlan.js");
8
9
  const { PATH_PARAM_UUID_GUIDANCE, MAX_TESTS_TO_GENERATE, buildTestQualityCriteria, buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildFewShotExamples, buildVerificationChecklist, } = await import("./recommendationSections.js");
9
10
  import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
10
11
  // ---------------------------------------------------------------------------
@@ -746,6 +747,42 @@ describe("buildRecommendationPrompt — zero-classified diff (SKYR-3820)", () =>
746
747
  });
747
748
  });
748
749
  // ---------------------------------------------------------------------------
750
+ // Tests — REGISTER step (SKYR-3879 Path B checkpoint)
751
+ // ---------------------------------------------------------------------------
752
+ describe("buildRecommendationPrompt — REGISTER step (SKYR-3879 Path B)", () => {
753
+ it("diff-scope execution plan includes the REGISTER step and its skyramp_register_test_plan instruction", () => {
754
+ const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
755
+ expect(prompt).toContain(`Step ${EXEC_STEP_REGISTER} — Register your test plan`);
756
+ expect(prompt).toContain("skyramp_register_test_plan");
757
+ });
758
+ it("diff-scope execution plan's GENERATE header references the REGISTER step", () => {
759
+ const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
760
+ expect(prompt).toContain(`registering via Step ${EXEC_STEP_REGISTER}`);
761
+ });
762
+ it("full-repo mode (no execution plan) never mentions skyramp_register_test_plan", () => {
763
+ const scenariosForFullRepo = [
764
+ minimalScenario({
765
+ scenarioName: "scenario-0",
766
+ category: "crud",
767
+ priority: "low",
768
+ testType: TestType.CONTRACT,
769
+ steps: [{ order: 1, method: "GET", path: "/api/items", description: "Get items", interactionType: "success", expectedStatusCode: 200 }],
770
+ }),
771
+ ];
772
+ const analysis = minimalAnalysis({
773
+ businessContext: { mainPurpose: "Test API", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: scenariosForFullRepo },
774
+ });
775
+ const prompt = buildRecommendationPrompt(analysis, AnalysisScope.FullRepo, 6);
776
+ expect(prompt).not.toContain("skyramp_register_test_plan");
777
+ });
778
+ it("diff-scope 'draft your own' fallback (no pre-drafted scenarios) still includes the REGISTER step — the execution plan is always used in diff scope", () => {
779
+ const prompt = buildRecommendationPrompt(minimalAnalysis(), AnalysisScope.CurrentBranchDiff, 5);
780
+ // minimalAnalysis() has no draftedScenarios and no branchDiffContext, so scored=[]
781
+ // but buildExecutionPlan still renders (see buildRecommendationPrompt's isDiffScope branch).
782
+ expect(prompt).toContain("skyramp_register_test_plan");
783
+ });
784
+ });
785
+ // ---------------------------------------------------------------------------
749
786
  // Tests — GENERATE slot allocation (UI vs backend slots)
750
787
  // ---------------------------------------------------------------------------
751
788
  describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
@@ -920,13 +957,13 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
920
957
  });
921
958
  const prompt = buildExecutionPlan([attackSurface, bugCaught, ordinary].map((scenario) => ({
922
959
  scenario,
923
- priority: scenario.category === "crud" ? "LOW" : "CRITICAL",
924
- novelty: "modified",
960
+ priority: scenario.category === "crud" ? PriorityTier.LOW : PriorityTier.CRITICAL,
961
+ novelty: Novelty.MODIFIED,
925
962
  })), 3, 3, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 3, false, false, false, new Set(["POST::flows::contract", "POST::orders::contract", "POST::customers::contract"]));
926
963
  expect(prompt).toContain("POST /api/flows/bulk_delete → 401");
927
964
  expect(prompt).toContain("POST /api/orders → 201");
928
965
  expect(prompt).not.toContain("POST /api/customers → 201");
929
- expect(prompt).toContain("except for `bug_caught` and attack-surface `security_boundary` items");
966
+ expect(prompt).toContain("`bug_caught` and attack-surface `security_boundary` items get the strictest reading");
930
967
  });
931
968
  it("keeps symmetric attack-surface siblings together before ordinary auth boundaries", () => {
932
969
  const directWidgets = minimalScenario({
@@ -959,8 +996,8 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
959
996
  });
960
997
  const prompt = buildExecutionPlan([directWidgets, directGadgets, widgetsBulk, gadgetsBulk].map((scenario) => ({
961
998
  scenario,
962
- priority: "CRITICAL",
963
- novelty: "modified",
999
+ priority: PriorityTier.CRITICAL,
1000
+ novelty: Novelty.MODIFIED,
964
1001
  })), 3, 4, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", 4, false);
965
1002
  const generatedBlock = prompt.slice(0, prompt.indexOf("#4 [ADDITIONAL]"));
966
1003
  const additionalBlock = prompt.slice(prompt.indexOf("#4 [ADDITIONAL]"));
@@ -975,7 +1012,7 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
975
1012
  // Backend-only PR (hasFrontendChanges=false) so every GENERATE slot is a backend
976
1013
  // scenario drawn from `scored` — this is where contract-vs-integration distribution
977
1014
  // applies. baseUrl/auth args mirror the existing buildExecutionPlan tests above.
978
- const contract = (name, path, priority = "HIGH") => ({
1015
+ const contract = (name, path, priority = PriorityTier.HIGH) => ({
979
1016
  scenario: minimalScenario({
980
1017
  scenarioName: name,
981
1018
  category: "crud",
@@ -983,9 +1020,9 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
983
1020
  steps: [{ order: 1, method: "POST", path, description: `POST ${path}`, interactionType: "success", expectedStatusCode: 201 }],
984
1021
  }),
985
1022
  priority: priority,
986
- novelty: "new",
1023
+ novelty: Novelty.NEW,
987
1024
  });
988
- const integration = (name, p1, p2, priority = "HIGH") => ({
1025
+ const integration = (name, p1, p2, priority = PriorityTier.HIGH) => ({
989
1026
  scenario: minimalScenario({
990
1027
  scenarioName: name,
991
1028
  category: "data_integrity",
@@ -996,7 +1033,7 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
996
1033
  ],
997
1034
  }),
998
1035
  priority: priority,
999
- novelty: "new",
1036
+ novelty: Novelty.NEW,
1000
1037
  });
1001
1038
  const plan = (items, maxGen, topN) => buildExecutionPlan(items, maxGen, topN, "http://localhost:3000", "Authorization", ", authScheme: \"Bearer\"", "", "seed", items.length, false);
1002
1039
  it("distributes GENERATE slots across contract AND integration when budget < candidates", () => {
@@ -1025,7 +1062,7 @@ describe("buildExecutionPlan — even-with-spillover test-type distribution", ()
1025
1062
  // A CRITICAL integration sits after several HIGH contracts in rank order.
1026
1063
  const items = [
1027
1064
  contract("c1", "/api/a"), contract("c2", "/api/b"), contract("c3", "/api/c"),
1028
- integration("crit", "/api/orders", "/api/orders/{id}", "CRITICAL"),
1065
+ integration("crit", "/api/orders", "/api/orders/{id}", PriorityTier.CRITICAL),
1029
1066
  ];
1030
1067
  const prompt = plan(items, 3, 6);
1031
1068
  const generated = prompt.slice(0, prompt.indexOf("[ADDITIONAL]") >= 0 ? prompt.indexOf("[ADDITIONAL]") : prompt.length);
@@ -16,7 +16,7 @@ export interface RelatedRepository {
16
16
  */
17
17
  export declare function parseRelatedRepositories(raw: string | undefined): RelatedRepository[] | undefined;
18
18
  export declare function getTestbotPrompt(prTitle: string, prDescription: string, summaryOutputFile: string, repositoryPath: string, baseBranch?: string, maxRecommendations?: number, maxGenerate?: number, _maxCritical?: number, // Reserved — accepted for API compat but not yet wired into prompt
19
- prNumber?: number, userPrompt?: string, services?: Service[], uiCredentials?: string, testsRepoDir?: string, relatedRepositories?: RelatedRepository[], primaryRepo?: string): string;
19
+ prNumber?: number, userPrompt?: string, services?: Service[], uiCredentials?: string, testsRepoDir?: string, relatedRepositories?: RelatedRepository[], primaryRepo?: string, planOnly?: boolean): string;
20
20
  /**
21
21
  * Read services from .skyramp/workspace.yml. Returns empty array if
22
22
  * the workspace file doesn't exist or can't be parsed.
@@ -47,7 +47,7 @@ export function parseRelatedRepositories(raw) {
47
47
  }
48
48
  }
49
49
  export function getTestbotPrompt(prTitle, prDescription, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE, _maxCritical = MAX_CRITICAL_TESTS, // Reserved — accepted for API compat but not yet wired into prompt
50
- prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo) {
50
+ prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false) {
51
51
  maxGenerate = Math.min(Math.max(maxGenerate, 0), maxRecommendations);
52
52
  // TODO(SKYR-3636 follow-up): migrate Task 1 + Task 2 step bodies to PromptPlan
53
53
  // (src/prompts/test-recommendation/promptPlan.ts) so step numbers don't have
@@ -65,6 +65,17 @@ prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories
65
65
  // testDirectory, relative to the delivery root (the configured test repo if set,
66
66
  // otherwise the primary repo).
67
67
  const hasRelatedRepos = !!relatedRepositories?.length;
68
+ // Task 1 maintenance step 2c branches in plan-only eval runs (SKYR-3879
69
+ // plan-only lane): the SUT is not running, so the pre-edit baseline
70
+ // execution is replaced by static-analysis-only verdicts. Defined before
71
+ // task1Section, which interpolates it.
72
+ let maintenanceBeforeExecStep;
73
+ if (planOnly) {
74
+ maintenanceBeforeExecStep = ` c. Plan-only run: skip the pre-edit baseline execution — the application is not running. Record every maintenance verdict from static drift analysis alone; execution statuses simply remain unrecorded.`;
75
+ }
76
+ else {
77
+ maintenanceBeforeExecStep = ` c. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every UPDATE/REGENERATE/DELETE test. Exclude tests marked \`[external]\`. Run them sequentially, not in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail.`;
78
+ }
68
79
  // For follow-up requests: emit the @skyramp-testbot header + guardrails + retrieve-recommendations step.
69
80
  // For first-run prompts: emit the full Task 1 analysis + maintenance section.
70
81
  const task1Section = userPrompt
@@ -131,7 +142,7 @@ ${hasRelatedRepos ? `
131
142
 
132
143
  b. Write \`updateInstructions\` for each UPDATE or REGENERATE test before calling \`skyramp_actions\` — articulating the change first prevents file content from overriding diff-based reasoning.
133
144
 
134
- c. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every UPDATE/REGENERATE/DELETE test. Exclude tests marked \`[external]\`. Run them sequentially, not in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail.
145
+ ${maintenanceBeforeExecStep}
135
146
 
136
147
  d. Call \`skyramp_actions\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) and apply the edits it returns.
137
148
 
@@ -343,19 +354,37 @@ ${hasRelatedRepos ? `
343
354
  const testDirInstruction = testsRepoDir
344
355
  ? `the \`<output_dir>\` from the \`<services>\` block, rooted under the test repository at \`${testsRepoDir}\` (i.e. \`${testsRepoDir}/<output_dir>\`). Write ALL test output files to paths under \`${testsRepoDir}\`, not under \`${repositoryPath}\`. Do NOT write any test files to the app repository.`
345
356
  : `${SERVICE_REFS.testDirRef}. Do NOT create a new \`tests/\` directory at the repo root — use that path. If no \`testDirectory\` is configured, default to the language-conventional location (e.g. \`src/test/java/...\` for Java, \`tests/\` for Python).`;
346
- const primaryRepoBlock = primaryRepo ? `<REPOSITORY>${primaryRepo}</REPOSITORY>\n` : '';
347
- return `<TITLE>${prTitle}</TITLE>
348
- <DESCRIPTION>${prDescription}</DESCRIPTION>
349
- ${primaryRepoBlock}<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
350
- ${relatedReposBlock}${testsRepoDirBlock}${serviceContext ? serviceContext + '\n' : ''}${uiCredentialsBlock ? uiCredentialsBlock + '\n' : ''}Use the Skyramp MCP server tools for all tasks below.
351
-
352
- ${task1Section}
353
-
354
- ## Task 2: Generate New Tests
357
+ // Task 3's non-zero-budget count rule branches in plan-only eval runs: the
358
+ // report DECLARES the final GENERATE selection instead of recording
359
+ // generated files. The zero-surface abstention rules above it are shared.
360
+ let task3CountRule;
361
+ if (planOnly) {
362
+ task3CountRule = `Otherwise (your Budget Plan is non-zero): this is a plan-only run — \`newTestsCreated\` DECLARES your final GENERATE selection instead of recording generated files: exactly one entry per item of your final GENERATE list (the list returned by \`skyramp_register_test_plan\` when that tool was available — it may contain fewer items than your committed budget — otherwise your Budget Plan selection, at most ${maxGenerate}), with \`fileName\` set to the file name you would have used. \`testResults\` must be \`[]\` — nothing was executed. The declaration itself is the deliverable; do not generate or backfill.`;
363
+ }
364
+ else {
365
+ task3CountRule = `Otherwise (your Budget Plan is non-zero): in \`newTestsCreated\`, you must have exactly as many budget-counting new tests as your committed Budget Plan's generate count (at most ${maxGenerate}). Only new files (ADD) created for the planned GENERATE items count toward this target — GENERATE items converted to UPDATE do not. You may also include at most one additional discovered-scenario file in \`newTestsCreated\` (the bug-catching test generated after all planned items); that extra test does **not** count against the budget. If you have fewer budget-counting new tests than your generate count, backfill from the remaining ADDITIONAL candidates before proceeding. Only proceed with fewer if all candidates failed after retry AND the fallback single-contract test also failed.`;
366
+ }
367
+ // Task 2 branches wholesale in plan-only eval runs (SKYR-3879 plan-only
368
+ // lane): the standard task mandates generation and execution, which a
369
+ // plan-only run must not do — branching the section (rather than appending
370
+ // an override) means the agent never sees conflicting instructions.
371
+ let task2Section;
372
+ if (planOnly) {
373
+ task2Section = `## Task 2: Commit the Test Plan (plan-only run)
374
+
375
+ This is a plan-only evaluation run: the application under test is NOT running, and this run evaluates test SELECTION only. Nothing is generated or executed in this task.
376
+
377
+ - Draft your complete candidate list exactly as the Execution Plan directs — every API test (contract / integration / batch-scenario) you would generate OR recommend for this PR, grounded in the analysis output and the diff. Favor tests that would FAIL if the changed logic were buggy, not just tests that exercise the new surface.
378
+ - If a tool named \`skyramp_register_test_plan\` is available, call it with the full candidate union — include a \`discriminator\` claim \`{kind, changedCodeAnchor}\` for every candidate that probes changed logic — and treat its returned GENERATE list as your final selection. If that tool is not available, commit to your Budget Plan's GENERATE selection (at most ${maxGenerate}).
379
+ - Skip UI and E2E candidates entirely — with no running app there are no blueprints to ground them, and this lane evaluates API test selection only.
380
+ - Take no other actions in this task: no test generation tools, no browser traces or blueprint captures, no test files written, no test executions. Proceed directly to ${taskRef(TASK_SUBMIT)}.`;
381
+ }
382
+ else {
383
+ task2Section = `## Task 2: Generate New Tests
355
384
 
356
385
  ${userPrompt ? "Generate only the tests that the user requested from the Additional Recommendations. The rules below still apply." : "Drift-based maintenance (Task 1) is complete. This step only processes the GENERATE list. Exception: if a GENERATE item targets a resource with an existing `[skyramp]` contract test, UPDATE that test file (see covered-resource handling below) — a new test case added to an existing file counts toward the budget and is reported in `newTestsCreated`."}
357
386
 
358
- - **MANDATORY — use the pre-ranked GENERATE list as-is**: The Execution Plan's GENERATE section governs ADD actions. You MUST generate exactly those scenarios in the exact order listed. Do NOT substitute, rename, or replace a GENERATE item. If parameter grounding uncovers a distinct bug-catching scenario not already in the GENERATE or ADDITIONAL list, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.${hasRelatedRepos ? `\n - **Multi-repo exception:** this run has related repositories, so the per-repo GENERATE lists are NOT final — they are candidates re-selected by the cross-repo round-robin described in Task 1's "Cross-repo test generation". Follow that pooled, type-distributed selection instead of any single repo's GENERATE list. (In single-repo runs the GENERATE list IS final — generate it exactly as-is.)` : ""}
387
+ - **MANDATORY — use the plan returned by \`skyramp_register_test_plan\` as-is**: Before generating anything, call \`skyramp_register_test_plan\` (\`stateFile\` required) with your complete candidate list — every test you would generate OR recommend, including the Execution Plan's own pre-ranked GENERATE/ADDITIONAL items and any candidate you drafted yourself, with a \`discriminator\` claim \`{kind, changedCodeAnchor}\` for candidates probing changed logic. Its returned GENERATE list — not the Execution Plan's raw pre-ranked GENERATE section — governs ADD actions from this point on. You MUST generate exactly those scenarios in the exact order listed, keeping each item's \`scenarioName\` exactly as registered — the generation tools match on it and reject renamed or substituted scenarios. If parameter grounding uncovers a distinct bug-catching scenario not already registered, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.${hasRelatedRepos ? `\n - **Multi-repo exception:** this run has related repositories, so the per-repo GENERATE lists are NOT final — they are candidates re-selected by the cross-repo round-robin described in Task 1's "Cross-repo test generation". Register the pooled, type-distributed selection instead of any single repo's GENERATE list. (In single-repo runs, register the GENERATE list exactly as-is.)` : ""}
359
388
  - **Do not fabricate tests outside the GENERATE list.** New test files cover NEW observable surface only — a new endpoint, or a newly-integrated component/route not already covered by an existing test. Changes that only modify, delete, or add fields to an EXISTING covered endpoint or component are maintenance: handle them in ${taskRef(TASK_ANALYZE_MAINTAIN)} by UPDATE/DELETE of the existing test, never by creating a new spec. If the GENERATE list is empty (deletion-only, cosmetic, or modification-of-existing PRs with no new surface), create zero new tests and proceed to ${taskRef(TASK_SUBMIT)} — do not invent a new spec to have something to report.
360
389
  - Scenario JSON files are always new files — always generate them for new methods. Every generated scenario JSON must have a corresponding new integration test generated from it via \`skyramp_integration_test_generation\`.
361
390
  - Covered-resource handling (aligns with Execution Plan Step 0): When a GENERATE item targets a resource that already has an existing test file covering the same endpoint:
@@ -416,7 +445,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
416
445
  ${CONTRACT_MODE_GUIDANCE}
417
446
  - ${PATH_PARAM_UUID_GUIDANCE}
418
447
  - **UI**: First check for existing Playwright trace \`.zip\` files in the repo (Testbot scans recursively up to 5 directory levels — the per-service output directories, \`frontend/\`, \`public/\`, \`.skyramp/\`, or any subdirectory).
419
- If a relevant trace exists (covers the UI changes in this PR), use it directly with \`skyramp_ui_test_generation\` and \`modularizeCode: false\`.
448
+ If a relevant trace exists (covers the UI changes in this PR), use it directly with \`skyramp_ui_test_generation\`, \`modularizeCode: false\`, and \`codeReuse: true\` (when generating a TypeScript/JavaScript Playwright test — the default; leave \`codeReuse\` unset for other languages).
420
449
  If NO relevant trace exists, **you MUST write out your full trace plan as text BEFORE calling \`browser_navigate\`**. Do not touch the browser until the plan is written.
421
450
 
422
451
  **Browser authentication (check BEFORE navigating)**: If \`<ui-credentials>\` appears in your context above, the app requires login. Parse the credentials — one per line, two supported formats:
@@ -460,7 +489,7 @@ ${CONTRACT_MODE_GUIDANCE}
460
489
  Follow the **UI Recording Workflow** section at the end of this prompt. Additional CI constraints:
461
490
  - Navigate **directly** to the deepest relevant URL (e.g. \`/orders/1/edit\` instead of \`/\` then \`/orders\` then \`/orders/1\`) — minimize multi-hop navigation so the trace stays focused on the scenario under test.
462
491
  - \`skyramp_export_zip\` outputPath: \`${repositoryPath}/.skyramp/<test_name>_trace.zip\`
463
- - \`skyramp_ui_test_generation\`: set \`modularizeCode: false\`
492
+ - \`skyramp_ui_test_generation\`: set \`modularizeCode: false\` and \`codeReuse: true\` (TypeScript/JavaScript Playwright only — the default; leave \`codeReuse\` unset for other languages)
464
493
  - **\`browser_assert\` — MANDATORY**: at least one per page navigated. Call multiple assertions in the same tool call batch when checking independent elements. If you navigate to 2 pages, assert on both. Each assertion should verify a business outcome (state change, computed value, error condition) — not just that an element is visible.
465
494
  - **\`browser_visual_snapshot\` — for visual/appearance checks**: when the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (not its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline on every run. Do NOT use \`browser_take_screenshot\` for this: it captures a throwaway image that is dropped at export and never appears in the generated test (use it only to view the page yourself).
466
495
  - **Wait for stable state before the second capture**: After performing an action that affects computed fields (filling a discount, submitting a form, adding an item), check the current page state before calling the second \`browser_blueprint\` (the capture after the action). If a computed field — total, price, count, derived text — still shows its initial empty or zero value (e.g. \`$0.00\`, \`0\`, \`Loading...\`, empty string), that means async data hasn't finished loading yet. Use \`browser_wait_for\` to wait up to 10 seconds for the field to update to a real value (for example, wait for the total to show a non-zero amount like \`$799.99\` instead of \`$0.00\`). Once the field shows a real value, THEN call the second \`browser_blueprint\` to capture stable state. If after 10 seconds the field still hasn't updated, skip the assertion on that field — don't capture and assert a value that hasn't loaded.
@@ -512,7 +541,7 @@ ${CONTRACT_MODE_GUIDANCE}
512
541
 
513
542
  **The Blueprint Citation Invariant applies during recording too.** Every assertion you emit cites element names — those names must come from blueprint captures, not invention. For N user-intent-level actions, the reference target is N+1 \`browser_blueprint\` calls (the first returns full, the rest return deltas). Traces that follow the pattern produce assertions grounded in observable state changes; traces that skip captures fall back to author-inferred assertions and risk citing names that don't exist in the rendered DOM.
514
543
 
515
- The rest of the UI workflow stays the same: trace plan, browser auth, navigation, export (\`skyramp_export_zip\`), generation (\`skyramp_ui_test_generation\`), \`skyramp_enhance_assertions\` post-call. Capture-act-capture adds blueprint captures alongside the existing steps; it doesn't replace anything.
544
+ The rest of the UI workflow stays the same: trace plan, browser auth, navigation, export (\`skyramp_export_zip\`), generation (\`skyramp_ui_test_generation\`), then \`skyramp_reuse_code\` (when \`codeReuse: true\`) and \`skyramp_enhance_assertions\` post-calls. Capture-act-capture adds blueprint captures alongside the existing steps; it doesn't replace anything.
516
545
  - **E2E**: Only if BOTH a backend trace \`.json\` AND a Playwright \`.zip\` already exist in the repo. Without both, move to \`additionalRecommendations\`.
517
546
  - Skip smoke tests entirely.
518
547
 
@@ -545,13 +574,24 @@ Do NOT use \`page.waitForTimeout()\` with fixed delays. Do NOT retry more than o
545
574
  **After generation, you MUST do exactly these steps — nothing more, nothing less:**
546
575
  1. **[MANDATORY] After \`skyramp_integration_test_generation\`**: Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated integration test file, \`testType: "integration"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.
547
576
  2. **[MANDATORY] After \`skyramp_contract_test_generation\` with \`providerMode\`**: Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated provider contract test file, \`testType: "contract"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file.
548
- 3. **[MANDATORY] After \`skyramp_ui_test_generation\`**: Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated UI test file, \`testType: "ui"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file. The HIGH-tier \`possibleAssertions\` from your second \`browser_blueprint\` captures (after each action) during trace recording are in your context — when the enhance instructions ask you to add assertions for state-changing actions, use those grounded candidates first (they contain exact computed values from the DOM delta, e.g. \`toHaveText('Total: $899.98')\`). Only fall back to deriving values from the test file or source code when no HIGH-tier candidate covers the action.
549
- 4. **Wait**: Do NOT proceed to test execution until steps 1–3 are complete and the verification checklist in the \`skyramp_enhance_assertions\` tool result has been validated for EVERY generated test file.
550
- Do not make any changes other than the assertion enhancements described above. For example: do not modify auth headers, cookies, tokens, env vars, or imports that the generation tool already set correctly — those are correct by construction and changing them breaks auth or execution.
577
+ 3. **[MANDATORY] After \`skyramp_ui_test_generation\` with \`codeReuse: true\`**: the generation result directs you to call \`skyramp_reuse_code\` — do this BEFORE \`skyramp_enhance_assertions\`. Two outcomes: (1) a response starting "No reusable POM layer detected" — this is a normal outcome, continue immediately (do NOT retry), and note "no POM layer — reuse skipped" in that test's \`testResults\` entry \`details\`; (2) a refactoring workflow — follow it to completion INCLUDING its verification loop (\`skyramp_reuse_code\` with \`verify: true\`), finish only when it reports PASSED, and include \`verification: passed\` in that test's \`testResults\` entry \`details\` in your final report. If a reused test later fails execution and the failure points at a substituted POM call, restore the saved \`<testFile>.raw.bak\` over the test file and re-run — do NOT hand-edit the customer's POM methods (this re-run counts toward the 2-attempt execution cap — prefer this restore over the generic timeout fix-up when the failing locator came from a POM substitution).
578
+ 4. **[MANDATORY] After \`skyramp_ui_test_generation\`**: Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated UI test file, \`testType: "ui"\`, and \`enhanceType: "generation"\`. Apply every instruction returned to that file. The HIGH-tier \`possibleAssertions\` from your second \`browser_blueprint\` captures (after each action) during trace recording are in your context — when the enhance instructions ask you to add assertions for state-changing actions, use those grounded candidates first (they contain exact computed values from the DOM delta, e.g. \`toHaveText('Total: $899.98')\`). Only fall back to deriving values from the test file or source code when no HIGH-tier candidate covers the action.
579
+ 5. **Wait**: Do NOT proceed to test execution until steps 1–4 are complete and the verification checklist in the \`skyramp_enhance_assertions\` tool result has been validated for EVERY generated test file.
580
+ Do not make any changes other than the code-reuse refactoring (step 3) and the assertion enhancements described above. For example: do not modify auth headers, cookies, tokens, env vars, or imports that the generation tool already set correctly — those are correct by construction and changing them breaks auth or execution.
551
581
 
552
582
  **Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete. Run these calls sequentially, not in parallel. Exclude tests marked \`[external]\`.
553
583
  - Only report test results for files you actually ran.
554
- **Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed.
584
+ **Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed.`;
585
+ }
586
+ const primaryRepoBlock = primaryRepo ? `<REPOSITORY>${primaryRepo}</REPOSITORY>\n` : '';
587
+ return `<TITLE>${prTitle}</TITLE>
588
+ <DESCRIPTION>${prDescription}</DESCRIPTION>
589
+ ${primaryRepoBlock}<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
590
+ ${relatedReposBlock}${testsRepoDirBlock}${serviceContext ? serviceContext + '\n' : ''}${uiCredentialsBlock ? uiCredentialsBlock + '\n' : ''}Use the Skyramp MCP server tools for all tasks below.
591
+
592
+ ${task1Section}
593
+
594
+ ${task2Section}
555
595
 
556
596
  ## Task 3: Submit Report
557
597
 
@@ -571,7 +611,7 @@ In these cases:
571
611
  - \`businessCaseAnalysis\` must be a one-sentence summary of what the PR actually does (do NOT leave it blank)
572
612
  - \`additionalRecommendations\` must be \`[]\` — do NOT recommend tests for a no-surface PR
573
613
 
574
- Otherwise (your Budget Plan is non-zero): in \`newTestsCreated\`, you must have exactly as many budget-counting new tests as your committed Budget Plan's generate count (at most ${maxGenerate}). Only new files (ADD) created for the planned GENERATE items count toward this target — GENERATE items converted to UPDATE do not. You may also include at most one additional discovered-scenario file in \`newTestsCreated\` (the bug-catching test generated after all planned items); that extra test does **not** count against the budget. If you have fewer budget-counting new tests than your generate count, backfill from the remaining ADDITIONAL candidates before proceeding. Only proceed with fewer if all candidates failed after retry AND the fallback single-contract test also failed.
614
+ ${task3CountRule}
575
615
 
576
616
  Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}" and \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
577
617
 
@@ -683,10 +723,16 @@ export function registerTestbotPrompt(server) {
683
723
  .string()
684
724
  .optional()
685
725
  .describe("The primary repository's owner/repo slug (where the testbot workflow runs). Used verbatim as the `repository` attribution for the primary repo's analysis, tests, and report items in multi-repo runs."),
726
+ planOnly: z
727
+ // Prompt/resource URL args arrive as strings — accept "true"/"1"
728
+ // alongside a real boolean; anything else (incl. undefined) is false.
729
+ .preprocess((v) => v === true || v === "true" || v === "1", z.boolean())
730
+ .optional()
731
+ .describe("Plan-only eval mode (eval harness only, SKYR-3879): run the full recommendation phase — analysis, maintenance verdicts, candidate drafting, plan registration — but generate and execute nothing; the report declares the final plan. Used by the eval pipeline to A/B selection changes without a running SUT."),
686
732
  },
687
733
  }, async (args) => {
688
734
  const services = await readWorkspaceServices(args.repositoryPath);
689
- let prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.maxCritical, args.prNumber, args.userPrompt, services.length ? services : undefined, args.uiCredentials, args.testsRepoDir, parseRelatedRepositories(args.relatedRepositories), args.primaryRepo);
735
+ let prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.maxCritical, args.prNumber, args.userPrompt, services.length ? services : undefined, args.uiCredentials, args.testsRepoDir, parseRelatedRepositories(args.relatedRepositories), args.primaryRepo, args.planOnly);
690
736
  if (args.workspaceValidationFailed) {
691
737
  prompt = buildWorkspaceRecoveryPrefix(args.repositoryPath) + prompt;
692
738
  }