@skyramp/mcp 0.3.2 → 0.3.4

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 (181) hide show
  1. package/build/index.js +47 -2
  2. package/build/playwright/PlaywrightTraceService.d.ts +8 -0
  3. package/build/playwright/PlaywrightTraceService.js +1 -0
  4. package/build/playwright/registerPlaywrightTools.js +42 -1
  5. package/build/prompts/enhance-assertions/sharedAssertionRules.js +19 -0
  6. package/build/prompts/pom-aware-code-reuse.js +17 -8
  7. package/build/prompts/test-maintenance/actionsInstructions.js +2 -2
  8. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -4
  9. package/build/prompts/test-recommendation/recommendationSections.d.ts +1 -1
  10. package/build/prompts/test-recommendation/recommendationSections.js +5 -5
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.js +13 -7
  12. package/build/prompts/testbot/testbot-prompts.d.ts +2 -12
  13. package/build/prompts/testbot/testbot-prompts.js +28 -16
  14. package/build/recommendation/discriminators.d.ts +7 -1
  15. package/build/recommendation/discriminators.js +16 -3
  16. package/build/resources/testbotResource.js +20 -4
  17. package/build/services/ScenarioGenerationService.js +5 -2
  18. package/build/services/TestExecutionService.d.ts +13 -8
  19. package/build/services/TestExecutionService.js +73 -26
  20. package/build/services/TestGenerationService.js +24 -9
  21. package/build/services/containerEnv.d.ts +12 -1
  22. package/build/services/containerEnv.js +118 -1
  23. package/build/tools/executeSkyrampTestTool.d.ts +9 -0
  24. package/build/tools/executeSkyrampTestTool.js +20 -6
  25. package/build/tools/execution-video-state.d.ts +21 -0
  26. package/build/tools/execution-video-state.js +51 -0
  27. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +31 -11
  28. package/build/tools/generate-tests/planGuard.d.ts +5 -5
  29. package/build/tools/generate-tests/planGuard.js +5 -17
  30. package/build/tools/queryProxyMocksTool.js +0 -1
  31. package/build/tools/submitReportTool.d.ts +83 -10
  32. package/build/tools/submitReportTool.js +179 -29
  33. package/build/tools/test-management/actionsTool.js +52 -41
  34. package/build/tools/test-management/analyzeChangesTool.d.ts +11 -0
  35. package/build/tools/test-management/analyzeChangesTool.js +37 -33
  36. package/build/tools/test-management/analyzeTestHealthTool.js +3 -3
  37. package/build/tools/test-management/registerTestPlanTool.js +113 -31
  38. package/build/types/TestAnalysis.d.ts +7 -3
  39. package/build/types/TestExecution.d.ts +14 -0
  40. package/build/types/TestTypes.js +3 -2
  41. package/build/types/TestbotPromptOptions.d.ts +34 -0
  42. package/build/types/TestbotPromptOptions.js +1 -0
  43. package/build/types/TestbotReport.d.ts +10 -0
  44. package/build/types/TestbotReport.js +10 -1
  45. package/build/types/index.d.ts +2 -0
  46. package/build/types/index.js +1 -0
  47. package/build/utils/AnalysisStateManager.d.ts +36 -2
  48. package/build/utils/AnalysisStateManager.js +34 -13
  49. package/build/utils/frontendSelectors.js +0 -1
  50. package/build/utils/gitStaging.d.ts +5 -0
  51. package/build/utils/gitStaging.js +1 -1
  52. package/build/utils/pom-catalog.d.ts +23 -0
  53. package/build/utils/pom-catalog.js +30 -0
  54. package/build/utils/pom-scope/pom-files.d.ts +14 -0
  55. package/build/utils/pom-scope/pom-files.js +32 -6
  56. package/build/utils/pom-scope/testIdDiscovery.d.ts +40 -0
  57. package/build/utils/pom-scope/testIdDiscovery.js +104 -0
  58. package/build/utils/reportVerification.d.ts +7 -2
  59. package/build/utils/reportVerification.js +9 -3
  60. package/build/utils/scenarioDrafting.js +7 -1
  61. package/build/utils/skyrampMdContent.d.ts +1 -1
  62. package/build/utils/skyrampMdContent.js +1 -1
  63. package/build/utils/urlPath.d.ts +37 -0
  64. package/build/utils/urlPath.js +55 -0
  65. package/build/utils/utils.d.ts +45 -0
  66. package/build/utils/utils.js +50 -0
  67. package/build/utils/versions.d.ts +3 -3
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.d.ts +15 -15
  70. package/build/utils/workspaceAuth.js +32 -17
  71. package/build/workspace/queryParamResolution.d.ts +93 -0
  72. package/build/workspace/queryParamResolution.js +201 -0
  73. package/build/workspace/workspace.d.ts +104 -0
  74. package/build/workspace/workspace.js +24 -0
  75. package/node_modules/playwright/ThirdPartyNotices.txt +319 -266
  76. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +154 -27
  77. package/node_modules/playwright/lib/dom-analyzer/crawler.js +2 -2
  78. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +1 -1
  79. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +6 -2
  80. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +3 -2
  81. package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +4 -3
  82. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +15 -3
  83. package/node_modules/playwright/lib/mcp/skyramp/specImport.js +781 -0
  84. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +14 -3
  85. package/node_modules/playwright/lib/mcp/test/resultCode.test.js +2 -1
  86. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +1 -1
  87. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +30 -0
  88. package/node_modules/playwright/lib/transform/babelBundleImpl.js +200 -199
  89. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +3 -3
  90. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.package-lock.json +3 -3
  91. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/aws-lambda/handler.js +16 -25
  92. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/bun/websocket.js +3 -1
  93. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/lambda-edge/handler.js +20 -4
  94. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +16 -25
  95. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/bun/websocket.js +3 -1
  96. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +20 -4
  97. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/client.js +10 -1
  98. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/utils.js +1 -1
  99. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/common.js +3 -1
  100. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/index.js +9 -1
  101. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/base.js +8 -14
  102. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/components.js +41 -21
  103. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/context.js +131 -5
  104. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/streaming.js +9 -7
  105. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/bearer-auth/index.js +1 -1
  106. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/compress/index.js +2 -1
  108. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -5
  109. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/index.js +2 -1
  110. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/language/language.js +10 -32
  111. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/method-override/index.js +5 -3
  112. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/serve-static/index.js +2 -2
  113. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/timing/timing.js +3 -1
  114. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/node.js +9 -0
  115. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/body.js +12 -4
  116. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/buffer.js +2 -1
  117. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/ipaddr.js +6 -1
  118. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/validator/validator.js +3 -3
  119. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/client.js +10 -1
  120. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/utils.js +1 -1
  121. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/common.js +3 -1
  122. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/index.js +9 -1
  123. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/base.js +15 -15
  124. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/components.js +42 -22
  125. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/context.js +129 -5
  126. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/streaming.js +10 -8
  127. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/bearer-auth/index.js +1 -1
  128. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cache/index.js +1 -1
  129. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/compress/index.js +2 -1
  130. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cors/index.js +2 -5
  131. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/index.js +2 -1
  132. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/language/language.js +10 -32
  133. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/method-override/index.js +5 -3
  134. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/serve-static/index.js +2 -2
  135. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/timing/timing.js +3 -1
  136. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/node.js +9 -0
  137. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/aws-lambda/handler.d.ts +1 -1
  138. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/lambda-edge/handler.d.ts +1 -1
  139. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/helper/websocket/index.d.ts +1 -1
  140. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/base.d.ts +1 -3
  141. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/context.d.ts +39 -0
  142. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/context-storage/index.d.ts +2 -2
  143. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/language/language.d.ts +18 -0
  144. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/body.d.ts +1 -1
  145. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/types.d.ts +1 -1
  146. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/body.js +12 -4
  147. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/buffer.js +2 -1
  148. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/ipaddr.js +6 -1
  149. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/validator/validator.js +3 -3
  150. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/package.json +29 -22
  151. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/package-lock.json +3 -3
  152. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  153. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +5 -1
  154. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/specReader.js +781 -0
  155. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +25 -6
  156. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +3 -1
  157. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  158. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +253 -27
  159. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-D0BjbCb7.js → codeMirrorModule-DtudTj_v.js} +1 -1
  160. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +422 -0
  161. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bzd72-bG.js → codeMirrorModule-FNMuBzX1.js} +1 -1
  163. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +1035 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +2 -0
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +5 -0
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +4 -0
  171. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/specReader.ts +1028 -0
  172. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +31 -8
  173. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +1 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +10 -6
  177. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/tsconfig.build.tsbuildinfo +0 -1
  178. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +0 -193
  179. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DzxTioTK.js +0 -809
  180. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.BGc30U3S.js +0 -2
  181. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.IaDrb29A.js +0 -5
package/build/index.js CHANGED
@@ -45,6 +45,8 @@ import { registerProgressResource } from "./resources/progressResource.js";
45
45
  import { AnalyticsService } from "./services/AnalyticsService.js";
46
46
  import { registerInitTriggerOnMCPInitialized } from "./utils/initAgent.js";
47
47
  import { isTestbotEnabled, isLocalDevEnabled } from "./utils/featureFlags.js";
48
+ import { findRoot, readTestIdAttribute } from "./utils/pom-scope/index.js";
49
+ import { discoverTestIdAttribute } from "./utils/pom-scope/testIdDiscovery.js";
48
50
  import { registerPlaywrightTools, registerTraceRecordingPrompt, getPlaywrightTraceService, } from "./playwright/index.js";
49
51
  // One-click routing instructions — injected into MCP server instructions so agents
50
52
  // know to delegate to the one-click tool rather than self-orchestrating.
@@ -212,8 +214,51 @@ if (isTestbotEnabled()) {
212
214
  logger.info("Testbot tools enabled via SKYRAMP_FEATURE_TESTBOT");
213
215
  }
214
216
  infrastructureTools.forEach((registerTool) => registerTool(server));
215
- // Register Playwright browser tools (trace recording via browser automation)
216
- registerPlaywrightTools(server).catch((err) => {
217
+ // Register Playwright browser tools (trace recording via browser automation).
218
+ //
219
+ // Tell the capture which attribute this repository uses for test ids. Without
220
+ // it the capture assumes Playwright's `data-testid`, and a repo using anything
221
+ // else gets no test-id anchor and a positional xpath for every element — which
222
+ // resolves at capture time and breaks later, so the miss never announces itself
223
+ // (SKYR-4136). This server is the only component that knows which repository it
224
+ // is running against.
225
+ //
226
+ // A declared `use.testIdAttribute` is authoritative. Failing that, the
227
+ // repository's own tests name the attribute whenever they select on it, so read
228
+ // it out of their selectors rather than guessing from a list of popular names.
229
+ // Never fatal: no answer leaves the capture on Playwright's default.
230
+ (async () => {
231
+ let testIdAttribute;
232
+ try {
233
+ const root = await findRoot(process.cwd());
234
+ testIdAttribute = await readTestIdAttribute(root);
235
+ if (testIdAttribute) {
236
+ logger.info(`testIdAttribute declared by the Playwright config: ${testIdAttribute}`);
237
+ }
238
+ else {
239
+ const discovered = await discoverTestIdAttribute(root);
240
+ testIdAttribute = discovered.attribute;
241
+ if (testIdAttribute) {
242
+ // Log the runner-up counts: on a part-migrated repo the most-used
243
+ // attribute wins, and a wrong pick should be visible here rather than
244
+ // showing up later as brittle selectors.
245
+ logger.info(`testIdAttribute discovered from test selectors: ${testIdAttribute}`, { counts: discovered.counts, filesScanned: discovered.filesScanned });
246
+ }
247
+ else {
248
+ // No name rule can reach an arbitrary convention like `xyz-id`, so name
249
+ // what the selectors did contain and the variable that overrides it.
250
+ // Editing the repo's own Playwright config is the wrong instruction: a
251
+ // suite may have several, and none of them at the root.
252
+ logger.info("No testIdAttribute declared or discovered — capture stays on Playwright's data-testid. " +
253
+ "Set PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE to override.", { attributesSeenInSelectors: discovered.observed, filesScanned: discovered.filesScanned });
254
+ }
255
+ }
256
+ }
257
+ catch (err) {
258
+ logger.warning("Could not determine this repository's testIdAttribute", { error: err });
259
+ }
260
+ await registerPlaywrightTools(server, { testIdAttribute });
261
+ })().catch((err) => {
217
262
  logger.error("Failed to register Playwright tools", { error: err });
218
263
  });
219
264
  // Global error handlers for crash telemetry
@@ -17,6 +17,14 @@ export interface PlaywrightTraceOptions {
17
17
  headless?: boolean;
18
18
  /** Directory for output files (zip, HAR). Defaults to cwd. */
19
19
  outputDir?: string;
20
+ /**
21
+ * The attribute this repository uses for test ids, when its Playwright config
22
+ * declares one. Forwarded to the blueprint capture, which otherwise only
23
+ * recognises the common conventions and falls back to positional xpaths —
24
+ * brittle, and silently so. Only this server knows which repository it runs
25
+ * against, so only this server can supply it.
26
+ */
27
+ testIdAttribute?: string;
20
28
  }
21
29
  export declare class PlaywrightTraceService {
22
30
  private _backend;
@@ -26,6 +26,7 @@ export class PlaywrightTraceService {
26
26
  this._backend = new TraceRecordingBackend({
27
27
  headless: this._options.headless,
28
28
  outputDir: this._options.outputDir || process.cwd(),
29
+ testIdAttribute: this._options.testIdAttribute,
29
30
  });
30
31
  // Initialize with a synthetic client info
31
32
  await this._backend.initialize({
@@ -4,11 +4,37 @@
4
4
  */
5
5
  import { z } from "zod";
6
6
  import { logger } from "../utils/logger.js";
7
+ import { dualChannelResult } from "../utils/utils.js";
7
8
  import { PlaywrightTraceService, } from "./PlaywrightTraceService.js";
8
9
  let _service = null;
9
10
  export function getPlaywrightTraceService() {
10
11
  return _service;
11
12
  }
13
+ /**
14
+ * The one browser tool whose result is re-shaped on the way out — see
15
+ * toStructuredBlueprintResult.
16
+ */
17
+ const BLUEPRINT_TOOL = "browser_blueprint";
18
+ /**
19
+ * Re-shape a browser_blueprint result onto both MCP result channels.
20
+ *
21
+ * SKYR-4194. The browser tools are registered here as passthroughs, and the fork
22
+ * returns the captured DOM as one `content[]` text block with no
23
+ * `structuredContent` — the shape whose spill the agent's file reader refuses.
24
+ * See `dualChannelResult` for why both channels carry the payload.
25
+ *
26
+ * An error result is returned untouched: the SDK's outputSchema check returns
27
+ * early on `isError`, so it needs no structuredContent.
28
+ */
29
+ function toStructuredBlueprintResult(result) {
30
+ if (result?.isError)
31
+ return result;
32
+ const blueprint = (result?.content ?? [])
33
+ .filter((block) => block?.type === "text")
34
+ .map((block) => block.text)
35
+ .join("\n");
36
+ return dualChannelResult({ blueprint });
37
+ }
12
38
  export async function registerPlaywrightTools(server, options) {
13
39
  _service = new PlaywrightTraceService(options);
14
40
  try {
@@ -68,9 +94,23 @@ export async function registerPlaywrightTools(server, options) {
68
94
  logger.info(`Filtering to ${filteredTools.length} essential tools (from ${tools.length} total)`);
69
95
  for (const tool of filteredTools) {
70
96
  const zodSchema = jsonSchemaToZod(tool.inputSchema);
97
+ const isBlueprint = tool.name === BLUEPRINT_TOOL;
71
98
  server.registerTool(tool.name, {
72
99
  description: tool.description || `Playwright tool: ${tool.name}`,
73
100
  inputSchema: zodSchema,
101
+ // SKYR-4194: only browser_blueprint returns its payload via
102
+ // structuredContent, which requires a declared outputSchema. The other
103
+ // browser tools return small results, and some return images, which this
104
+ // text-only re-shaping would drop.
105
+ ...(isBlueprint
106
+ ? {
107
+ outputSchema: {
108
+ blueprint: z
109
+ .string()
110
+ .describe("The captured PageBlueprint — sections, elements, and their logical names. Read and follow this field."),
111
+ },
112
+ }
113
+ : {}),
74
114
  }, async (params) => {
75
115
  if (!_service?.isInitialized()) {
76
116
  return {
@@ -83,7 +123,8 @@ export async function registerPlaywrightTools(server, options) {
83
123
  isError: true,
84
124
  };
85
125
  }
86
- return _service.callTool(tool.name, params);
126
+ const result = await _service.callTool(tool.name, params);
127
+ return isBlueprint ? toStructuredBlueprintResult(result) : result;
87
128
  });
88
129
  }
89
130
  logger.info(`Registered ${filteredTools.length} Playwright tools: ${filteredTools.map((t) => t.name).join(", ")}`);
@@ -154,6 +154,25 @@ expect(getResponseValue(productsPostResponse, "created_at")).toMatch(/^\\d{4}-\\
154
154
  },
155
155
  ],
156
156
  },
157
+ {
158
+ title: "Assert a nullable field is present, not merely null",
159
+ description: "Reading a field's value cannot tell a null field from a missing one — `get_response_value` / `getValue` return the same result for both, so `is None` does not prove the field exists. Use a schema check with an example body instead.",
160
+ subPoints: [
161
+ "An example carrying `null` for the field asserts presence and nullness together, and fails when the field is absent.",
162
+ "An example carrying a concrete value only asserts that value's type, so a nullable field checked against a string example fails once the real value is null.",
163
+ "`check_schema` / `checkSchema` takes the RESPONSE OBJECT, not a parsed body. The `@skyramp/skyramp` type declaration says otherwise and is wrong — passing a plain object makes every check silently return false (SKYR-4189).",
164
+ ],
165
+ examples: [
166
+ {
167
+ language: "python",
168
+ code: `assert skyramp.check_schema(order_response, '{"cancelled_at": null}')`,
169
+ },
170
+ {
171
+ language: "javascript",
172
+ code: `expect(checkSchema(orderResponse, '{"cancelled_at": null}')).toBe(true);`,
173
+ },
174
+ ],
175
+ },
157
176
  ];
158
177
  export function renderRule(index, rule) {
159
178
  const subPoints = rule.subPoints && rule.subPoints.length > 0
@@ -1,5 +1,9 @@
1
1
  import { generateSkyrampHeader, SKYRAMP_UTILS_HEADER } from "../utils/utils.js";
2
+ import { resolvePomCatalogPath } from "../utils/pom-catalog.js";
2
3
  export function getPomAwareCodeReusePrompt(testFile, language = "typescript", scopedPoms) {
4
+ // Resolved here, not left to the agent: the catalog only survives to the next
5
+ // run if it lands where delivery stages it (SKYR-4141).
6
+ const catalogPath = resolvePomCatalogPath(testFile);
3
7
  const isJs = language.toLowerCase() === "javascript";
4
8
  const ext = isJs ? "js" : "ts";
5
9
  const utilsFile = isJs ? "skyrampUtils.js" : "skyrampUtils.ts";
@@ -27,8 +31,6 @@ ${scopedPoms.tier2.map(renderPom).join("\n") || "(none)"}
27
31
 
28
32
  Do NOT glob or scan for other POM files. Proceed to STEP 2 and catalog exactly these files (plus any file they import or extend that you find essential while reading them).
29
33
 
30
- **Catalog-completeness guard for STEP 2a:** when checking for an existing \`skyramp-pom-catalog.md\`, it is only valid for THIS run if it already contains an entry for EVERY file listed above. If any listed file is missing from the catalog, do NOT reuse it wholesale — keep its existing entries and generate + append entries for the missing listed files (then use the updated catalog).
31
-
32
34
  **Decision:** continue to STEP 2.`
33
35
  : `## STEP 1: DETECT POM CLASSES IN THE PROJECT
34
36
 
@@ -89,10 +91,13 @@ ${step1Section}
89
91
 
90
92
  **Step 2a: Check for existing catalog (MANDATORY FIRST STEP)**
91
93
 
92
- Look for \`skyramp-pom-catalog.md\` walking UP from the test file's directory to the repository root; use the FIRST one found (catalogs from earlier runs may live at legacy locations — a found catalog is valid wherever it lives):
93
- - **If it exists:** Read it and verify it starts with \`<!-- skyramp-pom-catalog: auto-generated -->\`
94
- - Valid → Output: *"Using existing POM catalog from /path/to/skyramp-pom-catalog.md"* and **skip to STEP 2b**
95
- - Invalid → Continue to full catalog generation below
94
+ Read the catalog at EXACTLY this path the one canonical location. Do not search for it anywhere else:
95
+
96
+ \`${catalogPath}\`
97
+
98
+ - **If it exists:** verify it starts with \`<!-- skyramp-pom-catalog: auto-generated -->\` AND already contains an entry for EVERY POM file STEP 1 identified — the files listed in STEP 1 above, or the files your own glob found, whichever applies.
99
+ - Valid and complete → Output: *"Using existing POM catalog from ${catalogPath}"* and **skip to STEP 2b**
100
+ - Invalid, or missing an entry for any STEP 1 file → do NOT reuse it wholesale — keep its existing entries and continue to full catalog generation below, generating and appending entries only for the missing files
96
101
  - **If it doesn't exist:** Continue to full catalog generation below
97
102
 
98
103
  ---
@@ -121,10 +126,14 @@ Before writing a single line of refactored code, do the following:
121
126
  - Whether the method wraps a multi-step sequence using sub-properties or components (e.g. \`searchAssetsByCondition\` internally calls \`searchConditionArea.expandWhenClosed()\`, \`clearBtn.click()\`, \`selectItemName()\`, \`valueKeyword.fill()\`, \`searchBtn.click()\`) — flag these as **wrapping methods**
122
127
  - Flag methods as **timing-aware**, **network-aware**, or **wrapping** as appropriate — all three are high-value substitution targets
123
128
 
124
- 4. **Write the POM catalog — MANDATORY** — Write the complete catalog to \`skyramp-pom-catalog.md\` at the CANONICAL location: the top of the configured test directory (when this run's instructions declare one containing the test file), otherwise the nearest ancestor directory of the test file that contains a \`package.json\`, otherwise the repository root. Never invent a new location per run — determinism is what lets later runs reuse this file. This gives you a clean structured reference for STEP 2b and leaves a record for debugging.
129
+ 4. **Write the POM catalog — MANDATORY** — Write the complete catalog to EXACTLY this path, already resolved for this run, creating the parent directory first if it does not already exist:
130
+
131
+ \`${catalogPath}\`
132
+
133
+ Use it verbatim — do not re-derive it, shorten it, or pick a directory that looks more natural. Delivery commits this location; a catalog written anywhere else is discarded without warning, and the next run pays full POM rediscovery. This gives you a clean structured reference for STEP 2b and leaves a record for debugging.
125
134
 
126
135
  **BLOCKING: Do not proceed to STEP 2b until you have written this file and confirmed the path in a single output line:**
127
- *"POM catalog written to /path/to/skyramp-pom-catalog.md"*
136
+ *"POM catalog written to ${catalogPath}"*
128
137
 
129
138
  **Verification gate:** Before continuing to STEP 2b, confirm you either (a) found and used an existing catalog in Step 2a, OR (b) just wrote a new catalog in step 4 above. If neither is true, STOP and complete step 4 now.
130
139
 
@@ -15,7 +15,7 @@ export function buildFileRenameStrategy() {
15
15
  }
16
16
  /** Strategy string for in-place UPDATE edits. */
17
17
  export function buildUpdateStrategy() {
18
- return `Apply each file in update_context yourself with the Edit tool, using its current_content and the changes described in context. Preserve all existing test logic — only add or adjust what context describes.
18
+ return `Apply each file in update_context yourself with the Edit tool. Read the file first, then make the changes described in context. Preserve all existing test logic — only add or adjust what context describes.
19
19
 
20
20
  When a file provides edit_sites, apply each site directly using its anchor as the Edit target — do not re-grep or re-page the file to relocate the sites; they were already found for you.
21
21
 
@@ -25,7 +25,7 @@ After editing, re-read each file to confirm the change landed, then call skyramp
25
25
  }
26
26
  /** Strategy string for REGENERATE — call generation tool to overwrite the file. */
27
27
  export function buildRegenerateStrategy() {
28
- 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. Use existing_content to determine the test type, endpoint, auth pattern, and language. 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.`;
28
+ 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.`;
29
29
  }
30
30
  /** Strategy string for DELETE — remove obsolete test files whose covered endpoints/UI no longer exist. */
31
31
  export function buildDeleteStrategy() {
@@ -347,10 +347,7 @@ Static endpoint scan results are **best-effort hints only**. Do not assume the s
347
347
  ${staticHintCount > 0 ? `Static hints available: ${staticHintCount}. Verify every hinted method/path against source before using it.` : "Static hints available: 0. Build the endpoint list from source, router context, spec, and diff."}
348
348
 
349
349
  ${p.candidateRouteFiles && p.candidateRouteFiles.length > 0
350
- ? `Candidate files to inspect:\n${p.candidateRouteFiles
351
- .slice(0, 15)
352
- .map((f) => `- \`${f}\``)
353
- .join("\n")}${p.candidateRouteFiles.length > 15 ? `\n_(${p.candidateRouteFiles.length - 15} more files not shown)_` : ""}`
350
+ ? ""
354
351
  : staticHintCount > 0
355
352
  ? "Candidate files to inspect: none identified by static scanning. Verify the static hints against their source files, changed files, router context, and specs above."
356
353
  : "Candidate files to inspect: use the changed files and routing entry-point files above."}
@@ -32,4 +32,4 @@ export declare function buildTestQualityCriteria(): string;
32
32
  export declare function buildGenerationRules(isUIOnlyPR: boolean): string;
33
33
  export declare function buildVerificationChecklist(topN: number, maxGen: number, codeReviewStepLabel?: string): string;
34
34
  export declare function buildFewShotExamples(): string;
35
- export declare function buildToolWorkflows(authHeaderValue: string, authTypeValue?: string, explicitScheme?: string): string;
35
+ export declare function buildToolWorkflows(authHeaderValue: string, authTypeValue?: string, explicitScheme?: string, enrichStepLabel?: string): string;
@@ -1,6 +1,6 @@
1
1
  import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
2
2
  import { resolveServiceDetailsRef } from "../../utils/utils.js";
3
- import { WorkspaceAuthType, getAuthScheme, isAuthorizationHeaderName, AUTH_MIDDLEWARE_PATTERNS_STR } from "../../utils/workspaceAuth.js";
3
+ import { WorkspaceAuthType, getAuthScheme, isAuthorizationHeaderName } from "../../utils/workspaceAuth.js";
4
4
  // Cached at module-load — flags are process-wide and cannot change per call.
5
5
  const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
6
6
  const SERVICE_REFS = resolveServiceDetailsRef();
@@ -243,7 +243,7 @@ Reasoning: Catches a missing 404 guard on DELETE — verifies the handler return
243
243
  </example>
244
244
  </examples>`;
245
245
  }
246
- export function buildToolWorkflows(authHeaderValue, authTypeValue = "", explicitScheme) {
246
+ export function buildToolWorkflows(authHeaderValue, authTypeValue = "", explicitScheme, enrichStepLabel = "2") {
247
247
  const isAuthorizationHeader = isAuthorizationHeaderName(authHeaderValue);
248
248
  const noAuth = !authHeaderValue;
249
249
  let authGuidance;
@@ -251,7 +251,7 @@ export function buildToolWorkflows(authHeaderValue, authTypeValue = "", explicit
251
251
  if (noAuth) {
252
252
  authGuidance = `**Auth Verification Required:** The workspace config indicates no authentication, but you MUST verify this independently before omitting auth:
253
253
  1. **OpenAPI spec** \u2192 check \`securitySchemes\` / \`securityDefinitions\` for \`type: http\`, \`type: apiKey\`, or \`type: oauth2\`
254
- 2. **Source code** \u2192 look for known auth signals (${AUTH_MIDDLEWARE_PATTERNS_STR}).
254
+ 2. **Source code** \u2192 look for the auth middleware signals listed in Step ${enrichStepLabel} of the Execution Plan.
255
255
  3. **Route definitions** \u2192 check if routes have auth decorators or middleware applied
256
256
  4. **Still unknown** \u2192 proceed with \`authHeader: ""\` and note "auth pattern unrecognized" in your recommendation description.
257
257
  If you find auth requirements, pass the appropriate \`authHeader\` and \`authScheme\` to EVERY tool call. Only pass \`authHeader: ""\` if you confirm the API is truly unauthenticated.
@@ -270,7 +270,7 @@ If you find auth requirements, pass the appropriate \`authHeader\` and \`authSch
270
270
  authGuidance = `**Auth Scheme:** The workspace \`api.authType\` is \`"${authTypeValue}"\`.
271
271
  **Where to find the scheme** (check in order):
272
272
  1. **OpenAPI spec** \u2192 look at \`securitySchemes\` / \`securityDefinitions\` for \`type: http, scheme: bearer\` or \`type: apiKey\`
273
- 2. **Source code** \u2192 auth middleware signals: ${AUTH_MIDDLEWARE_PATTERNS_STR}
273
+ 2. **Source code** \u2192 the auth middleware signals listed in Step ${enrichStepLabel} of the Execution Plan
274
274
  3. **Workspace config** \u2192 use \`api.authType\` value as the scheme if source is inconclusive
275
275
  Pass the prefix as \`authScheme\` (e.g., \`"Bearer"\`, \`"Token"\`, \`"Basic"\`). If the API uses raw tokens with no prefix, pass \`authScheme: ""\`.
276
276
  **Do NOT guess the scheme.**
@@ -280,7 +280,7 @@ To skip auth entirely, pass \`authHeader: ""\`.`;
280
280
  authGuidance = `**Auth Scheme:** No \`api.authType\` in workspace config.
281
281
  **Where to find the scheme** (check in order):
282
282
  1. **OpenAPI spec** \u2192 look at \`securitySchemes\` / \`securityDefinitions\` for \`type: http, scheme: bearer\` or \`type: apiKey\`
283
- 2. **Source code** \u2192 auth middleware signals: ${AUTH_MIDDLEWARE_PATTERNS_STR}
283
+ 2. **Source code** \u2192 the auth middleware signals listed in Step ${enrichStepLabel} of the Execution Plan
284
284
  3. **Fallback** \u2192 use \`"Bearer"\` only if the project clearly uses JWT or OAuth; otherwise pass \`authScheme: ""\`
285
285
  Pass the prefix as \`authScheme\` (e.g., \`"Bearer"\`, \`"Token"\`, \`"Basic"\`). If the API uses raw tokens with no prefix, pass \`authScheme: ""\`.
286
286
  **Do NOT guess the scheme.**
@@ -5,7 +5,7 @@ import { logger } from "../../utils/logger.js";
5
5
  import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildFewShotExamples, buildVerificationChecklist, getAuthSnippets, MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, } from "./recommendationSections.js";
6
6
  import { CATEGORY_PRIORITY, Novelty, PriorityTier } from "../../types/TestRecommendation.js";
7
7
  import { buildScopeAssessmentSection, isFrontendFile } from "./scopeAssessment.js";
8
- import { buildExecutionPlan, EXEC_STEP_CODE_REVIEW } from "./diffExecutionPlan.js";
8
+ import { buildExecutionPlan, EXEC_STEP_CODE_REVIEW, EXEC_STEP_ENRICH } from "./diffExecutionPlan.js";
9
9
  import { buildFullRepoRecommendations } from "./fullRepoCatalog.js";
10
10
  import { ANALYSIS_STEP_EXTRACT } from "./analysisOutputPrompt.js";
11
11
  import { TASK_GENERATE, buildExternalCoverageSet, externalDedupKey, isAttackSurfaceSecurityBoundary, taskRef, } from "./recommendationShared.js";
@@ -345,8 +345,15 @@ Treat the endpoint lists above as static hints. If source/diff inspection finds
345
345
  diffEndpointKeys.add(`${m.method} ${ep.path}`);
346
346
  }
347
347
  }
348
- const summaryLines = allEndpoints
349
- .flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => ` ${m.method} ${ep.path} ${i.response.statusCode} (${i.type}): ${i.description}`)))
348
+ // SKYR-4188: `description` held exactly "<METHOD> <path>" in 11,273 of 11,273
349
+ // interaction lines on eval run 32227152533 a copy of the start of its own
350
+ // line. Emit it only when it says something the line does not already say.
351
+ const interactionLabel = (i, method, path) => i.description && i.description !== `${method} ${path}` ? `: ${i.description}` : "";
352
+ // detailBlocks repeats every summary line and adds the bodies. The two cover
353
+ // the same endpoints unless the scope is a diff, so outside a diff the summary
354
+ // is pure repetition.
355
+ const summaryLines = !isDiffScope ? "" : allEndpoints
356
+ .flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}`)))
350
357
  .join("\n");
351
358
  const detailEndpoints = isDiffScope
352
359
  ? allEndpoints.filter((ep) => (ep.methods ?? []).some((m) => diffEndpointKeys.has(`${m.method} ${ep.path}`)))
@@ -357,13 +364,12 @@ Treat the endpoint lists above as static hints. If source/diff inspection finds
357
364
  const qParams = i.request.queryParams ? `\n queryParams: ${JSON.stringify(i.request.queryParams)}` : "";
358
365
  const resBody = i.response.body ? `\n responseBody: ${JSON.stringify(i.response.body)}` : "";
359
366
  const headers = i.request.headers ? `\n headers: ${JSON.stringify(i.request.headers)}` : "";
360
- return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type}): ${i.description}${reqBody}${qParams}${resBody}${headers}`;
367
+ return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}${reqBody}${qParams}${resBody}${headers}`;
361
368
  })))
362
369
  .join("\n");
363
370
  interactionSection = `
364
371
  ## Endpoint Interactions
365
- ${summaryLines}
366
-
372
+ ${summaryLines ? `${summaryLines}\n` : ""}
367
373
  ### Detailed (request/response bodies)
368
374
  ${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies and query parameters.
369
375
  ${detailBlocks}
@@ -557,7 +563,7 @@ ${isDiffScope
557
563
  5. \`skyramp_ui_test_generation({ playwrightInput: "<absolute_path_to_zip>" })\`
558
564
 
559
565
  **For E2E tests:** Same browser recording flow, then call \`skyramp_e2e_test_generation\` with the zip.`
560
- : buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme))
566
+ : buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme, EXEC_STEP_ENRICH))
561
567
  : ""}
562
568
 
563
569
  ${mainSection}
@@ -1,21 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { type Service } from "../../workspace/workspace.js";
3
- /** One related repository to analyze as shared context in multi-repo mode. */
4
- export interface RelatedRepository {
5
- repositoryPath: string;
6
- baseBranch?: string;
7
- /** `owner/repo` slug, resolved by the action from the checkout's git remote.
8
- * Used verbatim as the `repository` argument/attribution for this repo so the
9
- * agent never has to infer it. */
10
- repo: string;
11
- }
2
+ import type { RelatedRepository, TestbotPromptOptions } from "../../types/TestbotPromptOptions.js";
12
3
  /**
13
4
  * Parse the JSON-encoded `relatedRepositories` argument passed via the testbot
14
5
  * prompt/resource. Returns undefined for missing/blank input or malformed JSON so the
15
6
  * caller cleanly falls back to single-repo behavior.
16
7
  */
17
8
  export declare function parseRelatedRepositories(raw: string | undefined): RelatedRepository[] | undefined;
18
- export declare function getTestbotPrompt(prTitle: string, prDescription: string, summaryOutputFile: string, repositoryPath: string, baseBranch?: string, maxRecommendations?: number, maxGenerate?: number, _maxCritical?: number, // Reserved — accepted for API compat but not yet wired into prompt
19
- prNumber?: number, userPrompt?: string, services?: Service[], uiCredentials?: string, testsRepoDir?: string, relatedRepositories?: RelatedRepository[], primaryRepo?: string, planOnly?: boolean, language?: string): string;
9
+ export declare function getTestbotPrompt(opts: TestbotPromptOptions): string;
20
10
  export declare function buildWorkspaceRecoveryPrefix(repositoryPath: string): string;
21
11
  export declare function registerTestbotPrompt(server: McpServer): void;
@@ -54,9 +54,10 @@ export function parseRelatedRepositories(raw) {
54
54
  return undefined;
55
55
  }
56
56
  }
57
- export function getTestbotPrompt(prTitle, prDescription, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE, _maxCritical = MAX_CRITICAL_TESTS, // Reserved — accepted for API compat but not yet wired into prompt
58
- prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language) {
59
- maxGenerate = Math.min(Math.max(maxGenerate, 0), maxRecommendations);
57
+ export function getTestbotPrompt(opts) {
58
+ const { prTitle, prDescription, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language, } = opts;
59
+ // maxCritical is intentionally unused (reserved) — see TestbotPromptOptions.
60
+ const maxGenerate = Math.min(Math.max(opts.maxGenerate ?? MAX_TESTS_TO_GENERATE, 0), maxRecommendations);
60
61
  // TODO(SKYR-3636 follow-up): migrate Task 1 + Task 2 step bodies to PromptPlan
61
62
  // (src/prompts/test-recommendation/promptPlan.ts) so step numbers don't have
62
63
  // to be hand-maintained when steps are added or reordered.
@@ -552,7 +553,7 @@ ${CONTRACT_MODE_GUIDANCE}
552
553
 
553
554
  4. **After** the action: \`browser_blueprint\` again. The response shape depends on whether the action navigated:
554
555
  - **Same URL (modal/tab/in-place mutation):** \`{ isFullCapture: false, pageHash, previousPageHash, delta, possibleAssertions }\`. The \`delta\` field contains \`elementsAdded\`, \`elementsRemoved\`, \`textChanges\`, \`repeatingCountChanges\`. The \`possibleAssertions\` field is a mechanical translation of those entries into Playwright \`expect(...)\` candidates — see step 5. An empty delta (all arrays empty) is itself a meaningful signal: the action did not change observable DOM (e.g. a silent failure the test should catch).
555
- - **Navigated to a new URL** (e.g. router transition, link click, programmatic \`browser_navigate\`): \`{ isFullCapture: true, pageHash, blueprint }\` — a fresh full capture of the new page. No \`possibleAssertions\` here (no delta to translate). Search the new blueprint for the elements your assertion will target.
556
+ - **Navigated to a new URL** (e.g. router transition, link click, programmatic \`browser_navigate\`): \`{ isFullCapture: true, pageHash, blueprint, possibleAssertions }\` — a fresh full capture of the new page. \`possibleAssertions\` is present here too, but short — there is no previous capture to compare against, so nothing in it tells you what the action changed. Search the new blueprint for the elements your assertion will target.
556
557
 
557
558
  5. **The second \`browser_blueprint\` response (the capture after the action) includes a \`possibleAssertions[]\` array — mechanical translations of delta entries into ready-to-use Playwright \`expect(...)\` candidates.** Each entry has \`{ code, rationale, tier }\`. Use tier to drive your assertion choices:
558
559
 
@@ -642,7 +643,7 @@ In these cases:
642
643
 
643
644
  ${task3CountRule}
644
645
 
645
- ${reportLanguageBlock}Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}" and \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
646
+ ${reportLanguageBlock}Call \`skyramp_submit_report\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking, and the report is written beside it. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
646
647
 
647
648
  ${hasRelatedRepos ? `
648
649
  - **MULTI-REPO attribution**: Set the \`repository\` field (\`owner/repo\`) on EVERY \`newTestsCreated\`, \`testResults\`, \`issuesFound\`, and \`additionalRecommendations\` item — including items about the PRIMARY repo — so each finding is unambiguously attributed. The primary repo's \`repository\` is \`${primaryRepo || "<the primary repo's owner/repo>"}\`; items derived from a related repo's diff (from the \`<related_repositories>\` analysis) carry that repo's \`repository\` value. In \`businessCaseAnalysis\`, include a short per-repo subsection and call out any cross-repo correlations you found.
@@ -651,13 +652,10 @@ ${hasRelatedRepos ? `
651
652
  - For \`testType: "contract"\` entries: **\`primaryEndpoint\` is required** (e.g. \`"GET /api/v1/users/{user_id}"\`). The tool will reject the submission without it — do not omit it or you will be forced to resubmit.
652
653
 
653
654
  ${getTraceRecordingPromptText({ outputDir: `${repositoryPath}/.skyramp`, modularize: false })}`;
654
- // TODO: merge stateFile and summaryOutputFile into a single file (follow-up PR).
655
- // Currently the stateFile path is derived server-side from RUNNER_TEMP (GitHub Actions only);
656
- // other CI systems (Jenkins, GitLab, Buildkite) require the caller to set RUNNER_TEMP.
657
- // Once merged, the path flows through summaryOutputFile already testbot-controlled and
658
- // CI-agnostic — eliminating the RUNNER_TEMP dependency and the LLM confusion that motivated
659
- // removing stateOutputFile from the prompt schema. Remove the RUNNER_TEMP branch in
660
- // AnalysisStateManager.ts when this is done.
655
+ // Neither path reaches the agent any more: SKYR-4147 made the report derive from the
656
+ // state file's directory, and that directory comes from the environment via
657
+ // runArtifactDir(). Keep it that way a path the model retypes out of this prose is a
658
+ // path the model can corrupt, which is the defect SKYR-4147 fixed.
661
659
  }
662
660
  export function buildWorkspaceRecoveryPrefix(repositoryPath) {
663
661
  return `IMPORTANT: The existing .skyramp/workspace.yml failed to parse or validate. Before proceeding with any tasks below, you MUST call skyramp_init_scan with workspacePath "${repositoryPath}" and force: true, then call skyramp_init_workspace with workspacePath "${repositoryPath}", the discovered services, scanToken, and force: true to regenerate the workspace file.\n\n`;
@@ -669,9 +667,6 @@ export function registerTestbotPrompt(server) {
669
667
  argsSchema: {
670
668
  prTitle: z.string().describe("Pull request title"),
671
669
  prDescription: z.string().describe("Pull request description/body"),
672
- summaryOutputFile: z
673
- .string()
674
- .describe("File path where the agent should write the testbot summary report"),
675
670
  repositoryPath: z
676
671
  .string()
677
672
  .default(".")
@@ -733,7 +728,24 @@ export function registerTestbotPrompt(server) {
733
728
  },
734
729
  }, async (args) => {
735
730
  const services = await readWorkspaceServices(args.repositoryPath);
736
- let prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.maxCritical, args.prNumber, args.userPrompt, services.length ? services : undefined, args.uiCredentials, args.testsRepoDir, parseRelatedRepositories(args.relatedRepositories), args.primaryRepo, args.planOnly, args.language);
731
+ let prompt = getTestbotPrompt({
732
+ prTitle: args.prTitle,
733
+ prDescription: args.prDescription,
734
+ repositoryPath: args.repositoryPath,
735
+ baseBranch: args.baseBranch,
736
+ maxRecommendations: args.maxRecommendations,
737
+ maxGenerate: args.maxGenerate,
738
+ maxCritical: args.maxCritical,
739
+ prNumber: args.prNumber,
740
+ userPrompt: args.userPrompt,
741
+ services: services.length ? services : undefined,
742
+ uiCredentials: args.uiCredentials,
743
+ testsRepoDir: args.testsRepoDir,
744
+ relatedRepositories: parseRelatedRepositories(args.relatedRepositories),
745
+ primaryRepo: args.primaryRepo,
746
+ planOnly: args.planOnly,
747
+ language: args.language,
748
+ });
737
749
  if (args.workspaceValidationFailed) {
738
750
  prompt = buildWorkspaceRecoveryPrefix(args.repositoryPath) + prompt;
739
751
  }
@@ -27,5 +27,11 @@ export interface DiscriminatorValidation {
27
27
  * 1. `changedCodeAnchor` must be a non-trivial string (>= 8 chars after trim)
28
28
  * occurring verbatim in `diffText` — grounds the claim in the real change.
29
29
  * 2. The `kind`-specific structural predicate must hold over `steps[]`.
30
+ *
31
+ * `reposWithoutDiff` names repos in the run whose section carries no branch
32
+ * diff (e.g. a related repo that fell back to its default branch, SKYR-4203).
33
+ * An anchor those repos' changes could contain cannot be disproven, so an
34
+ * unmatched anchor then fails with a "could not be checked" reason instead of
35
+ * the "not grounded" one — still unverified (no boost), but distinguishable.
30
36
  */
31
- export declare function validateDiscriminator(scenario: DraftedScenario, declared: DiscriminatorClaim, diffText: string): DiscriminatorValidation;
37
+ export declare function validateDiscriminator(scenario: DraftedScenario, declared: DiscriminatorClaim, diffText: string, reposWithoutDiff?: string[]): DiscriminatorValidation;
@@ -14,10 +14,16 @@ const MIN_NEAR_MISS_LENGTH = 3;
14
14
  * 1. `changedCodeAnchor` must be a non-trivial string (>= 8 chars after trim)
15
15
  * occurring verbatim in `diffText` — grounds the claim in the real change.
16
16
  * 2. The `kind`-specific structural predicate must hold over `steps[]`.
17
+ *
18
+ * `reposWithoutDiff` names repos in the run whose section carries no branch
19
+ * diff (e.g. a related repo that fell back to its default branch, SKYR-4203).
20
+ * An anchor those repos' changes could contain cannot be disproven, so an
21
+ * unmatched anchor then fails with a "could not be checked" reason instead of
22
+ * the "not grounded" one — still unverified (no boost), but distinguishable.
17
23
  */
18
- export function validateDiscriminator(scenario, declared, diffText) {
24
+ export function validateDiscriminator(scenario, declared, diffText, reposWithoutDiff = []) {
19
25
  try {
20
- const anchorResult = verifyAnchor(declared?.changedCodeAnchor, diffText);
26
+ const anchorResult = verifyAnchor(declared?.changedCodeAnchor, diffText, reposWithoutDiff);
21
27
  if (!anchorResult.verified)
22
28
  return anchorResult;
23
29
  const steps = Array.isArray(scenario?.steps) ? scenario.steps : [];
@@ -43,7 +49,7 @@ export function validateDiscriminator(scenario, declared, diffText) {
43
49
  }
44
50
  }
45
51
  // ── Anchor grounding ──
46
- function verifyAnchor(rawAnchor, diffText) {
52
+ function verifyAnchor(rawAnchor, diffText, reposWithoutDiff = []) {
47
53
  const anchor = typeof rawAnchor === "string" ? rawAnchor.trim() : "";
48
54
  if (anchor.length < MIN_ANCHOR_LENGTH) {
49
55
  return {
@@ -57,6 +63,13 @@ function verifyAnchor(rawAnchor, diffText) {
57
63
  // text — compare whitespace-collapsed forms with diff markers stripped, or
58
64
  // legitimate multi-line anchors are systematically false-demoted.
59
65
  if (!diff.includes(anchor) && !normalizeForAnchorMatch(diff).includes(normalizeForAnchorMatch(anchor))) {
66
+ if (reposWithoutDiff.length > 0) {
67
+ return {
68
+ verified: false,
69
+ reason: `changedCodeAnchor "${anchor}" was not found in the available PR diffs, and no branch diff exists for ` +
70
+ `${reposWithoutDiff.join(", ")} (likely checked out on the default branch), so the claim could not be checked against those changes.`,
71
+ };
72
+ }
60
73
  return {
61
74
  verified: false,
62
75
  reason: `changedCodeAnchor "${anchor}" does not occur verbatim in the PR diff, so the discriminator claim is not grounded in the actual change.`,
@@ -26,10 +26,26 @@ export function registerTestbotResource(server) {
26
26
  const maxCrit = parseInt(uri.searchParams.get("maxCritical") || "", 10);
27
27
  const repositoryPath = param("repositoryPath", ".");
28
28
  const services = await readWorkspaceServices(repositoryPath);
29
- const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined, parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined), uri.searchParams.get("primaryRepo") || undefined,
30
- // Plan-only eval lane (SKYR-3879): recommendation phase only, nothing
31
- // generated or executed. Same accepted spellings as the prompt schema.
32
- ["true", "1"].includes(uri.searchParams.get("planOnly") ?? ""), uri.searchParams.get("language") || undefined);
29
+ const prompt = getTestbotPrompt({
30
+ prTitle: param("prTitle", ""),
31
+ prDescription: param("prDescription", ""),
32
+ repositoryPath,
33
+ baseBranch: uri.searchParams.get("baseBranch") || undefined,
34
+ maxRecommendations: isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec,
35
+ maxGenerate: isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen,
36
+ maxCritical: isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit,
37
+ prNumber: isNaN(prNum) ? undefined : prNum,
38
+ userPrompt: uri.searchParams.get("userPrompt") || undefined,
39
+ services: services.length ? services : undefined,
40
+ uiCredentials: uri.searchParams.get("uiCredentials") || undefined,
41
+ testsRepoDir: uri.searchParams.get("testsRepoDir") || undefined,
42
+ relatedRepositories: parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined),
43
+ primaryRepo: uri.searchParams.get("primaryRepo") || undefined,
44
+ // Plan-only eval lane (SKYR-3879): recommendation phase only, nothing
45
+ // generated or executed. Same accepted spellings as the prompt schema.
46
+ planOnly: ["true", "1"].includes(uri.searchParams.get("planOnly") ?? ""),
47
+ language: uri.searchParams.get("language") || undefined,
48
+ });
33
49
  AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
34
50
  // Return the original URI — clients may use it to re-fetch the resource,
35
51
  // and the caller already has these params. Credentials never appear in
@@ -2,6 +2,7 @@ import { AUTH_PLACEHOLDER_TOKEN } from "../types/TestTypes.js";
2
2
  import { isAuthorizationHeaderName } from "../utils/workspaceAuth.js";
3
3
  import { inferExpectedStatus } from "../utils/httpDefaults.js";
4
4
  import { logger } from "../utils/logger.js";
5
+ import { deriveBasePath } from "../utils/urlPath.js";
5
6
  // Keys that trigger built-in prototype setters when used as bracket-notation
6
7
  // property names on a plain object — guard against prototype pollution from
7
8
  // LLM-controlled or user-controlled JSON input.
@@ -22,7 +23,10 @@ export class ScenarioGenerationService {
22
23
  let destination = params.destination;
23
24
  let scheme = "https";
24
25
  let port = 443;
25
- let basePath = "";
26
+ // Shared with generateBatchScenarioRestTool, which must predict this exact
27
+ // prefix to match workspace queryParamOverrides against the path this
28
+ // method will emit below (SKYR-4127). Keep it derived in one place.
29
+ const basePath = deriveBasePath(params.baseURL);
26
30
  if (params.baseURL) {
27
31
  try {
28
32
  const parsed = new URL(params.baseURL);
@@ -33,7 +37,6 @@ export class ScenarioGenerationService {
33
37
  : scheme === "https"
34
38
  ? 443
35
39
  : 80;
36
- basePath = parsed.pathname.replace(/\/$/, "");
37
40
  }
38
41
  catch {
39
42
  logger.warning("Could not parse baseURL, using destination param", {