@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
@@ -226,7 +226,7 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
226
226
  return null;
227
227
  }
228
228
  if (toolName === "browser_press_key")
229
- return JSON.stringify({ name: "press", key: args.key, selector: "", ...base });
229
+ return JSON.stringify({ name: "press", key: args.key, modifiers: modifiersArrayToMask(args.modifiers), selector: "", ...base });
230
230
  if (!code)
231
231
  return null;
232
232
  const extracted = extractLocatorFromCode(code);
@@ -304,23 +304,34 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
304
304
  signals: [],
305
305
  timestamp: String(timestamp),
306
306
  pageGuid,
307
- pageAlias: DEFAULT_PAGE_ALIAS,
308
- framePath: DEFAULT_FRAME_PATH
307
+ pageAlias: action.pageAlias ?? DEFAULT_PAGE_ALIAS,
308
+ framePath: action.framePath ?? DEFAULT_FRAME_PATH
309
309
  };
310
- const parts = code.split(":");
311
- const selectorPart = parts[1] || "";
312
310
  let selector = "";
313
311
  let expected = "";
314
312
  let substring = true;
315
- if (assertType === "visible") {
316
- selector = parts.slice(1).join(":");
317
- } else if (assertType === "text") {
318
- substring = parts[parts.length - 1] === "true";
319
- expected = parts[parts.length - 2] || "";
320
- selector = parts.slice(1, parts.length - 2).join(":");
321
- } else if (assertType === "value") {
322
- expected = parts[parts.length - 1] || "";
323
- selector = parts.slice(1, parts.length - 1).join(":");
313
+ if (args.selector !== void 0) {
314
+ selector = args.selector;
315
+ if (assertType === "text") {
316
+ expected = args.expected ?? "";
317
+ substring = args.substring !== false;
318
+ } else if (assertType === "value") {
319
+ expected = args.expected ?? "";
320
+ } else if (assertType === "checked") {
321
+ expected = String(args.checked === true || args.expected === "true" || args.expected === true);
322
+ }
323
+ } else {
324
+ const parts = code.split(":");
325
+ if (assertType === "visible") {
326
+ selector = parts.slice(1).join(":");
327
+ } else if (assertType === "text") {
328
+ substring = parts[parts.length - 1] === "true";
329
+ expected = parts[parts.length - 2] || "";
330
+ selector = parts.slice(1, parts.length - 2).join(":");
331
+ } else if (assertType === "value" || assertType === "checked") {
332
+ expected = parts[parts.length - 1] || "";
333
+ selector = parts.slice(1, parts.length - 1).join(":");
334
+ }
324
335
  }
325
336
  const locator = selectorToLocator(selector);
326
337
  switch (assertType) {
@@ -330,6 +341,8 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
330
341
  return JSON.stringify({ name: "assertVisible", selector, locator, ...base });
331
342
  case "value":
332
343
  return JSON.stringify({ name: "assertValue", selector, value: expected, locator, ...base });
344
+ case "checked":
345
+ return JSON.stringify({ name: "assertChecked", selector, checked: expected === "true", locator, ...base });
333
346
  default:
334
347
  return null;
335
348
  }
@@ -495,6 +508,11 @@ function buildJsonlContent(actions, browserName, harPath) {
495
508
  }
496
509
  continue;
497
510
  }
511
+ if (action.toolName === "browser_assert_api_request") {
512
+ lines.push(JSON.stringify({ name: "assertApiRequest", signals: [], timestamp: String(action.timestamp), pageGuid, pageAlias: alias, framePath: DEFAULT_FRAME_PATH }));
513
+ actionCount++;
514
+ continue;
515
+ }
498
516
  if ((action.toolName === "browser_type" || action.toolName === "browser_press_sequentially") && action.args.submit) {
499
517
  const fillLine = trackedActionToJsonl(action, pageGuid, action.timestamp);
500
518
  if (fillLine) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.2.1-rc.1",
3
+ "version": "0.2.2",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@modelcontextprotocol/sdk": "^1.24.3",
57
57
  "@playwright/test": "^1.55.0",
58
- "@skyramp/skyramp": "1.3.25",
58
+ "@skyramp/skyramp": "1.3.26",
59
59
  "dockerode": "^5.0.0",
60
60
  "fast-glob": "^3.3.3",
61
61
  "js-yaml": "^4.1.1",
@@ -1,261 +0,0 @@
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 domAnalyzer_exports = {};
30
- __export(domAnalyzer_exports, {
31
- default: () => domAnalyzer_default
32
- });
33
- module.exports = __toCommonJS(domAnalyzer_exports);
34
- var crypto = __toESM(require("crypto"));
35
- var fs = __toESM(require("fs"));
36
- var import_mcpBundle = require("playwright-core/lib/mcpBundle");
37
- var import_tool = require("./tool");
38
- var import_crawler = require("../../../dom-analyzer/crawler");
39
- var import_blueprint = require("../../../dom-analyzer/blueprint");
40
- var import_serialization = require("../../../dom-analyzer/serialization");
41
- function hashStorageState(path) {
42
- if (!path)
43
- return null;
44
- try {
45
- const contents = fs.readFileSync(path, "utf-8");
46
- return crypto.createHash("sha256").update(contents).digest("hex").slice(0, 16);
47
- } catch {
48
- const pathHash = crypto.createHash("sha256").update(path).digest("hex").slice(0, 16);
49
- return `unreadable:${pathHash}`;
50
- }
51
- }
52
- function cacheKey(entryUrl, storageStateHash, probeButtons) {
53
- return `${entryUrl}::${storageStateHash ?? "no-auth"}::${probeButtons}`;
54
- }
55
- function humanDuration(ms) {
56
- if (ms < 6e4)
57
- return `${Math.round(ms / 1e3)}s`;
58
- return `${Math.round(ms / 6e4)}m`;
59
- }
60
- const sitemapBuild = (0, import_tool.defineTool)({
61
- capability: "core",
62
- schema: {
63
- name: "browser_sitemap_build",
64
- title: "Build Sitemap (graph of PageBlueprints)",
65
- description: [
66
- "Crawl an application starting from a URL and build a Sitemap \u2014 a graph of PageBlueprints (one per URL)",
67
- "connected by navigation edges. The Sitemap is cached in the session.",
68
- "",
69
- "Call once at the start of a session. Subsequent calls within the TTL (~30 minutes) reuse the cached",
70
- "Sitemap unless `refresh: true` is passed. Use `browser_sitemap_query` to read already-crawled pages",
71
- "rather than re-calling this tool.",
72
- "",
73
- "Depth defaults to 5, maxPages to 50 (the real bound in practice).",
74
- "",
75
- '\u26A0\uFE0F SAFETY: By default, `probeButtons: "immutable-only"` skips destructive-looking buttons',
76
- "(Delete / Submit / Place Order / etc.) and buttons inside forms to prevent side effects during",
77
- 'crawling. Use `probeButtons: "all"` only against dev / staging environments \u2014 it will click every',
78
- "unique button, which can submit forms, create records, send notifications, or mutate server state."
79
- ].join("\n"),
80
- inputSchema: import_mcpBundle.z.object({
81
- url: import_mcpBundle.z.string().describe("Entry URL to start crawling from"),
82
- depth: import_mcpBundle.z.number().optional().default(5).describe("Max crawl depth (default: 5)"),
83
- maxPages: import_mcpBundle.z.number().optional().default(50).describe("Max pages to visit (default: 50)"),
84
- sameOriginOnly: import_mcpBundle.z.boolean().optional().default(true).describe("Only follow same-origin links (default: true)"),
85
- probeButtons: import_mcpBundle.z.enum(["immutable-only", "all", "none"]).optional().default("immutable-only").describe('Button-probing safety mode. Default: immutable-only (safe; skips destructive-verb buttons and buttons inside forms). Use "all" only against dev/staging \u2014 probes every button. "none" disables button probing entirely.'),
86
- playwrightStoragePath: import_mcpBundle.z.string().optional().describe("Path to a Playwright storageState.json file \u2014 cookies, localStorage, sessionStorage per origin. Use this to crawl apps behind a login."),
87
- refresh: import_mcpBundle.z.boolean().optional().default(false).describe("Force a full re-crawl, bypassing the cache")
88
- }),
89
- type: "readOnly"
90
- },
91
- handle: async (context, params, response) => {
92
- const { url, depth, maxPages, sameOriginOnly, probeButtons, playwrightStoragePath, refresh } = params;
93
- const normalizedUrl = (0, import_serialization.normalizeUrl)(url);
94
- const storageStateHash = hashStorageState(playwrightStoragePath);
95
- const probeMode = probeButtons;
96
- const key = cacheKey(normalizedUrl, storageStateHash, probeMode);
97
- const cached = context.sitemapCache.get(key);
98
- if (cached && !refresh) {
99
- const age = Date.now() - new Date(cached.sitemap.cachedAt).getTime();
100
- if (age < import_serialization.CRAWL_TTL_MS) {
101
- const pageCount2 = Object.keys(cached.sitemap.pages).length;
102
- response.addTextResult(
103
- `Reusing cached Sitemap from ${humanDuration(age)} ago.
104
- Entry: ${cached.sitemap.entryUrl}
105
- Pages: ${pageCount2}
106
- Edges: ${cached.sitemap.edges.length}
107
- Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
108
- Probe mode: ${probeMode}
109
-
110
- Use browser_sitemap_query to read any page. Pass refresh=true to recrawl.`
111
- );
112
- return;
113
- }
114
- }
115
- let sitemap;
116
- if (depth > 0) {
117
- sitemap = await (0, import_crawler.crawl)(url, {
118
- depth,
119
- maxPages,
120
- sameOriginOnly,
121
- probeButtons: probeMode,
122
- playwrightStoragePath
123
- });
124
- } else {
125
- sitemap = await (0, import_crawler.crawlSinglePage)(url, { playwrightStoragePath });
126
- }
127
- context.sitemapCache.set(key, { sitemap, storageStateHash, probeButtonsMode: probeMode });
128
- const pageCount = Object.keys(sitemap.pages).length;
129
- const pageList = Object.keys(sitemap.pages).map((u) => {
130
- const bp = sitemap.pages[u];
131
- const elCount = bp.sections.reduce((sum, s) => sum + s.elements.length + s.repeatingElements.length, 0);
132
- return ` - ${u} (${elCount} elements across ${bp.sections.length} sections)`;
133
- }).join("\n");
134
- response.addTextResult(
135
- `Sitemap built for ${sitemap.entryUrl}
136
- Pages: ${pageCount}
137
- Edges: ${sitemap.edges.length}
138
- Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
139
- Probe mode: ${probeMode}
140
-
141
- Pages:
142
- ${pageList}
143
-
144
- Use browser_sitemap_query to read page blueprints, edges, or derived views (mapJson, outline).`
145
- );
146
- }
147
- });
148
- const sitemapQuery = (0, import_tool.defineTool)({
149
- capability: "core",
150
- schema: {
151
- name: "browser_sitemap_query",
152
- title: "Query the cached Sitemap",
153
- description: [
154
- "Query the Sitemap cached by browser_sitemap_build. Use this instead of re-calling sitemap_build.",
155
- "",
156
- "Modes:",
157
- " page \u2014 returns the full canonical PageBlueprint for the given URL",
158
- " (sections with enrichment, logical names, XPaths)",
159
- " edges \u2014 returns navigation edges originating from the given URL",
160
- " mapJson \u2014 returns the derived flat logicalName \u2192 xpath map for the URL",
161
- " outline \u2014 returns the derived textual section-to-element hierarchy for the URL",
162
- " overview \u2014 (when url is omitted) returns page list + edge list summary"
163
- ].join("\n"),
164
- inputSchema: import_mcpBundle.z.object({
165
- type: import_mcpBundle.z.enum(["page", "edges", "mapJson", "outline"]).optional().describe("Query mode. Omit with url to get overview."),
166
- url: import_mcpBundle.z.string().optional().describe("Page URL to query. Omit to get overview across all pages.")
167
- }),
168
- type: "readOnly"
169
- },
170
- handle: async (context, params, response) => {
171
- if (context.sitemapCache.size === 0) {
172
- response.addError("No Sitemap available. Run browser_sitemap_build first.");
173
- return;
174
- }
175
- const latest = [...context.sitemapCache.values()].sort(
176
- (a, b) => new Date(b.sitemap.cachedAt).getTime() - new Date(a.sitemap.cachedAt).getTime()
177
- )[0];
178
- const sitemap = latest.sitemap;
179
- const { type, url } = params;
180
- if (!url || !type) {
181
- const overview = {
182
- schemaVersion: sitemap.schemaVersion,
183
- entryUrl: sitemap.entryUrl,
184
- cachedAt: sitemap.cachedAt,
185
- pages: Object.keys(sitemap.pages).map((u) => ({
186
- url: u,
187
- sections: sitemap.pages[u].sections.length,
188
- elements: sitemap.pages[u].sections.reduce(
189
- (s, sec) => s + sec.elements.length + sec.repeatingElements.length,
190
- 0
191
- ),
192
- pageHash: sitemap.pages[u].pageHash
193
- })),
194
- edges: sitemap.edges
195
- };
196
- response.addTextResult(JSON.stringify(overview, null, 2));
197
- return;
198
- }
199
- const normalizedQueryUrl = (0, import_serialization.normalizeUrl)(url);
200
- const blueprint2 = sitemap.pages[normalizedQueryUrl];
201
- if (!blueprint2) {
202
- response.addError(
203
- `Page not found in Sitemap: ${normalizedQueryUrl}. Available pages: ${Object.keys(sitemap.pages).join(", ")}`
204
- );
205
- return;
206
- }
207
- if (type === "page") {
208
- response.addTextResult(JSON.stringify(blueprint2, null, 2));
209
- return;
210
- }
211
- if (type === "edges") {
212
- const edges = sitemap.edges.filter((e) => e.from === normalizedQueryUrl);
213
- response.addTextResult(JSON.stringify({ url: normalizedQueryUrl, edges }, null, 2));
214
- return;
215
- }
216
- if (type === "mapJson") {
217
- response.addTextResult(JSON.stringify((0, import_blueprint.buildMap)(blueprint2), null, 2));
218
- return;
219
- }
220
- if (type === "outline") {
221
- response.addTextResult((0, import_blueprint.buildOutline)(blueprint2));
222
- return;
223
- }
224
- }
225
- });
226
- const blueprint = (0, import_tool.defineTabTool)({
227
- capability: "core",
228
- schema: {
229
- name: "browser_blueprint",
230
- title: "Build PageBlueprint for the current page",
231
- description: [
232
- "Build a PageBlueprint for the currently loaded page. Returns the canonical `sections` tree",
233
- "containing singular elements and repeating-element shapes, each carrying enrichment fields",
234
- "(mutability, widgetType, framePath, shadowRoot, stableId, testId).",
235
- "",
236
- "Call this only when the DOM has changed since the last known-good blueprint \u2014 e.g. after a",
237
- "modal opens, a form submits, a filter changes a list, or any mutable action. For pure navigation",
238
- "to an already-crawled URL, reuse the Sitemap cache via browser_sitemap_query.",
239
- "",
240
- "Logical names are stable \u2014 use them in generated test code. Refs (from browser_snapshot) are",
241
- "ephemeral \u2014 use them only for dispatching the next interaction, never in generated code."
242
- ].join("\n"),
243
- inputSchema: import_mcpBundle.z.object({}),
244
- type: "readOnly"
245
- },
246
- handle: async (tab, _params, response) => {
247
- try {
248
- const bp = await (0, import_blueprint.buildPageBlueprint)(tab.page);
249
- response.addTextResult(JSON.stringify(bp, null, 2));
250
- } catch (err) {
251
- if (err instanceof import_blueprint.BlueprintInvariantError) {
252
- response.addError(
253
- `Blueprint invariant violated: ${err.message}. This is a builder bug; please file an issue with the URL of the page that triggered it.`
254
- );
255
- return;
256
- }
257
- throw err;
258
- }
259
- }
260
- });
261
- var domAnalyzer_default = [sitemapBuild, sitemapQuery, blueprint];