@skyramp/mcp 0.3.9-rc.1 → 0.4.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) 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 +59 -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 +3 -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/drift-analysis-prompt.d.ts +0 -10
  19. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  20. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +5 -4
  21. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  22. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -466
  23. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  24. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  25. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  26. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  27. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  28. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  29. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  30. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  31. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  32. package/build/prompts/testbot/planDeclarations.js +9 -0
  33. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  34. package/build/prompts/testbot/testbot-prompts.js +254 -382
  35. package/build/recommendation/answers.d.ts +35 -0
  36. package/build/recommendation/answers.js +96 -0
  37. package/build/recommendation/registerPlan.d.ts +49 -0
  38. package/build/recommendation/registerPlan.js +117 -0
  39. package/build/recommendation/runVerifiers.d.ts +10 -0
  40. package/build/recommendation/runVerifiers.js +49 -0
  41. package/build/recommendation/subjectStep.d.ts +42 -0
  42. package/build/recommendation/subjectStep.js +86 -0
  43. package/build/recommendation/types.d.ts +166 -0
  44. package/build/recommendation/types.js +20 -0
  45. package/build/recommendation/verifierContracts.d.ts +215 -0
  46. package/build/recommendation/verifierContracts.js +255 -0
  47. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  48. package/build/recommendation/verifiers/changedFile.js +82 -0
  49. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  50. package/build/recommendation/verifiers/citedPath.js +35 -0
  51. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  52. package/build/recommendation/verifiers/coverage.js +617 -0
  53. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  54. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  55. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  56. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  57. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  58. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  59. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  60. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  61. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  62. package/build/recommendation/verifiers/removedElementGuarded.js +111 -0
  63. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  64. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  65. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  66. package/build/recommendation/verifiers/screenRoute.js +118 -0
  67. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  68. package/build/recommendation/verifiers/statedDifference.js +140 -0
  69. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  70. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  71. package/build/resources/analysisResources.js +1 -114
  72. package/build/resources/testbotResource.js +23 -13
  73. package/build/services/ModularizationService.js +2 -1
  74. package/build/services/TestDiscoveryService.d.ts +3 -72
  75. package/build/services/TestDiscoveryService.js +10 -303
  76. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  77. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  78. package/build/toolNames.d.ts +1 -0
  79. package/build/toolNames.js +1 -0
  80. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  81. package/build/tools/code-refactor/modularizationTool.js +2 -1
  82. package/build/tools/executeSkyrampTestTool.js +8 -0
  83. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  84. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  85. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  86. package/build/tools/generate-tests/planGuard.js +78 -18
  87. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  88. package/build/tools/one-click/oneClickTool.js +0 -5
  89. package/build/tools/submitReportTool.d.ts +43 -37
  90. package/build/tools/submitReportTool.js +571 -192
  91. package/build/tools/test-management/actionsTool.js +26 -5
  92. package/build/tools/test-management/analyzeChangesTool.d.ts +168 -57
  93. package/build/tools/test-management/analyzeChangesTool.js +194 -1266
  94. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  95. package/build/tools/test-management/index.d.ts +1 -0
  96. package/build/tools/test-management/index.js +1 -0
  97. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  98. package/build/tools/test-management/registerTestPlanTool.js +610 -542
  99. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  100. package/build/tools/test-management/resolveScreenTool.js +289 -0
  101. package/build/types/BlueprintDigest.d.ts +34 -0
  102. package/build/types/BlueprintDigest.js +1 -0
  103. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  104. package/build/types/RepositoryAnalysis.js +2 -58
  105. package/build/types/StepMethod.d.ts +40 -0
  106. package/build/types/StepMethod.js +77 -0
  107. package/build/types/TestRecommendation.d.ts +24 -24
  108. package/build/types/TestRecommendation.js +91 -89
  109. package/build/types/TestbotPromptOptions.d.ts +0 -4
  110. package/build/types/TestbotReport.d.ts +64 -2
  111. package/build/utils/AnalysisStateManager.d.ts +76 -115
  112. package/build/utils/AnalysisStateManager.js +147 -57
  113. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  114. package/build/utils/assertion-verify/metrics.js +85 -36
  115. package/build/utils/branchDiff.d.ts +63 -31
  116. package/build/utils/branchDiff.js +242 -94
  117. package/build/utils/containedPath.d.ts +18 -0
  118. package/build/utils/containedPath.js +73 -0
  119. package/build/utils/dartRouteExtractor.d.ts +18 -34
  120. package/build/utils/dartRouteExtractor.js +101 -173
  121. package/build/utils/featureFlags.d.ts +12 -0
  122. package/build/utils/featureFlags.js +14 -0
  123. package/build/utils/frontendSelectors.d.ts +34 -37
  124. package/build/utils/frontendSelectors.js +104 -154
  125. package/build/utils/pathMatching.d.ts +2 -4
  126. package/build/utils/pathMatching.js +2 -4
  127. package/build/utils/planMatchKeys.d.ts +38 -47
  128. package/build/utils/planMatchKeys.js +143 -81
  129. package/build/utils/removedUiElements.d.ts +8 -13
  130. package/build/utils/removedUiElements.js +21 -55
  131. package/build/utils/screenRoutes.d.ts +66 -0
  132. package/build/utils/screenRoutes.js +727 -0
  133. package/build/utils/sourceRouteExtractor.js +320 -112
  134. package/build/utils/testFileClassification.d.ts +11 -2
  135. package/build/utils/testFileClassification.js +44 -2
  136. package/build/utils/testFixtures.d.ts +5 -0
  137. package/build/utils/testFixtures.js +13 -0
  138. package/build/utils/utils.d.ts +0 -1
  139. package/build/utils/utils.js +0 -11
  140. package/build/utils/versions.d.ts +3 -3
  141. package/build/utils/versions.js +1 -1
  142. package/build/workspace/workspace.d.ts +12 -12
  143. package/package.json +3 -2
  144. package/plugin/.claude-plugin/plugin.json +8 -0
  145. package/plugin/plugin.json +6 -0
  146. package/plugin/prompts/declaring-a-plan.md +20 -0
  147. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  148. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  149. package/plugin/prompts/generate-tests/generation.md +108 -0
  150. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  151. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  152. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  153. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  154. package/plugin/prompts/plan-tests.md +42 -0
  155. package/plugin/prompts/testbot-task1.md +82 -0
  156. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  157. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  158. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  159. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  160. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  161. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  162. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  163. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  164. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  165. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  166. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  167. package/build/recommendation/budgeters/shared.d.ts +0 -32
  168. package/build/recommendation/budgeters/shared.js +0 -246
  169. package/build/recommendation/discriminators.d.ts +0 -37
  170. package/build/recommendation/discriminators.js +0 -379
  171. package/build/recommendation/diversity.d.ts +0 -47
  172. package/build/recommendation/diversity.js +0 -101
  173. package/build/recommendation/planRanker.d.ts +0 -65
  174. package/build/recommendation/planRanker.js +0 -83
  175. package/build/recommendation/testFixtures.d.ts +0 -25
  176. package/build/recommendation/testFixtures.js +0 -45
  177. package/build/types/FrontendIntegration.d.ts +0 -28
  178. package/build/types/FrontendIntegration.js +0 -22
  179. package/build/types/Recommendation.d.ts +0 -146
  180. package/build/types/Recommendation.js +0 -74
  181. package/build/utils/changedRoutes.d.ts +0 -29
  182. package/build/utils/changedRoutes.js +0 -87
  183. package/build/utils/frontendIntegration.d.ts +0 -9
  184. package/build/utils/frontendIntegration.js +0 -243
  185. package/build/utils/importerHop.d.ts +0 -135
  186. package/build/utils/importerHop.js +0 -489
  187. package/build/utils/pathAffinityClassification.d.ts +0 -49
  188. package/build/utils/pathAffinityClassification.js +0 -180
  189. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  190. package/build/utils/pythonMountPrefixes.js +0 -347
  191. package/build/utils/repoScanner.d.ts +0 -34
  192. package/build/utils/repoScanner.js +0 -300
  193. package/build/utils/routeParsers.d.ts +0 -95
  194. package/build/utils/routeParsers.js +0 -951
  195. package/build/utils/scenarioDrafting.d.ts +0 -92
  196. package/build/utils/scenarioDrafting.js +0 -951
  197. package/build/utils/subjectEndpoints.d.ts +0 -19
  198. package/build/utils/subjectEndpoints.js +0 -98
  199. package/build/utils/uiPageEnumerator.d.ts +0 -172
  200. 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);
@@ -171,6 +203,7 @@ export async function registerPlaywrightTools(server, options) {
171
203
  for (const tool of filteredTools) {
172
204
  const zodSchema = jsonSchemaToZod(tool.inputSchema);
173
205
  const isBlueprint = tool.name === BLUEPRINT_TOOL;
206
+ const isNavigate = tool.name === NAVIGATE_TOOL;
174
207
  server.registerTool(tool.name, {
175
208
  description: tool.description || `Playwright tool: ${tool.name}`,
176
209
  inputSchema: zodSchema,
@@ -187,7 +220,21 @@ export async function registerPlaywrightTools(server, options) {
187
220
  };
188
221
  }
189
222
  const result = await _service.callTool(tool.name, params);
190
- 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);
191
238
  });
192
239
  }
193
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
@@ -49,7 +49,8 @@ expect(trigger).to_be_focused()`,
49
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
50
  subPoints: [
51
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.",
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.",
53
54
  ],
54
55
  examples: [
55
56
  {
@@ -328,7 +329,7 @@ if (await existingSecret.isVisible().catch(() => false)) {
328
329
 
329
330
  ### Pre-Edit Assertion Analysis
330
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 \`[]\`.
331
- 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.
332
333
  2. Process — Replay → Identify → Fix or Add — walk through these three steps explicitly.
333
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?"
334
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()}
@@ -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>
@@ -2,9 +2,10 @@
2
2
  * UI Drift Analysis prompt sections for Component 2.
3
3
  *
4
4
  * Mirrors the driftAnalysisSections.ts pattern but for UI-specific drift detection.
5
- * These sections are included whenever `uiContext.changedFrontendFiles` is non-empty —
6
- * both browser tests (Playwright/Cypress/Selenium) and component/widget tests
7
- * (RTL/Flutter widget/vue-test-utils/angular-testbed) go through the same gate.
5
+ * These sections are ALWAYS included: the prompt tells the agent to grep for
6
+ * component tests itself, so nothing gates them on a server verdict about the
7
+ * diff. Browser tests (Playwright/Cypress/Selenium) and component/widget tests
8
+ * (RTL/Flutter widget/vue-test-utils/angular-testbed) read the same text.
8
9
  * Component tests always use source-only analysis regardless of blueprintCaptured.
9
10
  */
10
11
  import { buildUiAssignAction, EMPTY_TEST_LIST_PLACEHOLDER, NO_PRELOADED_TESTS_NOTE } from "./driftAnalysisShared.js";
@@ -39,7 +40,7 @@ Matching none does **not** mean IGNORE — first run the selector safety net: ma
39
40
 
40
41
  Build a detection list first: for each matched diff line, write one line: \`{pattern type} — "{diff line}" — affects {route/component}\`.
41
42
 
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
+ **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: assign **VERIFY** (the test must still appear as a maintenance row with its before/after status — never drop it from the verdicts), leave its source as it is, keep it \`Fail\`, and also record the finding in \`issuesFound\`. When the pre-edit run **passed**, nothing is stale (a missing baseline is created by that run): do not list \`rebaselineSnapshots\`. A newly generated test writes its own baseline on its first run and can never be stale, so this applies to existing specs only. The refresh rides the final execution and never costs an extra one: pass the \`rebaselineSnapshots\` list on that single call, and if a screenshot mismatch first appears on that final run, report \`Fail\` rather than adding another run. The execution result names which baselines were refreshed and which were not, and the report row's description carries the refreshed names automatically, so \`afterDetails\` states only the outcome (e.g. \`1 passed in 6.1s\`).
43
44
 
44
45
  **Rules (assign exactly one action per test; severity order above):**
45
46
  - **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).
@@ -1,25 +1,8 @@
1
- import { DraftedScenario } from "../../types/RepositoryAnalysis.js";
2
- import { Novelty, PriorityTier } from "../../types/TestRecommendation.js";
3
- /** "0" — Code Review: correctness analysis */
4
- export declare const EXEC_STEP_CODE_REVIEW: string;
5
- /** "1" — External test coverage verification */
6
- export declare const EXEC_STEP_COVERAGE: string;
7
- /** "2" — Parameter grounding & priority assignment */
1
+ /** "0" — Parameter grounding: the source values every tool call is filled from */
8
2
  export declare const EXEC_STEP_ENRICH: string;
9
- /** "3" — Diversity check */
10
- export declare const EXEC_STEP_DIVERSITY: string;
11
- /** "4" — Execute merged plan */
12
- export declare const EXEC_STEP_EXECUTE: string;
13
- /** "5" — Register test plan (SKYR-3879 Path B checkpoint) */
14
- export declare const EXEC_STEP_REGISTER: string;
15
- export declare function buildExecutionPlan(scored: Array<{
16
- scenario: DraftedScenario;
17
- priority: PriorityTier;
18
- novelty: Novelty;
19
- }>, maxGen: number, topN: number, baseUrl: string, authHeaderValue: string, authSchemeSnippet: string, authTypeValue: string, seed: string, endpointCount: number, isUIOnlyPR: boolean, hasFrontendChanges?: boolean, hasTraces?: boolean, externalCoverage?: Set<string>, relevantExternalTestPaths?: string[],
20
3
  /**
21
- * Whether the diff classified at least one new/modified/removed endpoint.
22
- * Defaults to true so existing callers keep the fixed-budget rendering.
23
- * See buildScopeAssessmentSection's hasClassifiedApiChanges (SKYR-3820).
4
+ * The generation half of the recommendation prompt. What to test and how to
5
+ * declare it is the planning procedure's job; this renders only how to fill a
6
+ * generation tool call once the plan is submitted.
24
7
  */
25
- hasApiChanges?: boolean): string;
8
+ export declare function buildExecutionPlan(hasTraces?: boolean, isDiffScope?: boolean): string;