@skyramp/mcp 0.1.1 → 0.1.3

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.
@@ -1,90 +1,229 @@
1
1
  import { getPersonaPrefix } from "../personas.js";
2
- export const UI_ASSERTIONS_PROMPT = `${getPersonaPrefix()}Your task is to enhance assertions for the given UI test.
2
+ import { maintenanceTaskSuffix, renderRule, } from "./sharedAssertionRules.js";
3
+ const UI_ASSERTION_CATEGORIES = [
4
+ {
5
+ name: "Critical UI Assertions",
6
+ rules: [
7
+ {
8
+ title: "Selector constraints",
9
+ description: "Every assertion uses a selector already in the file. Never invent `data-testid`, role names, or classes.",
10
+ subPoints: [
11
+ "No tautological assertions — locating an element by text X, then asserting it contains X.",
12
+ ],
13
+ examples: [
14
+ {
15
+ language: "javascript",
16
+ code: `await expect(page.getByTestId('badge-count')).toHaveText('3');`,
17
+ },
18
+ ],
19
+ },
20
+ {
21
+ title: "Page errors",
22
+ description: "Register `page.on('pageerror', ...)` before the first navigation and assert `expect(errors).toHaveLength(0)` at the end of the test.",
23
+ examples: [],
24
+ },
25
+ {
26
+ title: "Correct intended behavior",
27
+ description: "Assertions encode the expected outcome of the action, not the buggy runtime output observed in the trace.",
28
+ examples: [
29
+ {
30
+ language: "javascript",
31
+ code: `await expect(page.locator('[data-testid="session-row"]')).toHaveCount(3);`,
32
+ },
33
+ ],
34
+ },
35
+ {
36
+ title: "Tests target the changed behavior introduced by the pull request",
37
+ description: "At least one assertion targets the changed behavior on populated or updated state, not only empty or zero state.",
38
+ examples: [],
39
+ },
40
+ {
41
+ title: "Collection / repeated UI elements",
42
+ description: "For every collection page or selector matching multiple rendered nodes (items, rows, cards, badges, tabs, chips, definition rows, nav items), assert the exact count plus per-item content.",
43
+ subPoints: [
44
+ "Use `toHaveCount(N)` where N comes from the trace array length or rendered DOM (`0` for empty state).",
45
+ "Add at least one concrete per-item content assertion (`toHaveText`, `toHaveValue`, `toHaveAttribute`).",
46
+ "Empty trace → `toHaveCount(0)` paired with the empty-state text.",
47
+ ],
48
+ examples: [
49
+ {
50
+ language: "javascript",
51
+ code: `await expect(page.getByRole('row')).toHaveCount(2);
52
+ await expect(page.getByRole('cell', { name: 'admin@example.com' })).toHaveText('admin@example.com');`,
53
+ },
54
+ {
55
+ language: "javascript",
56
+ code: `await expect(page.getByTestId('notification-badge')).toHaveText('3');
57
+ await expect(page.getByTestId('notification-row')).toHaveCount(3);`,
58
+ },
59
+ {
60
+ language: "javascript",
61
+ code: `await expect(page.getByTestId('definition-row')).toHaveCount(3);
62
+ await expect(page.getByTestId('definition-row').nth(0)).toHaveText('Version 10.2.1');`,
63
+ },
64
+ ],
65
+ },
66
+ {
67
+ title: "Positive-path companion",
68
+ description: "Negative-only checks and URL-only routing tests must include at least one positive rendered-element assertion such as a heading, breadcrumb, title, link, count, or exact text.",
69
+ examples: [
70
+ {
71
+ language: "javascript",
72
+ code: `await expect(page).toHaveURL(/\\/users/);
73
+ await expect(page.getByRole('heading')).toHaveText('User Directory');`,
74
+ },
75
+ ],
76
+ },
77
+ {
78
+ title: "Captured network responses",
79
+ description: "For every `page.waitForResponse`, `page.on`, or `page.route`, assert at least one status, body, or header field. Body values used downstream must also appear as visible UI text.",
80
+ examples: [
81
+ {
82
+ language: "javascript",
83
+ code: `const loginResp = await page.waitForResponse('**/login');
84
+ expect(loginResp.status()).toBe(200);
85
+ expect((await loginResp.json()).user.email).toBe('admin@example.com');`,
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ },
91
+ {
92
+ name: "Computed Values",
93
+ rules: [
94
+ {
95
+ title: "Exact rendered values",
96
+ description: "When the exact text, value, or attribute is knowable from the trace or source, use `toHaveText`, `toHaveValue`, or `toHaveAttribute` — never `toBeVisible` or `toContainText`.",
97
+ subPoints: [
98
+ "Definition / info pages: assert the actual displayed values (version, count, date), not just the static label text.",
99
+ "Images: assert `toHaveAttribute('src', ...)` with the exact src — never visibility alone.",
100
+ ],
101
+ examples: [
102
+ {
103
+ language: "javascript",
104
+ code: `await expect(page.getByTestId('status')).toHaveText('Active');`,
105
+ },
106
+ {
107
+ language: "javascript",
108
+ code: `await expect(page.getByTestId('product-image')).toHaveAttribute('src', /expected-pattern/);`,
109
+ },
110
+ ],
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ name: "Post-edit State",
116
+ description: "Apply only when the test contains a state-changing action. Otherwise mark NOT APPLICABLE.",
117
+ rules: [
118
+ {
119
+ title: "Post-action visible state",
120
+ description: "After a state-changing action (form fill+submit, save / delete / create / toggle, checkbox click, hover / mouseout, refresh, reload, JS update, form edit), assert the visible updated outcome via `toHaveText`, `toHaveValue`, `toHaveAttribute`, or `toBeChecked`. The displayed values must reflect the edit.",
121
+ subPoints: [
122
+ "Toggle / checkbox renames: assert the new label or state, not `toBeVisible` on the renamed element.",
123
+ "Hover / mouseout: assert the overlay or state visible / hidden after the event.",
124
+ "List edit: when the test submits N items, assert exactly N rows. When the count is unknown before the action, read it before and assert the delta after.",
125
+ ],
126
+ examples: [
127
+ {
128
+ language: "javascript",
129
+ code: `await expect(page.getByTestId('description-checkbox')).toBeChecked();
130
+ await expect(page.getByTestId('description-label')).toHaveText('check_box Description');`,
131
+ },
132
+ {
133
+ language: "javascript",
134
+ code: `await expect(page.getByTestId('total')).toHaveText('$19.98');`,
135
+ },
136
+ {
137
+ language: "javascript",
138
+ code: `await expect(page.getByTestId('version')).toHaveText('10.2.1');`,
139
+ },
140
+ ],
141
+ },
142
+ ],
143
+ },
144
+ ];
145
+ function renderCategories(categories) {
146
+ let index = 0;
147
+ return categories
148
+ .map((category) => {
149
+ const rendered = category.rules
150
+ .map((rule) => {
151
+ index += 1;
152
+ return renderRule(index, rule);
153
+ })
154
+ .join("\n\n");
155
+ const header = `#### ${category.name}`;
156
+ return category.description
157
+ ? `${header}\n${category.description}\n\n${rendered}`
158
+ : `${header}\n${rendered}`;
159
+ })
160
+ .join("\n\n");
161
+ }
162
+ function renderAssertionCategoriesTemplate(categories) {
163
+ return categories
164
+ .map((category) => {
165
+ const ruleKeys = category.rules
166
+ .map((rule) => ` "${rule.title}": []`)
167
+ .join(",\n");
168
+ return ` "${category.name}": {\n${ruleKeys}\n }`;
169
+ })
170
+ .join(",\n");
171
+ }
172
+ export function getUIAssertionsPrompt(testFile, enhanceType) {
173
+ const categoryTemplate = renderAssertionCategoriesTemplate(UI_ASSERTION_CATEGORIES);
174
+ return `${getPersonaPrefix()}Your task is to enhance assertions for the given UI test file: \`${testFile}\`.${maintenanceTaskSuffix(enhanceType)}
175
+
3
176
  ### First Check
4
177
  If the generated test file has no \`expect()\` assertions, you MUST manually add them before anything else. Use \`import { expect } from '@skyramp/skyramp';\` — never from \`@playwright/test\`. If an existing import pulls \`expect\` from \`@playwright/test\`, move it to \`@skyramp/skyramp\` (keep \`test\` on the playwright line).
5
178
 
6
- ### Top Priorities
7
- You MUST output a \`<thinking>\` block that explicitly confirms each of these for the file:
8
- 1. **Selector inventory** — list every selector already present in the generated test file (\`data-testid\`, role + name, text, label, etc.). New assertions may use ONLY selectors from this list. Do NOT invent \`data-testid\` values, role names, or aria attributes.
9
- 2. **Process — Replay → Identify → Fix or Add** — walk through these three steps explicitly:
10
- a. **Replay the scenario mentally**: at each state-changing action (form submit, item add/edit/delete), ask: "What is the EXPECTED outcome based on the action performed?"
11
- b. **Identify expectation mismatches**: if the recorded trace shows a result that contradicts the action (e.g. removing 1 of 2 items but the page shows 3, submitting a form but getting a blank page, editing a field but the old value persists), that is an app bug the test should catch. List every mismatch you find.
12
- c. **Fix or add assertions** for each mismatch:
13
- - If an existing assertion uses the WRONG (buggy) value, edit it to assert the CORRECT expected value.
14
- - If no assertion exists for the buggy behavior, ADD one immediately after the action that triggers it.
15
- 3. Collection / list / grid / table page exact \`toHaveCount(N)\` OR explicit empty-state text + zero-count assertion. Heading or toolbar visibility is never enough.
16
- a. **\`toHaveCount(N)\` is MANDATORY whenever the test interacts with ANY repeated element** — rows, cards, list items, badges, nav links, breadcrumbs, definition rows, tab items, chips, table rows, accordion panels, etc. Use exact \`toHaveCount(N)\` (or \`toHaveCount(0)\` + empty-state text for the empty case). Single-element pages are exempt. Heading or toolbar visibility is NEVER enough when repeated elements are on screen.
17
- 4. Every known exact rendered value uses \`toHaveText('...')\`, not \`toBeVisible()\` or a regex.
18
- 5. Every state-changing action (submit, add, edit, delete) has a post-action assertion of the UPDATED state not the pre-action state.
19
- 6. \`page.on('pageerror', ...)\` listener registered BEFORE the first navigation; \`expect(errors).toHaveLength(0)\` at the end.
20
- 7. Computed/dynamic values (totals, counts, badges) asserted with exact expected values after each mutation.
21
- 8. Assertions encode the intended correct behavior. Buggy runtime errors, stale state, or crash output are only asserted when the intended UX is explicitly an error state.
22
- 9. At least one assertion targets the PR's core changed behavior. If the PR feature only activates with data (e.g. notifications badge, cart count, unread count), the test MUST create or seed that data — never assert only the empty/zero/default state.
23
-
24
- ### Assertion Strength (use the strongest applicable)
25
- - ✅ STRONG: \`toHaveCount(N)\`, \`toHaveText('Exact Value')\`, \`toHaveValue('input value')\`, \`toHaveAttribute('src', ...)\`
26
- - ⚠️ PARTIAL: \`toContainText('foo')\` — only when the full string is genuinely dynamic
27
- - ❌ WEAKEST: \`toBeVisible()\` — only when presence/absence is the actual test
28
-
29
- ### Strategic Placement
30
- 1. Collection pages: exact item/row/card count, OR empty-state text + zero-count, plus at least one concrete content assertion (cell, card, or text).
31
- ❌ BAD (collection page validated only by heading): \`expect(page.getByRole("heading", { name: "Activity Feed" })).toHaveText("Activity Feed")\`
32
- ❌ BAD (collection page validated only by toolbar): \`expect(page.getByRole("button", { name: "search" })).toBeVisible()\`
33
- ✅ GOOD: \`expect(page.getByRole("row")).toHaveCount(2)\` plus \`expect(page.getByRole("cell", { name: "admin@example.com" })).toHaveText("admin@example.com")\`
34
- 2. Read-only collection traces: still required to use the strongest read-only assertions above. Do not fall back to heading-only.
35
- 3. After each state-changing action: assert the visible UPDATED outcome (item appears, total recalculates, count changes).
36
- ❌ BAD (visibility when the recalculated total is known): \`expect(page.getByTestId('total')).toBeVisible()\`
37
- ✅ GOOD: \`expect(page.getByTestId('total')).toHaveText('$19.98')\`
179
+ ### Pre-Edit Assertion Analysis
180
+ Before editing the given test file, you must output a \`<thinking>\` block. The aim of the \`<thinking>\` block is to analyze each in-scope item (action, selector, or captured network response) in the given test file and output a JSON array that ensures no assertion rule is overlooked. The JSON array should match the template below — every assertion category and every rule title under it must appear as a key, even when the value is \`[]\`.
181
+ 1. Selector inventory — list every selector already present in the generated test file (\`data-testid\`, role + name, text, label, etc.). New assertions may use only selectors from this list. Do not invent \`data-testid\` values, role names, or aria attributes. Also note captured network responses, repeated element patterns, exact rendered text/value/attribute from trace/source, and existing \`toBeVisible()\` assertions whose exact text is knowable.
182
+ 2. Process — Replay → Identify → Fix or Add — walk through these three steps explicitly.
183
+ a. Replay the scenario mentally. At each state-changing action (form submit, item add/edit/delete), ask: "What is the EXPECTED outcome based on the action performed?"
184
+ b. Identify expectation mismatches. If the recorded trace shows a result that contradicts the action (e.g. removing 1 of 2 items but the page shows 3, submitting a form but getting a blank page, editing a field but the old value persists), that is an app bug the test should catch. List every mismatch you find.
185
+ c. Fix or add assertions for each mismatch.
186
+ - If an existing assertion uses the wrong (buggy) value, edit it to assert the correct expected value.
187
+ - If no assertion exists for the buggy behavior, add one immediately after the action that triggers it.
188
+ 3. Classify each in-scope action / selector / captured response by its applicable assertion category, marking each category APPLICABLE or NOT APPLICABLE.
189
+ - Critical UI Assertions applies when there is a collection / repeated element (\`toHaveCount\`), a pageerror handler, a captured network response, a negative-only or URL-only test needing a positive-path companion, or a tautological locator-by-text + assert-text to replace.
190
+ - Computed Values applies when there is a knowable exact text / value / attribute, including a definition-page displayed value or an image \`src\`.
191
+ - Post-edit State applies only when the test contains a state-changing action (form fill+submit, save / delete / create / toggle, checkbox click, hover / mouseout, refresh, reload, JS update, form edit); otherwise NOT APPLICABLE.
192
+ 4. For each in-scope item, output one JSON object using the template below. The output is an array — repeat the object template below once per in-scope item.
193
+ - \`action_or_selector_or_response\`: the selector, action, or captured network response this entry covers.
194
+ - \`assertion_categories\`: an object that MUST contain every category name below as a key. The value of each category is itself an object that MUST contain every rule title under that category as a key. For each rule, the value is an array of assertion lines you will add for this item under that rule. Use \`[]\` only when the rule does not apply to this item — every category key and every rule key must still be present. This forces you to consider every rule for every item.
38
195
 
39
- ❌ BAD (only confirms the placeholder is gone): \`expect(page.getByTestId('version')).not.toHaveText('—')\`
40
- ✅ GOOD: \`expect(page.getByTestId('version')).toHaveText('10.2.1')\`
41
- 4. Refresh / reload: assert the intended updated state still renders, not stale or error output.
42
- 5. Forms: assert displayed values reflect the edit, not the pre-edit state.
43
- 6. Lists modified by edits: assert exact item count after the edit (e.g. submitted 2 items → exactly 2 rows).
44
- 7. Lists with unknown exact count (test does not control the data): read the count before the action and assert it changed by the expected delta after.
45
- 8. Repeated UI elements (badges with counts, nav menu items, breadcrumb segments, definition rows on a panel/info page, tab list, chip group, accordion sections): assert \`toHaveCount(N)\` on the parent locator. Visibility on the container is never enough.
46
- ❌ BAD (badge with a numeric count, only label asserted): \`expect(page.getByText('Notifications')).toBeVisible()\`
47
- ✅ GOOD: \`expect(page.getByTestId('notification-badge')).toHaveText('3')\` PLUS \`expect(page.getByTestId('notification-row')).toHaveCount(3)\`
196
+ \`\`\`json
197
+ [{
198
+ "action_or_selector_or_response": "<selector | action | response>",
199
+ "assertion_categories": {
200
+ ${categoryTemplate}
201
+ }
202
+ }]
203
+ \`\`\`
48
204
 
49
- BAD (info/definition page with multiple rows, no count assertion): \`expect(page.getByRole('heading', { name: 'Server Info' })).toBeVisible()\`
50
- GOOD: \`expect(page.getByTestId('definition-row')).toHaveCount(3)\` PLUS \`toHaveText\` on each definition value (version string, Node version, OS string)
51
- 9. Image-affecting PRs: assert \`src\` attribute, not visibility.
52
- ❌ BAD (visibility does not verify the correct image): \`expect(page.getByTestId('product-image')).toBeVisible()\`
53
- ✅ GOOD: \`expect(page.getByTestId('product-image')).toHaveAttribute('src', /expected-pattern/)\`
54
- 10. Dynamic JS updates (no reload): assert the updated value immediately after the action.
55
- 11. Captured network responses (\`page.waitForResponse\`, \`page.on('response', ...)\`, \`page.route()\`): assert at least one field from the captured response — status, body value, or header. A captured-but-unasserted response is a no-op.
56
- ❌ BAD (response captured but never asserted): \`const loginResp = await page.waitForResponse('**/login')\`
57
- ✅ GOOD: \`const loginResp = await page.waitForResponse('**/login'); expect(loginResp.status()).toBe(200); expect((await loginResp.json()).user.email).toBe('admin@example.com')\`
205
+ ### Assertion Rules with Examples
206
+ Most-violated patterns — apply every time. (1) repeated elements → \`toHaveCount(N)\` + per-item \`toHaveText\`/\`toHaveValue\`/\`toHaveAttribute\` (Collection / repeated UI elements). (2) post-action state → \`toHaveText\`/\`toHaveValue\`/\`toHaveAttribute\`/\`toBeChecked\`, not \`toBeVisible\` when exact value is knowable (Post-action visible state). (3) routing/URL-only tests → at least one rendered-element exact text (Positive-path companion).
207
+ Strength order. \`toHaveCount\`/\`toHaveText\`/\`toHaveValue\`/\`toHaveAttribute\` > \`toContainText\` (only when string is genuinely dynamic) > \`toBeVisible\` (only when presence is the actual test).
58
208
 
59
- ### What NOT to Assert
60
- - Static page headings or boilerplate labels
61
- - Intermediate states (typing, dropdown opening)
62
- - Values already guaranteed by the action you just took
63
- - The same value with multiple selectors
64
- - Tautological assertions — locating an element by text X, then asserting it contains X
65
- ❌ BAD: \`page.getByText('My Activity').toContainText('My Activity')\`
66
- ✅ GOOD: \`expect(page.getByTestId('badge-count')).toHaveText('3')\`
67
- - Buggy error text or crash output as the expected result (unless the intended UX is explicitly an error state)
68
- ❌ BAD (asserting the bug as expected output): \`expect(page.getByText('s.expires.toISOString is not')).toHaveText('s.expires.toISOString is not a function')\`
69
- ✅ GOOD: assert the correct post-action UI state (e.g. \`expect(page.locator('[data-testid="session-row"]')).toHaveCount(3)\`) and let the test fail when the bug is present
209
+ ${renderCategories(UI_ASSERTION_CATEGORIES)}
70
210
 
71
- ### Verification of enhanced assertions
72
- 1. Every new assertion uses a selector that already appears in the generated file; no invented \`data-testid\`, role, or aria values
73
- 2. Every action-vs-trace mismatch identified during replay is encoded as either a corrected existing assertion or a new assertion right after the triggering action
74
- 3. **[CRITICAL] Every repeated-element selector the test interacted with has exact \`toHaveCount(N)\` (or \`toHaveCount(0)\` + empty-state text) PLUS at least one concrete content assertion.** Repeated elements include rows, cards, list items, badges, nav links, breadcrumbs, definition rows, tab items, chips, table rows, accordion panels. No bare visibility/heading-only assertions remain on repeated elements.
75
- 4. Every known exact value uses \`toHaveText\` instead of \`toBeVisible\` or a regex
76
- 5. Every state-changing action has a post-action assertion of the UPDATED state
77
- 6. \`pageerror\` listener registered before first navigation; \`expect(errors).toHaveLength(0)\` at end
78
- 7. Computed/dynamic values asserted with exact expected values after mutation
79
- 8. Assertions encode the intended correct behavior; buggy output only asserted for explicit error-state UX
80
- 9. At least one assertion targets the PR's core changed behavior
81
- 10. Test exercises a state-mutating flow not just the empty/zero/default state when the PR feature requires data to activate
82
- 11. No tautological assertions (element found by text X then asserts it contains X)
83
- 12. Image assertions use \`toHaveAttribute('src', ...)\` not just \`toBeVisible()\` when the PR affects images
84
- 13. Every captured network response (via \`page.waitForResponse\` / \`page.on('response')\` / \`page.route()\`) has at least one field asserted — status, body value, or header
85
- 14. \`expect\` imported from \`@skyramp/skyramp\`, not \`@playwright/test\`
211
+ ### What Not to Do
212
+ - Do not invent selectors, values, attributes, or assertions
213
+ - Do not use \`toBeVisible\` when exact text/value/state is knowable
214
+ - Do not use tautological locator-by-text + assert-text
215
+ - Do not use \`toContainText\` when the full string is known
216
+ - Do not assert static page headings, intermediate states, or values guaranteed by the action you just took
217
+ - Do not assert the same value with multiple selectors
218
+ - Do not import \`expect\` from \`@playwright/test\`
219
+ - Do not restructure, reorder, or modify existing code/imports/signatures
220
+ - Do not add comments or docstrings
221
+ - Do not remove or modify existing assertions
222
+ - Do not assert buggy/error text as expected (unless the intended UX is an error state)
86
223
 
87
- An item passes verification only when the assertion is present AND is a good assertion per the rules above. If any item is not satisfied — assertion missing, OR present but a bad assertion — add or fix it per the rules before completing, preferring the strongest applicable matcher (see \`### Assertion Strength\`).
224
+ ### Verification of Assertions
225
+ After adding all assertion lines in the given test file, verify that every applicable rule has been applied correctly to each in-scope action, selector, and captured response. If any are missing or weakly applied, fix them before completing.
88
226
 
89
227
  The goal is tests that FAIL when the app has bugs, not tests that simply replay what happened.
90
228
  `;
229
+ }
@@ -79,19 +79,23 @@ For GET list endpoints: identify query params (\`limit\`, \`offset\`, \`order\`,
79
79
  ${nextStep}`;
80
80
  }
81
81
  const changedFiles = p.parsedDiff?.changedFiles.join(", ") ?? "";
82
- // Whether the regex pre-detected any API endpoints used as a hint only.
83
- // Step 2 always asks the LLM to extract endpoints from the diff so unknown
84
- // frameworks (e.g. Spring class-level @RequestMapping, Django, Rails) are
85
- // covered even when the static regex returns nothing.
86
- const regexFoundEndpoints = p.parsedDiff && (p.parsedDiff.newEndpoints.length > 0 || p.parsedDiff.modifiedEndpoints.length > 0);
82
+ // Whether the scanner found API endpoints in any changed file.
83
+ const preDetectedEndpoints = p.parsedDiff && (p.parsedDiff.newEndpoints.length > 0 || p.parsedDiff.modifiedEndpoints.length > 0 || (p.parsedDiff.removedEndpoints?.length ?? 0) > 0);
87
84
  const diffFiles = p.parsedDiff?.changedFiles ?? [];
88
85
  const isUIOnly = diffFiles.length > 0 &&
89
- !regexFoundEndpoints &&
86
+ !preDetectedEndpoints &&
90
87
  diffFiles.every(f => FRONTEND_EXT.test(f));
91
88
  const diffHasJavaFiles = diffFiles.some(f => /\.(java|kt)$/.test(f));
92
- const diffSection = p.diffContent
93
- ? `\n<diff>\n${p.diffContent}\n</diff>`
94
- : "";
89
+ // Inline small diffs so the LLM sees them without a tool call. Large diffs
90
+ // stay as a temp file reference to avoid bloating the prompt.
91
+ const INLINE_DIFF_LIMIT = 12_000; // chars — roughly 300 lines
92
+ const canInline = p.diffContent && p.diffContent.length <= INLINE_DIFF_LIMIT;
93
+ const diffFileRef = canInline
94
+ ? `\n<diff>\n${p.diffContent}\n</diff>\n`
95
+ + (p.diffFilePath ? `Full diff also available at \`${p.diffFilePath}\`.\n` : "")
96
+ : p.diffFilePath
97
+ ? `\n**Full diff file**: \`${p.diffFilePath}\` — **you MUST read this file before proceeding to Step 2.** It contains the complete unified diff for this PR.\n`
98
+ : "";
95
99
  const step2 = isUIOnly
96
100
  ? `### Step 2: Identify consumed API endpoints and integration status
97
101
  UI-only PR — perform two checks:
@@ -105,26 +109,28 @@ If no production file imports, re-exports, or renders a changed component, mark
105
109
  Exception: if the same PR also adds a route/page file (e.g. under Next.js \`pages/\` or \`app/\`) that imports the component, the route IS the integration point — do NOT mark it as unintegrated.
106
110
  Do NOT apply the unintegrated heuristic to route/entrypoint files themselves — those are always reachable by convention.
107
111
  An unintegrated non-route component has no DOM node in the running app and cannot be browser-tested — it qualifies as a dead-code / unintegrated-component no-surface PR regardless of how complex the component logic is.`
108
- : p.diffContent
109
- ? `### Step 2: Extract new and modified API endpoints from the diff
110
- Read the \`<diff>\` above and identify every new or modified API endpoint — route registrations, handler methods, controller annotations. Then use the **Router Mounting / Nesting** section above to reconstruct the full URL path for each endpoint by chaining all parent router prefixes down to the handler (e.g. a handler in a file with \`prefix="/reviews"\` that is mounted at \`/{product_id}\` under a router mounted at \`/api/v1/products\` → full path \`/api/v1/products/{product_id}/reviews\`).
112
+ : (canInline || p.diffFilePath)
113
+ ? `### Step 2: Extract new, modified, and removed API endpoints from the diff
114
+ ${canInline ? "Read the `<diff>` above" : `Read the diff file at \`${p.diffFilePath}\``} and identify every new or modified API endpoint — route registrations, handler methods, controller annotations. Then use the **Router Mounting / Nesting** section above to reconstruct the full URL path for each endpoint by chaining all parent router prefixes down to the handler (e.g. a handler in a file with \`prefix="/reviews"\` that is mounted at \`/{product_id}\` under a router mounted at \`/api/v1/products\` → full path \`/api/v1/products/{product_id}/reviews\`).
111
115
  ${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
112
116
  For each endpoint found: note the HTTP method, full path, and source file.
113
- ${regexFoundEndpoints ? "The static analysis above pre-detected some endpoints — verify and augment with anything it missed." : "The static analysis did not detect endpoints for this framework rely on the diff to extract them."}
117
+ ${preDetectedEndpoints ? "The endpoint catalog above already lists some changed endpoints — verify and augment with anything it missed." : "No endpoints were pre-detected in the changed files extract them from the diff."}
118
+ **Also identify removed endpoints**: Look for deleted route annotations (lines starting with \`-\` in the diff) in modified files (files that still exist but had routes deleted). A removed endpoint is a route definition present in the base branch but absent in the current branch. Cross-reference against the scanned endpoint listing below — if a deleted route annotation's endpoint still appears there (e.g. moved to another file), it is NOT removed. Only flag endpoints that are truly gone from the codebase.
114
119
  **CRITICAL — Query params vs body:** For GET endpoints (especially search/filter/list),
115
120
  identify which parameters are URL query params vs request body. Look at framework-specific
116
121
  annotations (FastAPI \`Query()\`, Express \`req.query\`, Spring \`@RequestParam\`, etc.).
117
122
  Pass these as \`queryParams\` (not \`requestBody\`) when generating scenarios.`
118
- : `### Step 2: Extract new and modified API endpoints from source files
123
+ : `### Step 2: Extract new, modified, and removed API endpoints from source files
119
124
  No diff was available — read the changed source files listed above directly to identify new or modified API endpoints. Use the **Router Mounting / Nesting** section to reconstruct full paths.
120
125
  ${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
121
- For each endpoint found: note the HTTP method, full path, and source file.`;
126
+ For each endpoint found: note the HTTP method, full path, and source file.
127
+ Also compare against the endpoint catalog to identify any endpoints that appear in the catalog but are no longer present in the source files — these are removed endpoints.`;
122
128
  const criticalPatternStep = `### Step 2.5: Identify critical patterns for test categorization
123
129
  Look for these patterns in model/schema/handler files to inform test recommendations:
124
130
  - **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations
125
131
  - **Cascade deletes**: \`ON DELETE CASCADE\`, \`.onDelete("cascade")\`, manual cascade logic in delete handlers
126
132
  - **Permission checks**: auth middleware, ownership guards (\`req.user.id === resource.ownerId\`), role-based access control, \`isOwner\` assertions
127
- - **Breaking changes in diff**: route renames, auth header changes, removed required fields, changed status codes
133
+ - **Breaking changes in diff**: route renames, deleted route definitions (endpoints removed from modified files), auth header changes, removed required fields, changed status codes
128
134
  Tag each finding with its category (security_boundary, business_rule, data_integrity, breaking_change) for the recommendation step.`;
129
135
  const step3Content = useHealthFlow
130
136
  ? `### Step 3: Identify tests at risk of drift
@@ -160,8 +166,7 @@ Call \`skyramp_recommend_tests\` with:
160
166
  return `## Your Task — Enrich & Recommend (PR-scoped)
161
167
 
162
168
  ### Step 1: Read the changed files and diff
163
- ${changedFiles}${diffSection}
164
-
169
+ ${changedFiles}${diffFileRef}
165
170
  ${buildPathResolutionTableStep(p)}${step2}
166
171
 
167
172
  ${criticalPatternStep}
@@ -186,7 +191,7 @@ ${p.routerMountContext.map(f => `- \`${f}\``).join("\n")}`
186
191
  **Session ID**: \`${p.sessionId}\`
187
192
  **Repository**: \`${p.repositoryPath}\`
188
193
  **Analysis Scope**: \`${p.analysisScope}\`
189
- ${isDiffScope ? `**Diff endpoints**: ${(p.parsedDiff?.newEndpoints.length ?? 0) + (p.parsedDiff?.modifiedEndpoints.length ?? 0)}` : `**Pre-scanned endpoints**: ${p.scannedEndpoints.length}`}
194
+ ${isDiffScope ? `**Diff endpoints**: ${(p.parsedDiff?.newEndpoints.length ?? 0) + (p.parsedDiff?.modifiedEndpoints.length ?? 0) + (p.parsedDiff?.removedEndpoints?.length ?? 0)}` : `**Pre-scanned endpoints**: ${p.scannedEndpoints.length}`}
190
195
  ${routerSection}
191
196
  ${enrichment}
192
197
 
@@ -48,7 +48,7 @@ Before each GENERATE tool call, confirm WHERE each key value comes from:
48
48
  - **requestBody / responseBody fields** → source code schema (Zod, Pydantic, DTO), enriched scenario, or OpenAPI spec. **The generation tool rejects empty \`{}\` request bodies for POST/PUT/PATCH** — read the source schema first if the fields are unknown.
49
49
  - **endpointURL** → workspace \`baseUrl\` + endpoint path (both required — never path alone)
50
50
  - **authHeader / authScheme** → workspace config or OpenAPI \`securitySchemes\`
51
- - **FK path params** → chained from a prior step's response \`id\` field — not hardcoded
51
+ - **FK path params** → chained from a prior step's response (check the actual field name — it may be \`id\`, \`uuid\`, \`_id\`, or a resource-specific \`*_id\` field). The chaining source can be a response body (POST or GET), a response header (e.g. \`Location\`), or a cookie — not hardcoded
52
52
  - **Names / string values** → realistic; append timestamp suffix to avoid re-run conflicts
53
53
 
54
54
  ## Ranking Rule
@@ -35,9 +35,10 @@ function classifyNovelty(scenario, diffContext) {
35
35
  const paths = scenario.steps.map(s => s.path);
36
36
  const newPaths = new Set((diffContext.newEndpoints || []).map(ep => ep.path));
37
37
  const modPaths = new Set((diffContext.modifiedEndpoints || []).map(ep => ep.path));
38
+ const removedPaths = new Set((diffContext.removedEndpoints || []).map(ep => ep.path));
38
39
  if (paths.some(p => newPaths.has(p)))
39
40
  return "new";
40
- if (paths.some(p => modPaths.has(p)))
41
+ if (paths.some(p => modPaths.has(p) || removedPaths.has(p)))
41
42
  return "modified";
42
43
  return "existing";
43
44
  }
@@ -530,7 +531,7 @@ function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, auth
530
531
  ? `authHeader: "${authHeaderValue}"${authSchemeSnippet}`
531
532
  : "authHeader: <resolve from workspace or OpenAPI securitySchemes>; authScheme: <if Authorization>";
532
533
  const prereqNote = s.category === "new_endpoint"
533
- ? `\n**Prerequisite discovery**: Check for FK fields (product_id, user_id, order_id) in the endpoint's request body. If found, prepend a step to create that prerequisite resource first, then chain its \`id\` into the dependent step using template variable syntax.`
534
+ ? `\n**Prerequisite discovery**: Check for FK fields (product_id, user_id, order_id) in the endpoint's request body. If found, prepend a step to create that prerequisite resource first, then chain its primary key field into the dependent step using template variable syntax. Check the actual field name from the response body (\`id\`, \`uuid\`, \`_id\`, etc.), response header (\`Location\`), or cookie — do not assume \`id\`.`
534
535
  : "";
535
536
  const bugLine = s.bugCatchingTarget
536
537
  ? `**Bug to catch**: ${s.bugCatchingTarget}\n`
@@ -703,7 +704,7 @@ export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScop
703
704
  ? filteredChangedFiles.some(f => isFrontendFile(f))
704
705
  : false;
705
706
  const hasApiChanges = isDiffScope && diffContext
706
- ? (diffContext.newEndpoints.length > 0 || diffContext.modifiedEndpoints.length > 0)
707
+ ? (diffContext.newEndpoints.length > 0 || diffContext.modifiedEndpoints.length > 0 || (diffContext.removedEndpoints?.length ?? 0) > 0)
707
708
  : false;
708
709
  const isUIOnlyPR = hasFrontendChanges && !hasApiChanges;
709
710
  const hasTraces = (analysis.artifacts?.traceFiles?.length ?? 0) > 0 ||
@@ -719,9 +720,46 @@ Output should be concise and immediately actionable.`
719
720
  : `You are in **Repo mode**. Comprehensive test strategy across all endpoints.`;
720
721
  // ── Endpoint listing ──
721
722
  const allEndpoints = analysis.apiEndpoints.endpoints;
722
- const endpointLines = allEndpoints
723
- .flatMap((ep) => (ep.methods ?? []).map((m) => ` ${m.method} ${ep.path}${m.authRequired ? " [auth]" : ""} (${(m.interactions ?? []).length} interactions)`))
724
- .join("\n");
723
+ // In PR mode, identify which endpoints were changed so we can partition the listing.
724
+ const changedEndpointKeys = new Set();
725
+ if (isDiffScope && diffContext) {
726
+ for (const ep of [...(diffContext.newEndpoints || []), ...(diffContext.modifiedEndpoints || []), ...(diffContext.removedEndpoints || [])]) {
727
+ for (const m of (ep.methods ?? [])) {
728
+ changedEndpointKeys.add(`${m.method} ${ep.path}`);
729
+ }
730
+ }
731
+ }
732
+ const fmtEndpoint = (m, ep) => ` ${m.method} ${ep.path}${m.authRequired ? " [auth]" : ""} (${(m.interactions ?? []).length} interactions)`;
733
+ let endpointLines;
734
+ if (isDiffScope && changedEndpointKeys.size > 0) {
735
+ const changedLines = [];
736
+ const otherLines = [];
737
+ for (const ep of allEndpoints) {
738
+ for (const m of (ep.methods ?? [])) {
739
+ const line = fmtEndpoint(m, ep);
740
+ if (changedEndpointKeys.has(`${m.method} ${ep.path}`)) {
741
+ changedLines.push(line);
742
+ }
743
+ else {
744
+ otherLines.push(line);
745
+ }
746
+ }
747
+ }
748
+ // Removed endpoints no longer exist in allEndpoints (current catalog), so they
749
+ // would be silently absent from changedLines. Append them explicitly with a
750
+ // [removed] marker so the LLM knows to generate verify-404/deprecation tests.
751
+ for (const ep of (diffContext?.removedEndpoints || [])) {
752
+ for (const m of (ep.methods ?? [])) {
753
+ changedLines.push(` ${m.method} ${ep.path} [removed]`);
754
+ }
755
+ }
756
+ endpointLines = `**Changed in this PR:**\n${changedLines.join("\n") || " none"}\n\n**Other endpoints (reference only — do not prioritize for testing):**\n${otherLines.join("\n") || " none"}`;
757
+ }
758
+ else {
759
+ endpointLines = allEndpoints
760
+ .flatMap((ep) => (ep.methods ?? []).map((m) => fmtEndpoint(m, ep)))
761
+ .join("\n");
762
+ }
725
763
  const authMethod = analysis.authentication.method || "unknown";
726
764
  const authTypeValue = workspaceAuthType ?? "";
727
765
  let authHeaderValue;
@@ -803,9 +841,13 @@ New endpoints:
803
841
  ${fmtEps(diffContext.newEndpoints, (m) => `${m.sourceFile}, ${m.interactionCount} interactions`)}
804
842
  Modified endpoints:
805
843
  ${fmtEps(diffContext.modifiedEndpoints, (m) => `${m.sourceFile}, ${m.changeType}`)}
844
+ Removed endpoints:
845
+ ${fmtEps(diffContext.removedEndpoints ?? [], (m) => `${m.sourceFile}, removed`)}
806
846
  Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
807
847
 
808
848
  Focus on tests that validate these changes and how they interact with existing resources.
849
+ For removed endpoints: verify they now return 404 or the appropriate deprecation status code.
850
+ Allocate your test budget to endpoints listed under "Changed in this PR". Use other endpoints only as setup steps (e.g. creating a resource before testing its deletion).
809
851
  `;
810
852
  }
811
853
  // ── Interactions ──