@skyramp/mcp 0.3.8 → 0.4.0-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 (229) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -1
  2. package/build/commands/commandLibrary.js +3 -3
  3. package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
  4. package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
  5. package/build/commands/testThisEndpointCommand.js +35 -19
  6. package/build/index.js +9 -3
  7. package/build/playwright/blueprintDigest.d.ts +15 -0
  8. package/build/playwright/blueprintDigest.js +152 -0
  9. package/build/playwright/blueprintDigestStore.d.ts +31 -0
  10. package/build/playwright/blueprintDigestStore.js +117 -0
  11. package/build/playwright/registerPlaywrightTools.js +60 -12
  12. package/build/playwright/traceRecordingPrompt.js +8 -7
  13. package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
  14. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
  15. package/build/prompts/promptAssets.d.ts +20 -0
  16. package/build/prompts/promptAssets.js +55 -0
  17. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
  18. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  19. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
  23. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  24. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
  25. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  26. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  27. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  28. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  29. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  30. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  31. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  33. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  34. package/build/prompts/testbot/planDeclarations.js +9 -0
  35. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  36. package/build/prompts/testbot/testbot-prompts.js +256 -381
  37. package/build/recommendation/answers.d.ts +35 -0
  38. package/build/recommendation/answers.js +96 -0
  39. package/build/recommendation/registerPlan.d.ts +49 -0
  40. package/build/recommendation/registerPlan.js +117 -0
  41. package/build/recommendation/runVerifiers.d.ts +10 -0
  42. package/build/recommendation/runVerifiers.js +49 -0
  43. package/build/recommendation/subjectStep.d.ts +42 -0
  44. package/build/recommendation/subjectStep.js +86 -0
  45. package/build/recommendation/types.d.ts +163 -0
  46. package/build/recommendation/types.js +20 -0
  47. package/build/recommendation/verifierContracts.d.ts +382 -0
  48. package/build/recommendation/verifierContracts.js +263 -0
  49. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  50. package/build/recommendation/verifiers/changedFile.js +82 -0
  51. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  52. package/build/recommendation/verifiers/citedPath.js +35 -0
  53. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  54. package/build/recommendation/verifiers/coverage.js +617 -0
  55. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  56. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  57. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  58. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  59. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  60. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  61. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  62. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  63. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  64. package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
  65. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  66. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  67. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  68. package/build/recommendation/verifiers/screenRoute.js +118 -0
  69. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  70. package/build/recommendation/verifiers/statedDifference.js +140 -0
  71. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  72. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  73. package/build/resources/analysisResources.js +1 -114
  74. package/build/resources/testbotResource.js +23 -13
  75. package/build/services/ModularizationService.js +2 -1
  76. package/build/services/TestDiscoveryService.d.ts +3 -72
  77. package/build/services/TestDiscoveryService.js +10 -303
  78. package/build/services/containerEnv.d.ts +1 -1
  79. package/build/services/containerEnv.js +12 -0
  80. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  81. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  82. package/build/toolNames.d.ts +1 -0
  83. package/build/toolNames.js +1 -0
  84. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  85. package/build/tools/code-refactor/modularizationTool.js +2 -1
  86. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  87. package/build/tools/executeSkyrampTestTool.js +246 -19
  88. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  89. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  90. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  91. package/build/tools/generate-tests/planGuard.js +78 -18
  92. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  93. package/build/tools/one-click/oneClickTool.js +0 -5
  94. package/build/tools/submitReportTool.d.ts +48 -42
  95. package/build/tools/submitReportTool.js +576 -193
  96. package/build/tools/test-management/actionsTool.js +72 -4
  97. package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
  98. package/build/tools/test-management/analyzeChangesTool.js +212 -1219
  99. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  100. package/build/tools/test-management/index.d.ts +1 -0
  101. package/build/tools/test-management/index.js +1 -0
  102. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  103. package/build/tools/test-management/registerTestPlanTool.js +609 -542
  104. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  105. package/build/tools/test-management/resolveScreenTool.js +289 -0
  106. package/build/types/BlueprintDigest.d.ts +34 -0
  107. package/build/types/BlueprintDigest.js +1 -0
  108. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  109. package/build/types/RepositoryAnalysis.js +2 -58
  110. package/build/types/StepMethod.d.ts +40 -0
  111. package/build/types/StepMethod.js +77 -0
  112. package/build/types/TestAnalysis.d.ts +12 -0
  113. package/build/types/TestExecution.d.ts +4 -0
  114. package/build/types/TestRecommendation.d.ts +24 -24
  115. package/build/types/TestRecommendation.js +91 -89
  116. package/build/types/TestbotPromptOptions.d.ts +0 -4
  117. package/build/types/TestbotReport.d.ts +64 -2
  118. package/build/utils/AnalysisStateManager.d.ts +79 -113
  119. package/build/utils/AnalysisStateManager.js +147 -57
  120. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  121. package/build/utils/assertion-verify/metrics.js +85 -36
  122. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  123. package/build/utils/assertion-verify/ui-lints.js +32 -0
  124. package/build/utils/branchDiff.d.ts +63 -31
  125. package/build/utils/branchDiff.js +242 -94
  126. package/build/utils/containedPath.d.ts +18 -0
  127. package/build/utils/containedPath.js +73 -0
  128. package/build/utils/dartRouteExtractor.d.ts +18 -34
  129. package/build/utils/dartRouteExtractor.js +101 -173
  130. package/build/utils/featureFlags.d.ts +12 -0
  131. package/build/utils/featureFlags.js +14 -0
  132. package/build/utils/frontendSelectors.d.ts +48 -27
  133. package/build/utils/frontendSelectors.js +241 -80
  134. package/build/utils/pathMatching.d.ts +2 -4
  135. package/build/utils/pathMatching.js +2 -4
  136. package/build/utils/planMatchKeys.d.ts +38 -47
  137. package/build/utils/planMatchKeys.js +143 -81
  138. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  139. package/build/utils/rebaselineSnapshots.js +65 -0
  140. package/build/utils/removedUiElements.d.ts +22 -0
  141. package/build/utils/removedUiElements.js +106 -0
  142. package/build/utils/reportVerification.d.ts +2 -6
  143. package/build/utils/reportVerification.js +61 -2
  144. package/build/utils/screenRoutes.d.ts +66 -0
  145. package/build/utils/screenRoutes.js +727 -0
  146. package/build/utils/sourceRouteExtractor.js +320 -112
  147. package/build/utils/testFileClassification.d.ts +11 -2
  148. package/build/utils/testFileClassification.js +44 -2
  149. package/build/utils/testFixtures.d.ts +5 -0
  150. package/build/utils/testFixtures.js +13 -0
  151. package/build/utils/utils.d.ts +0 -1
  152. package/build/utils/utils.js +0 -11
  153. package/build/utils/versions.d.ts +3 -3
  154. package/build/utils/versions.js +1 -1
  155. package/build/workspace/workspace.d.ts +12 -12
  156. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  157. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  158. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  160. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  161. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  162. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  168. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  171. package/node_modules/playwright/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +8 -0
  174. package/plugin/plugin.json +6 -0
  175. package/plugin/prompts/declaring-a-plan.md +20 -0
  176. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  177. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  178. package/plugin/prompts/generate-tests/generation.md +108 -0
  179. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  180. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  181. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  182. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  183. package/plugin/prompts/plan-tests.md +42 -0
  184. package/plugin/prompts/testbot-task1.md +82 -0
  185. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  186. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  187. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  188. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  189. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  190. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  191. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  192. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  193. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  194. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  195. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  196. package/build/recommendation/budgeters/shared.d.ts +0 -32
  197. package/build/recommendation/budgeters/shared.js +0 -246
  198. package/build/recommendation/discriminators.d.ts +0 -37
  199. package/build/recommendation/discriminators.js +0 -379
  200. package/build/recommendation/diversity.d.ts +0 -47
  201. package/build/recommendation/diversity.js +0 -101
  202. package/build/recommendation/planRanker.d.ts +0 -65
  203. package/build/recommendation/planRanker.js +0 -83
  204. package/build/recommendation/testFixtures.d.ts +0 -25
  205. package/build/recommendation/testFixtures.js +0 -45
  206. package/build/types/FrontendIntegration.d.ts +0 -28
  207. package/build/types/FrontendIntegration.js +0 -22
  208. package/build/types/Recommendation.d.ts +0 -146
  209. package/build/types/Recommendation.js +0 -74
  210. package/build/utils/changedRoutes.d.ts +0 -29
  211. package/build/utils/changedRoutes.js +0 -87
  212. package/build/utils/frontendIntegration.d.ts +0 -9
  213. package/build/utils/frontendIntegration.js +0 -243
  214. package/build/utils/importerHop.d.ts +0 -135
  215. package/build/utils/importerHop.js +0 -489
  216. package/build/utils/pathAffinityClassification.d.ts +0 -49
  217. package/build/utils/pathAffinityClassification.js +0 -180
  218. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  219. package/build/utils/pythonMountPrefixes.js +0 -347
  220. package/build/utils/repoScanner.d.ts +0 -34
  221. package/build/utils/repoScanner.js +0 -300
  222. package/build/utils/routeParsers.d.ts +0 -95
  223. package/build/utils/routeParsers.js +0 -951
  224. package/build/utils/scenarioDrafting.d.ts +0 -92
  225. package/build/utils/scenarioDrafting.js +0 -951
  226. package/build/utils/subjectEndpoints.d.ts +0 -19
  227. package/build/utils/subjectEndpoints.js +0 -98
  228. package/build/utils/uiPageEnumerator.d.ts +0 -172
  229. package/build/utils/uiPageEnumerator.js +0 -474
@@ -0,0 +1,117 @@
1
+ import { currentRunStateFile } from "../utils/AnalysisStateManager.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { digestFromBlueprintText } from "./blueprintDigest.js";
4
+ /** The digests of this run's `browser_blueprint` captures. IN PROCESS, not in the
5
+ * state file: the only reader shares a process with the browser tools, and writing
6
+ * each digest would read-modify-write the state file once per capture. KEYED BY
7
+ * THE RUN, one slot — a new key means the previous run is over. */
8
+ let capturedRunStateFile;
9
+ let captures = [];
10
+ /** The url of the last capture SEEN, which is not the last one that produced a
11
+ * digest — see the drop branch below. */
12
+ let lastSeenUrl;
13
+ /** Where the browser IS, as the server saw it — from a navigation the server
14
+ * forwarded, or from the last capture that named a page. Kept beside
15
+ * `lastSeenUrl` rather than reusing it: that one is deliberately cleared by an
16
+ * unreadable payload so a delta cannot inherit a wrong page, and clearing it is
17
+ * exactly wrong for a reader asking "what page are we on". */
18
+ let browserUrl;
19
+ /** A run that takes more captures than this has a browsing problem of its own;
20
+ * the cap is here so a runaway loop cannot grow the process without bound. */
21
+ export const MAX_STORED_CAPTURES = 200;
22
+ /** Record one capture, or do nothing. NOTHING HERE MAY COST THE CAPTURE: the
23
+ * payload the agent receives is untouched and every failure is swallowed with a
24
+ * log line. Gated on `currentRunStateFile()`, not `resolveRunStatePath()`, because
25
+ * this gates BEHAVIOUR and `RUNNER_TEMP` is set in this repository's own CI. */
26
+ export function recordBlueprintCapture(result) {
27
+ try {
28
+ if (!result || result.isError)
29
+ return;
30
+ const runStateFile = currentRunStateFile();
31
+ if (runStateFile === undefined)
32
+ return;
33
+ if (runStateFile !== capturedRunStateFile) {
34
+ capturedRunStateFile = runStateFile;
35
+ captures = [];
36
+ lastSeenUrl = undefined;
37
+ browserUrl = undefined;
38
+ }
39
+ for (const block of result.content ?? []) {
40
+ if (captures.length >= MAX_STORED_CAPTURES)
41
+ return;
42
+ if (block?.type !== "text" || typeof block.text !== "string")
43
+ continue;
44
+ // The last url SEEN, not the last that produced a digest: after a dropped
45
+ // payload, a delta inherited the url before it and filed its elements
46
+ // under the wrong page.
47
+ const digest = digestFromBlueprintText(block.text, lastSeenUrl);
48
+ if (digest) {
49
+ captures.push(digest);
50
+ lastSeenUrl = digest.url;
51
+ if (digest.url)
52
+ browserUrl = { url: digest.url, source: "blueprint" };
53
+ }
54
+ else {
55
+ // Forget the previous url rather than carry it forward: an unreadable
56
+ // payload says nothing about which page we are on, and a WRONG url files a
57
+ // later delta under it. No url only costs the delta its inheritance.
58
+ lastSeenUrl = undefined;
59
+ }
60
+ }
61
+ }
62
+ catch (error) {
63
+ const detail = error instanceof Error ? error.message : String(error);
64
+ logger.warning(`Could not record a browser_blueprint digest — the capture itself is unaffected: ${detail}`);
65
+ }
66
+ }
67
+ /**
68
+ * Record where a `browser_navigate` took the browser.
69
+ *
70
+ * The server forwards the navigation, so it knows the destination without asking
71
+ * the agent — which is the point: a tool that exists to correct an agent's idea of
72
+ * which page it is on cannot take that page from the agent. Swallows everything,
73
+ * like the capture recorder: nothing here may cost a navigation.
74
+ */
75
+ export function recordBrowserNavigation(url) {
76
+ try {
77
+ if (typeof url !== "string" || !url.trim())
78
+ return;
79
+ const runStateFile = currentRunStateFile();
80
+ if (runStateFile === undefined)
81
+ return;
82
+ if (runStateFile !== capturedRunStateFile) {
83
+ capturedRunStateFile = runStateFile;
84
+ captures = [];
85
+ lastSeenUrl = undefined;
86
+ }
87
+ browserUrl = { url: url.trim(), source: "currentUrl" };
88
+ }
89
+ catch (error) {
90
+ const detail = error instanceof Error ? error.message : String(error);
91
+ logger.warning(`Could not record a browser navigation url — the navigation itself is unaffected: ${detail}`);
92
+ }
93
+ }
94
+ /** The page the browser is on, and how the server came to know it: a navigation it
95
+ * forwarded (`currentUrl`) or the last capture that named a page (`blueprint`). */
96
+ export function lastKnownBrowserUrl() {
97
+ const runStateFile = currentRunStateFile();
98
+ if (runStateFile === undefined || runStateFile !== capturedRunStateFile)
99
+ return undefined;
100
+ return browserUrl ? { ...browserUrl } : undefined;
101
+ }
102
+ /** This run's digests, and no other run's — the same key the capture stored
103
+ * them under. A copy: a reader that spliced the array would delete the run's
104
+ * evidence. */
105
+ export function capturedBlueprintDigests() {
106
+ const runStateFile = currentRunStateFile();
107
+ if (runStateFile === undefined || runStateFile !== capturedRunStateFile)
108
+ return [];
109
+ return [...captures];
110
+ }
111
+ /** For tests, and as a safety hook between runs. */
112
+ export function clearBlueprintDigests() {
113
+ capturedRunStateFile = undefined;
114
+ captures = [];
115
+ lastSeenUrl = undefined;
116
+ browserUrl = undefined;
117
+ }
@@ -5,6 +5,8 @@
5
5
  import { z } from "zod";
6
6
  import { logger } from "../utils/logger.js";
7
7
  import { PlaywrightTraceService, } from "./PlaywrightTraceService.js";
8
+ import { blueprintJsonSpan } from "./blueprintDigest.js";
9
+ import { recordBlueprintCapture, recordBrowserNavigation } from "./blueprintDigestStore.js";
8
10
  let _service = null;
9
11
  export function getPlaywrightTraceService() {
10
12
  return _service;
@@ -14,6 +16,41 @@ export function getPlaywrightTraceService() {
14
16
  * shrinkBlueprintResult.
15
17
  */
16
18
  const BLUEPRINT_TOOL = "browser_blueprint";
19
+ /**
20
+ * The one browser tool whose result carries a rule with it — see
21
+ * withAssertReminder.
22
+ */
23
+ const NAVIGATE_TOOL = "browser_navigate";
24
+ /**
25
+ * The line the agent reads the moment it has reached a page.
26
+ *
27
+ * The rule used to sit twice in the testbot start prompt, hundreds of lines
28
+ * before the page existed, and sb19 (run 33996699239) recorded a UI trace with
29
+ * ZERO browser_assert calls. A tool result is the one place the agent cannot
30
+ * read past.
31
+ */
32
+ const ASSERT_REMINDER = "Assert at least one business outcome on this page with `browser_assert` before you leave it.";
33
+ /**
34
+ * Append the assert rule to a successful browser_navigate result.
35
+ *
36
+ * The reminder rides the LAST text block, so it is the final thing in the
37
+ * payload whatever the fork wrote above it. A result carrying no text block at
38
+ * all gets one. An error result is returned untouched: the agent did not reach
39
+ * a page, so there is nothing to assert on.
40
+ */
41
+ function withAssertReminder(result) {
42
+ if (result?.isError)
43
+ return result;
44
+ const content = result?.content ?? [];
45
+ const last = content.map((block) => block?.type).lastIndexOf("text");
46
+ if (last < 0) {
47
+ return { ...result, content: [...content, { type: "text", text: ASSERT_REMINDER }] };
48
+ }
49
+ return {
50
+ ...result,
51
+ content: content.map((block, i) => i === last && block?.type === "text" ? { ...block, text: `${block.text}\n\n${ASSERT_REMINDER}` } : block),
52
+ };
53
+ }
17
54
  /**
18
55
  * Shrink a browser_blueprint result on the text channel it already arrives on.
19
56
  *
@@ -83,18 +120,13 @@ function dropNulls(value) {
83
120
  * that does not parse is returned as it arrived.
84
121
  */
85
122
  function shrinkBlueprintText(text) {
86
- const start = text.indexOf("{");
87
- if (start === -1)
123
+ // Where the JSON body starts and stops is `blueprintJsonSpan`'s answer, not a
124
+ // second reading of it here: the digest recorder reads the same body, and a
125
+ // drifted copy would silently deliver an unshrunk payload or an empty digest.
126
+ const span = blueprintJsonSpan(text);
127
+ if (!span)
88
128
  return text;
89
- // The fork can append more sections after `### Result` — `### Page` whenever
90
- // the title or URL changed since the last call, and `### Modal state`,
91
- // `### Snapshot` or `### Events` when the page produced them. Only the Result
92
- // section holds JSON, so the body has to stop at the next header; reading to
93
- // the end of the string makes the parse throw and costs the whole saving.
94
- // A `### ` at the start of a line cannot sit inside the JSON, because JSON
95
- // escapes the newline that would have to precede it.
96
- const nextSection = text.slice(start).search(/\n### /);
97
- const end = nextSection === -1 ? text.length : start + nextSection;
129
+ const { start, end } = span;
98
130
  // The blank line the fork puts after a section belongs to the payload, not to
99
131
  // the JSON, so it is put back byte-for-byte rather than parsed away.
100
132
  const body = text.slice(start, end);
@@ -154,6 +186,7 @@ export async function registerPlaywrightTools(server, options) {
154
186
  'browser_wait_for',
155
187
  'browser_take_screenshot',
156
188
  'browser_assert',
189
+ 'browser_assert_hidden',
157
190
  'browser_assert_api_request',
158
191
  'browser_assert_table_cell',
159
192
  'browser_mouse_action',
@@ -170,6 +203,7 @@ export async function registerPlaywrightTools(server, options) {
170
203
  for (const tool of filteredTools) {
171
204
  const zodSchema = jsonSchemaToZod(tool.inputSchema);
172
205
  const isBlueprint = tool.name === BLUEPRINT_TOOL;
206
+ const isNavigate = tool.name === NAVIGATE_TOOL;
173
207
  server.registerTool(tool.name, {
174
208
  description: tool.description || `Playwright tool: ${tool.name}`,
175
209
  inputSchema: zodSchema,
@@ -186,7 +220,21 @@ export async function registerPlaywrightTools(server, options) {
186
220
  };
187
221
  }
188
222
  const result = await _service.callTool(tool.name, params);
189
- return isBlueprint ? shrinkBlueprintResult(result) : result;
223
+ if (isNavigate) {
224
+ // The destination this server forwarded, so `skyramp_resolve_screen`
225
+ // can say where the browser actually is without asking the agent.
226
+ if (!result?.isError)
227
+ recordBrowserNavigation(params.url);
228
+ return withAssertReminder(result);
229
+ }
230
+ if (!isBlueprint)
231
+ return result;
232
+ // Read for the digest BEFORE the shrink, so the recorder sees the
233
+ // payload the fork produced rather than one this file rewrote. It
234
+ // returns nothing and never throws — what the agent gets back is
235
+ // decided by the line below and by nothing here.
236
+ recordBlueprintCapture(result);
237
+ return shrinkBlueprintResult(result);
190
238
  });
191
239
  }
192
240
  logger.info(`Registered ${filteredTools.length} Playwright tools: ${filteredTools.map((t) => t.name).join(", ")}`);
@@ -23,7 +23,7 @@ export function getTraceRecordingPromptText(opts) {
23
23
  : `- Do NOT run \`skyramp_modularization\` — skip modularization in CI.`;
24
24
  return `## Skyramp UI Test Recording
25
25
 
26
- ${getPersonaPrefix()} For UI recording, every action must be grounded in what \`browser_snapshot\` returns. If an element is not visible in the snapshot, do not interact with it.
26
+ ${getPersonaPrefix()} For UI recording, every action must be grounded in what \`browser_snapshot\` returns. If an element is not visible in the snapshot, do not interact with it. **The usual reason an element is missing is that the browser is on a page that never renders it, and the cheapest moment to find that out is before the first click, not after the fifth.** \`skyramp_resolve_screen\` reads the repository's route table and this server's own browser session, and answers with the url to open, or tells you no route renders the file at all. Ask it first (step 1 below); ask it again the moment an expected element is missing. Do not reach for \`browser_evaluate\` to hunt an element the snapshot does not hold — a missing element is a wrong-page symptom, and \`browser_evaluate\` cannot tell you the page is wrong. **Locating an element is \`browser_snapshot\`'s job, and reading a route parameter off the page is too**; \`browser_evaluate\` is for state no snapshot can express, not for finding links, buttons or form fields.
27
27
 
28
28
  ### Required workflow
29
29
 
@@ -31,12 +31,13 @@ Before starting, output a \`<thinking>\` block that maps each step of the user's
31
31
 
32
32
  Then execute in strict order:
33
33
 
34
- 1. **Navigate**: Call \`browser_navigate\` with the target URL. Always do this first, even if the browser appears to be on the correct page.
35
- 2. **Snapshot**: Call \`browser_snapshot\` to get the current ARIA tree and element refs.
36
- 3. **Interact**: Call the appropriate tool (\`browser_click\`, \`browser_type\`, \`browser_hover\`, etc.) using refs from the snapshot.
37
- 4. **Repeat steps 2–3** for each user action until all steps are complete.
38
- 5. **Export**: ${exportInstruction} Do NOT ask the user first — call it automatically.
39
- 6. **Generate**: ${generateInstruction}
34
+ 1. **Resolve the screen**: when this test is about a frontend file the diff changed, call \`skyramp_resolve_screen\` with that file BEFORE \`browser_navigate\`, and take the target URL from its answer rather than from your own reading of the source. A route parameter it could not fill comes back in \`needs\` with the pattern left in the url (e.g. \`/secrets/$envSlug\`) — that url is not navigable as-is: read the real value off the app (open the parent page, follow a link, or take it from a list) and substitute it. Never invent one, and never navigate the pattern spelling literally. On \`unmounted\`, skip the browser entirely and say so.
35
+ 2. **Navigate**: Call \`browser_navigate\` with the target URL. Always do this before interacting, even if the browser appears to be on the correct page.
36
+ 3. **Snapshot**: Call \`browser_snapshot\` to get the current ARIA tree and element refs.
37
+ 4. **Interact**: Call the appropriate tool (\`browser_click\`, \`browser_type\`, \`browser_hover\`, etc.) using refs from the snapshot.
38
+ 5. **Repeat steps 3–4** for each user action until all steps are complete.
39
+ 6. **Export**: ${exportInstruction} Do NOT ask the user first — call it automatically.
40
+ 7. **Generate**: ${generateInstruction}
40
41
 
41
42
  ### Cross-tool rules
42
43
 
@@ -10,7 +10,7 @@ const SHARED_RULES = [
10
10
  subPoints: [
11
11
  "`is not None` / `not.toBeNull()` is only acceptable when the value is genuinely unknown — for server-generated timestamps or opaque IDs. This rule does not apply to computed fields.",
12
12
  "When a response field's value equals the value sent in the request body, path, or query, assert that exact value rather than a null-check — the sent value is known and reproducible.",
13
- "Also applies to response-only fields the server always returns the same value for (`filename_download`, `content_type`, `size`, `url`, enum status after creation). Asserting only the id on a multi-key resource response is not sufficient.",
13
+ "Assert the exact value of a response-only field when the request this test sends determines it (`filename_download`, `content_type`, `size`, an enum status after creation). A value the app generates gets a format check when the test case depends on that value; leave the rest unasserted. Asserting only the id on a multi-key resource response is not sufficient.",
14
14
  "Range matchers like `toBeGreaterThanOrEqual(0)` and type-only checks like `typeof X === 'number'` are not acceptable for fields whose exact values are known.",
15
15
  "Assert the exact status code from the recorded trace or `expected_response_body` — for example, `201` for resource-creation endpoints. Action-style endpoints with verb path segments like `/run`, `/trigger`, `/execute`, `/deploy`, `/submit`, `/start`, `/stop`, or `/cancel` often use `200`, but assert the recorded or expected code. Permissive status matchers are never acceptable.",
16
16
  ],
@@ -41,7 +41,7 @@ expect(response.statusCode).toBe(201);`,
41
41
  code: `expect(response.statusCode).toBe(404);
42
42
  expect(getValue(response, "errors.0.message")).toBe("Item not found");
43
43
  expect(getValue(response, "errors.0.extensions.code")).toBe("RECORD_NOT_FOUND");
44
- expect(getValue(response, "errors.1")).toBeUndefined();`,
44
+ expect(getValue(response, "errors.1")).toBeNull();`,
45
45
  },
46
46
  ],
47
47
  },
@@ -92,12 +92,12 @@ expect(getValue(response, "data").length).toBeLessThanOrEqual(10); // request s
92
92
  language: "javascript",
93
93
  code: `expect(getValue(response, "results").length).toBe(2);
94
94
  expect(getValue(response, "results.0.id")).toBe("prod_001");
95
- expect(getValue(response, "results.2")).toBeUndefined();`,
95
+ expect(getValue(response, "results.2")).toBeNull();`,
96
96
  },
97
97
  {
98
98
  language: "javascript",
99
99
  code: `expect(getValue(response, "data").length).toBe(0);
100
- expect(getValue(response, "data.0")).toBeUndefined();`,
100
+ expect(getValue(response, "data.0")).toBeNull();`,
101
101
  },
102
102
  ],
103
103
  },
@@ -207,7 +207,7 @@ Before editing the given file, you must output a \`<thinking>\` block. The aim o
207
207
  2. Classify each response first by its response status type and then assign the applicable assertion rules to the response.
208
208
  1. Success with body (2xx with a response body): all assertion rules below may apply — echo-back of request fields, computed response fields, array / items validation, and chained values across steps.
209
209
  2. Success with no body (200/202/204 with an empty body — e.g. logout/cancel/submit action endpoints): assert the status code only. Also apply chained-values rules if a follow-up step uses this response's ID.
210
- 3. Error response (4xx/5xx with a body): assert every error body field with its exact value plus array / items validation on the \`errors[]\` array (exact length + per-item fields + next index undefined). Status code alone is never sufficient when a body is present — for example, also assert \`errors.0.extensions.code == 'INVALID_PAYLOAD'\` and that \`errors.1\` is undefined.
210
+ 3. Error response (4xx/5xx with a body): assert every error body field with its exact value plus array / items validation on the \`errors[]\` array (exact length + per-item fields + next index null). Status code alone is never sufficient when a body is present — for example, also assert \`errors.0.extensions.code == 'INVALID_PAYLOAD'\` and that \`errors.1\` is null.
211
211
  3. For each in-scope response, output one JSON object using the template below. The output is an array — one object per in-scope response.
212
212
  - \`step\`: the HTTP method, path, and response variable name for this request (e.g. \`POST /products → products_POST_response\`).
213
213
  - \`response_status\`: one of \`success\`, \`no_body\`, or \`error\` based on the classification in step 2.
@@ -233,14 +233,15 @@ ${scope}
233
233
 
234
234
  ### What not to do
235
235
  - Do not access response fields via dict syntax (\`response["field"]\`) or attribute access (\`response.field\`) — always use the SDK helper.
236
- - Do not assert \`not.toBeNull()\` / \`is not None\` on a field whose exact value is in the request body, prior response, or trace.
236
+ - Do not assert \`not.toBeNull()\` / \`is not None\` on a field whose exact value is in the request body or a prior response.
237
+ - Do not assert an exact value you learned only from a request you sent yourself while writing the test. Take the value from the diff, from what the request determines, or from a response this test received earlier.
237
238
  - Do not skip body assertions citing genuinely unpredictable fields — every assertable field still needs an assertion.
238
239
  - Do not use permissive status matchers (\`.toMatch(/^2/)\`, \`.toBeGreaterThanOrEqual(200)\`, \`checkStatusCode(response, '20x')\`).
239
- - Do not use shape-only or type-only assertions as a substitute for exact value validation. Forbidden patterns: \`Array.isArray(...)\`, \`typeof X === '...'\`, \`X instanceof Array\`, \`Object.keys(X).length > 0\`. When the recorded response contains actual values, assert them exactly.
240
+ - Do not use shape-only or type-only assertions as a substitute for exact value validation. Forbidden patterns: \`Array.isArray(...)\`, \`typeof X === '...'\`, \`X instanceof Array\`, \`Object.keys(X).length > 0\`. When a field's value comes from one of those sources, assert it exactly.
240
241
  - Do not use shape-only, containment-only, range-only, or weak-length as the sole assertion on a populated array.
241
242
  - Do not swap between \`getValue\` and \`getResponseValue\` — keep whichever SDK helper the file already imports.
242
243
  - Do not restructure, reformat, reorder, or modify existing code; do not add comments or docstrings.
243
- - Do not change function signatures, imports, or variable names.
244
+ - Do not change function signatures or variable names. You may add an import when a new assertion or a fix needs a helper — from \`@skyramp/skyramp\` or from the test's own helper module.
244
245
  - Do not remove existing assertions.
245
246
 
246
247
  ### Verification of Assertions
@@ -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,28 @@ 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; a spec that opens a deleted route must be anchored on a surviving page first (a retained element asserted on a live page in the same spec) — a spec whose only visited page is the dead URL is not a valid guard.",
53
+ "A renamed test id or accessible name (a removed `aria-label` included) is not a removed element: assert the element under its new identifier and never assert the retired identifier `toBeHidden()` — it passes today and turns into a false failure the day the name is reused.",
54
+ ],
55
+ examples: [
56
+ {
57
+ language: "javascript",
58
+ code: `await expect(page.getByTestId('navbar-sessionid-row')).toBeHidden();
59
+ await expect(page.locator('[data-cy="checkout-btn"]')).toBeHidden();
60
+ await expect(page.getByTestId('navbar-products')).toHaveText('Products');`,
61
+ },
62
+ {
63
+ language: "python",
64
+ code: `expect(page.get_by_test_id('navbar-sessionid-row')).to_be_hidden()
65
+ expect(page.get_by_test_id('navbar-products')).to_have_text('Products')`,
66
+ },
67
+ ],
68
+ },
47
69
  {
48
70
  title: "Page errors",
49
71
  description: "Register `page.on('pageerror', ...)` before the first navigation and assert `expect(errors).toHaveLength(0)` at the end of the test.",
@@ -307,7 +329,7 @@ if (await existingSecret.isVisible().catch(() => false)) {
307
329
 
308
330
  ### Pre-Edit Assertion Analysis
309
331
  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 \`[]\`.
310
- 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.
332
+ 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. An assertion the recorder dropped because it failed during recording may use the selector of the element named in that recording's blueprint capture. 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.
311
333
  2. Process — Replay → Identify → Fix or Add — walk through these three steps explicitly.
312
334
  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?"
313
335
  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.
@@ -0,0 +1,20 @@
1
+ /** Reader and markdown slicer for the prompt text shipped in `plugin/prompts/`.
2
+ *
3
+ * The markdown is the single source of the instruction text: the prompts read it
4
+ * back rather than carrying a second copy.
5
+ *
6
+ * These files are NOT skills. Each one carries `{placeholder}` spans that only a
7
+ * prompt render can fill — the repository path, a step number, the related-repo
8
+ * count — so an agent that loaded one as a skill would read the braces. They sit
9
+ * under `plugin/` rather than beside the code because `package.json` ships
10
+ * `plugin/**` and nothing copies markdown out of `src/`. */
11
+ /** Absolute path of the shipped `plugin/` directory. */
12
+ export declare function pluginRoot(): string;
13
+ export declare function readPromptAsset(relPath: string): string;
14
+ /** One section INCLUDING its heading line. The caller that renders a whole
15
+ * section wants the heading too, and retyping it in TypeScript let a heading
16
+ * edit in the markdown leave the copy behind. */
17
+ export declare function section(markdown: string, heading: string): string;
18
+ /** Body of one section: the lines under `heading`, up to the next heading of
19
+ * the same or a higher level. */
20
+ export declare function sectionBody(markdown: string, heading: string): string;
@@ -0,0 +1,55 @@
1
+ import { readFileSync } from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ /** Reader and markdown slicer for the prompt text shipped in `plugin/prompts/`.
5
+ *
6
+ * The markdown is the single source of the instruction text: the prompts read it
7
+ * back rather than carrying a second copy.
8
+ *
9
+ * These files are NOT skills. Each one carries `{placeholder}` spans that only a
10
+ * prompt render can fill — the repository path, a step number, the related-repo
11
+ * count — so an agent that loaded one as a skill would read the braces. They sit
12
+ * under `plugin/` rather than beside the code because `package.json` ships
13
+ * `plugin/**` and nothing copies markdown out of `src/`. */
14
+ /** Absolute path of the shipped `plugin/` directory. */
15
+ export function pluginRoot() {
16
+ // build/prompts/<file>.js or src/prompts/<file>.ts → package root → plugin/
17
+ const here = path.dirname(fileURLToPath(import.meta.url));
18
+ return path.resolve(here, "..", "..", "plugin");
19
+ }
20
+ export function readPromptAsset(relPath) {
21
+ return readFileSync(path.join(pluginRoot(), "prompts", relPath), "utf8");
22
+ }
23
+ function headingIndex(markdown, heading) {
24
+ // Anchored to line start so a heading quoted mid-paragraph cannot match.
25
+ if (markdown.startsWith(heading))
26
+ return 0;
27
+ const at = markdown.indexOf(`\n${heading}`);
28
+ return at === -1 ? -1 : at + 1;
29
+ }
30
+ /** Everything from `heading` to the end. Throws when the heading is absent: a
31
+ * silent empty string would render a prompt with a section quietly missing. */
32
+ function sliceFrom(markdown, heading) {
33
+ const at = headingIndex(markdown, heading);
34
+ if (at === -1)
35
+ throw new Error(`markdown section not found: ${heading}`);
36
+ return markdown.slice(at).trim();
37
+ }
38
+ /** One section INCLUDING its heading line. The caller that renders a whole
39
+ * section wants the heading too, and retyping it in TypeScript let a heading
40
+ * edit in the markdown leave the copy behind. */
41
+ export function section(markdown, heading) {
42
+ const body = sectionBody(markdown, heading);
43
+ return body ? `${heading}\n\n${body}` : heading;
44
+ }
45
+ /** Body of one section: the lines under `heading`, up to the next heading of
46
+ * the same or a higher level. */
47
+ export function sectionBody(markdown, heading) {
48
+ const level = heading.match(/^#+/)?.[0].length ?? 0;
49
+ const lines = sliceFrom(markdown, heading).split("\n").slice(1);
50
+ const end = lines.findIndex((line) => {
51
+ const h = line.match(/^(#+)\s/);
52
+ return h !== null && h[1].length <= level;
53
+ });
54
+ return (end === -1 ? lines : lines.slice(0, end)).join("\n").trim();
55
+ }
@@ -2,10 +2,24 @@ import { buildContextBlock, buildInputsBody, buildCommonSutErrorsSection, buildL
2
2
  import { getPersonaPrefix } from "../../personas.js";
3
3
  import { PromptPlan } from "../../test-recommendation/promptPlan.js";
4
4
  // ── Step body builders ────────────────────────────────────────────────────────
5
+ /**
6
+ * Step labels for the cross-references the bodies below make. Read from the
7
+ * plan's own declaration, not written out again: every one of these five
8
+ * references was off by one after `INIT_WORKSPACE` was added at the front.
9
+ * `_composePlan` is declared after the bodies, so this resolves on call.
10
+ */
11
+ const STEP = (key) => {
12
+ const label = _composePlan.labels[key];
13
+ // Without this a renamed key renders "Step undefined" and nothing goes red.
14
+ if (label === undefined) {
15
+ throw new Error(`dockerComposePrompt: no step named "${key}"`);
16
+ }
17
+ return label;
18
+ };
5
19
  function buildScanComposeBody(args) {
6
20
  const composeFile = args.sutSourceDockerComposeFile?.trim();
7
21
  if (composeFile) {
8
- return `A specific compose file was provided: \`${composeFile}\`. Use it directly as the compose file — no scanning or generating. (Strategy in Step 2 will be REUSE with this file.)`;
22
+ return `A specific compose file was provided: \`${composeFile}\`. Use it directly as the compose file — no scanning or generating. (Strategy in Step ${STEP("DECIDE")} will be REUSE with this file.)`;
9
23
  }
10
24
  return `Search the repository for existing docker-compose files:
11
25
  - \`docker-compose.yml\`, \`docker-compose.yaml\`, \`compose.yml\`, \`compose.yaml\` at root
@@ -19,7 +33,7 @@ For each compose file found, determine if it is:
19
33
  function buildDecideStrategyBody(args) {
20
34
  const composeFile = args.sutSourceDockerComposeFile?.trim();
21
35
  if (composeFile) {
22
- return `A specific compose file was provided in Step 1 (\`${composeFile}\`). Strategy is REUSE with this file. Confirm your decision:
36
+ return `A specific compose file was provided in Step ${STEP("SCAN")} (\`${composeFile}\`). Strategy is REUSE with this file. Confirm your decision:
23
37
 
24
38
  \`\`\`
25
39
  Decision: REUSE
@@ -27,7 +41,7 @@ Compose file: ${composeFile}
27
41
  Reason: provided by the user
28
42
  \`\`\``;
29
43
  }
30
- return `Choose between reuse and generate based on what Step 1 found:
44
+ return `Choose between reuse and generate based on what Step ${STEP("SCAN")} found:
31
45
  - If an application docker-compose file was found → strategy is REUSE.
32
46
  - If only infrastructure-only compose files exist, or no compose files found → strategy is GENERATE.
33
47
 
@@ -70,7 +84,7 @@ function getReuseDockerComposeInstructions(composeFile) {
70
84
  4. Determine the teardown command: \`docker compose -f ${ref} down -v\`
71
85
  5. If the compose file defines admin credentials in environment variables, create \`.skyramp/sut/get-auth-token.sh\` using those credentials.
72
86
 
73
- These resolved commands are wired onto the \`skyramp/testbot\` action step using the input semantics from Step 3 above.`;
87
+ These resolved commands are wired onto the \`skyramp/testbot\` action step using the input semantics from Step ${STEP("INPUTS")} above.`;
74
88
  }
75
89
  /** Generate instructions used when no reusable application compose file exists. */
76
90
  const GENERATE_DOCKER_COMPOSE_INSTRUCTIONS = `No usable application docker-compose was found. Generate one from scratch.
@@ -252,7 +266,7 @@ function buildApplyStrategyBody(args) {
252
266
  return `#### Reuse the provided Docker Compose file
253
267
  ${getReuseDockerComposeInstructions(composeFile)}`;
254
268
  }
255
- return `Apply the strategy selected in Step 2:
269
+ return `Apply the strategy selected in Step ${STEP("DECIDE")}:
256
270
 
257
271
  #### If REUSE selected — Reuse existing Docker Compose
258
272
  ${getReuseDockerComposeInstructions()}
@@ -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) {
@@ -11,17 +11,7 @@ export type UiDriftParams = {
11
11
  tests: Array<{
12
12
  testFile: string;
13
13
  }>;
14
- changedFrontendFiles: string[];
15
14
  blueprintCaptured?: boolean;
16
- /**
17
- * Changed frontend files imported by 2+ production files, with their importer
18
- * lists (server-computed). Drives the shared-component drift rule: a test may not
19
- * be assigned IGNORE for one of these files until every importer has been read.
20
- */
21
- sharedComponents?: Array<{
22
- file: string;
23
- importers: string[];
24
- }>;
25
15
  };
26
16
  /**
27
17
  * Build the drift analysis prompt.
@@ -110,17 +110,8 @@ export function buildDriftAnalysisPrompt(stateFile, apiTests, ui, repoPaths, ext
110
110
 
111
111
  **Signal:** ${signalNote}
112
112
 
113
- **Changed Frontend Files:**
114
- ${ui.changedFrontendFiles.length > 0
115
- ? ui.changedFrontendFiles.map(f => `- ${f}`).join("\n")
116
- : "(none)"}
117
- ${ui.sharedComponents && ui.sharedComponents.length > 0
118
- ? `
119
- **Shared frontend components (imported by 2+ production files — cross-consumer blast radius):**
120
- ${ui.sharedComponents.map(c => `- ${c.file} — ${c.importers.length} importer(s): ${c.importers.join(", ")}`).join("\n")}
121
- For any test whose changed target appears here, you MUST NOT assign IGNORE, and may assign UPDATE only after reading every listed importer (see "Component behavior changes").
122
- `
123
- : ""}
113
+ **Changed files:** read the diff. Your goal is to read the changes and come up with the list of affected test files in the existing repository. You MAY verify that a changed line is exercised by a test you selected by running it with \`skyramp_execute_test\`. Before you mark a test UPDATE, read the production files that import the changed component — grep for them yourself (see "Component behavior changes").
114
+
124
115
  **Instructions:**
125
116
  For each test in \`<tests_to_assess>\`, read it to determine its type, then apply the scope gate from the decision tree. Assign the appropriate action (DELETE, REGENERATE, UPDATE, VERIFY, IGNORE).
126
117
  </ui_analysis_context>