@skyramp/mcp 0.1.8 → 0.2.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -1,11 +1,158 @@
1
1
  import { AUTH_MIDDLEWARE_PATTERNS_STR } from "../../utils/workspaceAuth.js";
2
2
  import { resolveServiceDetailsRef } from "../../utils/utils.js";
3
3
  import { logger } from "../../utils/logger.js";
4
- import { TEST_CATEGORIES } from "../../types/TestRecommendation.js";
4
+ import { TEST_CATEGORIES, } from "../../types/TestRecommendation.js";
5
5
  import { buildScopeAssessmentSection } from "./scopeAssessment.js";
6
+ import { PromptPlan } from "./promptPlan.js";
6
7
  import { buildTestPatternGuidelines, buildTestQualityCriteria, buildGenerationRules, MAX_CRITICAL_TESTS, } from "./recommendationSections.js";
7
- import { externalDedupKey, scenarioCoverageKey } from "./recommendationShared.js";
8
+ import { TASK_ANALYZE_MAINTAIN, TESTBOT_TASK1_STEP_CODE_REVIEW, externalDedupKey, isAttackSurfaceSecurityBoundary, isOrdinaryDirectAuthBoundary, scenarioCoverageKey, taskStepRef, } from "./recommendationShared.js";
9
+ // ── Step body functions ───────────────────────────────────────────────────────
10
+ function _execCodeReviewBody(_ctx) {
11
+ const codeReviewRef = taskStepRef(TASK_ANALYZE_MAINTAIN, TESTBOT_TASK1_STEP_CODE_REVIEW);
12
+ return `If you already performed Code Review in ${codeReviewRef}, carry forward ALL \`<function_review>\` and \`<bug_found>\` blocks from that step.
13
+
14
+ If no prior \`<function_review>\` blocks exist (for example, standalone \`skyramp_analyze_changes\` usage), do the code review now: read all changed files and produce a \`<function_review>\` block for every changed function before proceeding.
15
+
16
+ The highest-severity \`<bug_found>\` block from this code review triggers a mandatory test in the GENERATE list:
17
+ - Category: \`bug_caught\`, priority: CRITICAL
18
+ - The promoted bug-catching test displaces the lowest-priority non-bug, non-protected GENERATE item. Preserve attack-surface \`security_boundary\` items for sibling destructive operations unless no other non-bug slot exists.
19
+ - **At most one promotion per run** — if multiple \`<bug_found>\` blocks exist, promote the HIGHEST severity flaw (break ties by the order they appear in the code review). Additional bug-catching tests go into ADDITIONAL with a note that they should be generated if budget allows.
20
+ - If the GENERATE list is empty (no pre-ranked items), the promoted bug_caught test becomes the GENERATE list`;
21
+ }
22
+ function _execCoverageBody(ctx) {
23
+ return `${ctx.externalTestFilesList}For every GENERATE item below, check its endpoint path and test type against the Existing Tests list (further down in the prompt).
24
+ - **\`[external]\` tests**: If the endpoint is already covered by an \`[external]\` test of the same type → skip the resource entirely (do NOT create or update), **except for \`bug_caught\` and attack-surface \`security_boundary\` items**. A protected item may be skipped only if the external test directly asserts the same flaw/bypass and would fail/pass based on that same bug; same endpoint/resource coverage alone is not enough. Backfill from ADDITIONAL using the priority order below:
25
+ 1. **BUG-CATCHING TESTS FIRST (CRITICAL)**: If source code analysis revealed a bug, logic error, or incorrect formula (e.g. discount math adding instead of subtracting, off-by-one errors, missing validation), CREATE A TEST THAT EXPOSES IT. The test SHOULD FAIL — that's the point. Document the bug. Example: if discount formula is wrong, test with discount=20% and assert correct math. If no bug found, skip to #2.
26
+ 2. **PR-endpoint edge cases**: Look for integration test candidates covering error paths, boundary values, or alternative scenarios for the SAME endpoints changed in the PR diff. If no suitable candidate exists in ADDITIONAL, derive one from your source-code enrichment findings.
27
+ 3. **Same-resource other scenarios**: Other HTTP methods or flows on the same resource group touched by the PR.
28
+ 4. **Cross-resource workflows involving the PR endpoint**: Integration scenarios that include the PR's changed endpoint as one of the steps.
29
+ 5. **Unrelated endpoint coverage (last resort)**: Tests for endpoints with no connection to the PR diff, only when ALL options above have been exhausted.
30
+ **Avoid backfilling with a test for a completely unrelated resource (e.g. \`POST /reviews\` when the PR only changes \`/orders\`) if any PR-endpoint edge-case integration test is feasible.**
31
+ - **Contract tests (\`[skyramp]\`)**: If an existing \`[skyramp]\` contract test already covers that resource path → UPDATE the existing test file instead of creating a new one. A new test case is a new test even if the file already exists — count it toward \`newTestsCreated\`.
32
+ - **Integration/scenario tests**: Always generate as a new file via the scenario pipeline, even if an existing integration test covers the same resource. A new multi-step scenario is a distinct test. Count it toward \`newTestsCreated\`.
33
+ - **UI tests**: Always generate as a new file. Count toward \`newTestsCreated\`.`;
34
+ }
35
+ function _execEnrichBody(ctx) {
36
+ return `Using the source code you read in the Code Review step, quote relevant source in \`<source_evidence>\` blocks — include route handler signatures, request body schema fields, response shapes, and computed field formulas. Use these quotes to derive tool call parameters. Resolve:
37
+ - **Auth middleware** — check for known signals (${AUTH_MIDDLEWARE_PATTERNS_STR}). If any match, override \`authHeader\` and \`authScheme\` even if workspace.yml says authType: none. **If no known signal matches but the diff shows security-adjacent code** (decorators like \`@requiresRole\`/\`@Protected\`, function names like \`validateToken\`/\`checkPermission\`/\`verifyHMAC\`, or imports from auth/security packages), read the relevant source file to determine the actual auth scheme before proceeding. Auth handling for \`skyramp_integration_test_generation\` with \`scenarioFile\` is covered in the Tool Workflows section below.
38
+ - Business rules and formulas (e.g. total_cost = compute * rate + memory * rate)
39
+ - State transitions and domain constraints (e.g. budget cannot drop below current spend)
40
+ - Validation logic (field constraints, cross-field dependencies)
41
+ - Security boundaries not covered by the structural candidates below
42
+
43
+ For non-bug candidates from the pre-ranked list, evaluate against these 6 dimensions and assign priority:
44
+ | Dimension | What to assess |
45
+ | Production Safety | Guards a critical boundary (auth, unique constraint, cascade delete, data integrity, breaking migration)? → HIGH |
46
+ | Bug-Finding Potential | Targets a known failure mode (race condition, data consistency, state transition, cascade effect)? → HIGH |
47
+ | Mutation Side Effects | Does PUT/PATCH modify a collection of child items (line items, cart entries) and trigger recalculation (totals, counts, amounts)? → HIGH — this is the most common source of user-reported bugs |
48
+ | User Journey Relevance | Reflects how real users interact (from traces, business flows, critical paths)? → HIGH or MEDIUM |
49
+ | Coverage Gap | Addresses an area with zero existing test coverage? → bump up one tier |
50
+ | Code Insight | Derived from actual implementation (spotted middleware pattern, N+1 risk, unique constraint)? → bump up one tier |
51
+
52
+ Quality gate — ask all three questions:
53
+ 1. "Would this test prevent a production incident?" → YES = HIGH priority regardless of other dimensions
54
+ 2. "Does this test exercise a real workflow or catch a real bug?" → YES = at least MEDIUM
55
+ 3. "Does this test cover a mutation that modifies child items and triggers total/amount recalculation?" → YES = HIGH priority, and prefer it for GENERATE over simple single-field update tests for the same endpoint
56
+
57
+ Assign category: bug_caught (for \`<bug_found>\` flaws from Step ${EXEC_STEP_CODE_REVIEW}) | ${TEST_CATEGORIES.join(" | ")}
58
+
59
+ ${buildTestPatternGuidelines()}
60
+
61
+ **Bug-catching test insertion (from Step ${EXEC_STEP_CODE_REVIEW} findings):**
62
+ At most one \`<bug_found>\` flaw is promoted into GENERATE per run (the highest-severity one; break ties by source order). That test gets category \`bug_caught\`, CRITICAL priority, and displaces the lowest-ranked non-bug, non-protected GENERATE item. Preserve attack-surface \`security_boundary\` items for sibling destructive operations; they guard bypasses created when one destructive endpoint is newly protected but equivalent destructive siblings are not. No further justification needed — the flaw's existence IS the justification. Additional \`<bug_found>\` flaws beyond the first are placed in ADDITIONAL at highest priority.
63
+
64
+ INSERT a non-bug source-code-derived candidate into the ranked list **only if ALL three conditions are met**:
65
+ 1. Priority is HIGH (it guards a critical boundary or would prevent a production incident)
66
+ 2. It is specific to THIS codebase — derived from a concrete business rule, formula, or constraint found in the changed files (not a general pattern that applies to any API)
67
+ 3. It is not already covered by a structural candidate in the list below
68
+
69
+ If these conditions are not met, add it to ADDITIONAL only — do NOT displace a pre-ranked GENERATE item.
70
+ **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.
71
+
72
+ 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.
73
+
74
+ **Source-code validation gates:**
75
+ - **Cascade vs referential integrity**: If both a cascade-delete and a delete-blocked scenario appear for the same resource pair, keep only the one matching the source FK delete policy (ON DELETE CASCADE / cascade=True / onDelete: 'CASCADE' → keep cascade-delete; RESTRICT/PROTECT/no annotation → keep delete-blocked). Remove the inapplicable variant.
76
+ - **Unique constraints**: Unique-constraint scenarios (duplicate POST → 409) are pre-drafted for all resources. Confirm enforcement before keeping: SQL UNIQUE index, Mongoose unique: true, Prisma @unique, or explicit duplicate-check code. If the backend is Redis, schema-less, or has no explicit constraint in the changed files, move to ADDITIONAL with a note — do NOT generate.`;
77
+ }
78
+ function _execDiversityBody(_ctx) {
79
+ return `**Bug-coverage gate (runs BEFORE dedup):**
80
+ 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:
81
+ - 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).
82
+ - 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
+ A \`bug_caught\` test is NEVER considered a "duplicate" of a non-bug test during the dedup below.
84
+
85
+ Each GENERATE item must exercise a **distinct code path** — not just different input values on the same path.
86
+
87
+ For each pair of GENERATE items, ask: same HTTP method + path + step sequence + expected status? → DUPLICATE. Keep the richer item; replace the other with a test from a different path below. Move the displaced item to ADDITIONAL.
88
+
89
+ **Good diversity — aim for this mix across GENERATE slots:**
90
+ - **Happy-path**: create prerequisites → call the new endpoint → verify computed fields and child collections
91
+ - **Error-path**: trigger a distinct error status (404 for non-existent resource, 422 for invalid input, 400 for malformed request — whichever the source code handles)
92
+ - **State-variation**: same endpoint, different logic branch (empty array, remove instead of add, boundary value that triggers a guard)
93
+
94
+ Same step sequence with only payload differences (e.g. 10% vs 5% discount both returning 200) = same code path = duplicate. Different scenario names do not make duplicate tests distinct.`;
95
+ }
96
+ function _execExecuteBody(ctx) {
97
+ return `Replace any scenario that pairs unrelated resources with one reflecting actual FK relationships in the codebase.
98
+ Use the field names and values from the \`<source_evidence>\` blocks you quoted in Step ${EXEC_STEP_ENRICH} to fill all tool call parameters. Prefer reusing Step ${EXEC_STEP_ENRICH} evidence when it already resolves a placeholder, but if a placeholder cannot be replaced with concrete values from files already read, you may read the specific schema, model, or handler file needed to resolve it. Assert response field values, not just status codes.
99
+
100
+ ${buildTestQualityCriteria()}
101
+
102
+ ${buildGenerationRules(ctx.isUIOnlyPR)}
103
+
104
+ **ADDITIONAL recommendations** are submitted via \`skyramp_submit_report\`. Refer to its schema for required fields. Only include recommendations that add distinct coverage beyond what was generated.
105
+
106
+ **Never mark a recommendation "blocked":** No OpenAPI spec → use source code for shapes. No traces → provide \`skyramp_start_trace_collection\` instructions. No backend trace → use the scenario pipeline.
107
+
108
+ **Critical-category minimum:** At least ${Math.min(MAX_CRITICAL_TESTS, ctx.maxGen)} of the ${ctx.maxGen} GENERATE items should be from HIGH-priority categories (security_boundary, business_rule, data_integrity, breaking_change). The pre-ranked plan below already prioritises this — only override if source-code enrichment reveals a higher-value candidate.
109
+
110
+ **Bug-catching test requirement (final gate):** Verify that the highest-severity \`<bug_found>\` flaw from Step ${EXEC_STEP_CODE_REVIEW} has a dedicated GENERATE item targeting it (test would FAIL on buggy code, PASS when fixed). At most one promotion per run. Step ${EXEC_STEP_DIVERSITY} should have already ensured this — if the promoted flaw still lacks a dedicated GENERATE test, replace the lowest-priority non-bug, non-protected GENERATE item NOW. Bug-catching tests take priority over ordinary structural coverage; preserve attack-surface \`security_boundary\` items for sibling destructive operations unless every other generated slot is higher value.`;
111
+ }
112
+ // ── PromptPlan declaration ─────────────────────────────────────────────────────
113
+ // Defines the execution-plan step structure. All five steps are non-conditional.
114
+ // startFrom: 0 produces labels "0", "1", "2", "3", "4".
115
+ //
116
+ // Labels:
117
+ // CODE_REVIEW → "0" (correctness analysis — dedicated bug detection step)
118
+ // COVERAGE → "1" (external test coverage pre-check, always present)
119
+ // ENRICH → "2" (source-code enrichment / parameter grounding)
120
+ // DIVERSITY → "3" (diversity check, always present)
121
+ // EXECUTE → "4" (execute plan, always present)
122
+ const _execPlan = new PromptPlan({ startFrom: 0 })
123
+ .addPhase("execution", "", { headerLevel: "hidden", stepFormat: "bold" })
124
+ .step("CODE_REVIEW", "Code Review — Correctness Analysis (before anything else)", _execCodeReviewBody)
125
+ .step("COVERAGE", "External test coverage verification", _execCoverageBody)
126
+ .step("ENRICH", "Parameter Grounding & Priority Assignment", _execEnrichBody)
127
+ .step("DIVERSITY", (ctx) => `Diversity check (using enriched knowledge from Step ${ctx.enrichStepLabel})`, _execDiversityBody)
128
+ .step("EXECUTE", "Execute merged plan in rank order", _execExecuteBody)
129
+ .done();
130
+ // ── Exported step label constants ─────────────────────────────────────────────
131
+ /** "0" — Code Review: correctness analysis */
132
+ export const EXEC_STEP_CODE_REVIEW = _execPlan.labels.CODE_REVIEW; // "0"
133
+ /** "1" — External test coverage verification */
134
+ export const EXEC_STEP_COVERAGE = _execPlan.labels.COVERAGE; // "1"
135
+ /** "2" — Parameter grounding & priority assignment */
136
+ export const EXEC_STEP_ENRICH = _execPlan.labels.ENRICH; // "2"
137
+ /** "3" — Diversity check */
138
+ export const EXEC_STEP_DIVERSITY = _execPlan.labels.DIVERSITY; // "3"
139
+ /** "4" — Execute merged plan */
140
+ export const EXEC_STEP_EXECUTE = _execPlan.labels.EXECUTE; // "4"
8
141
  const SERVICE_REFS = resolveServiceDetailsRef();
142
+ function prioritizeAttackSurfaceBundles(items) {
143
+ const reordered = [];
144
+ for (const item of items) {
145
+ if (isAttackSurfaceSecurityBoundary(item.scenario)) {
146
+ const firstDirectAuthIndex = reordered.findIndex(candidate => isOrdinaryDirectAuthBoundary(candidate.scenario));
147
+ if (firstDirectAuthIndex >= 0) {
148
+ reordered.splice(firstDirectAuthIndex, 0, item);
149
+ continue;
150
+ }
151
+ }
152
+ reordered.push(item);
153
+ }
154
+ return reordered;
155
+ }
9
156
  export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges = false, hasTraces = false, externalCoverage = new Set(), relevantExternalTestPaths = []) {
10
157
  const frontendUrl = "<frontend_url>";
11
158
  // Slot allocation:
@@ -19,22 +166,27 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
19
166
  : maxGen;
20
167
  // Filter out scenarios whose primary method + resource + test type is already covered by external tests.
21
168
  // Method-aware: an external test covering GET /orders won't block PUT /orders scenarios.
22
- // This is the programmatic complement to the prompt-level Step 0 dedup instructions.
169
+ // This is the programmatic complement to the prompt-level Step ${EXEC_STEP_COVERAGE} dedup instructions.
23
170
  const scoredAfterExternalDedup = externalCoverage.size > 0
24
- ? scored.filter(item => {
171
+ ? scored.filter((item) => {
25
172
  const key = externalDedupKey(item.scenario);
26
173
  if (externalCoverage.has(key)) {
174
+ if (item.scenario.category === "bug_caught" || isAttackSurfaceSecurityBoundary(item.scenario)) {
175
+ logger.info(`External dedup: preserving "${item.scenario.scenarioName}" (${key}) — protected bug/attack-surface scenario requires semantic flaw coverage`);
176
+ return true;
177
+ }
27
178
  logger.info(`External dedup: skipping "${item.scenario.scenarioName}" (${key}) — covered by external test`);
28
179
  return false;
29
180
  }
30
181
  return true;
31
182
  })
32
183
  : scored;
33
- const generateItems = scoredAfterExternalDedup.slice(0, Math.min(backendGenerateCount, scoredAfterExternalDedup.length));
34
- const rawAdditionalItems = scoredAfterExternalDedup.slice(backendGenerateCount, topN);
184
+ const slotOrderedItems = prioritizeAttackSurfaceBundles(scoredAfterExternalDedup);
185
+ const generateItems = slotOrderedItems.slice(0, Math.min(backendGenerateCount, slotOrderedItems.length));
186
+ const rawAdditionalItems = slotOrderedItems.slice(backendGenerateCount, topN);
35
187
  // Filter additional items whose primary resource + test type already appear in GENERATE
36
- const generatedCoverage = new Set(generateItems.map(item => scenarioCoverageKey(item.scenario)));
37
- const additionalItems = rawAdditionalItems.filter(item => !generatedCoverage.has(scenarioCoverageKey(item.scenario)));
188
+ const generatedCoverage = new Set(generateItems.map((item) => scenarioCoverageKey(item.scenario)));
189
+ const additionalItems = rawAdditionalItems.filter((item) => !generatedCoverage.has(scenarioCoverageKey(item.scenario)));
38
190
  const hasWorkspaceAuthType = !!authTypeValue && authTypeValue !== "none";
39
191
  // For skyramp_integration_test_generation with scenarioFile:
40
192
  // - If workspace has authType set: omit auth entirely — workspace handles Bearer prefix.
@@ -50,11 +202,11 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
50
202
  const rank = i + 1;
51
203
  const zipPath = `<repositoryPath>/.skyramp/ui_test_${rank}_trace.zip`;
52
204
  return hasTraces
53
- ? (`**#${rank} — GENERATE** | ui | workflow | new\n` +
205
+ ? `**#${rank} — GENERATE** | ui | workflow | new\n` +
54
206
  `Scenario: ui-test-from-trace-${rank} (rename from the actual changed component/flow)\n` +
55
207
  `Validates: UI interactions for a changed frontend component or flow.\n\n` +
56
- `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}`)
57
- : (`**#${rank} — GENERATE** | ui | workflow | new\n` +
208
+ `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}`
209
+ : `**#${rank} — GENERATE** | ui | workflow | new\n` +
58
210
  `Scenario: ui-test-for-changed-component-${rank} (rename from the actual changed component/flow)\n` +
59
211
  `Validates: UI interactions for changed frontend component/flow ${rank}.\n\n` +
60
212
  `**Tool workflow:**\n` +
@@ -63,19 +215,20 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
63
215
  ` 3. \`browser_snapshot()\` after each key interaction\n` +
64
216
  ` 4. \`skyramp_export_zip({ outputPath: "${zipPath}" })\` — absolute path\n` +
65
217
  ` 5. \`skyramp_ui_test_generation({ playwrightInput: "${zipPath}", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}\n\n` +
66
- `Each item must target a distinct changed component or user flow.`);
218
+ `Each item must target a distinct changed component or user flow.\n\n` +
219
+ `**Empty-page fallback**: If the page has no test data (empty state, no items to interact with), do NOT skip test generation. Instead: (a) write the Playwright test code directly from your code analysis — you already know the component structure, event handlers, and expected behavior from reading the diff; (b) use \`page.evaluate()\` or API calls to seed test data if possible; (c) at minimum, test that the UI renders correctly and that interactive elements exist with correct attributes. An empty page is NEVER a reason to produce zero tests.`;
67
220
  }).join("\n\n")
68
221
  : "";
69
222
  // Mixed PR: reserve the last GENERATE slot for a UI test for the changed frontend components.
70
223
  // Guard: skip when maxGen=0 (caller explicitly requested no generation)
71
224
  const uiRank = generateItems.length + 1;
72
- const uiPlaceholderBlock = (hasFrontendChanges && !isUIOnlyPR && maxGen > 0)
225
+ const uiPlaceholderBlock = hasFrontendChanges && !isUIOnlyPR && maxGen > 0
73
226
  ? hasTraces
74
- ? (`**#${uiRank} — GENERATE** | ui | workflow | new\n` +
227
+ ? `**#${uiRank} — GENERATE** | ui | workflow | new\n` +
75
228
  `Scenario: ui-test-for-changed-components (rename from the actual changed component/flow)\n` +
76
229
  `Validates: UI interactions for the changed frontend components in this PR.\n\n` +
77
- `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}`)
78
- : (`**#${uiRank} — GENERATE** | ui | workflow | new\n` +
230
+ `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}`
231
+ : `**#${uiRank} — GENERATE** | ui | workflow | new\n` +
79
232
  `Scenario: ui-test-for-changed-components (rename from the actual changed component/flow)\n` +
80
233
  `Validates: UI interactions for the changed frontend components in this PR.\n\n` +
81
234
  `**Tool workflow:**\n` +
@@ -84,9 +237,11 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
84
237
  ` 3. \`browser_snapshot()\` after each key interaction\n` +
85
238
  ` 4. \`skyramp_export_zip({ outputPath: "<repositoryPath>/.skyramp/ui_mixed_pr_trace.zip" })\` — absolute path\n` +
86
239
  ` 5. \`skyramp_ui_test_generation({ playwrightInput: "<repositoryPath>/.skyramp/ui_mixed_pr_trace.zip", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}\n\n` +
87
- `Derive scenario name and steps from the actual changed frontend files.`)
240
+ `Derive scenario name and steps from the actual changed frontend files.\n\n` +
241
+ `**Empty-page fallback**: If the page has no test data (empty state), write the Playwright test code directly from code analysis. An empty page is NEVER a reason to skip UI test generation.`
88
242
  : "";
89
- const generateBlocks = generateItems.map((item, i) => {
243
+ const generateBlocks = generateItems
244
+ .map((item, i) => {
90
245
  const rank = i + 1;
91
246
  const s = item.scenario;
92
247
  const testType = s.testType ?? (s.steps.length === 1 ? "contract" : "integration");
@@ -96,7 +251,9 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
96
251
  const isBodyMethod = ["POST", "PUT", "PATCH"].includes(step.method);
97
252
  const requestBodyData = step.requestBody && Object.keys(step.requestBody).length > 0
98
253
  ? `\n Request body: ${JSON.stringify(step.requestBody)} (pass as JSON string in tool call, NOT as object)`
99
- : (isBodyMethod ? `\n Request body: <derive from source code schemas>` : "");
254
+ : isBodyMethod
255
+ ? `\n Request body: <derive from source code schemas>`
256
+ : "";
100
257
  const authContext = authHeaderValue
101
258
  ? `\n authHeader: "${authHeaderValue}"${authSchemeSnippet}`
102
259
  : `\n authHeader: <resolve from workspace or OpenAPI securitySchemes>; authScheme: <if Authorization>`;
@@ -109,10 +266,13 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
109
266
  }
110
267
  else {
111
268
  // integration / e2e / ui — multi-step scenario pipeline
112
- const stepLines = s.steps.map((st) => {
269
+ const stepLines = s.steps
270
+ .map((st) => {
113
271
  const chains = st.chainsFrom
114
272
  ? ` (chains: ${Array.isArray(st.chainsFrom)
115
- ? st.chainsFrom.map(c => `${c.sourceField} from step ${c.sourceStep}`).join(", ")
273
+ ? st.chainsFrom
274
+ .map((c) => `${c.sourceField} from step ${c.sourceStep}`)
275
+ .join(", ")
116
276
  : `${st.chainsFrom.sourceField} from step ${st.chainsFrom.sourceStep}`})`
117
277
  : "";
118
278
  const bodyHint = st.bodyMustInclude?.length
@@ -125,13 +285,16 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
125
285
  ? ` [use requestBody: ${JSON.stringify(st.requestBody)} — pass as JSON string in tool call]`
126
286
  : "";
127
287
  return ` ${st.order}. ${st.method} ${st.path} → ${st.expectedStatusCode}: ${st.description}${chains}${bodyHint}${bodyData}${responseHint}`;
128
- }).join("\n");
288
+ })
289
+ .join("\n");
129
290
  let destinationHost = "localhost";
130
291
  try {
131
292
  const parsed = new URL(baseUrl);
132
293
  destinationHost = parsed.hostname;
133
294
  }
134
- catch { /* use localhost as fallback */ }
295
+ catch {
296
+ /* use localhost as fallback */
297
+ }
135
298
  const authContext = authHeaderValue
136
299
  ? `authHeader: "${authHeaderValue}"${authSchemeSnippet}`
137
300
  : "authHeader: <resolve from workspace or OpenAPI securitySchemes>; authScheme: <if Authorization>";
@@ -155,129 +318,95 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
155
318
  ` - From source: ${fromSource}\n\n` +
156
319
  `**Tool pipeline**:\n` +
157
320
  ` 1. skyramp_batch_scenario_test_generation (see tool description for parameter structure)\n` +
158
- ` 2. skyramp_integration_test_generation with returned scenarioFile${authHeaderOnlyRef ? ` and ${authHeaderOnlyRef.replace(/^,\s*/, '')}` : ""}\n` +
321
+ ` 2. skyramp_integration_test_generation with returned scenarioFile${authHeaderOnlyRef ? ` and ${authHeaderOnlyRef.replace(/^,\s*/, "")}` : ""}\n` +
159
322
  ` **Note**: requestBody/responseBody must be JSON strings (e.g. "{\\"field\\":\\"value\\"}"), not objects.` +
160
323
  prereqNote);
161
324
  }
162
- }).join("\n\n");
325
+ })
326
+ .join("\n\n");
163
327
  // Pre-ranked backend additional candidates — the LLM picks from these per its Budget Plan.
164
- const additionalLines = additionalItems.map((item, i) => {
328
+ const additionalLines = additionalItems
329
+ .map((item, i) => {
165
330
  const rank = maxGen + i + 1;
166
331
  const s = item.scenario;
167
332
  const testType = s.testType ?? (s.steps.length === 1 ? "contract" : "integration");
168
333
  const target = s.steps.length === 1
169
334
  ? `${s.steps[0].method} ${s.steps[0].path} → ${s.steps[0].expectedStatusCode}`
170
- : `Scenario: ${s.scenarioName} (${s.steps.map(st => `${st.method} ${st.path}`).join(" → ")})`;
335
+ : `Scenario: ${s.scenarioName} (${s.steps.map((st) => `${st.method} ${st.path}`).join(" → ")})`;
171
336
  return `#${rank} [ADDITIONAL] | ${testType} | ${s.category} | ${item.novelty}\n ${target}\n Validates: ${s.description}`;
172
- }).join("\n\n");
337
+ })
338
+ .join("\n\n");
339
+ // Phase C D-1.a: UI grounding guidance — fires whenever the PR has
340
+ // frontend changes (UI-only OR mixed). Tells the agent what to put in the
341
+ // `reasoning` field for UI test entries. This disambiguates the "Fill in
342
+ // placeholders from source code, then display verbatim" header that
343
+ // analyzeChangesTool wraps around this prompt: the catalog's STRUCTURE is
344
+ // frozen, but the `reasoning` CONTENT for UI entries should be blueprint-
345
+ // grounded using concrete elements the agent captured via browser_blueprint.
346
+ const uiGroundingGuidance = hasFrontendChanges ? `
347
+ **UI recommendation grounding — applies to \`testType: "ui"\` entries.** The \`reasoning\` field for \`testType: "ui"\` entries MUST contain at least three of {\`role\`, \`accessibleName\`, \`testId\`, \`stableId\`, \`logicalName\`} cited in key=value form. Entries that omit the tuple are not valid output for UI test types. The agent should call \`browser_blueprint\` on affected pages (if it hasn't already) and use the captured data.
348
+
349
+ **Field stability note for consumers:** \`testId\` and \`stableId\` are stable identifiers (from \`data-testid\` and unique \`id\` attributes respectively) — code-generation consumers can key off them. \`role\` and \`accessibleName\` are derived from ARIA and survive DOM reshuffles. \`logicalName\` is a **display handle, not a stable identifier** — it's derived from role + accessibleName + section context and drifts when the accessibleName text changes. Cite \`logicalName\` for readability in the \`reasoning\` field, but downstream consumers (test generators, scoping tools) should NOT key off it. Prefer \`testId\` > \`stableId\` > \`role + accessibleName\` > \`fingerprint\` for identity.
350
+
351
+ **Format for singular elements:**
352
+ > role=<role>, accessibleName="<Accessible Name>", testId=<test-id-or-null>, stableId=<id-or-null>, logicalName=<logical_name>
353
+
354
+ **Format for repeating elements (table rows, list items):** include \`contextText\` values from the row when the recommendation targets a specific row:
355
+ > role=<role>, accessibleName="<template>", testId=<null>, stableId=<null>, logicalName=<name>, contextText=["customer@example.com", "$129.99", "Pending"]
356
+
357
+ **Example (Edit Order form's Save button on /orders/{id}):**
358
+ > role=button, accessibleName="Save changes", testId=null, stableId=null, logicalName=save_changes_btn — verifies boundary-value clamping on discount_percent (0..100)
359
+
360
+ **Example (new-order-row recommendation on /orders):**
361
+ > role=button, accessibleName="View details for order 13", testId=null, stableId=null, logicalName=view_details_for_order_btn, contextText=["customer@example.com", "$129.99", "Pending"] — verifies new order row renders with correct customer + total + status
362
+
363
+ **Validates line — applies to \`testType: "ui"\` entries.** The \`Validates:\` line for UI entries should describe an observable behavior the test verifies — what changes on the page after the action, or what state the user can see. Ground this description in the captured blueprint when possible. Reference structural facts (an element appears, a count changes, a status text updates, a URL transitions) rather than implementation language (component names, props, internal state). The line should be readable to someone who has not seen the source diff.
364
+
365
+ **Scope clarification:** this grounding format applies **only** to \`testType: "ui"\` entries. Contract, integration, e2e, batch-scenario \`reasoning\` and \`Validates:\` fields use their existing conventions (endpoint paths, schemas, fixture chains) — do NOT reformat those. The "Fill in placeholders, then display verbatim" rule above refers to the CATALOG STRUCTURE (sections, ordering, test types); UI entries' \`reasoning\` and \`Validates:\` CONTENT follows this grounding format.
366
+
367
+ **If blueprint data isn't available** — agent skipped pre-scan, app unreachable, \`BlueprintInvariantError\`, or no candidate page covers the changed component — UI entries may fall back to source-grounded reasoning. Each such entry MUST be flagged with a leading \`[no-blueprint-data]\` marker in the \`reasoning\` field, and the failure mode must be logged in \`issuesFound\` with \`info\` severity naming the entry. Do NOT silently produce ungrounded reasoning without the marker.
368
+ ` : "";
173
369
  // UI/E2E guidance — the LLM adds as many as its Budget Plan calls for.
174
370
  // Note: if a UI test already occupies a GENERATE slot (uiPlaceholderBlock), that slot
175
371
  // satisfies the UI generate count — do not add it again in ADDITIONAL.
176
- const uiGuidance = !isUIOnlyPR ? `
372
+ // Only include when there are actually frontend changes (not backend-only PRs).
373
+ const uiGuidance = !isUIOnlyPR && hasFrontendChanges
374
+ ? `
177
375
  **UI/E2E tests (add per your Budget Plan):** If your Budget Plan requires UI/E2E items beyond what is already in your GENERATE list, append an [ADDITIONAL] entry for each. If a UI test already occupies a GENERATE slot above, that slot satisfies your UI/E2E generate count — do NOT add it again to ADDITIONAL. Tool workflow for each new item:
178
376
  - **E2E**: ${hasTraces ? "Use discovered trace/recording files with `skyramp_e2e_test_generation`." : "Add to additionalRecommendations with a note that both a backend API trace (`skyramp_start_trace_collection` / `skyramp_stop_trace_collection`) and a browser Playwright recording must be collected in a live environment first. Do NOT attempt `skyramp_e2e_test_generation` without both traces present."}
179
377
  - **UI**: ${hasTraces ? "Use an existing Playwright `.zip` trace with `skyramp_ui_test_generation`." : `Record a trace using \`browser_navigate\` + \`browser_snapshot\` + \`skyramp_export_zip\`, then call \`skyramp_ui_test_generation({ playwrightInput: "<zip_path>", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}.`}
180
- Derive scenario names and steps from the actual changed frontend files. If your Budget Plan calls for 0% UI/E2E, omit this entirely.` : "";
181
- const supplementNote = `\n**If your Budget Plan total exceeds the pre-ranked items listed above:** draft additional tests from source-code enrichment (Step 1). For each new or changed endpoint, identify boundary or variation scenarios — formula parameters, search/filter constraints, required field validation. Only after exhausting PR-specific scenarios, add generic patterns (auth boundary → 401, non-existent ID → 404). Do NOT supplement with tests whose endpoint + test type match a GENERATE item.`;
378
+ Derive scenario names and steps from the actual changed frontend files. If your Budget Plan calls for 0% UI/E2E, omit this entirely.`
379
+ : "";
380
+ const supplementNote = `\n**If your Budget Plan total exceeds the pre-ranked items listed above:** draft additional tests from source-code enrichment (Step ${EXEC_STEP_ENRICH}). For each new or changed endpoint, identify boundary or variation scenarios — formula parameters, search/filter constraints, required field validation. Only after exhausting PR-specific scenarios, add generic patterns (auth boundary → 401, non-existent ID → 404). Do NOT supplement with tests whose endpoint + test type match a GENERATE item.`;
182
381
  // ── PR / branch-diff mode: execution plan ────────────────────────────────
183
382
  const externalTestFilesList = relevantExternalTestPaths.length > 0
184
- ? `**Read these external test files first** (paths are relative to the \`repositoryPath\` you passed to \`skyramp_analyze_changes\` — prepend it to get the absolute path). Determine exactly which HTTP methods + paths each one covers. This is the definitive source of truth for external coverage:\n${relevantExternalTestPaths.map(p => `- \`${p}\``).join("\n")}\n\n`
383
+ ? `**Read these external test files first** (paths are relative to the \`repositoryPath\` you passed to \`skyramp_analyze_changes\` — prepend it to get the absolute path). Determine exactly which HTTP methods + paths each one covers. This is the definitive source of truth for external coverage:\n${relevantExternalTestPaths.map((p) => `- \`${p}\``).join("\n")}\n\n`
185
384
  : "";
385
+ const _ctx = {
386
+ externalTestFilesList,
387
+ maxGen,
388
+ isUIOnlyPR,
389
+ enrichStepLabel: EXEC_STEP_ENRICH,
390
+ };
186
391
  return `## Execution Plan
187
392
  Seed: ${seed} | Endpoints: ${endpointCount} | Max: ${maxGen} generate + up to ${Math.max(topN - maxGen, 0)} additional (your Budget Plan determines the exact count)
188
393
 
189
- ${buildScopeAssessmentSection(topN, maxGen, isUIOnlyPR)}
394
+ ${buildScopeAssessmentSection(topN, maxGen, isUIOnlyPR, isUIOnlyPR ? 100 : hasFrontendChanges ? undefined : 0, hasFrontendChanges)}
190
395
 
191
- **Step 0 — External test coverage verification (before executing anything)**
192
- ${externalTestFilesList}For every GENERATE item below, check its endpoint path and test type against the Existing Tests list (further down in the prompt).
193
- - **\`[external]\` tests**: If the endpoint is already covered by an \`[external]\` test of the same type → skip the resource entirely (do NOT create or update). Backfill from ADDITIONAL using the priority order below:
194
- 1. **BUG-CATCHING TESTS FIRST (CRITICAL)**: If source code analysis revealed a bug, logic error, or incorrect formula (e.g. discount math adding instead of subtracting, off-by-one errors, missing validation), CREATE A TEST THAT EXPOSES IT. The test SHOULD FAIL — that's the point. Document the bug. Example: if discount formula is wrong, test with discount=20% and assert correct math. If no bug found, skip to #2.
195
- 2. **PR-endpoint edge cases**: Look for integration test candidates covering error paths, boundary values, or alternative scenarios for the SAME endpoints changed in the PR diff. If no suitable candidate exists in ADDITIONAL, derive one from your source-code enrichment findings.
196
- 3. **Same-resource other scenarios**: Other HTTP methods or flows on the same resource group touched by the PR.
197
- 4. **Cross-resource workflows involving the PR endpoint**: Integration scenarios that include the PR's changed endpoint as one of the steps.
198
- 5. **Unrelated endpoint coverage (last resort)**: Tests for endpoints with no connection to the PR diff, only when ALL options above have been exhausted.
199
- **Avoid backfilling with a test for a completely unrelated resource (e.g. \`POST /reviews\` when the PR only changes \`/orders\`) if any PR-endpoint edge-case integration test is feasible.**
200
- - **Contract tests (\`[skyramp]\`)**: If an existing \`[skyramp]\` contract test already covers that resource path → UPDATE the existing test file instead of creating a new one. A new test case is a new test even if the file already exists — count it toward \`newTestsCreated\`.
201
- - **Integration/scenario tests**: Always generate as a new file via the scenario pipeline, even if an existing integration test covers the same resource. A new multi-step scenario is a distinct test. Count it toward \`newTestsCreated\`.
202
- - **UI tests**: Always generate as a new file. Count toward \`newTestsCreated\`.
396
+ ${_execPlan.render(_ctx)}
203
397
 
204
- **Step 1Source-Code Enrichment (before executing anything)**
205
- Read the source code for ALL changed files. Before generating each recommendation, quote the relevant source code in a <source_evidence> block — include the route handler signature, request body schema fields, response shape, and any computed field formulas. Use these quotes to derive tool call parameters. Look for:
206
- - **Auth middleware** — check for known signals (${AUTH_MIDDLEWARE_PATTERNS_STR}). If any match, override \`authHeader\` and \`authScheme\` even if workspace.yml says authType: none. **If no known signal matches but the diff shows security-adjacent code** (decorators like \`@requiresRole\`/\`@Protected\`, function names like \`validateToken\`/\`checkPermission\`/\`verifyHMAC\`, or imports from auth/security packages), read the relevant source file to determine the actual auth scheme before proceeding. Auth handling for \`skyramp_integration_test_generation\` with \`scenarioFile\` is covered in the Tool Workflows section below.
207
- - Business rules and formulas (e.g. total_cost = compute * rate + memory * rate)
208
- - State transitions and domain constraints (e.g. budget cannot drop below current spend)
209
- - Validation logic (field constraints, cross-field dependencies)
210
- - Security boundaries not covered by the structural candidates below
211
-
212
- For each one found, evaluate it against these 6 dimensions and assign priority:
213
- | Dimension | What to assess |
214
- | Production Safety | Guards a critical boundary (auth, unique constraint, cascade delete, data integrity, breaking migration)? → HIGH |
215
- | Bug-Finding Potential | Targets a known failure mode (race condition, data consistency, state transition, cascade effect)? → HIGH |
216
- | Mutation Side Effects | Does PUT/PATCH modify a collection of child items (line items, cart entries) and trigger recalculation (totals, counts, amounts)? → HIGH — this is the most common source of user-reported bugs |
217
- | User Journey Relevance | Reflects how real users interact (from traces, business flows, critical paths)? → HIGH or MEDIUM |
218
- | Coverage Gap | Addresses an area with zero existing test coverage? → bump up one tier |
219
- | Code Insight | Derived from actual implementation (spotted middleware pattern, N+1 risk, unique constraint)? → bump up one tier |
220
-
221
- Quality gate — ask all three questions:
222
- 1. "Would this test prevent a production incident?" → YES = HIGH priority regardless of other dimensions
223
- 2. "Does this test exercise a real workflow or catch a real bug?" → YES = at least MEDIUM
224
- 3. "Does this test cover a mutation that modifies child items and triggers total/amount recalculation?" → YES = HIGH priority, and prefer it for GENERATE over simple single-field update tests for the same endpoint
225
-
226
- Assign category: ${TEST_CATEGORIES.join(" | ")}
227
-
228
- ${buildTestPatternGuidelines()}
229
-
230
- INSERT a source-code-derived candidate into the ranked list **only if ALL three conditions are met**:
231
- 1. Priority is HIGH (it guards a critical boundary or would prevent a production incident)
232
- 2. It is specific to THIS codebase — derived from a concrete business rule, formula, or constraint found in the changed files (not a general pattern that applies to any API)
233
- 3. It is not already covered by a structural candidate in the list below
234
-
235
- If these conditions are not met, add it to ADDITIONAL only — do NOT displace a pre-ranked GENERATE item.
236
- **CRITICAL-tier items (category: new_endpoint) should never be displaced** — they test the actual endpoints introduced in this PR and must always occupy GENERATE slots.
237
-
238
- 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 ${maxGen} ranked items become GENERATE candidates.
239
-
240
- **Source-code validation gates (apply during Step 1):**
241
- - **Cascade vs referential integrity**: If both a cascade-delete and a delete-blocked scenario appear for the same resource pair, keep only the one matching the source FK delete policy (ON DELETE CASCADE / cascade=True / onDelete: 'CASCADE' → keep cascade-delete; RESTRICT/PROTECT/no annotation → keep delete-blocked). Remove the inapplicable variant.
242
- - **Unique constraints**: Unique-constraint scenarios (duplicate POST → 409) are pre-drafted for all resources. Confirm enforcement before keeping: SQL UNIQUE index, Mongoose unique: true, Prisma @unique, or explicit duplicate-check code. If the backend is Redis, schema-less, or has no explicit constraint in the changed files, move to ADDITIONAL with a note — do NOT generate.
243
-
244
- **Step 2 — Diversity check (using enriched knowledge from Step 1)**
245
- Each GENERATE item must exercise a **distinct code path** — not just different input values on the same path.
246
-
247
- For each pair of GENERATE items, ask: same HTTP method + path + step sequence + expected status? → DUPLICATE. Keep the richer item; replace the other with a test from a different path below. Move the displaced item to ADDITIONAL.
248
-
249
- **Good diversity — aim for this mix across GENERATE slots:**
250
- - **Happy-path**: create prerequisites → call the new endpoint → verify computed fields and child collections
251
- - **Error-path**: trigger a distinct error status (404 for non-existent resource, 422 for invalid input, 400 for malformed request — whichever the source code handles)
252
- - **State-variation**: same endpoint, different logic branch (empty array, remove instead of add, boundary value that triggers a guard)
253
-
254
- Same step sequence with only payload differences (e.g. 10% vs 5% discount both returning 200) = same code path = duplicate. Different scenario names do not make duplicate tests distinct.
255
-
256
- **Step 3 — Execute merged plan in rank order**
257
- Replace any scenario that pairs unrelated resources with one reflecting actual FK relationships in the codebase.
258
- Use the field names and values from the \`<source_evidence>\` blocks you quoted in Step 1 to fill all tool call parameters. Prefer reusing Step 1 evidence when it already resolves a placeholder, but if a placeholder cannot be replaced with concrete values from files already read, you may read the specific schema, model, or handler file needed to resolve it. Assert response field values, not just status codes.
259
-
260
- ${buildTestQualityCriteria()}
261
-
262
- ${buildGenerationRules(isUIOnlyPR)}
263
-
264
- **ADDITIONAL recommendations** are submitted via \`skyramp_submit_report\`. Refer to its schema for required fields. Only include recommendations that add distinct coverage beyond what was generated.
265
-
266
- **Never mark a recommendation "blocked":** No OpenAPI spec → use source code for shapes. No traces → provide \`skyramp_start_trace_collection\` instructions. No backend trace → use the scenario pipeline.
267
-
268
- **Critical-category minimum:** At least ${Math.min(MAX_CRITICAL_TESTS, maxGen)} of the ${maxGen} GENERATE items should be from HIGH-priority categories (security_boundary, business_rule, data_integrity, breaking_change). The pre-ranked plan below already prioritises this — only override if source-code enrichment reveals a higher-value candidate.
269
-
270
- ### GENERATE (process these EXACTLY as listed, in order — after completing Steps 0–2 above; if Step 0 converts an item to UPDATE, backfill the ADD slot from ADDITIONAL following the priority order in Step 0)
398
+ ### GENERATE (after completing Steps ${EXEC_STEP_CODE_REVIEW}–${EXEC_STEP_EXECUTE} above) generate exactly these items in order; add variations to ADDITIONAL instead. If Step ${EXEC_STEP_COVERAGE} converts an item to UPDATE, backfill from ADDITIONAL (priority order in Step ${EXEC_STEP_COVERAGE})
271
399
 
272
400
  ${isUIOnlyPR
273
- ? (uiGenerateBlocks || " (no UI generate items — derive scenarios from changed frontend files)")
274
- : ([generateBlocks, uiPlaceholderBlock].filter(Boolean).join("\n\n") || " (no pre-ranked generate items — draft your own based on endpoint analysis)")}
275
-
276
- **COMPLIANCE CHECK**: Before proceeding, verify your generate list matches the items above. If you plan to generate a scenario with a different name than what is listed (e.g. you want to generate "order-update-discount-calculation" but the plan says "orders-patch-add-items-recalculate"), STOP use the plan's scenario name and steps. Add your alternative to ADDITIONAL instead. One retry on failure then skip to next item.
401
+ ? uiGenerateBlocks ||
402
+ " (no UI generate items — derive scenarios from changed frontend files)"
403
+ : [generateBlocks, uiPlaceholderBlock].filter(Boolean).join("\n\n") ||
404
+ " (no pre-ranked generate items — draft your own based on endpoint analysis)"}
277
405
 
278
- ### ADDITIONAL (list in additionalRecommendations in this order after Step 1 insertion)
406
+ ### ADDITIONAL (list in additionalRecommendations in this order after Step ${EXEC_STEP_ENRICH} insertion)
279
407
 
280
408
  ${additionalLines || " (none pre-ranked)"}
409
+ ${uiGroundingGuidance}
281
410
  ${uiGuidance}
282
411
  ${supplementNote}
283
412