@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.
- package/build/index.js +47 -2
- package/build/playwright/PlaywrightTraceService.d.ts +8 -0
- package/build/playwright/PlaywrightTraceService.js +1 -0
- package/build/playwright/registerPlaywrightTools.js +42 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +19 -0
- package/build/prompts/pom-aware-code-reuse.js +17 -8
- package/build/prompts/test-maintenance/actionsInstructions.js +2 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -4
- package/build/prompts/test-recommendation/recommendationSections.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationSections.js +5 -5
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +13 -7
- package/build/prompts/testbot/testbot-prompts.d.ts +2 -12
- package/build/prompts/testbot/testbot-prompts.js +28 -16
- package/build/recommendation/discriminators.d.ts +7 -1
- package/build/recommendation/discriminators.js +16 -3
- package/build/resources/testbotResource.js +20 -4
- package/build/services/ScenarioGenerationService.js +5 -2
- package/build/services/TestExecutionService.d.ts +13 -8
- package/build/services/TestExecutionService.js +73 -26
- package/build/services/TestGenerationService.js +24 -9
- package/build/services/containerEnv.d.ts +12 -1
- package/build/services/containerEnv.js +118 -1
- package/build/tools/executeSkyrampTestTool.d.ts +9 -0
- package/build/tools/executeSkyrampTestTool.js +20 -6
- package/build/tools/execution-video-state.d.ts +21 -0
- package/build/tools/execution-video-state.js +51 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +31 -11
- package/build/tools/generate-tests/planGuard.d.ts +5 -5
- package/build/tools/generate-tests/planGuard.js +5 -17
- package/build/tools/queryProxyMocksTool.js +0 -1
- package/build/tools/submitReportTool.d.ts +83 -10
- package/build/tools/submitReportTool.js +179 -29
- package/build/tools/test-management/actionsTool.js +52 -41
- package/build/tools/test-management/analyzeChangesTool.d.ts +11 -0
- package/build/tools/test-management/analyzeChangesTool.js +37 -33
- package/build/tools/test-management/analyzeTestHealthTool.js +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +113 -31
- package/build/types/TestAnalysis.d.ts +7 -3
- package/build/types/TestExecution.d.ts +14 -0
- package/build/types/TestTypes.js +3 -2
- package/build/types/TestbotPromptOptions.d.ts +34 -0
- package/build/types/TestbotPromptOptions.js +1 -0
- package/build/types/TestbotReport.d.ts +10 -0
- package/build/types/TestbotReport.js +10 -1
- package/build/types/index.d.ts +2 -0
- package/build/types/index.js +1 -0
- package/build/utils/AnalysisStateManager.d.ts +36 -2
- package/build/utils/AnalysisStateManager.js +34 -13
- package/build/utils/frontendSelectors.js +0 -1
- package/build/utils/gitStaging.d.ts +5 -0
- package/build/utils/gitStaging.js +1 -1
- package/build/utils/pom-catalog.d.ts +23 -0
- package/build/utils/pom-catalog.js +30 -0
- package/build/utils/pom-scope/pom-files.d.ts +14 -0
- package/build/utils/pom-scope/pom-files.js +32 -6
- package/build/utils/pom-scope/testIdDiscovery.d.ts +40 -0
- package/build/utils/pom-scope/testIdDiscovery.js +104 -0
- package/build/utils/reportVerification.d.ts +7 -2
- package/build/utils/reportVerification.js +9 -3
- package/build/utils/scenarioDrafting.js +7 -1
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/urlPath.d.ts +37 -0
- package/build/utils/urlPath.js +55 -0
- package/build/utils/utils.d.ts +45 -0
- package/build/utils/utils.js +50 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +15 -15
- package/build/utils/workspaceAuth.js +32 -17
- package/build/workspace/queryParamResolution.d.ts +93 -0
- package/build/workspace/queryParamResolution.js +201 -0
- package/build/workspace/workspace.d.ts +104 -0
- package/build/workspace/workspace.js +24 -0
- package/node_modules/playwright/ThirdPartyNotices.txt +319 -266
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +154 -27
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +6 -2
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +3 -2
- package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +4 -3
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +15 -3
- package/node_modules/playwright/lib/mcp/skyramp/specImport.js +781 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +14 -3
- package/node_modules/playwright/lib/mcp/test/resultCode.test.js +2 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +1 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +30 -0
- package/node_modules/playwright/lib/transform/babelBundleImpl.js +200 -199
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.package-lock.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/aws-lambda/handler.js +16 -25
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/bun/websocket.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/lambda-edge/handler.js +20 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +16 -25
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/bun/websocket.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +20 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/client.js +10 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/utils.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/common.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/index.js +9 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/base.js +8 -14
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/components.js +41 -21
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/context.js +131 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/streaming.js +9 -7
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/bearer-auth/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/compress/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/language/language.js +10 -32
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/method-override/index.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/serve-static/index.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/timing/timing.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/node.js +9 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/body.js +12 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/buffer.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/ipaddr.js +6 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/validator/validator.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/client.js +10 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/utils.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/common.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/index.js +9 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/base.js +15 -15
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/components.js +42 -22
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/context.js +129 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/streaming.js +10 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/bearer-auth/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cache/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/compress/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cors/index.js +2 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/language/language.js +10 -32
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/method-override/index.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/serve-static/index.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/timing/timing.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/node.js +9 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/aws-lambda/handler.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/lambda-edge/handler.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/helper/websocket/index.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/base.d.ts +1 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/context.d.ts +39 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/context-storage/index.d.ts +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/language/language.d.ts +18 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/body.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/types.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/body.js +12 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/buffer.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/ipaddr.js +6 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/validator/validator.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/package.json +29 -22
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/package-lock.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/specReader.js +781 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +25 -6
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +253 -27
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-D0BjbCb7.js → codeMirrorModule-DtudTj_v.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +422 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bzd72-bG.js → codeMirrorModule-FNMuBzX1.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +1035 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +4 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/specReader.ts +1028 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +31 -8
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/tsconfig.build.tsbuildinfo +0 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DzxTioTK.js +0 -809
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.BGc30U3S.js +0 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
349
|
-
|
|
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})
|
|
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 {
|
|
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(
|
|
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(
|
|
58
|
-
prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language
|
|
59
|
-
|
|
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.
|
|
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 \`
|
|
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
|
-
//
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
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(
|
|
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(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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", {
|