@skyramp/mcp 0.3.5 → 0.3.6-rc.2.ac20

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 (164) hide show
  1. package/build/adapters/jestAdapter.js +3 -0
  2. package/build/adapters/mochaAdapter.js +2 -0
  3. package/build/adapters/playwrightAdapter.js +3 -0
  4. package/build/adapters/pytestAdapter.js +12 -0
  5. package/build/playwright/registerPlaywrightTools.js +92 -30
  6. package/build/playwright/traceRecordingPrompt.d.ts +6 -0
  7. package/build/playwright/traceRecordingPrompt.js +6 -2
  8. package/build/prompts/code-reuse.d.ts +1 -2
  9. package/build/prompts/code-reuse.js +182 -77
  10. package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
  11. package/build/prompts/modularization/integration-test-modularization.js +83 -41
  12. package/build/prompts/modularization/render.d.ts +18 -0
  13. package/build/prompts/modularization/render.js +12 -0
  14. package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
  15. package/build/prompts/modularization/ui-test-modularization.js +89 -47
  16. package/build/prompts/pom-aware-code-reuse.js +3 -1
  17. package/build/prompts/shared-helper-policy.d.ts +57 -0
  18. package/build/prompts/shared-helper-policy.js +135 -0
  19. package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
  20. package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
  21. package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
  22. package/build/prompts/test-recommendation/recommendationShared.js +90 -16
  23. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
  24. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
  25. package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
  26. package/build/prompts/testbot/testbot-prompts.js +81 -35
  27. package/build/recommendation/budgeters/shared.js +105 -27
  28. package/build/recommendation/discriminators.js +13 -2
  29. package/build/recommendation/planRanker.d.ts +6 -6
  30. package/build/recommendation/planRanker.js +6 -61
  31. package/build/services/AnalyticsService.d.ts +7 -0
  32. package/build/services/AnalyticsService.js +7 -1
  33. package/build/services/ModularizationService.js +1 -3
  34. package/build/services/TestDiscoveryService.d.ts +0 -2
  35. package/build/services/TestDiscoveryService.js +2 -37
  36. package/build/services/TestGenerationService.d.ts +16 -0
  37. package/build/services/TestGenerationService.js +86 -10
  38. package/build/services/containerEnv.js +13 -12
  39. package/build/tools/code-refactor/codeReuseTool.js +279 -93
  40. package/build/tools/code-refactor/enhance-state.d.ts +49 -0
  41. package/build/tools/code-refactor/enhance-state.js +109 -0
  42. package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
  43. package/build/tools/code-refactor/modularizationTool.js +9 -2
  44. package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
  45. package/build/tools/code-refactor/reuse-outcome.js +14 -4
  46. package/build/tools/code-refactor/reuse-state.d.ts +127 -5
  47. package/build/tools/code-refactor/reuse-state.js +628 -16
  48. package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
  49. package/build/tools/code-refactor/utils-verify-gates.js +100 -0
  50. package/build/tools/code-refactor/verify-gates.d.ts +2 -1
  51. package/build/tools/code-refactor/verify-gates.js +90 -25
  52. package/build/tools/executeSkyrampTestTool.d.ts +19 -0
  53. package/build/tools/executeSkyrampTestTool.js +158 -8
  54. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
  55. package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
  56. package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
  57. package/build/tools/generate-tests/generateUIRestTool.js +22 -0
  58. package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
  59. package/build/tools/generate-tests/scenarioLint.js +127 -19
  60. package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
  61. package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
  62. package/build/tools/runExistingTestsTool.d.ts +34 -2
  63. package/build/tools/runExistingTestsTool.js +104 -4
  64. package/build/tools/submitReportTool.d.ts +38 -38
  65. package/build/tools/submitReportTool.js +537 -120
  66. package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
  67. package/build/tools/test-management/analyzeChangesTool.js +71 -10
  68. package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
  69. package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
  70. package/build/tools/test-management/registerTestPlanTool.js +70 -12
  71. package/build/types/ExternalTestExecution.d.ts +67 -1
  72. package/build/types/Recommendation.d.ts +34 -5
  73. package/build/types/RepositoryAnalysis.d.ts +133 -114
  74. package/build/types/RepositoryAnalysis.js +1 -1
  75. package/build/types/ReuseOutcome.d.ts +102 -6
  76. package/build/types/ReuseOutcome.js +16 -2
  77. package/build/types/TestRecommendation.js +21 -3
  78. package/build/types/TestTypes.js +14 -8
  79. package/build/types/TestbotReport.d.ts +10 -1
  80. package/build/types/index.d.ts +2 -2
  81. package/build/types/index.js +1 -1
  82. package/build/utils/AnalysisStateManager.d.ts +57 -1
  83. package/build/utils/AnalysisStateManager.js +54 -5
  84. package/build/utils/branchDiff.d.ts +10 -0
  85. package/build/utils/branchDiff.js +28 -0
  86. package/build/utils/changedRoutes.d.ts +29 -0
  87. package/build/utils/changedRoutes.js +87 -0
  88. package/build/utils/featureFlags.d.ts +21 -0
  89. package/build/utils/featureFlags.js +23 -0
  90. package/build/utils/frontendIntegration.js +34 -4
  91. package/build/utils/importerHop.d.ts +2 -8
  92. package/build/utils/importerHop.js +15 -53
  93. package/build/utils/pathMatching.d.ts +38 -0
  94. package/build/utils/pathMatching.js +71 -0
  95. package/build/utils/pathSignatures.d.ts +22 -0
  96. package/build/utils/pathSignatures.js +57 -0
  97. package/build/utils/planMatchKeys.d.ts +16 -3
  98. package/build/utils/planMatchKeys.js +26 -10
  99. package/build/utils/pluralization.d.ts +10 -0
  100. package/build/utils/pluralization.js +18 -0
  101. package/build/utils/pom-catalog-parse.d.ts +52 -0
  102. package/build/utils/pom-catalog-parse.js +141 -0
  103. package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
  104. package/build/utils/pom-scope/selector-extractor.js +34 -8
  105. package/build/utils/pom-verify/verify.d.ts +6 -5
  106. package/build/utils/pom-verify/verify.js +8 -6
  107. package/build/utils/reportVerification.d.ts +64 -4
  108. package/build/utils/reportVerification.js +228 -3
  109. package/build/utils/reuseRouting.d.ts +3 -0
  110. package/build/utils/reuseRouting.js +50 -0
  111. package/build/utils/routeParsers.d.ts +2 -0
  112. package/build/utils/routeParsers.js +65 -8
  113. package/build/utils/scenarioDrafting.d.ts +1 -1
  114. package/build/utils/scenarioDrafting.js +57 -45
  115. package/build/utils/subjectEndpoints.d.ts +19 -0
  116. package/build/utils/subjectEndpoints.js +98 -0
  117. package/build/utils/testFileClassification.d.ts +11 -0
  118. package/build/utils/testFileClassification.js +47 -0
  119. package/build/utils/uiPageEnumerator.d.ts +45 -19
  120. package/build/utils/uiPageEnumerator.js +95 -51
  121. package/build/utils/utils-verify/allow.d.ts +16 -0
  122. package/build/utils/utils-verify/allow.js +68 -0
  123. package/build/utils/utils-verify/call-sites.d.ts +34 -0
  124. package/build/utils/utils-verify/call-sites.js +154 -0
  125. package/build/utils/utils-verify/index.d.ts +7 -0
  126. package/build/utils/utils-verify/index.js +7 -0
  127. package/build/utils/utils-verify/language-spec.d.ts +91 -0
  128. package/build/utils/utils-verify/language-spec.js +210 -0
  129. package/build/utils/utils-verify/locate.d.ts +39 -0
  130. package/build/utils/utils-verify/locate.js +199 -0
  131. package/build/utils/utils-verify/parse.d.ts +34 -0
  132. package/build/utils/utils-verify/parse.js +177 -0
  133. package/build/utils/utils-verify/stage.d.ts +24 -0
  134. package/build/utils/utils-verify/stage.js +107 -0
  135. package/build/utils/utils-verify/verify.d.ts +63 -0
  136. package/build/utils/utils-verify/verify.js +168 -0
  137. package/build/utils/utils.d.ts +3 -1
  138. package/build/utils/utils.js +3 -1
  139. package/build/workspace/workspace.d.ts +32 -32
  140. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
  141. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
  142. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
  143. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
  144. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
  145. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  146. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
  147. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
  148. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
  149. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
  150. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  151. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
  152. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
  153. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
  154. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  155. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  157. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  158. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  159. package/node_modules/playwright/package.json +1 -1
  160. package/package.json +2 -2
  161. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
  162. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
  163. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
@@ -1,37 +1,37 @@
1
- export function getModularizationPrompt(filePath) {
2
- return `# UI TEST MODULARIZATION - IMPROVE READABILITY AND REDUCE DUPLICATION
3
-
4
- **CRITICAL: Use the 'write' tool to save the modularized code to: ${filePath}**
5
- **DO NOT use search_replace - use the 'write' tool to overwrite the entire file with the modularized version**
6
-
7
- **ABSOLUTELY FORBIDDEN:**
8
- - DO NOT CREATE INTERFACES, CLASSES, TYPES, NEW FILES, OR NEW DATA STRUCTURES
9
- - DO NOT CHANGE TEST LOGIC, DATA VALUES, CALCULATIONS, OR ASSERTIONS
10
- - DO NOT CREATE/UPDATE DEPENDENCY FILES package.json, requirements.txt, pom.xml, build.gradle file
11
-
12
- ## STEP 1: READ THE ORIGINAL TEST
13
- Read ${filePath} completely and identify:
1
+ import { isModularizeFirstTarget } from "../../utils/reuseRouting.js";
2
+ import { PromptPlan } from "../test-recommendation/promptPlan.js";
3
+ import { ASSERTION_BOUNDARY_RULE_EXTRACT, BROWSER_HELPERS, BROWSER_VALUE_ASSERTIONS, } from "../shared-helper-policy.js";
4
+ import { renderModularizationPrompt, } from "./render.js";
5
+ // On modularize-first UI flows (utils reuse on, POM reuse off), downstream
6
+ // skyramp_reuse_code consolidates this file's helpers into the shared
7
+ // SkyrampUtils file, where the next scenario's test calls them. That changes
8
+ // what a helper may be named and may assert — the mechanism (logical-section
9
+ // extraction) is the same either way. E2E shares this prompt but is not a
10
+ // modularize-first target, so it never sees these rules.
11
+ const _plan = new PromptPlan()
12
+ .addPhase("main", "UI Test Modularization", {
13
+ headerLevel: "hidden",
14
+ stepFormat: "hash",
15
+ })
16
+ .step("READ", "READ THE ORIGINAL TEST", (p) => `Read ${p.filePath} completely and identify:
14
17
  - All test data values (names, prices, quantities, totals)
15
- - All test data including calculations and expected results
18
+ - All test data including calculations and expected results
16
19
  - The exact test flow
17
- - **Check if code is already modularized** (e.g., \`breakpoint_section_0\`, \`breakpoint_section_1\` functions)
18
-
19
- ## STEP 2: HANDLE PRE-MODULARIZED CODE
20
-
21
- **IF CODE IS ALREADY MODULARIZED** (has functions like \`breakpoint_section_0\`, \`breakpoint_section_1\`):
20
+ - **Check if code is already modularized** (e.g., \`breakpoint_section_0\`, \`breakpoint_section_1\` functions)`)
21
+ .step("PREMOD", "HANDLE PRE-MODULARIZED CODE", (p) => `**IF CODE IS ALREADY MODULARIZED** (has functions like \`breakpoint_section_0\`, \`breakpoint_section_1\`):
22
22
  1. **RENAME** functions with meaningful names that describe what they do
23
23
  - BAD: \`breakpoint_section_0\`, \`breakpoint_section_1\`
24
24
  - GOOD: \`createProduct\`, \`editProductPrice\`, \`createOrder\`
25
25
  2. **PARAMETERIZE** hardcoded values in these functions
26
26
  3. **DO NOT** change the function logic or structure
27
- 4. **DO NOT** create new functions - work with existing ones
28
- 5. **SKIP to STEP 5** for verification
27
+ 4. **DO NOT** create new functions - work with existing ones${p.utilsReuse
28
+ ? ` — with ONE exception, item 5
29
+ 5. **APPLY THE SHARED HELPER SHAPE** (the rules block inside Step ${_plan.labels.EXTRACT}; read that block, then return here): canonical intent names; value assertions moved out of the helper into the test body right after its call; a value assertion that sits BETWEEN two actions splits the function at that point into two intent-named functions — the only new functions this branch may create`
30
+ : ""}
31
+ ${p.utilsReuse ? "6" : "5"}. **SKIP to Step ${_plan.labels.VERIFY}** for verification
29
32
 
30
- **IF CODE IS NOT MODULARIZED**, proceed to STEP 3.
31
-
32
- ## STEP 3: IDENTIFY EXTRACTION OPPORTUNITIES
33
-
34
- Extract code in TWO scenarios:
33
+ **IF CODE IS NOT MODULARIZED**, proceed to Step ${_plan.labels.IDENTIFY}.`)
34
+ .step("IDENTIFY", "IDENTIFY EXTRACTION OPPORTUNITIES", (p) => `Extract code in TWO scenarios:
35
35
 
36
36
  **A) REPETITIVE CODE** - Code that repeats 2+ times with IDENTICAL structure but different data
37
37
  - Extract EACH occurrence as a separate helper call
@@ -39,15 +39,17 @@ Extract code in TWO scenarios:
39
39
 
40
40
  **B) LOGICAL SECTIONS** - Self-contained operations (5+ lines) that improve readability
41
41
  - Examples: "create product", "fill form", "verify result"
42
- - Must be cohesive - related operations that achieve one goal
42
+ - Must be cohesive - related operations that achieve one goal${p.utilsReuse
43
+ ? `
44
+ - This project consolidates test helpers into a shared utils file, so EVERY logical section that performs a user action (open a form, fill and submit it, add or remove an item, apply an option) becomes a named helper — the section does NOT need to repeat. Another scenario's test will call the same helper with its own values.`
45
+ : ""}
43
46
 
44
47
  **Don't extract:**
45
- - Code less than 5 lines (unless highly repetitive)
46
- - Navigation sequences (\`navbar-\` clicks, \`page.goto\`)
47
-
48
- ## STEP 3: EXTRACT INTO HELPERS
49
-
50
- **GOLDEN RULE: Copy the original code exactly, only add parameters for values that differ**
48
+ - Code less than 5 lines (unless highly repetitive${p.utilsReuse
49
+ ? ", or a cohesive user-action section — those become helpers regardless of length, per B above"
50
+ : ""})
51
+ - Navigation sequences (\`navbar-\` clicks, \`page.goto\`)`)
52
+ .step("EXTRACT", "EXTRACT INTO HELPERS", (p) => `**GOLDEN RULE: Copy the original code exactly, only add parameters for values that differ**
51
53
 
52
54
  **CRITICAL - AVOID BUGS:**
53
55
  - Copy the code block EXACTLY as-is from the original test
@@ -62,9 +64,11 @@ Extract code in TWO scenarios:
62
64
  - **Replace ALL hardcoded values with parameters** - No leftover hardcoded fills (selectors are the ONLY exception: selectors must remain exactly as in the original code)
63
65
  - **NEVER create new conditional logic** (no ternaries, no if/else, no loops to derive parameter values)
64
66
  - **NEVER add logic that wasn't in the original code**
65
- - Keep ALL existing logic, calculations, and assertions unchanged
67
+ - Keep ALL existing logic, calculations, and assertions unchanged${p.utilsReuse
68
+ ? " — ONE exception: a value assertion that would land inside a helper is placed in the test body instead (same selector, same expected value, same relative order): after the helper call if it trailed the helper's actions, or between two helper calls — splitting the helper there — if it sat between actions. That relocation is the only assertion move allowed; see SHARED HELPER SHAPE below"
69
+ : ""}
66
70
  - Keep ALL lines in the same order (except: remove fills with wrong/temporary hardcoded values)
67
-
71
+ ${p.utilsReuse ? `\n${sharedHelperShape()}\n` : ""}
68
72
  **PARAMETERIZATION BEST PRACTICES:**
69
73
  1. **Use individual parameters, not data objects** - Makes helper usage clearer
70
74
  - GOOD: \`createProduct(page, name, price, category)\`
@@ -129,25 +133,27 @@ await addItem(page, "item1");
129
133
  await addItem(page, "item2");
130
134
  await addItem(page, "item3");
131
135
 
132
- \`\`\`
133
-
134
- ## STEP 4: CALL HELPERS WITH EXACT SAME VALUES
135
- Use the EXACT same values from the original test when calling helpers.
136
-
137
- ## STEP 5: VERIFY - CRITICAL CHECKS
138
-
139
- **BUGS TO AVOID:**
136
+ \`\`\``)
137
+ .step("USE", "CALL HELPERS WITH EXACT SAME VALUES", () => `Use the EXACT same values from the original test when calling helpers.`)
138
+ .step("VERIFY", "VERIFY - CRITICAL CHECKS", (p) => `**BUGS TO AVOID:**
140
139
  - [ ] **NO TYPE ANNOTATIONS FOR \`page\` IN FUNCTION SIGNATURES** - Parameters must be untyped (e.g., \`page\`, not \`page: Page\`)
141
140
  - [ ] **NO RETURN TYPES** - Do not add \`: Promise<void>\` or any return type annotations
142
- - [ ] **ALL field mappings verified** - EVERY \`.fill()\` uses the correct parameter. Check field gets quantity parameter, NOT hardcoded
141
+ - [ ] **ALL field mappings verified** - EVERY \`.fill()\` uses the correct parameter. Check field gets quantity parameter, NOT hardcoded
143
142
  - [ ] **No duplicate helpers** - Consolidate similar helpers (e.g., one createOrder vs createSimpleOrder + createOrderWithItems)
144
143
  - [ ] **No nested helpers** - Helpers don't call other helpers unnecessarily
145
144
  - [ ] **Clear parameters** - Individual parameters (name, price) not data objects
146
145
  - [ ] **No duplicate actions** - Don't click "Add Product" button in test AND in helper - choose one place
147
146
  - [ ] **No new loops** - Exception: acceptable if it improves readability and items are explicit at call site
148
-
147
+ ${p.utilsReuse
148
+ ? `
149
+ **SHARED HELPER SHAPE:**
150
+ - [ ] Every helper name is a canonical intent (\`openEditOrderForm\`, \`applyDiscount\`) with no scenario value or adjective in it
151
+ - [ ] No helper contains a value assertion (${BROWSER_VALUE_ASSERTIONS}) — each sits in the test body, right after the helper call it verifies
152
+ - [ ] Every selector inside a helper is byte-identical to the original test
153
+ `
154
+ : ""}
149
155
  **DATA INTEGRITY:**
150
- - [ ] Helpers are exact copies of original + parameters only
156
+ - [ ] Helpers are exact copies of original + parameters only${p.utilsReuse ? " (minus the value assertions relocated to the test body)" : ""}
151
157
  - [ ] All data values match the original test exactly
152
158
  - [ ] All calculations produce identical results
153
159
  - [ ] Helper calls use the same values as original
@@ -167,5 +173,41 @@ Use the EXACT same values from the original test when calling helpers.
167
173
 
168
174
  If ANY answer is "no", fix it before submitting.
169
175
 
170
- **When in doubt, don't extract - keep original code unchanged.**`;
176
+ **When in doubt, don't extract - keep original code unchanged.**`)
177
+ .done();
178
+ // Shared-file helper shape: the reuse pass moves these helpers into SkyrampUtils,
179
+ // where the next scenario's test calls them with its own values. A helper is
180
+ // therefore named for the action it performs and carries no claim about this
181
+ // scenario's data — a `toHaveText("Total: $899.98")` inside a shared helper
182
+ // fails every other caller. Selectors stay verbatim regardless (a selector is
183
+ // not scenario data; parameterizing it invents a template the recorder never saw).
184
+ function sharedHelperShape() {
185
+ return `**SHARED HELPER SHAPE (this project consolidates helpers into a shared utils file):**
186
+ - ${BROWSER_HELPERS.namingRule}
187
+ - **PARAMETERS**: \`page\`, plus every value this scenario types or selects (${BROWSER_HELPERS.liftableLiterals}). Selectors are NEVER parameters — they stay verbatim inside the helper body.
188
+ - ${BROWSER_HELPERS.assertionRule} ${ASSERTION_BOUNDARY_RULE_EXTRACT}
189
+ - Helpers are module-level named functions in this file; the test body becomes navigation + a sequence of helper calls + this scenario's assertions, in the original order.`;
190
+ }
191
+ export function getModularizationPrompt(filePath, testType, language) {
192
+ const params = {
193
+ filePath,
194
+ utilsReuse: isModularizeFirstTarget(testType, language),
195
+ };
196
+ return renderModularizationPrompt({
197
+ title: "UI TEST MODULARIZATION - IMPROVE READABILITY AND REDUCE DUPLICATION",
198
+ forbidden: [
199
+ "DO NOT CREATE INTERFACES, CLASSES, TYPES, NEW FILES, OR NEW DATA STRUCTURES",
200
+ `DO NOT CHANGE TEST LOGIC, DATA VALUES, CALCULATIONS, OR ASSERTIONS${params.utilsReuse ? " (moving a value assertion out of a helper into the test body, verbatim, is a placement — not a change)" : ""}`,
201
+ "DO NOT CREATE/UPDATE DEPENDENCY FILES package.json, requirements.txt, pom.xml, build.gradle file",
202
+ ],
203
+ plan: _plan,
204
+ params,
205
+ });
206
+ }
207
+ /** Structured view of which steps render for the given inputs (for tests). */
208
+ export function previewModularizationPlan(filePath, testType, language) {
209
+ return _plan.preview({
210
+ filePath,
211
+ utilsReuse: isModularizeFirstTarget(testType, language),
212
+ });
171
213
  }
@@ -148,9 +148,11 @@ Use this exact format for the file:
148
148
  - **Properties:**
149
149
  - \`propertyName\` — selector: \`exact-selector-string\`
150
150
  - **Methods:**
151
- - \`methodName(param1, param2)\` — description; ⏱ timing-aware: waitForTimeout(Xms) + sleep(Ys) | 🌐 network-aware: waitForResponse('**/pattern**') | (omit tags if neither)
151
+ - \`methodName(param1, param2)\` — description; selectors: \`login-submit-btn\`, \`table-row-*\`; ⏱ timing-aware: waitForTimeout(Xms) + sleep(Ys) | 🌐 network-aware: waitForResponse('**/pattern**') | (omit tags if neither)
152
152
  \`\`\`
153
153
 
154
+ **Selectors on methods are REQUIRED.** List every selector string the method touches, each in backticks, in the same form the source uses. If the method builds a selector from a parameter or matches a family of elements, record the WILDCARD form, not one concrete instance — \`o2-table-row-*\` for \`starts-with(@data-test,"o2-table-row-")\`, \`function-list-name-cell-*\` for \`[data-test="function-list-name-cell-\${name}"]\`. A method that touches no selector (a pure wait, a getter over another method) records \`selectors: none\`.
155
+
154
156
  **Tier-2 entries in the catalog:** for files cataloged at signature level, the entry contains the class name, the import path, a \`- **Methods:**\` list of names/signatures only, and the matched selectors from STEP 1 under \`- **Properties:**\`. Mark such entries with \`<!-- depth: signature -->\` on the line after the class heading so a later run knows the entry is shallow.
155
157
 
156
158
  **Free-function helper modules** use the same format with the module basename as the heading (add \`(module functions, not a class)\` after it), each exported function as a Method, and no Iframe line unless a function scopes one. They substitute as imported calls — \`import { bookFirstEvent } from '<path>'; await bookFirstEvent(page);\` — under the same evidence and confidence rules as class methods, and are verified the same way.
@@ -0,0 +1,57 @@
1
+ /** Why the assertion rule exists — quoted wherever the rule is stated. */
2
+ export declare const SHARED_HELPER_WHY = "A scenario-specific assertion inside a helper carries one scenario's data into a shared function and fails the next scenario that calls it \u2014 the helper is shared, the claims are not.";
3
+ export declare const BROWSER_STRUCTURAL_WAITS = "`waitForResponse`, `waitForLoadState`, `waitFor({ state: \"visible\" })`, and `toBeVisible` on a STRUCTURAL locator (a test id, or a role + name of a control) \u2014 a check that an element rendered, not what it says";
4
+ /**
5
+ * Relocation never reorders execution. Two variants, because the two ends of the
6
+ * chain may do different things with a helper whose value assertion sits BETWEEN
7
+ * two actions: modularization (which authors helpers) splits it; reuse (which
8
+ * only moves existing helpers) leaves it local.
9
+ */
10
+ export declare const ASSERTION_BOUNDARY_RULE_EXTRACT = "Relocation must preserve the original order: an assertion that sits BETWEEN two actions is a HELPER BOUNDARY \u2014 end the helper before it and start the next helper after it (each named per the naming rule), so the assertion sits between the two calls in the test body exactly where it was. Never move an assertion across an action.";
11
+ export declare const ASSERTION_BOUNDARY_RULE_MOVE = "Relocation must preserve the original order: only a TRAILING assertion (after the helper's last action) may move to the test body right after the call. A helper whose excluded assertion sits BETWEEN two actions is NOT moved and NOT split \u2014 leave it local in the test file; splitting is modularization's job, not this step's.";
12
+ export declare const BROWSER_VALUE_ASSERTIONS = "any `expect` whose matcher OR locator carries scenario data: `toHaveText`, `toContainText`, `toHaveValue`, `toHaveCount`, `toHaveURL` with a literal, or ANY matcher \u2014 `toBeVisible` included \u2014 on a locator that embeds scenario text such as `getByText(\"Total: $899.98\")`";
13
+ /**
14
+ * A helper kept local must never share a name with a utils export — the import
15
+ * and the local definition would then describe two different behaviours under
16
+ * one name, and a later reader (or a deterministic gate) cannot tell which runs.
17
+ */
18
+ export declare const NO_SHADOWING_RULE = "If a helper stays local for any reason, it must NOT share a name with any export of the utils file \u2014 rename the local one to state what differs (e.g. `submitEditOrderFormNoWait`). A test file never defines a helper with the same name as a utils export.";
19
+ export interface HelperPolicy {
20
+ kind: "api" | "browser";
21
+ /** How two helpers are recognised as "the same step" (lower-case phrase). */
22
+ sameStep: string;
23
+ /** The literals that may legitimately differ between two versions of a step. */
24
+ liftableLiterals: string;
25
+ /**
26
+ * Differences that are generator noise, not structure — two bodies that differ
27
+ * only by these are the same step. Empty when the kind has none.
28
+ */
29
+ structuralNoise: string;
30
+ /** Canonical naming rule (full sentence(s)). */
31
+ namingRule: string;
32
+ /** What a shared helper may and may not assert (full sentence(s)). */
33
+ assertionRule: string;
34
+ /** Where a relocated assertion goes in the test body. */
35
+ relocationTarget: string;
36
+ /**
37
+ * Order-preserving rule for MOVING an existing helper (reuse). Empty when the
38
+ * kind's helpers cannot carry a between-actions assertion (API: one request
39
+ * plus trailing assertions).
40
+ */
41
+ moveRule: string;
42
+ /** Naming example for STEP 4b's procedure, matching the rendered worked example. */
43
+ namingExample: string;
44
+ /** "One X = one helper" closing line for a utils file. */
45
+ oneHelperRule: (fileName: string) => string;
46
+ /** Kind-specific near-duplicate / merge section for STEP 4b of code reuse. */
47
+ nearDupRules: (fileName: string) => string;
48
+ /**
49
+ * Replacement for the generic STEP 4b worked example when helpers are headed
50
+ * to the shared file. Empty string → no example beyond the kind's own section.
51
+ */
52
+ nearDupExample: (testFile: string) => string;
53
+ }
54
+ export declare const API_HELPERS: HelperPolicy;
55
+ export declare const BROWSER_HELPERS: HelperPolicy;
56
+ /** Which helper policy a test type's helpers follow. */
57
+ export declare function selectHelperPolicy(testType?: string): HelperPolicy;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Shared-helper policy — the ONE place that says what a helper destined for the
3
+ * shared SkyrampUtils file may be named and may assert. Both the modularization
4
+ * prompts (which create the helpers) and the code-reuse prompt (which moves and
5
+ * merges them) read these fragments, so the rule cannot drift between the two
6
+ * ends of the modularize → reuse chain.
7
+ *
8
+ * Two kinds of helper exist, keyed by what the test type drives:
9
+ * - API helpers (integration) wrap one SDK request: equivalence is method+path,
10
+ * scenario values travel via `data_override`, the only assertion is the
11
+ * status code.
12
+ * - Browser helpers (ui) wrap a Playwright action sequence: equivalence is the
13
+ * primitive sequence with identical selectors, scenario values are fill
14
+ * values / literal arguments, the helper holds actions + structural waits.
15
+ */
16
+ import { isBrowserTestType } from "../utils/reuseRouting.js";
17
+ /** Why the assertion rule exists — quoted wherever the rule is stated. */
18
+ export const SHARED_HELPER_WHY = "A scenario-specific assertion inside a helper carries one scenario's data into a shared function and fails the next scenario that calls it — the helper is shared, the claims are not.";
19
+ export const BROWSER_STRUCTURAL_WAITS = '`waitForResponse`, `waitForLoadState`, `waitFor({ state: "visible" })`, and `toBeVisible` on a STRUCTURAL locator (a test id, or a role + name of a control) — a check that an element rendered, not what it says';
20
+ /**
21
+ * Relocation never reorders execution. Two variants, because the two ends of the
22
+ * chain may do different things with a helper whose value assertion sits BETWEEN
23
+ * two actions: modularization (which authors helpers) splits it; reuse (which
24
+ * only moves existing helpers) leaves it local.
25
+ */
26
+ export const ASSERTION_BOUNDARY_RULE_EXTRACT = "Relocation must preserve the original order: an assertion that sits BETWEEN two actions is a HELPER BOUNDARY — end the helper before it and start the next helper after it (each named per the naming rule), so the assertion sits between the two calls in the test body exactly where it was. Never move an assertion across an action.";
27
+ export const ASSERTION_BOUNDARY_RULE_MOVE = "Relocation must preserve the original order: only a TRAILING assertion (after the helper's last action) may move to the test body right after the call. A helper whose excluded assertion sits BETWEEN two actions is NOT moved and NOT split — leave it local in the test file; splitting is modularization's job, not this step's.";
28
+ export const BROWSER_VALUE_ASSERTIONS = 'any `expect` whose matcher OR locator carries scenario data: `toHaveText`, `toContainText`, `toHaveValue`, `toHaveCount`, `toHaveURL` with a literal, or ANY matcher — `toBeVisible` included — on a locator that embeds scenario text such as `getByText("Total: $899.98")`';
29
+ const BROWSER_LIFTABLE = "fill values, option labels, or literal arguments";
30
+ const API_LIFTABLE = "body literals, `data_override` values, or expected status";
31
+ const API_ASSERTION_MERGE_LINE = `a merged helper asserts ONLY the status code. If either version carries response-body assertions, relocate them to that version's test body (applied to the returned response) before merging. ${SHARED_HELPER_WHY}`;
32
+ // (API helpers are one request + trailing assertions, so the move variant's
33
+ // between-actions case does not arise; API_HELPERS.moveRule is empty.)
34
+ function oneHelperLine(fileName, unit) {
35
+ return `One ${unit} = one helper in \`${fileName}\`.`;
36
+ }
37
+ /**
38
+ * A helper kept local must never share a name with a utils export — the import
39
+ * and the local definition would then describe two different behaviours under
40
+ * one name, and a later reader (or a deterministic gate) cannot tell which runs.
41
+ */
42
+ export const NO_SHADOWING_RULE = "If a helper stays local for any reason, it must NOT share a name with any export of the utils file — rename the local one to state what differs (e.g. `submitEditOrderFormNoWait`). A test file never defines a helper with the same name as a utils export.";
43
+ export const API_HELPERS = {
44
+ kind: "api",
45
+ sameStep: "the SAME method+path",
46
+ liftableLiterals: API_LIFTABLE,
47
+ structuralNoise: "",
48
+ namingRule: "**NAME** each helper by method + resource ONLY: `create_product`, `update_order`, `get_order`, `get_orders_list`. Scenario adjectives in names are FORBIDDEN (`create_discount_test_product`, `update_order_50_discount`) — other tests define the same step, and a shared utils file can hold only one canonical name per step. Scenario data belongs in ARGUMENTS, never in the name.",
49
+ assertionRule: `**THE ONLY ASSERTION INSIDE A SHARED HELPER IS THE STATUS CODE**, written in the test's own assertion syntax (Python \`assert response.status_code == expected_status\`; TypeScript/JavaScript \`expect(response.status).toBe(expectedStatus)\`), with the expected status a parameter defaulted to this scenario's expected code. EVERY response-body assertion (echo-backs, computed totals, field checks) stays in the test body, applied to the response the helper returns. ${SHARED_HELPER_WHY}`,
50
+ relocationTarget: "the test body, applied to the response the helper returns",
51
+ moveRule: "",
52
+ namingExample: "E.g. `update_order`, not `update_order_discount` or `update_order_50_discount`.",
53
+ oneHelperRule: (fileName) => `One method+path = one helper in \`${fileName}\`.`,
54
+ // The API section below carries its own worked example; the generic
55
+ // Playwright one would teach intent-naming and locator merges to a Python prompt.
56
+ nearDupExample: () => "",
57
+ // API request helpers are one SDK call + assertions, so near-duplicate
58
+ // equivalence is judged by method+path, not Playwright primitives — and the
59
+ // SDK's data_override makes literal-lifting mechanical, which is why this
60
+ // merge may lift body literals that the Playwright rules refuse.
61
+ nearDupRules: (fileName) => `
62
+ ### API request helpers: near-duplicate definition and merge (this flow's tests)
63
+
64
+ For helpers that wrap one SDK request call (e.g. \`client.send_request(...)\`), judge near-duplication by the REQUEST, not by the criteria above. Two such helpers are near-duplicates when **ALL** of the following are true:
65
+
66
+ 1. **Same method and same path** (path parameters count as the same path).
67
+ 2. **Same call structure** — same request-call shape; no extra steps in one but not the other.
68
+ 3. **Differences are limited to**: literals inside the request-body string, values in \`data_override\`, the expected status code, or which values are lifted to parameters.
69
+
70
+ Merge procedure for an API near-duplicate pair — these rules OVERRIDE the generic procedure below on two points: body literals ARE liftable differences (via \`data_override\`), and defaults are REQUIRED (the generic "no defaults" rule applies to Playwright helpers only):
71
+ - **Name**: method + resource only (\`create_product\`, \`update_order\`) — never a scenario adjective.
72
+ - **Lift each differing body literal into a parameter applied via \`data_override\`, with the parameter's DEFAULT set to the literal the incumbent version used.** Defaults make the merge additive: every existing call site keeps its behavior without edits; only the other test passes its values explicitly.
73
+ - **Union the parameter lists**; a parameter one version lacked gets the default from that version's literal. A differing expected status lifts the same way: \`expected_status\` parameter, default from the incumbent.
74
+ - **Assertions**: ${API_ASSERTION_MERGE_LINE}
75
+ - Structural differences (different items-array shape, extra request) → NOT near-duplicates; leave them alone.
76
+
77
+ Worked example: \`update_order_discount(client, headers, order_id, product_id)\` with \`"discount_percent": 25\` in the body, and \`update_order_50_discount(client, headers, order_id, product_id, quantity)\` with \`"discount_percent": 50\` → ONE helper \`update_order(client, headers, order_id, product_id, discount_percent=25, quantity=2)\` that applies \`discount_percent\` and \`quantity\` via \`data_override\`. ${oneHelperLine(fileName, "method+path")}
78
+ `,
79
+ };
80
+ export const BROWSER_HELPERS = {
81
+ kind: "browser",
82
+ sameStep: "the SAME action sequence (same Playwright primitives in the same order, selectors identical)",
83
+ liftableLiterals: BROWSER_LIFTABLE,
84
+ structuralNoise: "`waitForTimeout(...)` lines and the `getValue(playwrightRequestN, ...)` value wrapper around a fill are RECORDER NOISE, not structure — two bodies that differ only by them perform the same step; keep the utils version (it carries the waits) and import it",
85
+ namingRule: "**NAME** each helper by the user INTENT it performs, shared across tests: `openEditOrderForm`, `applyDiscount`, `submitProductForm`, `removeOrderItem`. Scenario values or adjectives in names are FORBIDDEN (`applyFiftyPercentDiscount`, `editOrder1Discount`, `createExpensiveProduct`) — another test performs the same action with different data, and a shared utils file holds ONE canonical name per action. Scenario data belongs in ARGUMENTS, never in the name.",
86
+ assertionRule: `**A SHARED HELPER CONTAINS ACTIONS AND STRUCTURAL WAITS ONLY**: clicks, fills, selects, ${BROWSER_STRUCTURAL_WAITS}. EVERY VALUE ASSERTION (${BROWSER_VALUE_ASSERTIONS}) stays in the test body, placed immediately after the helper call it verifies, with its selector verbatim and its expected value unchanged. ${SHARED_HELPER_WHY}`,
87
+ relocationTarget: "the test body, immediately after the helper call, selector verbatim",
88
+ moveRule: ASSERTION_BOUNDARY_RULE_MOVE,
89
+ namingExample: "E.g. `applyDiscount`, not `applyTwentyPercentDiscount`.",
90
+ oneHelperRule: (fileName) => `One action sequence = one helper in \`${fileName}\`.`,
91
+ nearDupExample: (testFile) => `### Worked example (shared-file browser helpers)
92
+
93
+ \`edit_order_apply_discount.spec.ts\` has:
94
+ \`\`\`ts
95
+ export async function applyDiscount(page) {
96
+ await page.getByTestId("edit-order-input-discount").fill("10");
97
+ await page.getByTestId("edit-order-submit-btn").click();
98
+ }
99
+ \`\`\`
100
+
101
+ \`${testFile}\` (current) has:
102
+ \`\`\`ts
103
+ export async function applyTwentyPercentDiscount(page) {
104
+ await page.getByTestId("edit-order-input-discount").fill("20");
105
+ await page.getByTestId("edit-order-submit-btn").click();
106
+ }
107
+ \`\`\`
108
+
109
+ These ARE near-duplicates: same primitive sequence, selectors byte-identical, the only difference is the fill literal. Correct merge (name = intent, the literal becomes a parameter passed exactly as it was written — no conversion or other logic added, selectors untouched):
110
+
111
+ \`\`\`ts
112
+ export async function applyDiscount(page, discountPercent) {
113
+ await page.getByTestId("edit-order-input-discount").fill(discountPercent);
114
+ await page.getByTestId("edit-order-submit-btn").click();
115
+ }
116
+ \`\`\`
117
+
118
+ Both test files import and call \`applyDiscount(page, "10")\` / \`applyDiscount(page, "20")\`. A pair whose selectors differ (\`getByTestId("edit-order-submit-btn")\` vs \`getByRole("button", { name: "Save" })\`, or one side carrying \`.first()\`) is NOT a near-duplicate — leave both in place.`,
119
+ // Browser helpers already have the Playwright near-duplicate criteria in the
120
+ // generic STEP 4b text; what they lack for a shared file is the
121
+ // defaulted-parameter merge (so an incumbent utils helper can absorb a new
122
+ // scenario without touching its existing call sites) and the assertion rule.
123
+ nearDupRules: (fileName) => `
124
+ ### Browser action helpers: merging into an existing \`${fileName}\` helper (this flow's tests)
125
+
126
+ For this flow the rules below OVERRIDE two points of the generic text above and below: (a) selectors are NOT liftable differences — two helpers whose selectors differ are NOT near-duplicates (criterion 2 applies to ${BROWSER_LIFTABLE} only); (b) the procedure's "no defaults" rule applies to cross-test pairs (both call sites pass values explicitly) but NOT to merges into a helper that already lives in \`${fileName}\` (STEP 3 / STEP 5b), which MUST use defaults so existing call sites stay untouched. Merge additively:
127
+ - **Name**: the INTENT the action sequence performs (\`openEditOrderForm\`, \`applyDiscount\`, \`submitProductForm\`) — never a scenario adjective or value (\`applyFiftyPercentDiscount\`, \`editOrder1Discount\`). ${oneHelperLine(fileName, "intent")}
128
+ - **Lift each differing literal into a parameter with its DEFAULT set to the literal the incumbent version used** (${BROWSER_LIFTABLE}). Defaults make the merge additive: every existing call site keeps its behavior without edits; only the new test passes its values explicitly. Selectors stay verbatim in the body.
129
+ - **Assertions**: before merging, apply the assertion rule to both versions — relocate a trailing value assertion to that version's test body immediately after the helper call. ${ASSERTION_BOUNDARY_RULE_MOVE} ${SHARED_HELPER_WHY}
130
+ `,
131
+ };
132
+ /** Which helper policy a test type's helpers follow. */
133
+ export function selectHelperPolicy(testType) {
134
+ return isBrowserTestType(testType) ? BROWSER_HELPERS : API_HELPERS;
135
+ }
@@ -1,12 +1,12 @@
1
- import { roundRobinByType } from "../../recommendation/diversity.js";
1
+ import { CandidateSource, computeCandidateId } from "../../types/Recommendation.js";
2
+ import { fixedNBudgeter } from "../../recommendation/budgeters/fixedNBudgeter.js";
2
3
  import { AUTH_MIDDLEWARE_PATTERNS_STR } from "../../utils/workspaceAuth.js";
3
4
  import { resolveServiceDetailsRef } from "../../utils/utils.js";
4
- import { logger } from "../../utils/logger.js";
5
5
  import { TEST_CATEGORIES, } from "../../types/TestRecommendation.js";
6
6
  import { buildScopeAssessmentSection } from "./scopeAssessment.js";
7
7
  import { PromptPlan } from "./promptPlan.js";
8
8
  import { buildTestPatternGuidelines, buildTestQualityCriteria, buildGenerationRules, MAX_CRITICAL_TESTS, } from "./recommendationSections.js";
9
- import { TASK_ANALYZE_MAINTAIN, TESTBOT_TASK1_STEP_CODE_REVIEW, externalDedupKey, isAttackSurfaceSecurityBoundary, isOrdinaryDirectAuthBoundary, scenarioCoverageKey, taskStepRef, } from "./recommendationShared.js";
9
+ import { TASK_ANALYZE_MAINTAIN, TESTBOT_TASK1_STEP_CODE_REVIEW, taskStepRef, } from "./recommendationShared.js";
10
10
  // ── Step body functions ───────────────────────────────────────────────────────
11
11
  function _execCodeReviewBody(_ctx) {
12
12
  const codeReviewRef = taskStepRef(TASK_ANALYZE_MAINTAIN, TESTBOT_TASK1_STEP_CODE_REVIEW);
@@ -69,7 +69,7 @@ INSERT a non-bug source-code-derived candidate into the ranked list **only if AL
69
69
  3. It is not already covered by a structural candidate in the list below
70
70
 
71
71
  If these conditions are not met, add it to ADDITIONAL only — do NOT displace a pre-ranked GENERATE item.
72
- **CRITICAL-tier items (category: new_endpoint) and attack-surface \`security_boundary\` items should never be displaced by non-bug candidates** — they test the actual endpoints introduced in this PR or sibling destructive endpoints that could bypass the changed auth boundary. However, bug-catching tests CAN displace them only after all lower-value non-bug slots are exhausted.
72
+ **Attack-surface \`security_boundary\` items should never be displaced by non-bug candidates** — they test sibling destructive endpoints that could bypass the changed auth boundary. However, bug-catching tests CAN displace them only after all lower-value non-bug slots are exhausted.
73
73
 
74
74
  When a qualifying candidate is inserted: place it HIGH before MEDIUM before LOW; within the same priority, source-code-derived candidates go BEFORE structural ones. Re-number ranks after insertion. The top ${ctx.maxGen} ranked items become GENERATE candidates.
75
75
 
@@ -80,8 +80,8 @@ When a qualifying candidate is inserted: place it HIGH before MEDIUM before LOW;
80
80
  function _execDiversityBody(_ctx) {
81
81
  return `**Bug-coverage gate (runs BEFORE dedup):**
82
82
  Verify that the highest-severity \`<bug_found>\` flaw from Step ${EXEC_STEP_CODE_REVIEW} has exactly one GENERATE item with category \`bug_caught\` targeting it — meaning the test would FAIL on the current buggy code and PASS once the flaw is fixed. At most one promotion per run (per Step ${EXEC_STEP_CODE_REVIEW} cap). If the promoted flaw has no targeting \`bug_caught\` GENERATE item:
83
- - Check ADDITIONAL for a matching test → promote it into the lowest-priority non-bug, non-CRITICAL GENERATE slot first (lowest category rank per \`crud > error_handling > workflow > data_validation > data_integrity > business_rule\`; preserve attack-surface \`security_boundary\` and internal \`new_endpoint\` items unless no lower-priority slot exists).
84
- - If no ADDITIONAL candidate matches, create a new \`bug_caught\` test and insert it, displacing the lowest-priority non-bug, non-CRITICAL GENERATE item first; displace an attack-surface \`security_boundary\` or other CRITICAL item only when every GENERATE slot is higher priority.
83
+ - Check ADDITIONAL for a matching test → promote it into the lowest-priority non-bug, non-CRITICAL GENERATE slot first (lowest category rank per \`crud > error_handling > workflow > data_validation > data_integrity > business_rule\`; preserve attack-surface \`security_boundary\` items unless no lower-priority slot exists).
84
+ - If no ADDITIONAL candidate matches, create a new \`bug_caught\` test and insert it, displacing the lowest-priority non-bug, non-CRITICAL GENERATE item first; displace an attack-surface \`security_boundary\` item only when every GENERATE slot is higher priority.
85
85
  A \`bug_caught\` test is NEVER considered a "duplicate" of a non-bug test during the dedup below.
86
86
 
87
87
  Each GENERATE item must exercise a **distinct code path** — not just different input values on the same path.
@@ -98,7 +98,7 @@ Same step sequence with only payload differences (e.g. 10% vs 5% discount both r
98
98
  function _execRegisterBody(_ctx) {
99
99
  return `Register your complete candidate list — every test you would generate OR recommend — via \`skyramp_register_test_plan\` (\`stateFile\` required). Include a discriminator claim (\`discriminator\` field — valid kinds and anchor rules are in the tool schema) for candidates probing the changed logic identified in Step ${EXEC_STEP_CODE_REVIEW}/Step ${EXEC_STEP_ENRICH}.
100
100
 
101
- The returned GENERATE list is mandatory and final — generation tools reject unregistered scenarios. If the tool demotes a discriminator claim (returned in \`demotions\` with a reason), either strengthen the claim — a step that actually exercises the declared \`kind\`, or a verbatim anchor that occurs in the diff — or drop it; the candidate itself stays in the plan either way.`;
101
+ The returned GENERATE list is mandatory and final — generation tools reject unregistered scenarios. If the tool demotes a discriminator claim (returned in \`demotions\` with a reason), either strengthen the claim — a step that actually exercises the declared \`kind\`, or a verbatim anchor that occurs in the diff — or drop it. A demoted candidate normally stays in the plan. If it also left the plan for another recorded reason, the report lists it under "Demoted AND removed".`;
102
102
  }
103
103
  function _execExecuteBody(ctx) {
104
104
  return `Replace any scenario that pairs unrelated resources with one reflecting actual foreign-key relationships in the codebase.
@@ -149,20 +149,6 @@ export const EXEC_STEP_EXECUTE = _execPlan.labels.EXECUTE; // "4"
149
149
  /** "5" — Register test plan (SKYR-3879 Path B checkpoint) */
150
150
  export const EXEC_STEP_REGISTER = _execPlan.labels.REGISTER; // "5"
151
151
  const SERVICE_REFS = resolveServiceDetailsRef();
152
- function prioritizeAttackSurfaceBundles(items) {
153
- const reordered = [];
154
- for (const item of items) {
155
- if (isAttackSurfaceSecurityBoundary(item.scenario)) {
156
- const firstDirectAuthIndex = reordered.findIndex(candidate => isOrdinaryDirectAuthBoundary(candidate.scenario));
157
- if (firstDirectAuthIndex >= 0) {
158
- reordered.splice(firstDirectAuthIndex, 0, item);
159
- continue;
160
- }
161
- }
162
- reordered.push(item);
163
- }
164
- return reordered;
165
- }
166
152
  export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges = false, hasTraces = false, externalCoverage = new Set(), relevantExternalTestPaths = [],
167
153
  /**
168
154
  * Whether the diff classified at least one new/modified/removed endpoint.
@@ -184,40 +170,58 @@ hasApiChanges = true) {
184
170
  : hasFrontendChanges
185
171
  ? Math.max(0, maxGen - 1)
186
172
  : maxGen;
187
- // Filter out scenarios whose primary method + resource + test type is already covered by external tests.
188
- // Method-aware: an external test covering GET /orders won't block PUT /orders scenarios.
189
- // This is the programmatic complement to the prompt-level Step ${EXEC_STEP_COVERAGE} dedup instructions.
190
- const scoredAfterExternalDedup = externalCoverage.size > 0
191
- ? scored.filter((item) => {
192
- const key = externalDedupKey(item.scenario);
193
- if (externalCoverage.has(key)) {
194
- if (item.scenario.category === "bug_caught" || isAttackSurfaceSecurityBoundary(item.scenario)) {
195
- logger.info(`External dedup: preserving "${item.scenario.scenarioName}" (${key}) — protected bug/attack-surface scenario requires semantic flaw coverage`);
196
- return true;
197
- }
198
- logger.info(`External dedup: skipping "${item.scenario.scenarioName}" (${key}) — covered by external test`);
199
- return false;
200
- }
201
- return true;
202
- })
203
- : scored;
204
- const slotOrderedItems = prioritizeAttackSurfaceBundles(scoredAfterExternalDedup);
205
- // Distribute the backend GENERATE slots evenly across the test types present
206
- // (contract vs integration) with spillover, instead of a pure top-N rank slice —
207
- // so a backend change isn't starved of (say) integration coverage just because
208
- // contract scenarios out-ranked it. See roundRobinByType.
209
- const generateItems = roundRobinByType(slotOrderedItems, Math.min(backendGenerateCount, slotOrderedItems.length));
210
- // ADDITIONAL = everything not chosen for GENERATE, in original rank order, capped
211
- // at the remaining budget. Computed by set-difference (not a positional slice)
212
- // because round-robin may pick items that aren't the first backendGenerateCount by
213
- // rank — a positional slice would double-list a generated item or drop one.
214
- const generateSet = new Set(generateItems);
215
- const rawAdditionalItems = slotOrderedItems
216
- .filter((it) => !generateSet.has(it))
217
- .slice(0, Math.max(0, topN - backendGenerateCount));
218
- // Filter additional items whose primary resource + test type already appear in GENERATE
219
- const generatedCoverage = new Set(generateItems.map((item) => scenarioCoverageKey(item.scenario)));
220
- const additionalItems = rawAdditionalItems.filter((item) => !generatedCoverage.has(scenarioCoverageKey(item.scenario)));
173
+ // Both this renderer and the register-plan path run the SAME selection
174
+ // pipeline — `runBudget` — instead of two copies of it (SKYR-4214 item 7).
175
+ // This file used to hold its own external dedup, attack-surface ordering,
176
+ // round-robin GENERATE pick and ADDITIONAL set-difference, so a fix in one
177
+ // copy left the other wrong.
178
+ //
179
+ // The two are NOT guaranteed to produce the same GENERATE set, and this
180
+ // comment must not claim they are: `selectPlan` re-ranks and calls
181
+ // `diversityBalancedBudgeter` (planRanker.ts:96) where this calls
182
+ // `fixedNBudgeter`, and the two differ in the `pick` step —
183
+ // diversityBalancedBudgeter.test.ts covers that divergence. What is now
184
+ // shared is the dedup rule, the ordering and the ADDITIONAL cap.
185
+ //
186
+ // `runBudget` needs a Candidate, which carries provenance and a stable id
187
+ // that only the register-plan path fills. Everything reaching this renderer
188
+ // is server-drafted, and `computeCandidateId` is a pure function of the
189
+ // scenario, so both fields are derivable here.
190
+ //
191
+ // The context deliberately reports no frontend work, whatever this PR
192
+ // contains: this renderer never had the UI-promotion loop, and the backend
193
+ // count it computed above already applied the UI reservation. Passing the
194
+ // real flags would introduce UI slots into the prompt's GENERATE list, which
195
+ // is a behaviour change this item does not carry.
196
+ const candidates = scored.map((item) => ({
197
+ ...item,
198
+ source: CandidateSource.SERVER,
199
+ candidateId: computeCandidateId(item.scenario),
200
+ }));
201
+ const selection = fixedNBudgeter.select(candidates, {
202
+ maxGenerate: backendGenerateCount,
203
+ maxTotal: topN,
204
+ isUIOnlyPR: false,
205
+ hasFrontendChanges: false,
206
+ externalCoverage,
207
+ // This renderer never receives the branch diff, so it cannot tell whether the
208
+ // PR wrote tests of its own. `true` is the same abstain direction the analyze
209
+ // end takes for an unknown (SKYR-4024): the covered-candidate reserve stays
210
+ // shut here, and those candidates are reported in the Removed section below
211
+ // rather than promoted to ADDITIONAL.
212
+ diffChangesTestFiles: true,
213
+ });
214
+ const generateItems = selection.generate;
215
+ const additionalItems = selection.additional;
216
+ // A candidate the selection stage removed — covered by an external test, or
217
+ // past the maxTotal cap — leaves no trace in the two lists above. In the
218
+ // recommendation-only flow (analyze_changes plus this prompt, with no
219
+ // register call) this section is the ONLY place the agent can read that a
220
+ // candidate existed and why it went, which is what SKYR-4214 exists to fix.
221
+ const scenarioNameById = new Map(candidates.map((c) => [c.candidateId, c.scenario.scenarioName]));
222
+ const removedLines = selection.dropped
223
+ .map((drop) => `- ${scenarioNameById.get(drop.candidateId) ?? drop.candidateId}: ${drop.reason}`)
224
+ .join("\n");
221
225
  const hasWorkspaceAuthType = !!authTypeValue && authTypeValue !== "none";
222
226
  // For skyramp_integration_test_generation with scenarioFile:
223
227
  // - If workspace has authType set: omit auth entirely — workspace handles Bearer prefix.
@@ -432,7 +436,9 @@ ${isUIOnlyPR
432
436
  ### ADDITIONAL (list in additionalRecommendations in this order after Step ${EXEC_STEP_ENRICH} insertion)
433
437
 
434
438
  ${additionalLines || " (none pre-ranked)"}
435
- ${uiGroundingGuidance}
439
+ ${removedLines
440
+ ? `\n### Removed (${selection.dropped.length}) — candidates the selection stage dropped before ranking. They are NOT in the lists above; do not re-add them.\n\n${removedLines}\n`
441
+ : ""}${uiGroundingGuidance}
436
442
  ${uiGuidance}
437
443
  ${supplementNote}
438
444