@skyramp/mcp 0.3.2 → 0.3.3
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/prompts/pom-aware-code-reuse.js +17 -8
- package/build/prompts/testbot/testbot-prompts.d.ts +2 -12
- package/build/prompts/testbot/testbot-prompts.js +24 -5
- package/build/resources/testbotResource.js +21 -4
- package/build/services/TestExecutionService.d.ts +13 -8
- package/build/services/TestExecutionService.js +48 -25
- package/build/services/containerEnv.js +24 -0
- package/build/tools/queryProxyMocksTool.js +0 -1
- package/build/tools/submitReportTool.js +10 -1
- package/build/tools/test-management/actionsTool.js +12 -2
- package/build/types/TestAnalysis.d.ts +7 -3
- package/build/types/TestbotPromptOptions.d.ts +35 -0
- package/build/types/TestbotPromptOptions.js +1 -0
- package/build/types/index.d.ts +1 -0
- 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/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- 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 +9 -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({
|
|
@@ -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
|
|
|
@@ -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, summaryOutputFile, 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
|
|
|
@@ -733,7 +734,25 @@ export function registerTestbotPrompt(server) {
|
|
|
733
734
|
},
|
|
734
735
|
}, async (args) => {
|
|
735
736
|
const services = await readWorkspaceServices(args.repositoryPath);
|
|
736
|
-
let prompt = getTestbotPrompt(
|
|
737
|
+
let prompt = getTestbotPrompt({
|
|
738
|
+
prTitle: args.prTitle,
|
|
739
|
+
prDescription: args.prDescription,
|
|
740
|
+
summaryOutputFile: args.summaryOutputFile,
|
|
741
|
+
repositoryPath: args.repositoryPath,
|
|
742
|
+
baseBranch: args.baseBranch,
|
|
743
|
+
maxRecommendations: args.maxRecommendations,
|
|
744
|
+
maxGenerate: args.maxGenerate,
|
|
745
|
+
maxCritical: args.maxCritical,
|
|
746
|
+
prNumber: args.prNumber,
|
|
747
|
+
userPrompt: args.userPrompt,
|
|
748
|
+
services: services.length ? services : undefined,
|
|
749
|
+
uiCredentials: args.uiCredentials,
|
|
750
|
+
testsRepoDir: args.testsRepoDir,
|
|
751
|
+
relatedRepositories: parseRelatedRepositories(args.relatedRepositories),
|
|
752
|
+
primaryRepo: args.primaryRepo,
|
|
753
|
+
planOnly: args.planOnly,
|
|
754
|
+
language: args.language,
|
|
755
|
+
});
|
|
737
756
|
if (args.workspaceValidationFailed) {
|
|
738
757
|
prompt = buildWorkspaceRecoveryPrefix(args.repositoryPath) + prompt;
|
|
739
758
|
}
|
|
@@ -26,10 +26,27 @@ 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
|
+
summaryOutputFile: param("summaryOutputFile", ""),
|
|
33
|
+
repositoryPath,
|
|
34
|
+
baseBranch: uri.searchParams.get("baseBranch") || undefined,
|
|
35
|
+
maxRecommendations: isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec,
|
|
36
|
+
maxGenerate: isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen,
|
|
37
|
+
maxCritical: isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit,
|
|
38
|
+
prNumber: isNaN(prNum) ? undefined : prNum,
|
|
39
|
+
userPrompt: uri.searchParams.get("userPrompt") || undefined,
|
|
40
|
+
services: services.length ? services : undefined,
|
|
41
|
+
uiCredentials: uri.searchParams.get("uiCredentials") || undefined,
|
|
42
|
+
testsRepoDir: uri.searchParams.get("testsRepoDir") || undefined,
|
|
43
|
+
relatedRepositories: parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined),
|
|
44
|
+
primaryRepo: uri.searchParams.get("primaryRepo") || undefined,
|
|
45
|
+
// Plan-only eval lane (SKYR-3879): recommendation phase only, nothing
|
|
46
|
+
// generated or executed. Same accepted spellings as the prompt schema.
|
|
47
|
+
planOnly: ["true", "1"].includes(uri.searchParams.get("planOnly") ?? ""),
|
|
48
|
+
language: uri.searchParams.get("language") || undefined,
|
|
49
|
+
});
|
|
33
50
|
AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
|
|
34
51
|
// Return the original URI — clients may use it to re-fetch the resource,
|
|
35
52
|
// and the caller already has these params. Credentials never appear in
|
|
@@ -2,7 +2,7 @@ import { TestExecutionResult, BatchExecutionResult, TestExecutionOptions, Progre
|
|
|
2
2
|
import { EXECUTOR_DOCKER_IMAGE } from "../utils/versions.js";
|
|
3
3
|
export { EXECUTOR_DOCKER_IMAGE };
|
|
4
4
|
export declare const PLAYWRIGHT_CONFIG_FILES: string[];
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const MOUNT_SCAN_SKIP_DIRS: string[];
|
|
6
6
|
export declare const TEST_REPO_CONTAINER_SUBDIR = ".skyramp-test-repo";
|
|
7
7
|
export declare const MOUNT_NULL_ITEMS: string[];
|
|
8
8
|
/**
|
|
@@ -38,18 +38,23 @@ export declare function detectSessionFiles(testFilePath: string): string[];
|
|
|
38
38
|
*/
|
|
39
39
|
export declare function findExcludedPaths(dir: string, excludedItems: string[]): string[];
|
|
40
40
|
/**
|
|
41
|
-
* Find every
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* Find every `node_modules` directory under `dir`, including the top-level
|
|
42
|
+
* `<dir>/node_modules`. Never descends into a `node_modules`. Used to shadow
|
|
43
|
+
* SHADOWED_NESTED_PACKAGES inside each one: a repo-local `@playwright/test`
|
|
44
|
+
* becomes a second physical copy and trips Playwright's "Requiring
|
|
45
|
+
* @playwright/test second time" guard.
|
|
46
|
+
*
|
|
47
|
+
* The top-level one is deliberately NOT special-cased (SKYR-4140). When
|
|
48
|
+
* `workspacePath` is itself an npm package root — the standard `tests/<suite>/`
|
|
49
|
+
* layout — its node_modules IS the only one, so skipping it left the shadow
|
|
50
|
+
* machinery with nothing to act on and no customer dependency resolvable.
|
|
51
|
+
*
|
|
47
52
|
* Skips `.git` (nothing importable lives there) and does not follow symlinks —
|
|
48
53
|
* by design (a symlinked nested node_modules either dangles in-container,
|
|
49
54
|
* falling through to NODE_PATH's global copy, or its real-path target is
|
|
50
55
|
* walked directly).
|
|
51
56
|
*/
|
|
52
|
-
export declare function
|
|
57
|
+
export declare function findNodeModulesDirs(dir: string): string[];
|
|
53
58
|
export declare class TestExecutionService {
|
|
54
59
|
private docker;
|
|
55
60
|
private imageReady;
|
|
@@ -54,8 +54,11 @@ export const PLAYWRIGHT_CONFIG_FILES = [
|
|
|
54
54
|
"playwright.config.js",
|
|
55
55
|
"playwright.config.mjs",
|
|
56
56
|
];
|
|
57
|
-
// Directories
|
|
58
|
-
|
|
57
|
+
// Directories the recursive shadow scans must never descend into. node_modules
|
|
58
|
+
// IS mounted (see mountRepoTree) — but walking it would shadow every package's
|
|
59
|
+
// own package.json with empty JSON via MOUNT_NULL_ITEMS, breaking resolution
|
|
60
|
+
// for the whole tree.
|
|
61
|
+
export const MOUNT_SCAN_SKIP_DIRS = [
|
|
59
62
|
"node_modules",
|
|
60
63
|
];
|
|
61
64
|
// Reserved container subdir under /home/user where a cross-repo test repository
|
|
@@ -354,7 +357,7 @@ export function detectSessionFiles(testFilePath) {
|
|
|
354
357
|
*/
|
|
355
358
|
export function findExcludedPaths(dir, excludedItems) {
|
|
356
359
|
const results = [];
|
|
357
|
-
const shouldSkipDir = (entry) => excludedItems.includes(entry.name) ||
|
|
360
|
+
const shouldSkipDir = (entry) => excludedItems.includes(entry.name) || MOUNT_SCAN_SKIP_DIRS.includes(entry.name);
|
|
358
361
|
for (const [entry, fullPath] of walkDir(dir, { shouldSkipDir })) {
|
|
359
362
|
// Only shadow files — mounting /dev/null to a directory target causes Docker errors
|
|
360
363
|
if (excludedItems.includes(entry.name)) {
|
|
@@ -364,18 +367,23 @@ export function findExcludedPaths(dir, excludedItems) {
|
|
|
364
367
|
return results;
|
|
365
368
|
}
|
|
366
369
|
/**
|
|
367
|
-
* Find every
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
370
|
+
* Find every `node_modules` directory under `dir`, including the top-level
|
|
371
|
+
* `<dir>/node_modules`. Never descends into a `node_modules`. Used to shadow
|
|
372
|
+
* SHADOWED_NESTED_PACKAGES inside each one: a repo-local `@playwright/test`
|
|
373
|
+
* becomes a second physical copy and trips Playwright's "Requiring
|
|
374
|
+
* @playwright/test second time" guard.
|
|
375
|
+
*
|
|
376
|
+
* The top-level one is deliberately NOT special-cased (SKYR-4140). When
|
|
377
|
+
* `workspacePath` is itself an npm package root — the standard `tests/<suite>/`
|
|
378
|
+
* layout — its node_modules IS the only one, so skipping it left the shadow
|
|
379
|
+
* machinery with nothing to act on and no customer dependency resolvable.
|
|
380
|
+
*
|
|
373
381
|
* Skips `.git` (nothing importable lives there) and does not follow symlinks —
|
|
374
382
|
* by design (a symlinked nested node_modules either dangles in-container,
|
|
375
383
|
* falling through to NODE_PATH's global copy, or its real-path target is
|
|
376
384
|
* walked directly).
|
|
377
385
|
*/
|
|
378
|
-
export function
|
|
386
|
+
export function findNodeModulesDirs(dir) {
|
|
379
387
|
const results = [];
|
|
380
388
|
const walk = (current) => {
|
|
381
389
|
let entries;
|
|
@@ -392,8 +400,7 @@ export function findNestedNodeModules(dir) {
|
|
|
392
400
|
continue;
|
|
393
401
|
const full = path.join(current, entry.name);
|
|
394
402
|
if (entry.name === "node_modules") {
|
|
395
|
-
|
|
396
|
-
results.push(full); // skip the top-level node_modules
|
|
403
|
+
results.push(full);
|
|
397
404
|
continue; // never descend into a node_modules
|
|
398
405
|
}
|
|
399
406
|
walk(full);
|
|
@@ -593,17 +600,16 @@ export class TestExecutionService {
|
|
|
593
600
|
hostConfig.Mounts.push(mount);
|
|
594
601
|
};
|
|
595
602
|
// Mount one repo tree (the workspace, or a cross-repo test repository) into
|
|
596
|
-
// the container
|
|
603
|
+
// the container.
|
|
597
604
|
//
|
|
598
605
|
// Each top-level entry is bind-mounted at the canonical container path and —
|
|
599
606
|
// when mirrorToHost is set — at its host-absolute path too. The dual mount
|
|
600
607
|
// lets the test resolve any absolute reference the codegen happens to embed
|
|
601
608
|
// (storageState, fixture paths, snapshots) — including the host workspace
|
|
602
609
|
// path that Playwright's TypeScript loader sometimes produces from
|
|
603
|
-
// `__dirname` — without needing source-code detection.
|
|
604
|
-
// (
|
|
605
|
-
//
|
|
606
|
-
// PLAYWRIGHT_CONFIG_FILES shadows (minimal config) are applied at both
|
|
610
|
+
// `__dirname` — without needing source-code detection. MOUNT_NULL_ITEMS
|
|
611
|
+
// shadows (package.json → empty JSON, etc.), node_modules package shadows,
|
|
612
|
+
// and PLAYWRIGHT_CONFIG_FILES shadows (minimal config) are applied at both
|
|
607
613
|
// targets too so the protections survive regardless of which path the test
|
|
608
614
|
// resolves to.
|
|
609
615
|
//
|
|
@@ -611,7 +617,7 @@ export class TestExecutionService {
|
|
|
611
617
|
// later mounts over earlier ones, so the top-level entry mounts must be
|
|
612
618
|
// pushed BEFORE the shadow mounts that override paths inside them.
|
|
613
619
|
//
|
|
614
|
-
// The three recursive scans (findExcludedPaths × 2 +
|
|
620
|
+
// The three recursive scans (findExcludedPaths × 2 + findNodeModulesDirs)
|
|
615
621
|
// are deliberately kept as separate walks rather than fused into one pass:
|
|
616
622
|
// each skips node_modules/.git so the cost is small, and separate walks keep
|
|
617
623
|
// each shadow category's semantics independently testable.
|
|
@@ -623,11 +629,13 @@ export class TestExecutionService {
|
|
|
623
629
|
// otherwise the entry mount would win the dedup race and the repo's own
|
|
624
630
|
// config (which may import dotenv, or simply isn't the minimal/generated
|
|
625
631
|
// one) would leak into the container.
|
|
632
|
+
//
|
|
633
|
+
// node_modules is NOT filtered out (SKYR-4140): it carries the customer's
|
|
634
|
+
// test-support deps, and the per-package shadows below neutralise the only
|
|
635
|
+
// copies that must not load from the repo.
|
|
626
636
|
const entriesToMount = fs
|
|
627
637
|
.readdirSync(hostRoot)
|
|
628
|
-
.filter((file) => !
|
|
629
|
-
!MOUNT_NULL_ITEMS.includes(file) &&
|
|
630
|
-
!PLAYWRIGHT_CONFIG_FILES.includes(file));
|
|
638
|
+
.filter((file) => !MOUNT_NULL_ITEMS.includes(file) && !PLAYWRIGHT_CONFIG_FILES.includes(file));
|
|
631
639
|
for (const file of entriesToMount) {
|
|
632
640
|
const source = path.join(hostRoot, file);
|
|
633
641
|
pushMount({ Type: "bind", Target: path.join(containerRoot, file), Source: source });
|
|
@@ -644,10 +652,9 @@ export class TestExecutionService {
|
|
|
644
652
|
pushMount({ Type: "bind", Source: source, Target: absolutePath });
|
|
645
653
|
}
|
|
646
654
|
}
|
|
647
|
-
// Shadow SHADOWED_NESTED_PACKAGES inside every
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
for (const nested of findNestedNodeModules(hostRoot)) {
|
|
655
|
+
// Shadow SHADOWED_NESTED_PACKAGES inside every mounted node_modules — the
|
|
656
|
+
// repo's own top-level one included (see findNodeModulesDirs).
|
|
657
|
+
for (const nested of findNodeModulesDirs(hostRoot)) {
|
|
651
658
|
for (const pkg of SHADOWED_NESTED_PACKAGES) {
|
|
652
659
|
const pkgPath = path.join(nested, pkg);
|
|
653
660
|
if (!fs.existsSync(pkgPath))
|
|
@@ -711,6 +718,22 @@ export class TestExecutionService {
|
|
|
711
718
|
if (crossRepoTestRoot) {
|
|
712
719
|
mountRepoTree(crossRepoTestRoot, path.join(containerMountPath, TEST_REPO_CONTAINER_SUBDIR), true);
|
|
713
720
|
}
|
|
721
|
+
// runner.sh invokes `npx playwright test`, and npx resolves binaries from
|
|
722
|
+
// <cwd>/node_modules/.bin before PATH — cwd is always containerMountPath. Now
|
|
723
|
+
// that node_modules is mounted (SKYR-4140), a repo-local .bin/playwright would
|
|
724
|
+
// run instead of the image's global CLI. The per-package shadows above do not
|
|
725
|
+
// cover it under a pnpm layout, where .bin/playwright symlinks into .pnpm/
|
|
726
|
+
// rather than into node_modules/playwright. Shadow this one .bin — the only
|
|
727
|
+
// one npx can reach — so binary resolution falls through to PATH as before.
|
|
728
|
+
// Pushed after mountRepoTree so it layers over the node_modules entry mount.
|
|
729
|
+
if (fs.existsSync(path.join(workspacePath, "node_modules", ".bin"))) {
|
|
730
|
+
pushMount({
|
|
731
|
+
Type: "bind",
|
|
732
|
+
Source: EMPTY_DIR_PATH,
|
|
733
|
+
Target: path.join(containerMountPath, "node_modules", ".bin"),
|
|
734
|
+
ReadOnly: true,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
714
737
|
// Mount video output directory for browser tests (writable)
|
|
715
738
|
if (browserTest && videoHostDir) {
|
|
716
739
|
pushMount({
|
|
@@ -44,6 +44,30 @@ export function buildContainerEnv(options, saveStoragePath, hostEnv = process.en
|
|
|
44
44
|
env.push(`${key}=${shouldRewrite ? rewriteLocalhostForDocker(val) : val}`);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
// Credentials that generated UI tests read from the environment instead of
|
|
48
|
+
// carrying as literals (SKYR-3862). Without this the container never sees
|
|
49
|
+
// them and every generated login fails its guard.
|
|
50
|
+
//
|
|
51
|
+
// Forwarded on presence, not truthiness: the generated guard deliberately
|
|
52
|
+
// distinguishes an unset variable (a misconfiguration, fails fast) from one
|
|
53
|
+
// set to "" (valid on forms that accept a blank password). A truthy check —
|
|
54
|
+
// as used for the URL vars above — would drop the empty case and collapse it
|
|
55
|
+
// into the failure case.
|
|
56
|
+
//
|
|
57
|
+
// TODO(SKYR-4110): only ONE credential pair is forwarded, because testbot
|
|
58
|
+
// exports only the first credential from its `uiCredentials` input. That input
|
|
59
|
+
// was designed to carry several, so the agent can pick per test case by
|
|
60
|
+
// authorization role — so a multi-role suite can get a credential its test did
|
|
61
|
+
// not expect, surfacing as a false 403. The fix is a keyed form
|
|
62
|
+
// (SKYRAMP_UI_PASSWORD_<KEY>), which needs no new forwarding logic here: the
|
|
63
|
+
// loop above already forwards SKYRAMP_TEST_SERVICE_URL_* by prefix, so the
|
|
64
|
+
// same approach applies. Keep the presence-not-truthiness rule when it lands.
|
|
65
|
+
for (const key of ["SKYRAMP_UI_USERNAME", "SKYRAMP_UI_PASSWORD"]) {
|
|
66
|
+
const val = hostEnv[key];
|
|
67
|
+
if (val !== undefined) {
|
|
68
|
+
env.push(`${key}=${val}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
47
71
|
if (hostEnv.SKYRAMP_DEBUG) {
|
|
48
72
|
env.push(`SKYRAMP_DEBUG=${hostEnv.SKYRAMP_DEBUG}`);
|
|
49
73
|
}
|
|
@@ -275,7 +275,6 @@ function startPortForward(kubeconfig, namespace, target, remotePort) {
|
|
|
275
275
|
async function queryProtocolSamples(baseUrl, protocol, pageSize, destination) {
|
|
276
276
|
const collected = [];
|
|
277
277
|
let offset = 0;
|
|
278
|
-
// eslint-disable-next-line no-constant-condition
|
|
279
278
|
while (true) {
|
|
280
279
|
const params = new URLSearchParams({
|
|
281
280
|
protocol,
|
|
@@ -591,9 +591,18 @@ export function registerSubmitReportTool(server) {
|
|
|
591
591
|
// without it, the cheapest compliant move is a token no-op edit to the
|
|
592
592
|
// mis-claimed file, which pollutes the delivered branch and leaves the report
|
|
593
593
|
// crediting the wrong file (observed in the Ghost rc.pom-3 tester run, D-04).
|
|
594
|
+
// The two claim kinds have DIFFERENT remedies and must say so. newTestsCreated
|
|
595
|
+
// is a tool input, so it can be corrected here. A testMaintenance row is built
|
|
596
|
+
// from stateData.maintenanceVerdicts and cannot be — saying so stops the agent
|
|
597
|
+
// permuting testMaintenanceDetails, which is what it did for 15 rounds in
|
|
598
|
+
// SKYR-4129. Do NOT offer re-running skyramp_actions to restate the verdict:
|
|
599
|
+
// this branch only fires when the edit is genuinely absent, so rewriting the
|
|
600
|
+
// record to match that would be the tamper path, not the fix.
|
|
594
601
|
const changedList = changedFiles.slice(0, 20).map((f) => ` - ${f}`).join("\n");
|
|
595
602
|
errorResult = toolError(`${unbacked.length} report claim(s) are not backed by any change in the working tree — ` +
|
|
596
|
-
`Testbot will not report file work that hasn't actually been made.
|
|
603
|
+
`Testbot will not report file work that hasn't actually been made. Do NOT make a token edit to the claimed file just to satisfy this check.\n` +
|
|
604
|
+
`For a newTestsCreated claim: create the file, correct the claim's fileName to the file you actually created (see the changed files below), or remove the claim.\n` +
|
|
605
|
+
`For a testMaintenance claim: make the edit on the named file. That row is derived from the maintenance triage, not from this tool's input, so it cannot be corrected by changing what you pass here.\n` +
|
|
597
606
|
`Unbacked claims:\n${unbacked.map((u) => ` - ${u}`).join("\n")}\n` +
|
|
598
607
|
`Files with actual working-tree changes:\n${changedList || " (none)"}`);
|
|
599
608
|
return errorResult;
|
|
@@ -265,14 +265,20 @@ export function registerActionsTool(server) {
|
|
|
265
265
|
});
|
|
266
266
|
return;
|
|
267
267
|
}
|
|
268
|
-
// Resolve and validate pomFile with the same path-traversal guard as testFilePath
|
|
268
|
+
// Resolve and validate pomFile with the same path-traversal guard as testFilePath,
|
|
269
|
+
// minus that guard's `=== repoRoot` allowance: the repo root is a directory, never
|
|
270
|
+
// a page object. Admitting it would make it the edit target, and every consumer
|
|
271
|
+
// that resolves it relative to repoRoot gets the empty string — which the report's
|
|
272
|
+
// working-tree check reads as "outside this checkout, exempt", waving through an
|
|
273
|
+
// UPDATE that changed nothing. Dropping it here means pomFile falls back to the
|
|
274
|
+
// spec (`pomFile ?? testFilePath`) for both the edit and the check.
|
|
269
275
|
let resolvedPomFile;
|
|
270
276
|
if (rec.pomFile) {
|
|
271
277
|
const rawPom = path.isAbsolute(rec.pomFile)
|
|
272
278
|
? rec.pomFile
|
|
273
279
|
: path.resolve(repoRoot ?? "", rec.pomFile);
|
|
274
280
|
resolvedPomFile = path.resolve(rawPom);
|
|
275
|
-
if (repoRoot && !resolvedPomFile.startsWith(repoRoot + path.sep)
|
|
281
|
+
if (repoRoot && !resolvedPomFile.startsWith(repoRoot + path.sep)) {
|
|
276
282
|
logger.warning(`Skipping pomFile outside repo root: ${rec.pomFile}`);
|
|
277
283
|
resolvedPomFile = undefined;
|
|
278
284
|
}
|
|
@@ -315,6 +321,10 @@ export function registerActionsTool(server) {
|
|
|
315
321
|
ignoredTestFiles,
|
|
316
322
|
maintenanceVerdicts: recommendations.map((r) => ({
|
|
317
323
|
testFilePath: r.testFilePath,
|
|
324
|
+
// Persisted because the edit lands here, not in testFilePath. Dropping it
|
|
325
|
+
// left skyramp_submit_report unable to tell a POM-backed UPDATE from a
|
|
326
|
+
// fabricated one, so it rejected real maintenance work (SKYR-4129).
|
|
327
|
+
pomFile: r.pomFile,
|
|
318
328
|
action: r.action,
|
|
319
329
|
rationale: r.rationale,
|
|
320
330
|
testType: r.testType,
|
|
@@ -34,12 +34,16 @@ export interface MaintenanceActionCore {
|
|
|
34
34
|
testType: TestType;
|
|
35
35
|
endpoint: string;
|
|
36
36
|
rationale: string;
|
|
37
|
+
/** When selectors are abstracted into a page object, pomFile is the POM path to edit.
|
|
38
|
+
* testFilePath remains the spec (for catalog lookup and report); pomFile is what gets
|
|
39
|
+
* patched. Part of the core shape because the EXECUTED file (the spec, which carries the
|
|
40
|
+
* execution records) and the EDITED file (the POM) are structurally different, and every
|
|
41
|
+
* downstream stage that reasons about the edit needs both — notably the report-time
|
|
42
|
+
* working-tree check, which otherwise calls a POM-backed UPDATE unbacked (SKYR-4129). */
|
|
43
|
+
pomFile?: string;
|
|
37
44
|
}
|
|
38
45
|
/** Normalized internal recommendation built from LLM-supplied args.recommendations. */
|
|
39
46
|
export interface DriftRecommendation extends MaintenanceActionCore {
|
|
40
|
-
/** When selectors are abstracted into a page object, pomFile is the POM path to edit.
|
|
41
|
-
* testFilePath remains the spec (for catalog lookup and report); pomFile is what gets patched. */
|
|
42
|
-
pomFile?: string;
|
|
43
47
|
priority: RecommendationPriority;
|
|
44
48
|
estimatedWork: EstimatedWork;
|
|
45
49
|
updateInstructions: string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Service } from "../workspace/workspace.js";
|
|
2
|
+
/** One related repository to analyze as shared context in multi-repo mode. */
|
|
3
|
+
export interface RelatedRepository {
|
|
4
|
+
repositoryPath: string;
|
|
5
|
+
baseBranch?: string;
|
|
6
|
+
/** `owner/repo` slug, resolved by the action from the checkout's git remote.
|
|
7
|
+
* Used verbatim as the `repository` argument/attribution for this repo so the
|
|
8
|
+
* agent never has to infer it. */
|
|
9
|
+
repo: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Options accepted by getTestbotPrompt (src/prompts/testbot/testbot-prompts.ts).
|
|
13
|
+
* Shared here — rather than declared inline in the prompt module — so testbot.git
|
|
14
|
+
* can import this exact type instead of hand-maintaining its own mirror.
|
|
15
|
+
*/
|
|
16
|
+
export interface TestbotPromptOptions {
|
|
17
|
+
prTitle: string;
|
|
18
|
+
prDescription: string;
|
|
19
|
+
summaryOutputFile: string;
|
|
20
|
+
repositoryPath: string;
|
|
21
|
+
baseBranch?: string;
|
|
22
|
+
maxRecommendations?: number;
|
|
23
|
+
maxGenerate?: number;
|
|
24
|
+
/** Reserved — accepted for API compat but not yet wired into the prompt. */
|
|
25
|
+
maxCritical?: number;
|
|
26
|
+
prNumber?: number;
|
|
27
|
+
userPrompt?: string;
|
|
28
|
+
services?: Service[];
|
|
29
|
+
uiCredentials?: string;
|
|
30
|
+
testsRepoDir?: string;
|
|
31
|
+
relatedRepositories?: RelatedRepository[];
|
|
32
|
+
primaryRepo?: string;
|
|
33
|
+
planOnly?: boolean;
|
|
34
|
+
language?: string;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/build/types/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { TestType, HttpMethod } from "./TestTypes.js";
|
|
|
4
4
|
export type { TestbotReport } from "./TestbotReport.js";
|
|
5
5
|
export { ReuseDeclinedBy, ReuseVerificationOutcome } from "./ReuseOutcome.js";
|
|
6
6
|
export type { ReuseOutcome, ReuseSkippedEntry } from "./ReuseOutcome.js";
|
|
7
|
+
export type { RelatedRepository, TestbotPromptOptions, } from "./TestbotPromptOptions.js";
|
|
@@ -74,7 +74,6 @@ export function extractChangedSelectors(fileDiff) {
|
|
|
74
74
|
// `data-testid="x"`, `data-testid={'x'}`, `data-testid={`x`}`. The closing delimiter
|
|
75
75
|
// deliberately rejects interpolated template literals (`data-testid={`members-${id}`}`),
|
|
76
76
|
// which would otherwise leak a partial prefix (`members-`) into the selector set.
|
|
77
|
-
// eslint-disable-next-line no-useless-escape
|
|
78
77
|
const ATTR = /\bdata-(?:test-?id|test|cy|qa)\b\s*=\s*\{?\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/g;
|
|
79
78
|
const GET_BY_TEST_ID = /getByTestId\(\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/g;
|
|
80
79
|
const KEBAB = /[a-z][a-z0-9]*(?:-{1,2}[a-z0-9]+)+/g;
|