@skyramp/mcp 0.2.1-rc.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/build/playwright/registerPlaywrightTools.js +10 -0
  2. package/build/prompts/test-maintenance/drift-analysis-prompt.js +98 -87
  3. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +92 -60
  4. package/build/prompts/test-maintenance/driftAnalysisSections.js +139 -197
  5. package/build/prompts/test-recommendation/scopeAssessment.js +106 -5
  6. package/build/prompts/test-recommendation/scopeAssessment.test.js +128 -1
  7. package/build/prompts/testbot/testbot-prompts.js +6 -9
  8. package/build/prompts/testbot/testbot-prompts.test.js +38 -22
  9. package/build/services/TestDiscoveryService.js +39 -9
  10. package/build/tools/test-management/actionsTool.js +166 -148
  11. package/build/tools/test-management/analyzeChangesTool.js +10 -12
  12. package/build/tools/test-management/analyzeTestHealthTool.js +10 -22
  13. package/build/tools/test-management/uiAnalyzeChangesTool.js +8 -2
  14. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +47 -0
  15. package/build/utils/dartRouteExtractor.js +319 -0
  16. package/build/utils/dartRouteExtractor.test.js +307 -0
  17. package/build/utils/docker.test.js +1 -1
  18. package/build/utils/uiPageEnumerator.js +67 -0
  19. package/build/utils/uiPageEnumerator.test.js +222 -0
  20. package/build/utils/versions.js +1 -1
  21. package/node_modules/playwright/lib/mcp/skyramp/assertApiRequestTool.js +46 -0
  22. package/node_modules/playwright/lib/mcp/skyramp/index.js +10 -0
  23. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +313 -0
  24. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +146 -0
  25. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +519 -52
  26. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +32 -14
  27. package/package.json +2 -2
  28. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +0 -261
  29. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  30. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  31. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  32. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var loadTraceTool_exports = {};
20
+ __export(loadTraceTool_exports, {
21
+ decodeModifiers: () => decodeModifiers,
22
+ describeStep: () => describeStep,
23
+ describeStopReason: () => describeStopReason,
24
+ listStepsFrom: () => listStepsFrom,
25
+ loadTraceMcpTool: () => loadTraceMcpTool,
26
+ loadTraceSchema: () => loadTraceSchema,
27
+ replayActions: () => replayActions,
28
+ urlMatchesPattern: () => urlMatchesPattern
29
+ });
30
+ module.exports = __toCommonJS(loadTraceTool_exports);
31
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
32
+ var import_utils = require("playwright-core/lib/utils");
33
+ var import_tool = require("../sdk/tool");
34
+ const loadTraceSchema = {
35
+ name: "skyramp_load_trace",
36
+ title: "Load and replay a Skyramp trace",
37
+ description: [
38
+ "Load a previously recorded Skyramp trace (.zip or .jsonl/.txt) and replay its actions against the live browser to restore state, then continue recording from that point.",
39
+ "The replayed actions are merged into the current recording, so subsequent browser_* steps append to them and skyramp_export_zip writes a combined trace.",
40
+ "STOPPING EARLY: when the user wants to stop partway through the loaded trace before continuing, set ONE of stopAtStep / stopAtUrl / stopBefore based on their prompt. Omit all three to replay the whole trace.",
41
+ "- stopAtStep: replay steps 1..N then stop (N is 1-based, from the numbered step list).",
42
+ "- stopAtUrl: stop once the active page URL matches this glob/regex/substring (checked after each action).",
43
+ '- stopBefore: stop right BEFORE the first action whose description contains this text (case-insensitive), e.g. "Checkout" to stop before clicking Checkout.',
44
+ "If unsure where to stop, first call with dryRun:true to read the numbered step list, then call again with the chosen stop parameter."
45
+ ].join(" "),
46
+ inputSchema: import_mcpBundle.z.object({
47
+ path: import_mcpBundle.z.string().describe("Absolute path to the trace file (.zip, .jsonl, or .txt)."),
48
+ stopAtStep: import_mcpBundle.z.number().int().positive().optional().describe("Replay steps 1..N (1-based), then stop before continuing."),
49
+ stopAtUrl: import_mcpBundle.z.string().optional().describe("Stop once the active page URL matches this glob/regex/substring."),
50
+ stopBefore: import_mcpBundle.z.string().optional().describe("Stop before the first action whose description contains this text (case-insensitive)."),
51
+ dryRun: import_mcpBundle.z.boolean().optional().describe("Parse and list the steps WITHOUT replaying. Use to choose a stop point."),
52
+ speed: import_mcpBundle.z.enum(["fast", "slow"]).optional().describe('Replay speed. "slow" adds a 1s delay between actions (default "fast").')
53
+ }),
54
+ // Replaying drives the live browser and mutates the recording, so this is an
55
+ // action tool (readOnlyHint:false / destructiveHint:true), not readOnly.
56
+ type: "action"
57
+ };
58
+ function loadTraceMcpTool() {
59
+ return (0, import_tool.toMcpTool)(loadTraceSchema);
60
+ }
61
+ function decodeModifiers(mask) {
62
+ if (!mask)
63
+ return [];
64
+ const out = [];
65
+ if (mask & 1) out.push("Alt");
66
+ if ((mask & 6) === 6) {
67
+ out.push("ControlOrMeta");
68
+ } else {
69
+ if (mask & 2) out.push("Control");
70
+ if (mask & 4) out.push("Meta");
71
+ }
72
+ if (mask & 8) out.push("Shift");
73
+ return out;
74
+ }
75
+ const METADATA_ONLY_ACTIONS = /* @__PURE__ */ new Set([
76
+ "marker",
77
+ "comment",
78
+ "beginBlock",
79
+ "endBlock",
80
+ "openPage",
81
+ "closePage",
82
+ "modalOpen",
83
+ "modalClose",
84
+ "iframeLoad",
85
+ "visualSnapshot",
86
+ "tableSnapshot",
87
+ "domSnapshot",
88
+ "selectArea",
89
+ "penTool",
90
+ "assertApiRequest"
91
+ ]);
92
+ function describeStep(action, index) {
93
+ const a = action.action;
94
+ const onPage = action.frame.pageAlias && action.frame.pageAlias !== "page" ? ` [${action.frame.pageAlias}]` : "";
95
+ let detail = "";
96
+ if (a.name === "navigate")
97
+ detail = a.url;
98
+ else if (a.selector)
99
+ detail = (0, import_utils.asLocator)("javascript", a.selector);
100
+ if ((a.name === "fill" || a.name === "assertText") && a.text !== void 0)
101
+ detail += ` = ${JSON.stringify(a.text)}`;
102
+ else if (a.name === "assertValue" && a.value !== void 0)
103
+ detail += ` = ${JSON.stringify(a.value)}`;
104
+ else if (a.name === "press" && a.key !== void 0)
105
+ detail += ` ${a.key}`;
106
+ return `#${index + 1} ${a.name}${onPage}${detail ? ` ${detail}` : ""}`;
107
+ }
108
+ function listStepsFrom(allActions, fromIndex) {
109
+ if (fromIndex >= allActions.length)
110
+ return "(none remaining)";
111
+ return allActions.slice(fromIndex).map((a, i) => describeStep(a, fromIndex + i)).join("\n");
112
+ }
113
+ const ENTER_FRAME = " >> internal:control=enter-frame >> ";
114
+ function buildFullSelector(framePath, selector) {
115
+ return [...framePath ?? [], selector].join(ENTER_FRAME);
116
+ }
117
+ function describeStopReason(result) {
118
+ switch (result.stopReason) {
119
+ case "done":
120
+ return "Reached the end of the trace.";
121
+ case "stopAtStep":
122
+ return `Stopped at the requested step (${result.completedCount}).`;
123
+ case "stopAtUrl":
124
+ return "Stopped because the active page URL matched the requested pattern.";
125
+ case "stopBefore":
126
+ return "Stopped just before the requested action.";
127
+ case "error":
128
+ return "Stopped because an action failed.";
129
+ }
130
+ }
131
+ async function replayActions(actionsList, callbacks, params) {
132
+ const delay = params.speed === "slow" ? 1e3 : 0;
133
+ const stopBefore = params.stopBefore?.toLowerCase();
134
+ let completedCount = 0;
135
+ for (let i = 0; i < actionsList.length; i++) {
136
+ const action = actionsList[i];
137
+ if (stopBefore && describeStep(action, i).toLowerCase().includes(stopBefore))
138
+ return { completedCount, stopIndex: i, stopReason: "stopBefore" };
139
+ const alias = action.frame.pageAlias || "page";
140
+ const page = callbacks.pageForAlias(alias);
141
+ if (!METADATA_ONLY_ACTIONS.has(action.action.name)) {
142
+ if (!page) {
143
+ return {
144
+ completedCount,
145
+ stopIndex: i,
146
+ stopReason: "error",
147
+ error: { message: `No live page for alias "${alias}"`, actionName: action.action.name, stepIndex: i + 1 }
148
+ };
149
+ }
150
+ try {
151
+ await performClientAction(page, action);
152
+ } catch (e) {
153
+ return {
154
+ completedCount,
155
+ stopIndex: i,
156
+ stopReason: "error",
157
+ error: { message: e.message, actionName: action.action.name, stepIndex: i + 1 }
158
+ };
159
+ }
160
+ }
161
+ completedCount = i + 1;
162
+ if (params.stopAtStep !== void 0 && completedCount >= params.stopAtStep)
163
+ return { completedCount, stopIndex: i + 1, stopReason: "stopAtStep" };
164
+ if (params.stopAtUrl) {
165
+ const currentUrl = page?.url() ?? "";
166
+ if (urlMatchesPattern(currentUrl, params.stopAtUrl))
167
+ return { completedCount, stopIndex: i + 1, stopReason: "stopAtUrl" };
168
+ }
169
+ if (delay)
170
+ await new Promise((resolve) => setTimeout(resolve, delay));
171
+ }
172
+ return { completedCount, stopIndex: actionsList.length, stopReason: "done" };
173
+ }
174
+ function urlMatchesPattern(currentUrl, pattern) {
175
+ const regexLiteral = /^\/(.*)\/([a-z]*)$/i.exec(pattern);
176
+ if (regexLiteral) {
177
+ try {
178
+ return new RegExp(regexLiteral[1], regexLiteral[2]).test(currentUrl);
179
+ } catch {
180
+ }
181
+ }
182
+ if (/[*{]/.test(pattern))
183
+ return (0, import_utils.urlMatches)(void 0, currentUrl, pattern);
184
+ return currentUrl.toLowerCase().includes(pattern.toLowerCase());
185
+ }
186
+ const DEFAULT_ACTION_TIMEOUT = 15e3;
187
+ async function performClientAction(page, actionInContext) {
188
+ const action = actionInContext.action;
189
+ const opts = { timeout: DEFAULT_ACTION_TIMEOUT };
190
+ if (action.name === "navigate") {
191
+ await page.goto(action.url, { ...opts, waitUntil: "domcontentloaded" });
192
+ return;
193
+ }
194
+ if (action.name === "waitForTimeout") {
195
+ await new Promise((resolve) => setTimeout(resolve, action.duration ?? 0));
196
+ return;
197
+ }
198
+ if (action.name === "waitForSelector") {
199
+ const state = action.state === "hidden" ? "hidden" : "visible";
200
+ const waitSelector = buildFullSelector(actionInContext.frame.framePath, action.selector);
201
+ await page.locator(waitSelector).first().waitFor({ state, ...opts }).catch(() => {
202
+ });
203
+ return;
204
+ }
205
+ if (action.name === "dragAndDrop" || action.name === "dragTo") {
206
+ const fp = actionInContext.frame.framePath;
207
+ const sourceSel = buildFullSelector(fp, action.source ?? action.selector);
208
+ const targetSel = buildFullSelector(fp, action.target ?? action.targetSelector);
209
+ await page.locator(sourceSel).dragTo(page.locator(targetSel), opts);
210
+ return;
211
+ }
212
+ if (action.name === "press" && !action.selector) {
213
+ const modifiers = decodeModifiers(action.modifiers);
214
+ const shortcut = [...modifiers, action.key].join("+");
215
+ await page.keyboard.press(shortcut);
216
+ return;
217
+ }
218
+ if (!action.selector)
219
+ return;
220
+ const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
221
+ const locator = page.locator(selector);
222
+ switch (action.name) {
223
+ case "click": {
224
+ const clickOpts = { ...opts };
225
+ if (action.button && action.button !== "left")
226
+ clickOpts.button = action.button;
227
+ if (action.clickCount && action.clickCount > 1)
228
+ clickOpts.clickCount = action.clickCount;
229
+ if (action.position)
230
+ clickOpts.position = action.position;
231
+ const modifiers = decodeModifiers(action.modifiers);
232
+ if (modifiers.length)
233
+ clickOpts.modifiers = modifiers;
234
+ await locator.click(clickOpts);
235
+ return;
236
+ }
237
+ case "hover":
238
+ await locator.hover({ ...opts, ...action.position ? { position: action.position } : {} });
239
+ return;
240
+ case "fill":
241
+ await locator.fill(action.text ?? "", opts);
242
+ return;
243
+ case "pressSequentially":
244
+ await locator.pressSequentially(action.text ?? "", opts);
245
+ return;
246
+ case "press": {
247
+ const modifiers = decodeModifiers(action.modifiers);
248
+ const shortcut = [...modifiers, action.key].join("+");
249
+ await locator.press(shortcut, opts);
250
+ return;
251
+ }
252
+ case "check":
253
+ await locator.check(opts);
254
+ return;
255
+ case "uncheck":
256
+ await locator.uncheck(opts);
257
+ return;
258
+ case "select":
259
+ await locator.selectOption(action.options ?? [], opts);
260
+ return;
261
+ case "setInputFiles":
262
+ await locator.setInputFiles(action.files ?? [], opts);
263
+ return;
264
+ case "fileChooser": {
265
+ const fileChooserPromise = page.waitForEvent("filechooser");
266
+ await locator.click(opts);
267
+ const fileChooser = await fileChooserPromise;
268
+ await fileChooser.setFiles(action.files ?? []);
269
+ return;
270
+ }
271
+ // Assertions: re-run against the live page so a drifted trace fails loudly
272
+ // (faithful to the manual recorder's replay).
273
+ case "assertText": {
274
+ const actual = await locator.textContent(opts) ?? "";
275
+ const substring = action.substring !== false;
276
+ const ok = substring ? actual.includes(action.text) : actual.trim() === String(action.text).trim();
277
+ if (!ok)
278
+ throw new Error(`assertText failed: expected text to ${substring ? "contain" : "equal"} ${JSON.stringify(action.text)}, got ${JSON.stringify(actual)}`);
279
+ return;
280
+ }
281
+ case "assertValue": {
282
+ const actual = await locator.inputValue(opts);
283
+ if (actual !== action.value)
284
+ throw new Error(`assertValue failed: expected ${JSON.stringify(action.value)}, got ${JSON.stringify(actual)}`);
285
+ return;
286
+ }
287
+ case "assertChecked": {
288
+ const checked = await locator.isChecked(opts);
289
+ if (checked !== !!action.checked)
290
+ throw new Error(`assertChecked failed: expected checked=${!!action.checked}, got ${checked}`);
291
+ return;
292
+ }
293
+ case "assertVisible": {
294
+ const visible = await locator.isVisible(opts);
295
+ if (!visible)
296
+ throw new Error(`assertVisible failed: element is not visible`);
297
+ return;
298
+ }
299
+ default:
300
+ return;
301
+ }
302
+ }
303
+ // Annotate the CommonJS export names for ESM import in node:
304
+ 0 && (module.exports = {
305
+ decodeModifiers,
306
+ describeStep,
307
+ describeStopReason,
308
+ listStepsFrom,
309
+ loadTraceMcpTool,
310
+ loadTraceSchema,
311
+ replayActions,
312
+ urlMatchesPattern
313
+ });
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var skyRampImport_exports = {};
30
+ __export(skyRampImport_exports, {
31
+ actionsFromFile: () => actionsFromFile,
32
+ actionsFromPath: () => actionsFromPath,
33
+ actionsFromZip: () => actionsFromZip,
34
+ parseJsonl: () => parseJsonl
35
+ });
36
+ module.exports = __toCommonJS(skyRampImport_exports);
37
+ var import_fs = __toESM(require("fs"));
38
+ var import_utils = require("playwright-core/lib/utils");
39
+ const SKYRAMP_ACTIVITIES_FILE = "skyramp_playwright.txt";
40
+ const ACTION_NAMES = /* @__PURE__ */ new Set([
41
+ "marker",
42
+ "comment",
43
+ "beginBlock",
44
+ "endBlock",
45
+ "check",
46
+ "click",
47
+ "hover",
48
+ "closePage",
49
+ "fill",
50
+ "pressSequentially",
51
+ "navigate",
52
+ "openPage",
53
+ "press",
54
+ "select",
55
+ "uncheck",
56
+ "setInputFiles",
57
+ "assertText",
58
+ "assertValue",
59
+ "assertChecked",
60
+ "assertVisible",
61
+ "assertSnapshot",
62
+ "visualSnapshot",
63
+ "assertTableCell",
64
+ "dragTo",
65
+ "dragAndDrop",
66
+ "diagramNodeAdd",
67
+ "diagramLinkAdd",
68
+ "selectArea",
69
+ "tableSnapshot",
70
+ "domSnapshot",
71
+ "fileChooser",
72
+ "penTool",
73
+ "mouse.wheel",
74
+ "mouse.move",
75
+ "mouse.down",
76
+ "mouse.up",
77
+ "modalOpen",
78
+ "modalClose",
79
+ "iframeLoad",
80
+ "assertApiRequest",
81
+ "waitForSelector",
82
+ "waitForTimeout"
83
+ ]);
84
+ function isActionName(name) {
85
+ return ACTION_NAMES.has(name);
86
+ }
87
+ async function actionsFromZip(zipPath) {
88
+ const zip = new import_utils.ZipFile(zipPath);
89
+ try {
90
+ const entries = await zip.entries();
91
+ const entry = entries.find((e) => e === SKYRAMP_ACTIVITIES_FILE) ?? entries.find((e) => e.endsWith(".jsonl")) ?? entries.find((e) => e.endsWith(".txt"));
92
+ if (!entry)
93
+ throw new Error(`No activities file found in ${zipPath}. Expected '${SKYRAMP_ACTIVITIES_FILE}' or a .jsonl/.txt file.`);
94
+ const buffer = await zip.read(entry);
95
+ return parseJsonl(buffer.toString("utf-8"));
96
+ } finally {
97
+ zip.close();
98
+ }
99
+ }
100
+ function actionsFromFile(jsonlPath) {
101
+ return parseJsonl(import_fs.default.readFileSync(jsonlPath, "utf-8"));
102
+ }
103
+ async function actionsFromPath(tracePath) {
104
+ return tracePath.endsWith(".zip") ? await actionsFromZip(tracePath) : actionsFromFile(tracePath);
105
+ }
106
+ function parseJsonl(content) {
107
+ const result = [];
108
+ for (const line of content.split("\n")) {
109
+ const trimmed = line.trim();
110
+ if (!trimmed)
111
+ continue;
112
+ const action = parseLine(trimmed);
113
+ if (action)
114
+ result.push(action);
115
+ }
116
+ return result;
117
+ }
118
+ function parseLine(line) {
119
+ let entry;
120
+ try {
121
+ entry = JSON.parse(line);
122
+ } catch {
123
+ return null;
124
+ }
125
+ if (!entry.name || !isActionName(entry.name))
126
+ return null;
127
+ const { pageGuid, pageAlias, framePath, locator: _locator, ...actionFields } = entry;
128
+ const frame = {
129
+ pageGuid: pageGuid ?? "",
130
+ pageAlias: pageAlias ?? "page",
131
+ framePath: framePath ?? []
132
+ };
133
+ const ts = typeof entry.timestamp === "number" ? entry.timestamp : typeof entry.timestamp === "string" && /^\d+$/.test(entry.timestamp.trim()) ? Number(entry.timestamp.trim()) : Date.now();
134
+ return {
135
+ frame,
136
+ action: actionFields,
137
+ startTime: ts
138
+ };
139
+ }
140
+ // Annotate the CommonJS export names for ESM import in node:
141
+ 0 && (module.exports = {
142
+ actionsFromFile,
143
+ actionsFromPath,
144
+ actionsFromZip,
145
+ parseJsonl
146
+ });