@skyramp/mcp 0.3.8 → 0.3.9-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/build/playwright/registerPlaywrightTools.js +1 -0
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +22 -1
  3. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  4. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  5. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +4 -1
  6. package/build/prompts/test-recommendation/diffExecutionPlan.js +1 -0
  7. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  8. package/build/prompts/testbot/testbot-prompts.js +10 -7
  9. package/build/services/containerEnv.d.ts +1 -1
  10. package/build/services/containerEnv.js +12 -0
  11. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  12. package/build/tools/executeSkyrampTestTool.js +246 -19
  13. package/build/tools/submitReportTool.d.ts +5 -5
  14. package/build/tools/submitReportTool.js +5 -1
  15. package/build/tools/test-management/actionsTool.js +71 -3
  16. package/build/tools/test-management/analyzeChangesTool.d.ts +10 -0
  17. package/build/tools/test-management/analyzeChangesTool.js +73 -0
  18. package/build/types/TestAnalysis.d.ts +12 -0
  19. package/build/types/TestExecution.d.ts +4 -0
  20. package/build/utils/AnalysisStateManager.d.ts +7 -0
  21. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  22. package/build/utils/assertion-verify/ui-lints.js +32 -0
  23. package/build/utils/frontendSelectors.d.ts +33 -0
  24. package/build/utils/frontendSelectors.js +196 -5
  25. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  26. package/build/utils/rebaselineSnapshots.js +65 -0
  27. package/build/utils/removedUiElements.d.ts +34 -0
  28. package/build/utils/removedUiElements.js +153 -0
  29. package/build/utils/reportVerification.d.ts +2 -6
  30. package/build/utils/reportVerification.js +61 -2
  31. package/build/utils/versions.d.ts +3 -3
  32. package/build/utils/versions.js +1 -1
  33. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  34. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  35. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  36. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  37. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  38. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  39. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  40. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  41. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  42. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  43. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  44. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  45. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  46. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  47. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  48. package/node_modules/playwright/package.json +1 -1
  49. package/package.json +3 -3
@@ -154,6 +154,7 @@ export async function registerPlaywrightTools(server, options) {
154
154
  'browser_wait_for',
155
155
  'browser_take_screenshot',
156
156
  'browser_assert',
157
+ 'browser_assert_hidden',
157
158
  'browser_assert_api_request',
158
159
  'browser_assert_table_cell',
159
160
  'browser_mouse_action',
@@ -33,7 +33,7 @@ expect(trigger).to_be_focused()`,
33
33
  },
34
34
  {
35
35
  title: "Selector constraints",
36
- description: "Every assertion uses a selector already in the file. Never invent `data-testid`, role names, or classes.",
36
+ description: "Every assertion uses a selector already in the file. Never invent `data-testid`, role names, or classes. One exception: a `data-*` attribute + value listed in `uiContext.removedElements` (from the `skyramp_analyze_changes` output in your context) may be asserted `toBeHidden()` even though no element carries it any more — `getByTestId(value)` for `data-testid`, `page.locator('[data-cy=\"value\"]')` for any other attribute — see Removed element guard. A `-` line in the raw diff is not enough on its own: renames and moved elements also appear there.",
37
37
  subPoints: [
38
38
  "No tautological assertions — locating an element by text X, then asserting it contains X.",
39
39
  ],
@@ -44,6 +44,27 @@ expect(trigger).to_be_focused()`,
44
44
  },
45
45
  ],
46
46
  },
47
+ {
48
+ title: "Removed element guard",
49
+ description: "When the PR deletes a rendered element (a `data-*` attribute + value listed in `uiContext.removedElements`) and the test visits the page that rendered it, assert `toBeHidden()` on it — `getByTestId(value)` when the attribute is `data-testid`, otherwise `page.locator('[<attribute>=\"<value>\"]')` (never `getByTestId` for `data-cy`/`data-qa`: Playwright resolves it against `data-testid` and the guard would pass vacuously). `toBeHidden` passes when the locator matches nothing or a single non-visible node (it is strict on multiple matches, like every locator assertion), which is the right contract for a removed element; do not substitute a `toBeVisible` on an unrelated element. Pair it with at least one positive assertion on a retained element of the same page so the test cannot pass on a blank or un-rendered page.",
50
+ subPoints: [
51
+ "Anchor on the removed attribute+value or role+name, never on text or position — a text-based absence check breaks when unrelated siblings change.",
52
+ "Do not add a guard for an element whose whole page or route was removed — that is maintenance of the tests that covered it, not a new assertion.",
53
+ ],
54
+ examples: [
55
+ {
56
+ language: "javascript",
57
+ code: `await expect(page.getByTestId('navbar-sessionid-row')).toBeHidden();
58
+ await expect(page.locator('[data-cy="checkout-btn"]')).toBeHidden();
59
+ await expect(page.getByTestId('navbar-products')).toHaveText('Products');`,
60
+ },
61
+ {
62
+ language: "python",
63
+ code: `expect(page.get_by_test_id('navbar-sessionid-row')).to_be_hidden()
64
+ expect(page.get_by_test_id('navbar-products')).to_have_text('Products')`,
65
+ },
66
+ ],
67
+ },
47
68
  {
48
69
  title: "Page errors",
49
70
  description: "Register `page.on('pageerror', ...)` before the first navigation and assert `expect(errors).toHaveLength(0)` at the end of the test.",
@@ -16,6 +16,8 @@ export declare function buildRenameStrategy(): string;
16
16
  export declare function buildFileRenameStrategy(): string;
17
17
  /** Strategy string for in-place UPDATE edits. */
18
18
  export declare function buildUpdateStrategy(): string;
19
+ /** Strategy string for UPDATE entries that refresh visual-snapshot baselines (SKYR-4298). */
20
+ export declare function buildRebaselineStrategy(): string;
19
21
  /** Strategy string for REGENERATE — call generation tool to overwrite the file. */
20
22
  export declare function buildRegenerateStrategy(): string;
21
23
  /** Strategy string for DELETE — remove obsolete test files whose covered endpoints/UI no longer exist. */
@@ -27,6 +29,8 @@ export declare function buildUpdateFileInstruction(params: {
27
29
  suggestedNewFile?: string;
28
30
  updateInstructions?: string;
29
31
  rationale?: string;
32
+ /** Visual baselines to refresh at final execution instead of editing (SKYR-4298). */
33
+ rebaselineSnapshots?: string[];
30
34
  }): string;
31
35
  /** Per-file instruction block for a single REGENERATE recommendation. */
32
36
  export declare function buildRegenerateFileInstruction(params: {
@@ -23,6 +23,10 @@ UPDATE is an in-place edit to the existing baseline file — do not create a new
23
23
 
24
24
  After editing, re-read each file to confirm the change landed, then call skyramp_enhance_assertions with each updated file path.`;
25
25
  }
26
+ /** Strategy string for UPDATE entries that refresh visual-snapshot baselines (SKYR-4298). */
27
+ export function buildRebaselineStrategy() {
28
+ return `Some update_context entries carry rebaseline_snapshots: the diff changed how the page or element those toHaveScreenshot() baselines capture looks, so the committed PNGs are stale and must be replaced, not the spec. For those baselines do NOT edit the toHaveScreenshot() call, do NOT raise maxDiffPixelRatio, do NOT remove the assertion, and do NOT touch the PNG yourself. Refresh them at the final execution: call skyramp_execute_test for that spec with rebaselineSnapshots set to exactly the listed filenames — SmartPlaywright re-captures each one and the new PNG lands beside the spec, where the Testbot PR shows it as an image diff for review. An entry with edit_required: false (no context) has nothing to edit: skip the Edit/re-read/skyramp_enhance_assertions steps for that file — the refresh at final execution is its whole maintenance. When you call skyramp_execute_test for the refresh, set testFile to the entry's spec_file (NOT its file, which is the page object for a POM-backed update). The execution result names which baselines were refreshed and which were not; report only the refreshed ones as refreshed. Never pass rebaselineSnapshots on the phase: "before" run — that run is the evidence that the baseline is stale.`;
29
+ }
26
30
  /** Strategy string for REGENERATE — call generation tool to overwrite the file. */
27
31
  export function buildRegenerateStrategy() {
28
32
  return `For each file in regenerate_context, call the appropriate generation tool (skyramp_integration_test_generation or skyramp_contract_test_generation) with outputDir set to the file's directory and output set to the filename. The generation tool will overwrite the file. Do NOT use skyramp_ui_test_generation here — UI test regeneration requires a recorded trace (playwrightInput) and must be handled separately.`;
@@ -34,6 +38,7 @@ export function buildDeleteStrategy() {
34
38
  /** Per-file instruction block for a single UPDATE recommendation. */
35
39
  export function buildUpdateFileInstruction(params) {
36
40
  const { testFile, renames, suggestedNewFile, updateInstructions, rationale } = params;
41
+ const rebaselineSnapshots = params.rebaselineSnapshots ?? [];
37
42
  const renameTable = renames.length > 0 ? [
38
43
  `**Endpoint Rename Detected — Path Substitution Required:**\n`,
39
44
  `| Old Path | New Path | Method |`,
@@ -49,13 +54,20 @@ export function buildUpdateFileInstruction(params) {
49
54
  ].join("\n") : "";
50
55
  const changeBlock = updateInstructions
51
56
  ? `**What to change:**\n\n${updateInstructions}\n`
52
- : renames.length === 0
57
+ : renames.length === 0 && rebaselineSnapshots.length === 0
53
58
  ? [
54
59
  rationale ? `**Why:** ${rationale}\n` : "",
55
60
  `**Action:** Update this test file based on the rationale above.\n`,
56
61
  ].filter(Boolean).join("\n")
57
62
  : "";
58
- return `\n### ${testFile}\n\n${renameTable}${changeBlock}`;
63
+ const rebaselineBlock = rebaselineSnapshots.length > 0
64
+ ? [
65
+ `**Visual baselines to refresh:** ${rebaselineSnapshots.map((s) => `\`${s}\``).join(", ")}`,
66
+ rationale && !updateInstructions ? `**Why:** ${rationale}` : "",
67
+ `**Action:** Do not edit these \`toHaveScreenshot()\` calls, their \`maxDiffPixelRatio\`, or the PNGs. At the final execution, call \`skyramp_execute_test\` for this spec with \`rebaselineSnapshots: ${JSON.stringify(rebaselineSnapshots)}\` so SmartPlaywright re-captures them; the refreshed PNGs are delivered with the Testbot PR as image diffs.\n`,
68
+ ].filter(Boolean).join("\n")
69
+ : "";
70
+ return `\n### ${testFile}\n\n${renameTable}${changeBlock}${rebaselineBlock}`;
59
71
  }
60
72
  /** Per-file instruction block for a single REGENERATE recommendation. */
61
73
  export function buildRegenerateFileInstruction(params) {
@@ -39,10 +39,12 @@ Matching none does **not** mean IGNORE — first run the selector safety net: ma
39
39
 
40
40
  Build a detection list first: for each matched diff line, write one line: \`{pattern type} — "{diff line}" — affects {route/component}\`.
41
41
 
42
+ **Visual-snapshot baselines.** A test that calls \`toHaveScreenshot("<name>.png")\` pixel-compares a page, element, or region against a committed baseline PNG. For such a test only — this does not widen the detection list for any other test — also treat as a reaching change any appearance edit inside what the call captures: a CSS/Tailwind/styled-components change, a changed literal string that renders, a swapped image or icon, a reordered or added visible element. When such a change reaches what one of those calls captures, the baseline is stale — the assertion's expected value is the image, and it must be refreshed exactly as a text assertion would be corrected. Assign **UPDATE** and list the affected baseline filenames in \`rebaselineSnapshots\` (only those whose captured page/element/region the diff reaches — an unaffected baseline in the same spec stays as it is). The refresh happens at execution via \`skyramp_execute_test\`'s \`rebaselineSnapshots\`; never edit the \`toHaveScreenshot()\` call, raise \`maxDiffPixelRatio\`, delete the assertion, or hand-edit a PNG to make it pass — those hide real regressions. When a pre-edit run reports \`Screenshot comparison failed\` and NO diff line explains the mismatch for that capture, the baseline is not stale: leave the test as it is, keep it \`Fail\`, and record the finding in \`issuesFound\`.
43
+
42
44
  **Rules (assign exactly one action per test; severity order above):**
43
45
  - **DELETE** when the test's target is gone: all routes/pages it covers no longer exist; all critical selectors were removed with no migration path; or, for a component/widget test, the component it renders was deleted (source removed with no re-export; report-only for external tests).
44
46
  - **REGENERATE** when the interaction model changed so fundamentally that the test's action sequence is broken with no migration path: the flow itself changed, not just selector values (e.g. a form replaced by a modal, buttons replaced by a dropdown, a click-to-update button replaced by an auto-updating stepper). Re-recording from scratch is the only viable approach. Broken-selector count alone does not decide REGENERATE; if the flow still works with targeted edits, it is UPDATE.
45
- - **UPDATE** when the test's flow still holds but something it relies on shifted on a page or component it exercises: a selector (className, data-testid, aria-*) was renamed or moved, a new selector was added that the test should adopt or assert (including a dynamic class like \`+ \`btn--\${variant}\`\`), or an assertion needs adjusting. Fix it in place in the existing file, never a new spec for the same page or component. An added selector is still drift, not a no-op. Read the test/POM to confirm the component is targeted and prefer UPDATE; fall back to VERIFY only when reading leaves impact undetermined.
47
+ - **UPDATE** when the test's flow still holds but something it relies on shifted on a page or component it exercises: a selector (className, data-testid, aria-*) was renamed or moved, a new selector was added that the test should adopt or assert (including a dynamic class like \`+ \`btn--\${variant}\`\`), an assertion needs adjusting, or a \`toHaveScreenshot()\` baseline captures something whose appearance the diff changed (see **Visual-snapshot baselines** above — name the stale files in \`rebaselineSnapshots\`). Fix it in place in the existing file, never a new spec for the same page or component. An added selector is still drift, not a no-op. Read the test/POM to confirm the component is targeted and prefer UPDATE; fall back to VERIFY only when reading leaves impact undetermined.
46
48
  - **VERIFY** when the test may be impacted but you cannot confirm breakage without reading more context (e.g. the affected selectors live in a page object or indirect import you have not read). Read that file before escalating to UPDATE.
47
49
  - **IGNORE** when the changed code cannot reach this test's routes or rendered components (the diff touches different routes or components). If you cannot point to a diff line affecting this test's routes, selectors, or rendered components, the action is IGNORE or VERIFY, not UPDATE.
48
50
  </ui_decision_rules>`;
@@ -182,6 +184,7 @@ Test: {testFile}
182
184
  Action: UPDATE
183
185
  Rationale: UPDATE because {quoted diff line}; name the concrete before→after value (e.g. data-testid "old-id" → "new-id", class "btn-old" → "btn-new", route "/old" → "/new") — this rationale is the report's record of what was maintained, so a vague "selector changed" is not enough
184
186
  UpdateInstructions: {what must change — specific selectors to update, routes to patch, assertions to add}
187
+ RebaselineSnapshots: {only when a toHaveScreenshot() baseline captures what the diff visibly changed — the stale filenames, e.g. ["page-001.png"]; omit otherwise}
185
188
  \`\`\`
186
189
  \`\`\`
187
190
  Test: {testFile}
@@ -48,6 +48,7 @@ function _execCoverageBody(ctx) {
48
48
  **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.**
49
49
  - **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\`.
50
50
  - **\`[removed]\` endpoints**: If a GENERATE item targets an endpoint marked \`[removed]\` — the route was deleted in this PR, not renamed — generate a single contract test that asserts \`404 Not Found\`. Do not generate success-path (2xx) tests for removed endpoints. The purpose is a regression guard that catches the endpoint being accidentally re-added.
51
+ - **Removed UI elements** (\`uiContext.removedElements\` in the \`skyramp_analyze_changes\` output): the grounding for the removal test the budget rule already requires — it names each removed element (a \`data-*\` attribute and value) and the page it rendered on. Per PAGE listed, not per id: if the plan already has a UI candidate on that page, that trace carries the guard (record \`browser_assert_hidden\` per listed element on that page — \`testId\` for \`data-testid\`, otherwise \`attribute\` + \`value\` — plus the page's ordinary assertions); otherwise register ONE candidate \`verify-removed-ui-<page-slug>\` (\`testType: "ui"\`, category \`breaking_change\`; \`skyramp_register_test_plan\` requires at least one step — use a placeholder \`GET <page path>\` → 200). Every guard spec pairs its \`toBeHidden()\` assertions with at least one positive assertion on a retained element of the same page; never substitute a \`toBeVisible\` on an unrelated element for the guard.
51
52
  - **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\`.
52
53
  - **UI tests**: Always generate as a new file. Count toward \`newTestsCreated\`.`;
53
54
  }
@@ -213,7 +213,7 @@ export function isTestFile(filePath) {
213
213
  * frontend GENERATE list is placeholders the agent must fill from the diff itself.
214
214
  */
215
215
  function changedBehaviorSection(effectiveGenerate, budgetPlanTiming = "") {
216
- return `**Fill the budget from changed behavior:** The ceiling above is the most this run may plan, not a quota to reach or a default to cut. Enumerate every observable behavior this diff changes — each option value added, removed, renamed or reordered; each label, element, route, state, validation rule or computed value that renders or behaves differently — and give each one its own candidate. Treat no change as too trivial to test: a changed option value that no test asserts is a test. A changed style value counts too — a spacing, size, color, radius, font weight, icon size or class name that now renders differently is a changed behavior; its candidate asserts the new computed style or class on that element, or records a \`browser_visual_snapshot\` of it. A removed option, element or feature is a changed behavior too: its candidate asserts the removed thing is absent from the page where it used to render — the removal is what this PR ships, so the removal is what gets tested.
216
+ return `**Fill the budget from changed behavior:** The ceiling above is the most this run may plan, not a quota to reach or a default to cut. Enumerate every observable behavior this diff changes — each option value added, removed, renamed or reordered; each label, element, route, state, validation rule or computed value that renders or behaves differently — and give each one its own candidate. Treat no change as too trivial to test: a changed option value that no test asserts is a test. A changed style value counts too — a spacing, size, color, radius, font weight, icon size or class name that now renders differently is a changed behavior; its candidate asserts the new computed style or class on that element, or records a \`browser_visual_snapshot\` of it. A removed option, element or feature is a changed behavior too: its candidate asserts the removed thing is absent from the page where it used to render — the removal is what this PR ships, so the removal is what gets tested. When the \`skyramp_analyze_changes\` output carries \`uiContext.removedElements\`, take the removed elements (each a \`data-*\` attribute and value) and the page to assert on from that list (it excludes renames, moves and deleted files, and names the page the element rendered on) instead of reading them off the raw diff; a removed element the list does not carry still gets its candidate, grounded in the diff.
217
217
 
218
218
  A changed behavior is something a user can observe differently after this diff. Code the diff deletes, dedupes or simplifies without changing what renders is not a changed behavior — its coverage is maintenance of the tests that reach it — and a pre-existing defect you notice nearby is an \`issuesFound\` entry, not a slot in this budget. Only a comment, whitespace or equivalent-value edit (\`0px\` → \`0\`) changes nothing observable.
219
219
 
@@ -144,7 +144,7 @@ export function getTestbotPrompt(opts) {
144
144
  maintenanceBeforeExecStep = ` d. Plan-only run: skip the pre-edit baseline execution — the application is not running. Record every maintenance verdict from static drift analysis alone; execution statuses simply remain unrecorded.`;
145
145
  }
146
146
  else {
147
- maintenanceBeforeExecStep = ` d. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every UPDATE/REGENERATE/DELETE test. Exclude tests marked \`[external]\` — those are baselined in step 2(a) via \`skyramp_run_existing_tests\`. Run them sequentially, not in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail.`;
147
+ maintenanceBeforeExecStep = ` d. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every UPDATE/REGENERATE/DELETE test. Exclude tests marked \`[external]\` — those are baselined in step 2(a) via \`skyramp_run_existing_tests\`. Run them sequentially, not in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail. Never pass \`rebaselineSnapshots\` here: a \`Screenshot comparison failed\` on this run is the evidence that a visual baseline is stale, and the refresh belongs to the final execution.`;
148
148
  }
149
149
  // For follow-up requests: emit the @skyramp-testbot header + guardrails + retrieve-recommendations step.
150
150
  // For first-run prompts: emit the full Task 1 analysis + maintenance section.
@@ -233,7 +233,7 @@ ${maintenanceBeforeExecStep}
233
233
  **Requirement check — does the code do what the PR says it does?** Read the \`<TITLE>\` and \`<DESCRIPTION>\` above as a statement of *intended* behavior and check the diff against it, requirement by requirement. The description is not outranked by the code: when the two disagree, that is a \`requirement_conflict\`, not a case of "the description loses". Detection, the requirements-file rules, the four false-red pre-checks and the promotion ordering are specified in the Code Review step of the Execution Plan \`skyramp_analyze_changes\` returned — follow them there rather than improvising; read any requirements file the description names from \`${repositoryPath}\`.
234
234
  - **On a mismatch, do both.** (a) Add an \`issuesFound\` entry at severity \`high\` or \`critical\` — never \`medium\` or below — quoting the requirement and naming the file and line that contradicts it. (b) Carry it into Task 2 as a \`requirement_conflict\` candidate: a test asserting **what the description requires**, which therefore FAILS on the current code. Where no such test is recordable, record the Execution Plan's VERIFY-style entry in \`additionalRecommendations\` instead — never resolve the disagreement by asserting what the code does.
235
235
 
236
- 4. **Blueprint Citation Invariant** (UI test recommendations only). Every named UI element in your recommendation must correspond to an element actually captured in one of the blueprints you captured from the UI Blueprint Capture section of \`skyramp_analyze_changes\`. For **every** UI recommendation — both \`newTestsCreated[]\` entries (tests you generated) AND \`additionalRecommendations[]\` entries (deferred tests) with \`testType: "ui"\` — populate **four** fields: \`description\`, \`reasoning\`, \`targetElements\`, and \`pageContext\`. \`description\` and \`reasoning\` carry different roles — fill BOTH (see Field 4 below). The structured fields (\`targetElements\`, \`pageContext\`) are required on UI recs in BOTH arrays; non-UI recs (contract / integration / e2e / batch-scenario) MUST omit them.
236
+ 4. **Blueprint Citation Invariant** (UI test recommendations only). Every named UI element in your recommendation must correspond to an element actually captured in one of the blueprints you captured from the UI Blueprint Capture section of \`skyramp_analyze_changes\` — with one exception: a removal guard's target is an element the PR removed, which no blueprint can contain; it is grounded in \`uiContext.removedElements\` instead (see the negative-test note below). For **every** UI recommendation — both \`newTestsCreated[]\` entries (tests you generated) AND \`additionalRecommendations[]\` entries (deferred tests) with \`testType: "ui"\` — populate **four** fields: \`description\`, \`reasoning\`, \`targetElements\`, and \`pageContext\`. \`description\` and \`reasoning\` carry different roles — fill BOTH (see Field 4 below). The structured fields (\`targetElements\`, \`pageContext\`) are required on UI recs in BOTH arrays; non-UI recs (contract / integration / e2e / batch-scenario) MUST omit them.
237
237
 
238
238
  **Field 1 — \`targetElements\`** (the elements the test targets — array, length 1+):
239
239
  \`\`\`json
@@ -347,7 +347,7 @@ ${maintenanceBeforeExecStep}
347
347
  "reasoning": "Would assert the **Edit Order** button (\`edit-order-btn\`) is not rendered on cancelled orders; enforces the conditional-render guard added in OrderDetail.tsx:118."
348
348
  }
349
349
  \`\`\`
350
- (For negative tests, ground \`targetElements\` against a captured page where the element DOES render — so the verifier can confirm the citation is real.)
350
+ (For negative tests, ground \`targetElements\` against a captured page where the element DOES render — so the verifier can confirm the citation is real. **Exception — removal guards:** an element listed in \`uiContext.removedElements\` renders on no page any more, so no blueprint can contain it. For its \`verify-removed-ui-<testId>\` recommendation, cite the element from that list (\`testId\` for a \`data-testid\`; otherwise name the \`attribute="value"\` pair in \`reasoning\` and leave \`testId\` null), take \`role\` and \`accessibleName\` from the deleted JSX in the diff, set \`pageContext.url\` to the page the list names, and prefix \`reasoning\` with \`[removed-element]\`.)
351
351
 
352
352
  *Page-level / lifecycle test (no single dominant element), recommendatory voice:*
353
353
  \`\`\`json
@@ -392,13 +392,13 @@ ${maintenanceBeforeExecStep}
392
392
 
393
393
  **Self-check before submitting (per UI rec, in both \`newTestsCreated\` and \`additionalRecommendations\`):**
394
394
  - **Both \`description\` AND \`reasoning\` are populated.** \`description\` is a 2-4 sentence walkthrough; \`reasoning\` is one sentence about what's verified + why. They must not duplicate each other.
395
- - Every entry in \`targetElements\` must appear verbatim in a captured blueprint element (matched on \`accessibleName\` + \`role\`).
395
+ - Every entry in \`targetElements\` must appear verbatim in a captured blueprint element (matched on \`accessibleName\` + \`role\`) — except a removal guard's entry, whose \`testId\` (or, for a non-\`data-testid\` attribute, the \`attribute="value"\` named in \`reasoning\`) must instead appear in \`uiContext.removedElements\` and whose \`reasoning\` carries the \`[removed-element]\` prefix.
396
396
  - Every element name appearing in \`reasoning\` must match an entry's \`accessibleName\` exactly. Every backticked identifier in \`reasoning\` must come from a \`testId\` or \`stableId\` of an entry.
397
397
  - **Voice matches the array:** \`newTestsCreated\` uses descriptive voice ("verifies", "asserts", "documents"). \`additionalRecommendations\` uses recommendatory voice ("would verify", "should assert", "would cover").
398
- - \`pageContext.url\` must equal the URL of the BlueprintCapture you lifted \`targetElements\` from.
398
+ - \`pageContext.url\` must equal the URL of the BlueprintCapture you lifted \`targetElements\` from (for a removal guard: the page \`uiContext.removedElements\` names for that id).
399
399
  - Never mix two elements' fields in one \`targetElements\` entry — each entry is one captured element.
400
400
  - No internal jargon in \`reasoning\` or \`description\`: no \`logicalName\`, \`fingerprint\`, \`pageHash\`, \`xpath\`.
401
- - If any check fails, re-capture and verify, or drop that recommendation.
401
+ - If any check fails, re-capture and verify, or drop that recommendation (a removal guard passes these checks through the \`removedElements\` grounding above, never through invention).
402
402
 
403
403
  **Non-UI entries (contract / integration / e2e / batch-scenario) are unaffected.** Use their pre-existing reasoning formats. Do NOT add \`targetElements\` or \`pageContext\` to non-UI entries — the schema rejects them.
404
404
 
@@ -576,6 +576,7 @@ ${CONTRACT_MODE_GUIDANCE}
576
576
  - \`skyramp_export_zip\` outputPath: \`${repositoryPath}/.skyramp/<test_name>_trace.zip\`
577
577
  - \`skyramp_ui_test_generation\`: set \`modularizeCode: false\` and \`codeReuse: true\` (TypeScript/JavaScript Playwright only — the default; leave \`codeReuse\` unset for other languages)
578
578
  - **\`browser_assert\` — MANDATORY**: at least one per page navigated. Call multiple assertions in the same tool call batch when checking independent elements. If you navigate to 2 pages, assert on both. Each assertion should verify a business outcome (state change, computed value, error condition) — not just that an element is visible.
579
+ - **Removal guard — when \`uiContext.removedElements\` is non-empty**: the server has already resolved which elements this PR removed — each identified by a \`data-*\` attribute and value — and the page each rendered on (renames, moves and deleted files are excluded). Per listed PAGE: \`browser_navigate\` to it, wait for the app to render, then call \`browser_assert_hidden\` once per listed element using the target the list shows — \`testId: "<value>"\` when the attribute is \`data-testid\`, otherwise \`attribute: "<name>", value: "<value>"\` (e.g. \`attribute: "data-cy"\`); no \`ref\` — the element no longer exists, so no snapshot has one; the tool checks the live page and refuses to record on a blank or un-rendered page. Then at least one \`browser_assert\` on a RETAINED element of the same page verifying its text, value or state. If a UI trace for that page is already planned, record the guard assertions inside that trace; otherwise this is its own trace and spec, \`verify-removed-ui-<page-slug>\`. The generated test asserts \`toBeHidden()\` on \`getByTestId(...)\` or \`page.locator('[data-cy="..."]')\` accordingly. A removed element the server did not list still gets its test (the budget rule requires it) — ground that one in the diff and say so in \`reasoning\`.
579
580
  - **\`browser_visual_snapshot\` — for visual/appearance checks**: when the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (not its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline on every run. Do NOT use \`browser_take_screenshot\` for this: it captures a throwaway image that is dropped at export and never appears in the generated test (use it only to view the page yourself).
580
581
  - **Wait for stable state before the second capture**: After performing an action that affects computed fields (filling a discount, submitting a form, adding an item), check the current page state before calling the second \`browser_blueprint\` (the capture after the action). If a computed field — total, price, count, derived text — still shows its initial empty or zero value (e.g. \`$0.00\`, \`0\`, \`Loading...\`, empty string), that means async data hasn't finished loading yet. Use \`browser_wait_for\` to wait up to 10 seconds for the field to update to a real value (for example, wait for the total to show a non-zero amount like \`$799.99\` instead of \`$0.00\`). Once the field shows a real value, THEN call the second \`browser_blueprint\` to capture stable state. If after 10 seconds the field still hasn't updated, skip the assertion on that field — don't capture and assert a value that hasn't loaded.
581
582
  If \`browser_navigate\` fails (app not running / connection refused), apply skip condition (a) above: move the intended test to \`additionalRecommendations\` with the failure reason AND record the outage in \`issuesFound\`.
@@ -651,6 +652,8 @@ If a test **execution** (\`skyramp_execute_test\`) fails for a newly generated t
651
652
  If you cannot point at the missing or wrong line in application code, the app is not at fault — shared or seeded data, parallel test workers, or setup the test itself never did explain the collision. Treat that as an infrastructure failure: fix it and retry once as in step 3.
652
653
 
653
654
  **If you did confirm the missing or wrong operation in the source, do NOT make the test pass.** Never add a reset, cleanup, or setup call for isolation. Never weaken the assertion — no \`==\` to \`>=\`, no exact value to a range. A failing test whose diagnosis names a pre-existing bug is the most valuable output of this run; a passing version of it reports nothing.
655
+
656
+ **Visual snapshots — maintained tests only, and never a third run.** A newly generated test writes its baseline on its first run and cannot be stale, so this applies to an existing spec whose committed baseline the PR made out of date. That mismatch is known from the \`phase: "before"\` run, and the refresh rides the final run: pass the \`rebaseline_snapshots\` list \`skyramp_actions\` returned as \`rebaselineSnapshots\` on that one call. It never costs an extra execution — if a screenshot mismatch on a maintained test first appears on the final run, report \`Fail\`; do not add a run. The execution result names which baselines were refreshed and which were not; the report row's description carries the refreshed names automatically, so \`afterDetails\` states only the outcome (e.g. \`1 passed in 6.1s\`). If no diff line explains a mismatch, keep the test \`Fail\` and add an \`issuesFound\` entry. Never raise \`maxDiffPixelRatio\`, never remove the \`toHaveScreenshot()\` call, never edit or delete a PNG by hand, and never pass \`rebaselineSnapshots\` for a mismatch the diff does not explain.
654
657
  3. Apply a targeted fix and retry **once** only for **infrastructure failures** — that means exactly **2 total \`skyramp_execute_test\` calls per test file** for these cases. Examples of infrastructure failures worth fixing:
655
658
  - Assertion mismatch from floating-point precision, or an expected value mis-transcribed from the observed response or computed with an arithmetic slip. If application behavior outside the diff explains the mismatch, it is not an infrastructure failure — use step 2 instead.
656
659
  - Import error, syntax error, or missing dependency in the generated test file
@@ -673,7 +676,7 @@ Do not make any changes other than the code-reuse refactoring (step 3 and the ge
673
676
 
674
677
  **Execution timing:**
675
678
  - **beforeStatus** (maintained tests only): execute each maintained test file **once at the start** (before any edits) to capture \`beforeStatus\`. This is the only execution allowed before edits.
676
- - **Final execution**: Do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together. **Execute tests SEQUENTIALLY (one at a time)** — do NOT send multiple \`skyramp_execute_test\` calls in the same tool call batch, as concurrent execution overwhelms the stdio transport and causes MCP disconnection. Exclude tests marked \`[external]\`.
679
+ - **Final execution**: Do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together. For a maintained spec whose \`skyramp_actions\` entry carried \`rebaseline_snapshots\`, pass that exact list as \`rebaselineSnapshots\` on this run and only this run. **Execute tests SEQUENTIALLY (one at a time)** — do NOT send multiple \`skyramp_execute_test\` calls in the same tool call batch, as concurrent execution overwhelms the stdio transport and causes MCP disconnection. Exclude tests marked \`[external]\`.
677
680
  - Only report test results for files you actually ran.
678
681
  **Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed.`;
679
682
  }
@@ -22,4 +22,4 @@ export declare function isValidEnvVarName(name: string): boolean;
22
22
  /**
23
23
  * Build the environment variable array for the Docker executor container.
24
24
  */
25
- export declare function buildContainerEnv(options: Pick<TestExecutionOptions, "token" | "language" | "useHostNetwork">, saveStoragePath?: string, hostEnv?: Record<string, string | undefined>, passthroughNames?: string[]): string[];
25
+ export declare function buildContainerEnv(options: Pick<TestExecutionOptions, "token" | "language" | "useHostNetwork" | "rebaselineSnapshots">, saveStoragePath?: string, hostEnv?: Record<string, string | undefined>, passthroughNames?: string[]): string[];
@@ -77,6 +77,18 @@ export function buildContainerEnv(options, saveStoragePath, hostEnv = process.en
77
77
  ...(options.token ? [`SKYRAMP_TEST_TOKEN=${options.token}`] : []),
78
78
  "SKYRAMP_IN_DOCKER=true",
79
79
  ];
80
+ // Visual-snapshot baselines this run replaces (SKYR-4298). SmartPlaywright
81
+ // reads SKYRAMP_UPDATE_SNAPSHOTS and re-captures each named baseline through
82
+ // its first-run path instead of comparing. Set here, first-class, because the
83
+ // SKYRAMP_ prefix is reserved from workspace passthrough below — nothing else
84
+ // can put it in the container. Omitted entirely when nothing is to refresh, so
85
+ // the default stays "compare".
86
+ const rebaseline = (options.rebaselineSnapshots ?? [])
87
+ .map((n) => n.trim())
88
+ .filter(Boolean);
89
+ if (rebaseline.length > 0) {
90
+ env.push(`SKYRAMP_UPDATE_SNAPSHOTS=${rebaseline.join(",")}`);
91
+ }
80
92
  // Skyramp-generated tests are standalone HTTP tests that never need host repo
81
93
  // conftest.py files or pytest configuration. --noconftest prevents loading any
82
94
  // conftest in the test directory tree (avoids missing deps like boto3, django).
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { TestExecutionResult } from "../types/TestExecution.js";
3
3
  import { TestType } from "../types/TestTypes.js";
4
+ import { MaintenanceActionCore, TestAnalysisResult } from "../types/TestAnalysis.js";
4
5
  export declare const CONTRACT_EXECUTION_MODES: readonly ["provider", "consumer"];
5
6
  export type ContractExecutionMode = (typeof CONTRACT_EXECUTION_MODES)[number];
6
7
  /**
@@ -20,6 +21,16 @@ export declare function shouldInjectSkyrampBaseUrl(testType: TestType, contractM
20
21
  * its own attachment line.
21
22
  */
22
23
  export declare function withVideoInfo(output: string, videoPath?: string): string;
24
+ /**
25
+ * Normalize the `rebaselineSnapshots` request (SKYR-4298): dedupe and drop blanks,
26
+ * and refuse it on the pre-edit run. A refresh there would overwrite the very
27
+ * evidence that the baseline is stale, and `beforeStatus` would read Pass for a
28
+ * test that failed against the committed image.
29
+ */
30
+ export declare function resolveRebaselineSnapshots(requested: string[] | undefined, phase: "before" | "after" | undefined): {
31
+ snapshots: string[];
32
+ error?: string;
33
+ };
23
34
  /**
24
35
  * The failure text the agent receives. Everything it needs has to be in here:
25
36
  * only a tool's return value reaches the transcript, and Claude Code does not
@@ -37,5 +48,74 @@ export declare function withVideoInfo(output: string, videoPath?: string): strin
37
48
  * agent at authentication on a run whose output was empty, and then it keyed off
38
49
  * the bare number, which pointed it there on a test count or a line number.
39
50
  */
51
+ /** The on-disk files a requested baseline name resolves to; empty when none matches. */
52
+ export type BaselineFileState = Array<{
53
+ file: string;
54
+ size: number;
55
+ mtimeMs: number;
56
+ }>;
57
+ /**
58
+ * Snapshot of the requested baselines under `<spec>-snapshots/` (SKYR-4298): for
59
+ * each requested name, every PNG whose name matches the stem (a spec may hold both
60
+ * `<stem>-linux.png` and `<stem>-chromium-linux.png`; latching onto one of them would
61
+ * misreport the other) with its size and mtime. Taken before and after the run so
62
+ * the tool can tell the agent which baselines were actually rewritten — SmartPlaywright
63
+ * is the only party that knows the exact filename, and an executor image that lacks
64
+ * SKYRAMP_UPDATE_SNAPSHOTS (or a name matching no toHaveScreenshot call) leaves
65
+ * every file untouched.
66
+ */
67
+ export declare function readBaselineState(specFile: string, requested: string[]): Record<string, BaselineFileState>;
68
+ /**
69
+ * Which requested baselines changed on disk between two readBaselineState calls, and
70
+ * which files carried the change (the ones to stage).
71
+ */
72
+ export declare function diffBaselineState(before: Record<string, BaselineFileState>, after: Record<string, BaselineFileState>): {
73
+ refreshed: string[];
74
+ notRefreshed: string[];
75
+ refreshedFiles: Record<string, string[]>;
76
+ };
77
+ /**
78
+ * Authorize a refresh against the persisted maintenance state (SKYR-4298). The
79
+ * parameter description tells the agent to pass exactly what skyramp_actions
80
+ * returned, but prose cannot tell an authorized list from an invented one, so the
81
+ * tool checks: an UPDATE verdict for this spec must list every requested name, and
82
+ * the spec's phase:"before" run must already be recorded — a refresh before that
83
+ * run would overwrite the very evidence that the baseline is stale, whatever
84
+ * `phase` says or omits. A newly generated test has no verdict and no committed
85
+ * baseline, so it can never be refreshed here.
86
+ */
87
+ export declare function authorizeRebaseline(stateData: {
88
+ existingTests?: TestAnalysisResult[];
89
+ maintenanceVerdicts?: MaintenanceActionCore[];
90
+ } | null | undefined, testFile: string, requested: string[]): {
91
+ error?: string;
92
+ };
93
+ /**
94
+ * Reconcile the persisted verdict with what the executor actually did (SKYR-4298).
95
+ * An executor image whose @skyramp/skyramp predates SKYRAMP_UPDATE_SNAPSHOTS rewrites
96
+ * nothing; left alone, the verdict would still promise a refresh, the report gate
97
+ * would refuse the report, and nothing in the prompt makes the agent's way out
98
+ * deterministic. So: names that were not refreshed are dropped from the verdict; a
99
+ * rebaseline-only UPDATE with nothing left becomes VERIFY (the test stays red, the
100
+ * rationale says why), and an UPDATE that also carried edits keeps UPDATE and is
101
+ * held to its edit. The report then reflects what happened, not what was asked.
102
+ */
103
+ export declare function applyRefreshOutcomeToVerdicts(verdicts: MaintenanceActionCore[], testFile: string, outcome: {
104
+ refreshed: string[];
105
+ notRefreshed: string[];
106
+ }, executorImage: string): {
107
+ verdicts: MaintenanceActionCore[];
108
+ note?: string;
109
+ };
110
+ /**
111
+ * The refresh outcome line appended to the execution result, on pass and on fail
112
+ * alike. Without it a stale-baseline run that could not refresh reads as a bare
113
+ * "Screenshot comparison failed", and the report gate then tells the agent to run
114
+ * with rebaselineSnapshots set — which it already did.
115
+ */
116
+ export declare function describeRefreshOutcome(outcome: {
117
+ refreshed: string[];
118
+ notRefreshed: string[];
119
+ }): string;
40
120
  export declare function buildExecutionFailureText(result: TestExecutionResult): string;
41
121
  export declare function registerExecuteSkyrampTestTool(server: McpServer): void;