@rolepod/uiproof 0.4.1 → 0.6.0
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +2 -2
- package/CHANGELOG.md +168 -0
- package/README.md +26 -7
- package/dist/bin/rolepod-uiproof.js +1678 -59
- package/dist/bin/rolepod-uiproof.js.map +1 -1
- package/dist/index.d.ts +635 -10
- package/dist/index.js +1716 -73
- package/dist/index.js.map +1 -1
- package/dist/schemas/tools.json +34 -1
- package/package.json +1 -1
- package/skills/audit-a11y/SKILL.md +9 -0
- package/skills/check-errors/SKILL.md +123 -0
- package/skills/scaffold-e2e/SKILL.md +23 -0
- package/skills/verify-ui/SKILL.md +146 -70
- package/skills/visual-diff/SKILL.md +9 -0
|
@@ -175,7 +175,7 @@ function runInstallMobile() {
|
|
|
175
175
|
|
|
176
176
|
// src/cli/replay.ts
|
|
177
177
|
import { readFile } from "fs/promises";
|
|
178
|
-
import { resolve as
|
|
178
|
+
import { resolve as resolve4 } from "path";
|
|
179
179
|
|
|
180
180
|
// src/artifact/ArtifactStore.ts
|
|
181
181
|
import { randomUUID } from "crypto";
|
|
@@ -204,16 +204,49 @@ var log = {
|
|
|
204
204
|
// src/artifact/ArtifactStore.ts
|
|
205
205
|
var ArtifactStore = class {
|
|
206
206
|
rootDir;
|
|
207
|
+
mode;
|
|
208
|
+
baselineRoot;
|
|
207
209
|
constructor(opts = {}) {
|
|
208
|
-
|
|
210
|
+
const detectedParent = process.env.ROLEPOD_PARENT === "1";
|
|
211
|
+
this.mode = opts.mode ?? (detectedParent ? "with-parent" : "standalone");
|
|
212
|
+
if (opts.rootDir !== void 0) {
|
|
213
|
+
this.rootDir = opts.rootDir;
|
|
214
|
+
} else if (this.mode === "with-parent") {
|
|
215
|
+
this.rootDir = resolve2(process.cwd(), ".rolepod", "evidence");
|
|
216
|
+
} else {
|
|
217
|
+
this.rootDir = resolve2(process.cwd(), ".rolepod-uiproof", "artifacts");
|
|
218
|
+
}
|
|
219
|
+
this.baselineRoot = resolve2(process.cwd(), ".rolepod-uiproof", "baselines");
|
|
209
220
|
}
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Allocate a fresh run dir and ensure it exists.
|
|
223
|
+
*
|
|
224
|
+
* - standalone: `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
|
|
225
|
+
* - with-parent: `./.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
|
|
226
|
+
*
|
|
227
|
+
* `prefix` is preserved for back-compat with v0.5 callers; new callers
|
|
228
|
+
* should also pass `opts.skill` so the with-parent path can be derived
|
|
229
|
+
* unambiguously and the manifest can be emitted with the canonical
|
|
230
|
+
* skill name.
|
|
231
|
+
*/
|
|
232
|
+
async startRun(prefix = "run", opts = {}) {
|
|
233
|
+
const ts = this.timestampSlug();
|
|
234
|
+
const skill = opts.skill ?? prefix;
|
|
235
|
+
let runId;
|
|
236
|
+
if (this.mode === "with-parent") {
|
|
237
|
+
runId = `${ts}-rolepod-uiproof-${skill}`;
|
|
238
|
+
} else {
|
|
239
|
+
runId = `${prefix}_${ts}_${randomUUID().slice(0, 8)}`;
|
|
240
|
+
}
|
|
213
241
|
const runDir = resolve2(this.rootDir, runId);
|
|
214
242
|
await mkdir(runDir, { recursive: true });
|
|
215
|
-
log.debug("artifact run started", {
|
|
216
|
-
|
|
243
|
+
log.debug("artifact run started", {
|
|
244
|
+
run_id: runId,
|
|
245
|
+
dir: runDir,
|
|
246
|
+
mode: this.mode,
|
|
247
|
+
skill
|
|
248
|
+
});
|
|
249
|
+
return { runId, runDir, skill, mode: this.mode };
|
|
217
250
|
}
|
|
218
251
|
async writeScreenshot(runDir, buf, name) {
|
|
219
252
|
const path = resolve2(runDir, `${name}.png`);
|
|
@@ -241,7 +274,7 @@ var ArtifactStore = class {
|
|
|
241
274
|
}
|
|
242
275
|
/** Root for stored visual baselines: `./.rolepod-uiproof/baselines/`. */
|
|
243
276
|
get baselineDir() {
|
|
244
|
-
return
|
|
277
|
+
return this.baselineRoot;
|
|
245
278
|
}
|
|
246
279
|
timestampSlug() {
|
|
247
280
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -582,6 +615,34 @@ var AppiumEngine = class {
|
|
|
582
615
|
throw new UnsupportedPlatformError(_session.platform);
|
|
583
616
|
}
|
|
584
617
|
// -------------------------------------------------------------------------
|
|
618
|
+
// v0.5 cross-platform additions — mobile stubs.
|
|
619
|
+
// These ship as `not_implemented_in_v05` until the mobile gesture work lands.
|
|
620
|
+
// -------------------------------------------------------------------------
|
|
621
|
+
async hover(_session, _ref) {
|
|
622
|
+
throw new RolepodMcpError(
|
|
623
|
+
"engine_error",
|
|
624
|
+
"hover is not yet implemented for mobile (Appium). Use long-press via custom gesture if needed."
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
async drag(_session, _fromRef, _toRef) {
|
|
628
|
+
throw new RolepodMcpError(
|
|
629
|
+
"engine_error",
|
|
630
|
+
"drag is not yet implemented for mobile (Appium). Use the W3C Actions API directly if needed."
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
async fillForm(session, fields) {
|
|
634
|
+
for (const f of fields) {
|
|
635
|
+
const v = typeof f.value === "boolean" ? String(f.value) : f.value;
|
|
636
|
+
await this.type(session, f.ref, v);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async uploadFile(_session, _ref, _filePath) {
|
|
640
|
+
throw new RolepodMcpError(
|
|
641
|
+
"engine_error",
|
|
642
|
+
"upload_file is not supported on mobile (Appium)."
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
// -------------------------------------------------------------------------
|
|
585
646
|
// Internals
|
|
586
647
|
// -------------------------------------------------------------------------
|
|
587
648
|
async loadWdio() {
|
|
@@ -696,6 +757,7 @@ function treeIncludesText(node, text) {
|
|
|
696
757
|
|
|
697
758
|
// src/engine/PlaywrightEngine.ts
|
|
698
759
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
760
|
+
import { resolve as resolvePath, isAbsolute } from "path";
|
|
699
761
|
import {
|
|
700
762
|
chromium,
|
|
701
763
|
firefox,
|
|
@@ -805,6 +867,83 @@ function parseAriaSnapshot(snapshotYaml) {
|
|
|
805
867
|
}
|
|
806
868
|
|
|
807
869
|
// src/engine/PlaywrightEngine.ts
|
|
870
|
+
var CONSOLE_BUFFER_CAP = 1e3;
|
|
871
|
+
var NETWORK_BUFFER_CAP = 1e3;
|
|
872
|
+
var NETWORK_PRESETS = {
|
|
873
|
+
offline: { offline: true, downloadThroughput: 0, uploadThroughput: 0, latency: 0 },
|
|
874
|
+
"slow-3g": {
|
|
875
|
+
offline: false,
|
|
876
|
+
// 500 Kbps down / 500 Kbps up / 400ms RTT
|
|
877
|
+
downloadThroughput: 500 * 1024 / 8,
|
|
878
|
+
uploadThroughput: 500 * 1024 / 8,
|
|
879
|
+
latency: 400
|
|
880
|
+
},
|
|
881
|
+
"fast-3g": {
|
|
882
|
+
offline: false,
|
|
883
|
+
downloadThroughput: 1.5 * 1024 * 1024 / 8,
|
|
884
|
+
uploadThroughput: 750 * 1024 / 8,
|
|
885
|
+
latency: 150
|
|
886
|
+
},
|
|
887
|
+
"slow-4g": {
|
|
888
|
+
offline: false,
|
|
889
|
+
downloadThroughput: 4 * 1024 * 1024 / 8,
|
|
890
|
+
uploadThroughput: 3 * 1024 * 1024 / 8,
|
|
891
|
+
latency: 100
|
|
892
|
+
},
|
|
893
|
+
"fast-4g": {
|
|
894
|
+
offline: false,
|
|
895
|
+
downloadThroughput: 9 * 1024 * 1024 / 8,
|
|
896
|
+
uploadThroughput: 4.5 * 1024 * 1024 / 8,
|
|
897
|
+
latency: 60
|
|
898
|
+
},
|
|
899
|
+
"no-throttling": {
|
|
900
|
+
offline: false,
|
|
901
|
+
downloadThroughput: -1,
|
|
902
|
+
uploadThroughput: -1,
|
|
903
|
+
latency: 0
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
function pushRing(buf, entry, cap) {
|
|
907
|
+
buf.push(entry);
|
|
908
|
+
if (buf.length > cap) buf.splice(0, buf.length - cap);
|
|
909
|
+
}
|
|
910
|
+
function mapConsoleLevel(t) {
|
|
911
|
+
switch (t) {
|
|
912
|
+
case "error":
|
|
913
|
+
return "error";
|
|
914
|
+
case "warning":
|
|
915
|
+
return "warning";
|
|
916
|
+
case "info":
|
|
917
|
+
return "info";
|
|
918
|
+
case "debug":
|
|
919
|
+
return "debug";
|
|
920
|
+
case "trace":
|
|
921
|
+
return "trace";
|
|
922
|
+
default:
|
|
923
|
+
return "log";
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function formatConsoleLocation(msg) {
|
|
927
|
+
try {
|
|
928
|
+
const loc = msg.location();
|
|
929
|
+
if (!loc?.url) return void 0;
|
|
930
|
+
return `${loc.url}:${loc.lineNumber}:${loc.columnNumber}`;
|
|
931
|
+
} catch {
|
|
932
|
+
return void 0;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function findNetworkEntry(buf, req) {
|
|
936
|
+
const url = req.url();
|
|
937
|
+
const method = req.method();
|
|
938
|
+
for (let i = buf.length - 1; i >= 0; i--) {
|
|
939
|
+
const e = buf[i];
|
|
940
|
+
if (!e) continue;
|
|
941
|
+
if (e.url === url && e.method === method && e.status === void 0 && !e.failure) {
|
|
942
|
+
return e;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
808
947
|
var PlaywrightEngine = class {
|
|
809
948
|
id = "playwright";
|
|
810
949
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -820,35 +959,80 @@ var PlaywrightEngine = class {
|
|
|
820
959
|
if (opts.viewport) contextOptions.viewport = opts.viewport;
|
|
821
960
|
if (opts.user_agent) contextOptions.userAgent = opts.user_agent;
|
|
822
961
|
if (opts.locale) contextOptions.locale = opts.locale;
|
|
962
|
+
if (opts.capture?.har) {
|
|
963
|
+
contextOptions.recordHar = { path: opts.capture.har.path };
|
|
964
|
+
}
|
|
965
|
+
if (opts.capture?.video) {
|
|
966
|
+
contextOptions.recordVideo = {
|
|
967
|
+
dir: opts.capture.video.dir,
|
|
968
|
+
size: opts.capture.video.sizeWidth && opts.capture.video.sizeHeight ? {
|
|
969
|
+
width: opts.capture.video.sizeWidth,
|
|
970
|
+
height: opts.capture.video.sizeHeight
|
|
971
|
+
} : void 0
|
|
972
|
+
};
|
|
973
|
+
}
|
|
823
974
|
const context = await browser.newContext(contextOptions);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
975
|
+
if (opts.capture?.trace) {
|
|
976
|
+
await context.tracing.start({
|
|
977
|
+
screenshots: true,
|
|
978
|
+
snapshots: true,
|
|
979
|
+
sources: false
|
|
980
|
+
});
|
|
827
981
|
}
|
|
982
|
+
const page = await context.newPage();
|
|
828
983
|
const sessionId = randomUUID3();
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
session,
|
|
984
|
+
const internals = {
|
|
985
|
+
session: {
|
|
986
|
+
id: sessionId,
|
|
987
|
+
platform: "web",
|
|
988
|
+
browser,
|
|
989
|
+
context,
|
|
990
|
+
mainPage: page
|
|
991
|
+
},
|
|
838
992
|
refIndex: /* @__PURE__ */ new Map(),
|
|
839
993
|
snapshotGeneration: 0,
|
|
840
994
|
refGeneration: -1,
|
|
841
|
-
lastSnapshotAt: null
|
|
995
|
+
lastSnapshotAt: null,
|
|
996
|
+
pages: [page],
|
|
997
|
+
activePageIndex: 0,
|
|
998
|
+
consoleBuffer: [],
|
|
999
|
+
networkBuffer: [],
|
|
1000
|
+
networkInflight: /* @__PURE__ */ new Map(),
|
|
1001
|
+
networkNextId: 1,
|
|
1002
|
+
dialogArming: null,
|
|
1003
|
+
captureOpts: opts.capture,
|
|
1004
|
+
traceStarted: !!opts.capture?.trace
|
|
1005
|
+
};
|
|
1006
|
+
this.attachPageListeners(internals, page);
|
|
1007
|
+
context.on("page", (newPage) => {
|
|
1008
|
+
internals.pages.push(newPage);
|
|
1009
|
+
this.attachPageListeners(internals, newPage);
|
|
842
1010
|
});
|
|
1011
|
+
if (opts.url) {
|
|
1012
|
+
await page.goto(opts.url, { waitUntil: "domcontentloaded" });
|
|
1013
|
+
}
|
|
1014
|
+
this.sessions.set(sessionId, internals);
|
|
843
1015
|
log.info("session opened", {
|
|
844
1016
|
session_id: sessionId,
|
|
845
1017
|
browser: browserName,
|
|
846
|
-
url: opts.url ?? null
|
|
1018
|
+
url: opts.url ?? null,
|
|
1019
|
+
capture: opts.capture ? Object.keys(opts.capture).filter(
|
|
1020
|
+
(k) => opts.capture[k]
|
|
1021
|
+
) : []
|
|
847
1022
|
});
|
|
848
1023
|
return { id: sessionId, platform: "web" };
|
|
849
1024
|
}
|
|
850
1025
|
async close(session) {
|
|
851
1026
|
const s = this.requireSession(session.id);
|
|
1027
|
+
if (s.traceStarted && s.captureOpts?.trace) {
|
|
1028
|
+
const tracePath = resolvePath(s.captureOpts.trace.artifactDir, "trace.zip");
|
|
1029
|
+
await s.session.context.tracing.stop({ path: tracePath }).catch((err) => {
|
|
1030
|
+
log.warn("trace stop failed", {
|
|
1031
|
+
session_id: session.id,
|
|
1032
|
+
err: String(err)
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
852
1036
|
await s.session.context.close().catch((err) => {
|
|
853
1037
|
log.warn("context close failed", { session_id: session.id, err: String(err) });
|
|
854
1038
|
});
|
|
@@ -860,7 +1044,7 @@ var PlaywrightEngine = class {
|
|
|
860
1044
|
}
|
|
861
1045
|
async snapshot(session, mode = "visible") {
|
|
862
1046
|
const s = this.requireSession(session.id);
|
|
863
|
-
const ariaYaml = await s.
|
|
1047
|
+
const ariaYaml = await this.activePage(s).ariaSnapshot({ mode: "ai" });
|
|
864
1048
|
const { tree, refIndex } = parseAriaSnapshot(ariaYaml);
|
|
865
1049
|
void mode;
|
|
866
1050
|
s.snapshotGeneration += 1;
|
|
@@ -870,7 +1054,7 @@ var PlaywrightEngine = class {
|
|
|
870
1054
|
return {
|
|
871
1055
|
session_id: session.id,
|
|
872
1056
|
platform: "web",
|
|
873
|
-
url_or_screen: s.
|
|
1057
|
+
url_or_screen: this.activePage(s).url(),
|
|
874
1058
|
taken_at: s.lastSnapshotAt,
|
|
875
1059
|
tree
|
|
876
1060
|
};
|
|
@@ -890,7 +1074,7 @@ var PlaywrightEngine = class {
|
|
|
890
1074
|
}
|
|
891
1075
|
async key(session, key) {
|
|
892
1076
|
const s = this.requireSession(session.id);
|
|
893
|
-
await s.
|
|
1077
|
+
await this.activePage(s).keyboard.press(key);
|
|
894
1078
|
this.invalidateRefs(s);
|
|
895
1079
|
}
|
|
896
1080
|
async scroll(session, dir, amount = 400, ref) {
|
|
@@ -904,13 +1088,13 @@ var PlaywrightEngine = class {
|
|
|
904
1088
|
[dx, dy]
|
|
905
1089
|
);
|
|
906
1090
|
} else {
|
|
907
|
-
await s.
|
|
1091
|
+
await this.activePage(s).mouse.wheel(dx, dy);
|
|
908
1092
|
}
|
|
909
1093
|
this.invalidateRefs(s);
|
|
910
1094
|
}
|
|
911
1095
|
async waitFor(session, cond, timeoutMs = 1e4) {
|
|
912
1096
|
const s = this.requireSession(session.id);
|
|
913
|
-
const page = s
|
|
1097
|
+
const page = this.activePage(s);
|
|
914
1098
|
switch (cond.kind) {
|
|
915
1099
|
case "text_visible":
|
|
916
1100
|
await page.getByText(cond.text, { exact: false }).first().waitFor({ state: "visible", timeout: timeoutMs });
|
|
@@ -930,14 +1114,14 @@ var PlaywrightEngine = class {
|
|
|
930
1114
|
}
|
|
931
1115
|
async screenshot(session, fullPage = false) {
|
|
932
1116
|
const s = this.requireSession(session.id);
|
|
933
|
-
return s.
|
|
1117
|
+
return this.activePage(s).screenshot({ fullPage });
|
|
934
1118
|
}
|
|
935
1119
|
async navigate(session, url) {
|
|
936
1120
|
const s = this.requireSession(session.id);
|
|
937
1121
|
if (s.session.platform !== "web") {
|
|
938
1122
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
939
1123
|
}
|
|
940
|
-
await s.
|
|
1124
|
+
await this.activePage(s).goto(url, { waitUntil: "domcontentloaded" });
|
|
941
1125
|
this.invalidateRefs(s);
|
|
942
1126
|
}
|
|
943
1127
|
/**
|
|
@@ -951,7 +1135,7 @@ var PlaywrightEngine = class {
|
|
|
951
1135
|
if (s.session.platform !== "web") {
|
|
952
1136
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
953
1137
|
}
|
|
954
|
-
return s
|
|
1138
|
+
return this.activePage(s);
|
|
955
1139
|
}
|
|
956
1140
|
/** Increment generation; the next ref-using call will see them as stale. */
|
|
957
1141
|
bumpGeneration(sessionId) {
|
|
@@ -959,8 +1143,311 @@ var PlaywrightEngine = class {
|
|
|
959
1143
|
this.invalidateRefs(s);
|
|
960
1144
|
}
|
|
961
1145
|
// -------------------------------------------------------------------------
|
|
1146
|
+
// v0.5 — input additions
|
|
1147
|
+
// -------------------------------------------------------------------------
|
|
1148
|
+
async hover(session, ref) {
|
|
1149
|
+
const s = this.requireSession(session.id);
|
|
1150
|
+
const locator = this.resolveLocator(s, ref);
|
|
1151
|
+
await locator.hover();
|
|
1152
|
+
}
|
|
1153
|
+
async drag(session, fromRef, toRef) {
|
|
1154
|
+
const s = this.requireSession(session.id);
|
|
1155
|
+
const from = this.resolveLocator(s, fromRef);
|
|
1156
|
+
const to = this.resolveLocator(s, toRef);
|
|
1157
|
+
await from.dragTo(to);
|
|
1158
|
+
this.invalidateRefs(s);
|
|
1159
|
+
}
|
|
1160
|
+
async fillForm(session, fields) {
|
|
1161
|
+
const s = this.requireSession(session.id);
|
|
1162
|
+
for (const field of fields) {
|
|
1163
|
+
const locator = this.resolveLocator(s, field.ref);
|
|
1164
|
+
const kind = field.kind;
|
|
1165
|
+
if (kind === "checkbox" || kind === "radio") {
|
|
1166
|
+
const checked = typeof field.value === "boolean" ? field.value : field.value === "true" || field.value === "on";
|
|
1167
|
+
await locator.setChecked(checked);
|
|
1168
|
+
} else if (kind === "select") {
|
|
1169
|
+
await locator.selectOption(String(field.value));
|
|
1170
|
+
} else {
|
|
1171
|
+
await locator.fill(String(field.value));
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
this.invalidateRefs(s);
|
|
1175
|
+
}
|
|
1176
|
+
async uploadFile(session, ref, filePath) {
|
|
1177
|
+
const s = this.requireSession(session.id);
|
|
1178
|
+
if (!isAbsolute(filePath)) {
|
|
1179
|
+
throw new RolepodMcpError(
|
|
1180
|
+
"invalid_input",
|
|
1181
|
+
`upload_file requires an absolute path; got "${filePath}".`,
|
|
1182
|
+
{ file_path: filePath }
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const locator = this.resolveLocator(s, ref);
|
|
1186
|
+
await locator.setInputFiles(filePath);
|
|
1187
|
+
this.invalidateRefs(s);
|
|
1188
|
+
}
|
|
1189
|
+
// -------------------------------------------------------------------------
|
|
1190
|
+
// v0.5 — web-only extensions (not on Engine interface; tools cast to
|
|
1191
|
+
// PlaywrightEngine before calling).
|
|
1192
|
+
// -------------------------------------------------------------------------
|
|
1193
|
+
/**
|
|
1194
|
+
* Pre-arm a one-shot dialog handler for the next dialog raised on the
|
|
1195
|
+
* active page. Returns when either the dialog fires (and is handled)
|
|
1196
|
+
* or the timeout elapses. The caller is expected to trigger the
|
|
1197
|
+
* dialog (via click etc.) AFTER arming.
|
|
1198
|
+
*/
|
|
1199
|
+
async handleDialog(sessionId, opts) {
|
|
1200
|
+
const s = this.requireSession(sessionId);
|
|
1201
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1202
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
1203
|
+
if (s.dialogArming) {
|
|
1204
|
+
s.dialogArming.resolve(false);
|
|
1205
|
+
}
|
|
1206
|
+
return new Promise((resolve7) => {
|
|
1207
|
+
const arming = {
|
|
1208
|
+
action: opts.action,
|
|
1209
|
+
text: opts.text,
|
|
1210
|
+
expiresAt,
|
|
1211
|
+
resolve: (handled) => {
|
|
1212
|
+
s.dialogArming = null;
|
|
1213
|
+
resolve7({ handled });
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
s.dialogArming = arming;
|
|
1217
|
+
const timer = setTimeout(() => {
|
|
1218
|
+
if (s.dialogArming === arming) {
|
|
1219
|
+
s.dialogArming = null;
|
|
1220
|
+
resolve7({ handled: false });
|
|
1221
|
+
}
|
|
1222
|
+
}, timeoutMs);
|
|
1223
|
+
timer.unref?.();
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
getConsole(sessionId, opts) {
|
|
1227
|
+
const s = this.requireSession(sessionId);
|
|
1228
|
+
const levels = opts?.levels;
|
|
1229
|
+
const contains = opts?.contains;
|
|
1230
|
+
const limit = opts?.limit ?? 50;
|
|
1231
|
+
let entries = s.consoleBuffer;
|
|
1232
|
+
if (levels && levels.length > 0) {
|
|
1233
|
+
entries = entries.filter((e) => levels.includes(e.level));
|
|
1234
|
+
}
|
|
1235
|
+
if (contains) {
|
|
1236
|
+
entries = entries.filter((e) => e.text.includes(contains));
|
|
1237
|
+
}
|
|
1238
|
+
const result = entries.slice(-limit);
|
|
1239
|
+
if (opts?.clear) s.consoleBuffer = [];
|
|
1240
|
+
return result;
|
|
1241
|
+
}
|
|
1242
|
+
getNetwork(sessionId, opts) {
|
|
1243
|
+
const s = this.requireSession(sessionId);
|
|
1244
|
+
let entries = s.networkBuffer;
|
|
1245
|
+
if (opts?.urlPattern) {
|
|
1246
|
+
if (opts.patternKind === "regex") {
|
|
1247
|
+
const re = new RegExp(opts.urlPattern);
|
|
1248
|
+
entries = entries.filter((e) => re.test(e.url));
|
|
1249
|
+
} else {
|
|
1250
|
+
entries = entries.filter((e) => e.url.includes(opts.urlPattern));
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (opts?.method) {
|
|
1254
|
+
const m = opts.method.toUpperCase();
|
|
1255
|
+
entries = entries.filter((e) => e.method.toUpperCase() === m);
|
|
1256
|
+
}
|
|
1257
|
+
if (opts?.statusRange) {
|
|
1258
|
+
const { min, max } = opts.statusRange;
|
|
1259
|
+
entries = entries.filter(
|
|
1260
|
+
(e) => e.status !== void 0 && e.status >= min && e.status <= max
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
if (opts?.onlyFailed) {
|
|
1264
|
+
entries = entries.filter(
|
|
1265
|
+
(e) => !!e.failure || e.status !== void 0 && e.status >= 400
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
const limit = opts?.limit ?? 50;
|
|
1269
|
+
const result = entries.slice(-limit);
|
|
1270
|
+
if (opts?.clear) s.networkBuffer = [];
|
|
1271
|
+
return result;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Read the consoleBuffer/networkBuffer directly without filtering —
|
|
1275
|
+
* used by verify_ui_flow expect evaluators.
|
|
1276
|
+
*/
|
|
1277
|
+
peekBuffers(sessionId) {
|
|
1278
|
+
const s = this.requireSession(sessionId);
|
|
1279
|
+
return { console: s.consoleBuffer, network: s.networkBuffer };
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Runtime mutation of context-level emulation. CPU + network throttle
|
|
1283
|
+
* use CDP and only work on chromium; everything else is cross-browser.
|
|
1284
|
+
*/
|
|
1285
|
+
async setEnv(sessionId, opts) {
|
|
1286
|
+
const s = this.requireSession(sessionId);
|
|
1287
|
+
const page = this.activePage(s);
|
|
1288
|
+
const ctx = s.session.context;
|
|
1289
|
+
if (opts.viewport) {
|
|
1290
|
+
await page.setViewportSize(opts.viewport);
|
|
1291
|
+
}
|
|
1292
|
+
if (opts.offline !== void 0) {
|
|
1293
|
+
await ctx.setOffline(opts.offline);
|
|
1294
|
+
}
|
|
1295
|
+
if (opts.geolocation) {
|
|
1296
|
+
await ctx.setGeolocation(opts.geolocation);
|
|
1297
|
+
}
|
|
1298
|
+
if (opts.extraHeaders) {
|
|
1299
|
+
await ctx.setExtraHTTPHeaders(opts.extraHeaders);
|
|
1300
|
+
}
|
|
1301
|
+
if (opts.colorScheme || opts.reducedMotion) {
|
|
1302
|
+
await page.emulateMedia({
|
|
1303
|
+
...opts.colorScheme ? { colorScheme: opts.colorScheme } : {},
|
|
1304
|
+
...opts.reducedMotion ? { reducedMotion: opts.reducedMotion } : {}
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
if (opts.networkThrottle || opts.cpuThrottle !== void 0) {
|
|
1308
|
+
const browserName = ctx.browser()?.browserType().name();
|
|
1309
|
+
if (browserName !== "chromium") {
|
|
1310
|
+
throw new RolepodMcpError(
|
|
1311
|
+
"unsupported_engine",
|
|
1312
|
+
`networkThrottle / cpuThrottle require chromium (CDP-backed); current browser is "${browserName}".`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
const cdp = await ctx.newCDPSession(page);
|
|
1316
|
+
try {
|
|
1317
|
+
if (opts.networkThrottle) {
|
|
1318
|
+
const preset = NETWORK_PRESETS[opts.networkThrottle];
|
|
1319
|
+
await cdp.send("Network.enable");
|
|
1320
|
+
await cdp.send("Network.emulateNetworkConditions", preset);
|
|
1321
|
+
}
|
|
1322
|
+
if (opts.cpuThrottle !== void 0) {
|
|
1323
|
+
await cdp.send("Emulation.setCPUThrottlingRate", {
|
|
1324
|
+
rate: opts.cpuThrottle
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
} finally {
|
|
1328
|
+
await cdp.detach().catch(() => void 0);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
this.invalidateRefs(s);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Execute a JavaScript function in the page context. ALWAYS gated by
|
|
1335
|
+
* the tool layer (`ROLEPOD_ALLOW_EVAL=1`); this method does not enforce
|
|
1336
|
+
* the env check.
|
|
1337
|
+
*/
|
|
1338
|
+
async evaluate(sessionId, script, args) {
|
|
1339
|
+
const s = this.requireSession(sessionId);
|
|
1340
|
+
const page = this.activePage(s);
|
|
1341
|
+
return page.evaluate(
|
|
1342
|
+
({ src, a }) => (
|
|
1343
|
+
// eslint-disable-next-line no-new-func
|
|
1344
|
+
new Function("args", `return (async () => { ${src} })();`)(a)
|
|
1345
|
+
),
|
|
1346
|
+
{ src: script, a: args ?? [] }
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
listPages(sessionId) {
|
|
1350
|
+
const s = this.requireSession(sessionId);
|
|
1351
|
+
return s.pages.map((p, i) => ({
|
|
1352
|
+
index: i,
|
|
1353
|
+
url: p.url(),
|
|
1354
|
+
title_promise: p.title(),
|
|
1355
|
+
active: i === s.activePageIndex
|
|
1356
|
+
}));
|
|
1357
|
+
}
|
|
1358
|
+
async switchPage(sessionId, index) {
|
|
1359
|
+
const s = this.requireSession(sessionId);
|
|
1360
|
+
if (index < 0 || index >= s.pages.length) {
|
|
1361
|
+
throw new RolepodMcpError(
|
|
1362
|
+
"invalid_input",
|
|
1363
|
+
`Page index ${index} out of range (have ${s.pages.length} page(s)).`,
|
|
1364
|
+
{ index, available: s.pages.length }
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
s.activePageIndex = index;
|
|
1368
|
+
this.invalidateRefs(s);
|
|
1369
|
+
}
|
|
1370
|
+
// -------------------------------------------------------------------------
|
|
962
1371
|
// Internal helpers
|
|
963
1372
|
// -------------------------------------------------------------------------
|
|
1373
|
+
activePage(s) {
|
|
1374
|
+
return s.pages[s.activePageIndex] ?? s.session.mainPage;
|
|
1375
|
+
}
|
|
1376
|
+
attachPageListeners(s, page) {
|
|
1377
|
+
page.on("console", (msg) => {
|
|
1378
|
+
const level = mapConsoleLevel(msg.type());
|
|
1379
|
+
pushRing(
|
|
1380
|
+
s.consoleBuffer,
|
|
1381
|
+
{
|
|
1382
|
+
level,
|
|
1383
|
+
text: msg.text(),
|
|
1384
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1385
|
+
location: formatConsoleLocation(msg)
|
|
1386
|
+
},
|
|
1387
|
+
CONSOLE_BUFFER_CAP
|
|
1388
|
+
);
|
|
1389
|
+
});
|
|
1390
|
+
page.on("request", (req) => {
|
|
1391
|
+
const id = s.networkNextId++;
|
|
1392
|
+
s.networkInflight.set(req.url() + "::" + req.method() + "::" + id, {
|
|
1393
|
+
id,
|
|
1394
|
+
startedAt: Date.now(),
|
|
1395
|
+
resourceType: req.resourceType()
|
|
1396
|
+
});
|
|
1397
|
+
pushRing(
|
|
1398
|
+
s.networkBuffer,
|
|
1399
|
+
{
|
|
1400
|
+
id,
|
|
1401
|
+
url: req.url(),
|
|
1402
|
+
method: req.method(),
|
|
1403
|
+
resource_type: req.resourceType(),
|
|
1404
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1405
|
+
},
|
|
1406
|
+
NETWORK_BUFFER_CAP
|
|
1407
|
+
);
|
|
1408
|
+
});
|
|
1409
|
+
page.on("response", (res) => {
|
|
1410
|
+
const req = res.request();
|
|
1411
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1412
|
+
if (entry) {
|
|
1413
|
+
entry.status = res.status();
|
|
1414
|
+
entry.duration_ms = Date.now() - new Date(entry.ts).getTime();
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
page.on("requestfailed", (req) => {
|
|
1418
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1419
|
+
if (entry) {
|
|
1420
|
+
entry.failure = req.failure()?.errorText ?? "request failed";
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
page.on("dialog", (dialog) => {
|
|
1424
|
+
void this.handlePageDialog(s, dialog);
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
async handlePageDialog(s, dialog) {
|
|
1428
|
+
const arm = s.dialogArming;
|
|
1429
|
+
if (!arm || Date.now() > arm.expiresAt) {
|
|
1430
|
+
await dialog.dismiss().catch(() => void 0);
|
|
1431
|
+
if (arm) arm.resolve(false);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
try {
|
|
1435
|
+
if (arm.action === "accept") {
|
|
1436
|
+
await dialog.accept();
|
|
1437
|
+
} else if (arm.action === "accept_with_text") {
|
|
1438
|
+
await dialog.accept(arm.text ?? "");
|
|
1439
|
+
} else {
|
|
1440
|
+
await dialog.dismiss();
|
|
1441
|
+
}
|
|
1442
|
+
arm.resolve(true);
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
log.warn("dialog handle failed", {
|
|
1445
|
+
session_id: s.session.id,
|
|
1446
|
+
err: String(err)
|
|
1447
|
+
});
|
|
1448
|
+
arm.resolve(false);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
964
1451
|
requireSession(sessionId) {
|
|
965
1452
|
const s = this.sessions.get(sessionId);
|
|
966
1453
|
if (!s) {
|
|
@@ -989,7 +1476,7 @@ var PlaywrightEngine = class {
|
|
|
989
1476
|
if (meta.ref.startsWith("s")) {
|
|
990
1477
|
throw new UnknownRefError(s.session.id, ref);
|
|
991
1478
|
}
|
|
992
|
-
return s.
|
|
1479
|
+
return this.activePage(s).locator(`aria-ref=${meta.ref}`);
|
|
993
1480
|
}
|
|
994
1481
|
invalidateRefs(s) {
|
|
995
1482
|
s.snapshotGeneration += 1;
|
|
@@ -1125,6 +1612,10 @@ var SessionRegistry = class {
|
|
|
1125
1612
|
}
|
|
1126
1613
|
};
|
|
1127
1614
|
|
|
1615
|
+
// src/tools/composite/verify_ui_flow.ts
|
|
1616
|
+
import { readdir } from "fs/promises";
|
|
1617
|
+
import { resolve as resolvePath2 } from "path";
|
|
1618
|
+
|
|
1128
1619
|
// src/schema/tools.ts
|
|
1129
1620
|
import { z } from "zod";
|
|
1130
1621
|
var platformSchema = z.enum(["web", "ios", "android"]);
|
|
@@ -1228,6 +1719,143 @@ var browserNavigateShape = {
|
|
|
1228
1719
|
url: z.string().url()
|
|
1229
1720
|
};
|
|
1230
1721
|
var browserNavigateSchema = z.object(browserNavigateShape);
|
|
1722
|
+
var browserHoverShape = {
|
|
1723
|
+
session_id: z.string().min(1),
|
|
1724
|
+
ref: z.string().min(1)
|
|
1725
|
+
};
|
|
1726
|
+
var browserHoverSchema = z.object(browserHoverShape);
|
|
1727
|
+
var browserDragShape = {
|
|
1728
|
+
session_id: z.string().min(1),
|
|
1729
|
+
from_ref: z.string().min(1),
|
|
1730
|
+
to_ref: z.string().min(1)
|
|
1731
|
+
};
|
|
1732
|
+
var browserDragSchema = z.object(browserDragShape);
|
|
1733
|
+
var fillFieldKindSchema = z.enum([
|
|
1734
|
+
"input",
|
|
1735
|
+
"select",
|
|
1736
|
+
"checkbox",
|
|
1737
|
+
"radio"
|
|
1738
|
+
]);
|
|
1739
|
+
var fillFormFieldSchema = z.object({
|
|
1740
|
+
ref: z.string().min(1),
|
|
1741
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1742
|
+
kind: fillFieldKindSchema.optional()
|
|
1743
|
+
});
|
|
1744
|
+
var browserFillFormShape = {
|
|
1745
|
+
session_id: z.string().min(1),
|
|
1746
|
+
fields: z.array(fillFormFieldSchema).min(1)
|
|
1747
|
+
};
|
|
1748
|
+
var browserFillFormSchema = z.object(browserFillFormShape);
|
|
1749
|
+
var browserUploadFileShape = {
|
|
1750
|
+
session_id: z.string().min(1),
|
|
1751
|
+
ref: z.string().min(1),
|
|
1752
|
+
file_path: z.string().min(1)
|
|
1753
|
+
};
|
|
1754
|
+
var browserUploadFileSchema = z.object(browserUploadFileShape);
|
|
1755
|
+
var dialogActionSchema = z.enum([
|
|
1756
|
+
"accept",
|
|
1757
|
+
"dismiss",
|
|
1758
|
+
"accept_with_text"
|
|
1759
|
+
]);
|
|
1760
|
+
var browserHandleDialogShape = {
|
|
1761
|
+
session_id: z.string().min(1),
|
|
1762
|
+
action: dialogActionSchema,
|
|
1763
|
+
/** Only used when action='accept_with_text'. */
|
|
1764
|
+
text: z.string().optional(),
|
|
1765
|
+
/**
|
|
1766
|
+
* Arming behavior: registers a one-shot handler for the NEXT dialog
|
|
1767
|
+
* raised on the page. Call this BEFORE the action that triggers the
|
|
1768
|
+
* dialog (e.g. before clicking the button that calls `confirm()`).
|
|
1769
|
+
* Default 30s if no dialog appears, handler is auto-removed.
|
|
1770
|
+
*/
|
|
1771
|
+
timeout_ms: z.number().int().positive().optional()
|
|
1772
|
+
};
|
|
1773
|
+
var browserHandleDialogSchema = z.object(browserHandleDialogShape);
|
|
1774
|
+
var consoleLevelSchema = z.enum([
|
|
1775
|
+
"error",
|
|
1776
|
+
"warning",
|
|
1777
|
+
"info",
|
|
1778
|
+
"log",
|
|
1779
|
+
"debug",
|
|
1780
|
+
"trace"
|
|
1781
|
+
]);
|
|
1782
|
+
var browserConsoleShape = {
|
|
1783
|
+
session_id: z.string().min(1),
|
|
1784
|
+
/** Filter to only these levels. Default: errors+warnings. */
|
|
1785
|
+
levels: z.array(consoleLevelSchema).optional(),
|
|
1786
|
+
/** Substring match on message text. */
|
|
1787
|
+
contains: z.string().optional(),
|
|
1788
|
+
/** Drop all buffered messages after returning. */
|
|
1789
|
+
clear: z.boolean().default(false),
|
|
1790
|
+
/** Cap on returned messages (artifact still holds full ring buffer). */
|
|
1791
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1792
|
+
};
|
|
1793
|
+
var browserConsoleSchema = z.object(browserConsoleShape);
|
|
1794
|
+
var browserNetworkShape = {
|
|
1795
|
+
session_id: z.string().min(1),
|
|
1796
|
+
/** Substring or regex (per `pattern_kind`) match on URL. */
|
|
1797
|
+
url_pattern: z.string().optional(),
|
|
1798
|
+
pattern_kind: z.enum(["substring", "regex"]).default("substring"),
|
|
1799
|
+
method: z.string().optional(),
|
|
1800
|
+
/** Inclusive range — e.g. `{min: 400, max: 599}` for any error response. */
|
|
1801
|
+
status_range: z.object({
|
|
1802
|
+
min: z.number().int().min(100).max(599),
|
|
1803
|
+
max: z.number().int().min(100).max(599)
|
|
1804
|
+
}).optional(),
|
|
1805
|
+
only_failed: z.boolean().default(false),
|
|
1806
|
+
/** Write the full HAR file for this session to artifacts/{runId}/network.har. */
|
|
1807
|
+
export_har: z.boolean().default(false),
|
|
1808
|
+
/** Drop buffered entries after returning. */
|
|
1809
|
+
clear: z.boolean().default(false),
|
|
1810
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1811
|
+
};
|
|
1812
|
+
var browserNetworkSchema = z.object(browserNetworkShape);
|
|
1813
|
+
var networkPresetSchema = z.enum([
|
|
1814
|
+
"offline",
|
|
1815
|
+
"slow-3g",
|
|
1816
|
+
"fast-3g",
|
|
1817
|
+
"slow-4g",
|
|
1818
|
+
"fast-4g",
|
|
1819
|
+
"no-throttling"
|
|
1820
|
+
]);
|
|
1821
|
+
var geolocationSchema = z.object({
|
|
1822
|
+
latitude: z.number().min(-90).max(90),
|
|
1823
|
+
longitude: z.number().min(-180).max(180),
|
|
1824
|
+
accuracy: z.number().nonnegative().optional()
|
|
1825
|
+
});
|
|
1826
|
+
var browserSetEnvShape = {
|
|
1827
|
+
session_id: z.string().min(1),
|
|
1828
|
+
viewport: viewportSchema.optional(),
|
|
1829
|
+
offline: z.boolean().optional(),
|
|
1830
|
+
geolocation: geolocationSchema.optional(),
|
|
1831
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1832
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1833
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1834
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1835
|
+
/** CPU slowdown multiplier (1 = no throttle, 4 = 4x slower). Chromium only. */
|
|
1836
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1837
|
+
};
|
|
1838
|
+
var browserSetEnvSchema = z.object(browserSetEnvShape);
|
|
1839
|
+
var browserEvaluateShape = {
|
|
1840
|
+
session_id: z.string().min(1),
|
|
1841
|
+
script: z.string().min(1),
|
|
1842
|
+
args: z.array(z.unknown()).optional()
|
|
1843
|
+
};
|
|
1844
|
+
var browserEvaluateSchema = z.object(browserEvaluateShape);
|
|
1845
|
+
var browserPagesShape = {
|
|
1846
|
+
session_id: z.string().min(1)
|
|
1847
|
+
};
|
|
1848
|
+
var browserPagesSchema = z.object(browserPagesShape);
|
|
1849
|
+
var browserSwitchPageShape = {
|
|
1850
|
+
session_id: z.string().min(1),
|
|
1851
|
+
index: z.number().int().nonnegative()
|
|
1852
|
+
};
|
|
1853
|
+
var browserSwitchPageSchema = z.object(browserSwitchPageShape);
|
|
1854
|
+
var verifyFillFieldSchema = z.object({
|
|
1855
|
+
query: z.string(),
|
|
1856
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1857
|
+
kind: fillFieldKindSchema.optional()
|
|
1858
|
+
});
|
|
1231
1859
|
var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
1232
1860
|
z.object({ kind: z.literal("click"), query: z.string() }),
|
|
1233
1861
|
z.object({
|
|
@@ -1238,7 +1866,44 @@ var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
|
1238
1866
|
}),
|
|
1239
1867
|
z.object({ kind: z.literal("key"), key: z.string() }),
|
|
1240
1868
|
z.object({ kind: z.literal("wait_for"), condition: waitConditionSchema }),
|
|
1241
|
-
z.object({ kind: z.literal("navigate"), url: z.string().url() })
|
|
1869
|
+
z.object({ kind: z.literal("navigate"), url: z.string().url() }),
|
|
1870
|
+
// v0.5 additions
|
|
1871
|
+
z.object({ kind: z.literal("hover"), query: z.string() }),
|
|
1872
|
+
z.object({
|
|
1873
|
+
kind: z.literal("drag"),
|
|
1874
|
+
from_query: z.string(),
|
|
1875
|
+
to_query: z.string()
|
|
1876
|
+
}),
|
|
1877
|
+
z.object({
|
|
1878
|
+
kind: z.literal("fill_form"),
|
|
1879
|
+
fields: z.array(verifyFillFieldSchema).min(1)
|
|
1880
|
+
}),
|
|
1881
|
+
z.object({
|
|
1882
|
+
kind: z.literal("upload"),
|
|
1883
|
+
query: z.string(),
|
|
1884
|
+
file_path: z.string().min(1)
|
|
1885
|
+
}),
|
|
1886
|
+
z.object({
|
|
1887
|
+
kind: z.literal("dialog"),
|
|
1888
|
+
action: dialogActionSchema,
|
|
1889
|
+
text: z.string().optional()
|
|
1890
|
+
}),
|
|
1891
|
+
z.object({
|
|
1892
|
+
kind: z.literal("set_env"),
|
|
1893
|
+
viewport: viewportSchema.optional(),
|
|
1894
|
+
offline: z.boolean().optional(),
|
|
1895
|
+
geolocation: geolocationSchema.optional(),
|
|
1896
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1897
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1898
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1899
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1900
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1901
|
+
}),
|
|
1902
|
+
z.object({
|
|
1903
|
+
kind: z.literal("switch_page"),
|
|
1904
|
+
index: z.number().int().nonnegative()
|
|
1905
|
+
}),
|
|
1906
|
+
z.object({ kind: z.literal("evaluate"), script: z.string().min(1) })
|
|
1242
1907
|
]);
|
|
1243
1908
|
var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
1244
1909
|
z.object({ kind: z.literal("text_visible"), text: z.string() }),
|
|
@@ -1248,6 +1913,28 @@ var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
|
1248
1913
|
kind: z.literal("ref_in_state"),
|
|
1249
1914
|
query: z.string(),
|
|
1250
1915
|
state: z.enum(["visible", "enabled", "focused"])
|
|
1916
|
+
}),
|
|
1917
|
+
// v0.5 additions
|
|
1918
|
+
z.object({
|
|
1919
|
+
kind: z.literal("no_console_errors"),
|
|
1920
|
+
exclude_patterns: z.array(z.string()).optional()
|
|
1921
|
+
}),
|
|
1922
|
+
z.object({
|
|
1923
|
+
kind: z.literal("no_failed_requests"),
|
|
1924
|
+
exclude_patterns: z.array(z.string()).optional(),
|
|
1925
|
+
/** When true, only 5xx counts as a failure. Default false (4xx + 5xx). */
|
|
1926
|
+
allow_4xx: z.boolean().optional()
|
|
1927
|
+
}),
|
|
1928
|
+
z.object({
|
|
1929
|
+
kind: z.literal("request_made"),
|
|
1930
|
+
url_pattern: z.string(),
|
|
1931
|
+
method: z.string().optional(),
|
|
1932
|
+
min_count: z.number().int().positive().optional()
|
|
1933
|
+
}),
|
|
1934
|
+
z.object({
|
|
1935
|
+
kind: z.literal("response_status"),
|
|
1936
|
+
url_pattern: z.string(),
|
|
1937
|
+
status: z.number().int().min(100).max(599)
|
|
1251
1938
|
})
|
|
1252
1939
|
]);
|
|
1253
1940
|
var captureKindSchema = z.enum([
|
|
@@ -1255,7 +1942,8 @@ var captureKindSchema = z.enum([
|
|
|
1255
1942
|
"har",
|
|
1256
1943
|
"console",
|
|
1257
1944
|
"a11y_tree",
|
|
1258
|
-
"video"
|
|
1945
|
+
"video",
|
|
1946
|
+
"trace"
|
|
1259
1947
|
]);
|
|
1260
1948
|
var verifyUiFlowShape = {
|
|
1261
1949
|
mode: z.enum(["assert", "reproduce"]).default("assert"),
|
|
@@ -1333,6 +2021,19 @@ var ToolNames = {
|
|
|
1333
2021
|
browserWaitFor: "rolepod_browser_wait_for",
|
|
1334
2022
|
browserScreenshot: "rolepod_browser_screenshot",
|
|
1335
2023
|
browserNavigate: "rolepod_browser_navigate",
|
|
2024
|
+
// v0.5 atomics
|
|
2025
|
+
browserHover: "rolepod_browser_hover",
|
|
2026
|
+
browserDrag: "rolepod_browser_drag",
|
|
2027
|
+
browserFillForm: "rolepod_browser_fill_form",
|
|
2028
|
+
browserUploadFile: "rolepod_browser_upload_file",
|
|
2029
|
+
browserHandleDialog: "rolepod_browser_handle_dialog",
|
|
2030
|
+
browserConsole: "rolepod_browser_console",
|
|
2031
|
+
browserNetwork: "rolepod_browser_network",
|
|
2032
|
+
browserSetEnv: "rolepod_browser_set_env",
|
|
2033
|
+
browserEvaluate: "rolepod_browser_evaluate",
|
|
2034
|
+
browserPages: "rolepod_browser_pages",
|
|
2035
|
+
browserSwitchPage: "rolepod_browser_switch_page",
|
|
2036
|
+
// composite
|
|
1336
2037
|
verifyUiFlow: "rolepod_verify_ui_flow",
|
|
1337
2038
|
auditA11y: "rolepod_audit_a11y",
|
|
1338
2039
|
visualDiff: "rolepod_visual_diff",
|
|
@@ -1367,6 +2068,37 @@ async function ddmin(input, reproduces) {
|
|
|
1367
2068
|
return current;
|
|
1368
2069
|
}
|
|
1369
2070
|
|
|
2071
|
+
// src/util/manifest.ts
|
|
2072
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
2073
|
+
import { resolve as resolve3 } from "path";
|
|
2074
|
+
var ROLEPOD_PROTOCOL_VERSION = "rolepod/v1";
|
|
2075
|
+
async function writeManifest(input) {
|
|
2076
|
+
const manifest = {
|
|
2077
|
+
protocol: ROLEPOD_PROTOCOL_VERSION,
|
|
2078
|
+
plugin: "rolepod-uiproof",
|
|
2079
|
+
skill: input.skill,
|
|
2080
|
+
phase: input.phase,
|
|
2081
|
+
status: input.status,
|
|
2082
|
+
summary: input.summary,
|
|
2083
|
+
started_at: input.startedAt,
|
|
2084
|
+
finished_at: input.finishedAt,
|
|
2085
|
+
artifacts: input.artifacts,
|
|
2086
|
+
metadata: input.metadata ?? {}
|
|
2087
|
+
};
|
|
2088
|
+
const path = resolve3(input.runDir, "manifest.json");
|
|
2089
|
+
try {
|
|
2090
|
+
await writeFile2(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
2091
|
+
return path;
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
log.warn("manifest write failed", {
|
|
2094
|
+
run_dir: input.runDir,
|
|
2095
|
+
skill: input.skill,
|
|
2096
|
+
err: String(err)
|
|
2097
|
+
});
|
|
2098
|
+
return void 0;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
1370
2102
|
// src/tools/result.ts
|
|
1371
2103
|
function ok(value) {
|
|
1372
2104
|
return {
|
|
@@ -1408,7 +2140,11 @@ var verifyUiFlowTool = {
|
|
|
1408
2140
|
inputShape: verifyUiFlowShape,
|
|
1409
2141
|
build(ctx) {
|
|
1410
2142
|
return safeHandler(async (args) => {
|
|
1411
|
-
const
|
|
2143
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2144
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
2145
|
+
"verify",
|
|
2146
|
+
{ skill: "verify-ui" }
|
|
2147
|
+
);
|
|
1412
2148
|
const initial = await runFlow(ctx, args, args.steps, runDir, {
|
|
1413
2149
|
captureEvidence: true,
|
|
1414
2150
|
bundleName: "replay.json"
|
|
@@ -1433,17 +2169,75 @@ var verifyUiFlowTool = {
|
|
|
1433
2169
|
attempts: min.attempts
|
|
1434
2170
|
};
|
|
1435
2171
|
}
|
|
2172
|
+
const manifestPath = await writeManifest({
|
|
2173
|
+
runDir,
|
|
2174
|
+
skill,
|
|
2175
|
+
phase: "verify",
|
|
2176
|
+
status: initial.passed ? "pass" : "fail",
|
|
2177
|
+
summary: buildVerifySummary(args, initial),
|
|
2178
|
+
startedAt,
|
|
2179
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2180
|
+
artifacts: flattenVerifyEvidence(initial.evidence),
|
|
2181
|
+
metadata: {
|
|
2182
|
+
mode: args.mode,
|
|
2183
|
+
step_count: args.steps.length,
|
|
2184
|
+
expect_count: args.expect.length,
|
|
2185
|
+
...initial.finalUrl !== void 0 ? { final_url: initial.finalUrl } : {}
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
if (manifestPath) result.manifest = manifestPath;
|
|
1436
2189
|
return ok(result);
|
|
1437
2190
|
});
|
|
1438
2191
|
}
|
|
1439
2192
|
};
|
|
2193
|
+
function buildVerifySummary(args, outcome) {
|
|
2194
|
+
const stepCount = args.steps.length;
|
|
2195
|
+
const expectCount = args.expect.length;
|
|
2196
|
+
if (outcome.passed) {
|
|
2197
|
+
return `${stepCount} step(s), ${expectCount} expect(s) passed`;
|
|
2198
|
+
}
|
|
2199
|
+
if (outcome.failedAtStep !== void 0) {
|
|
2200
|
+
return `failed at step ${outcome.failedAtStep}: ${outcome.failureReason ?? "unknown"}`;
|
|
2201
|
+
}
|
|
2202
|
+
return `failed: ${outcome.failureReason ?? "unknown"}`;
|
|
2203
|
+
}
|
|
2204
|
+
function flattenVerifyEvidence(ev) {
|
|
2205
|
+
const out = [];
|
|
2206
|
+
for (const s of ev.screenshots) out.push({ type: "screenshot", path: s });
|
|
2207
|
+
if (ev.replay_bundle) out.push({ type: "replay_bundle", path: ev.replay_bundle });
|
|
2208
|
+
if (ev.console) out.push({ type: "console", path: ev.console });
|
|
2209
|
+
if (ev.a11y_tree) out.push({ type: "a11y_tree", path: ev.a11y_tree });
|
|
2210
|
+
if (ev.har) out.push({ type: "har", path: ev.har });
|
|
2211
|
+
if (ev.trace) out.push({ type: "trace", path: ev.trace });
|
|
2212
|
+
if (ev.video) for (const v of ev.video) out.push({ type: "video", path: v });
|
|
2213
|
+
return out;
|
|
2214
|
+
}
|
|
2215
|
+
function buildCaptureOptions(captures, runDir) {
|
|
2216
|
+
const cap = {};
|
|
2217
|
+
if (captures.has("har")) {
|
|
2218
|
+
cap.har = { path: resolvePath2(runDir, "network.har") };
|
|
2219
|
+
}
|
|
2220
|
+
if (captures.has("video")) {
|
|
2221
|
+
cap.video = { dir: resolvePath2(runDir, "videos") };
|
|
2222
|
+
}
|
|
2223
|
+
if (captures.has("trace")) {
|
|
2224
|
+
cap.trace = { artifactDir: runDir };
|
|
2225
|
+
}
|
|
2226
|
+
return Object.keys(cap).length > 0 ? cap : void 0;
|
|
2227
|
+
}
|
|
1440
2228
|
async function runFlow(ctx, args, steps, runDir, opts) {
|
|
1441
2229
|
const evidence = { screenshots: [] };
|
|
2230
|
+
const captures = new Set(args.capture ?? ["screenshot"]);
|
|
1442
2231
|
let passed = false;
|
|
1443
2232
|
let failedAtStep;
|
|
1444
2233
|
let failureReason;
|
|
1445
2234
|
let finalSnapshot;
|
|
1446
|
-
const
|
|
2235
|
+
const openOpts = { ...args.open };
|
|
2236
|
+
const captureCfg = buildCaptureOptions(captures, runDir);
|
|
2237
|
+
if (captureCfg) {
|
|
2238
|
+
openOpts.capture = captureCfg;
|
|
2239
|
+
}
|
|
2240
|
+
const session = await ctx.registry.open(openOpts);
|
|
1447
2241
|
const engine = ctx.registry.engineFor(session.id);
|
|
1448
2242
|
const sessionHandle = { id: session.id, platform: session.platform };
|
|
1449
2243
|
try {
|
|
@@ -1462,7 +2256,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1462
2256
|
const failures = [];
|
|
1463
2257
|
for (let i = 0; i < args.expect.length; i++) {
|
|
1464
2258
|
const expectation = args.expect[i];
|
|
1465
|
-
if (!evaluateExpect(expectation, finalSnapshot)) {
|
|
2259
|
+
if (!evaluateExpect(expectation, finalSnapshot, engine, session.id)) {
|
|
1466
2260
|
failures.push(`expect[${i}] ${describeExpect(expectation)}`);
|
|
1467
2261
|
}
|
|
1468
2262
|
}
|
|
@@ -1476,8 +2270,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1476
2270
|
passed = false;
|
|
1477
2271
|
} finally {
|
|
1478
2272
|
if (opts.captureEvidence) {
|
|
1479
|
-
|
|
1480
|
-
if (wantScreenshot) {
|
|
2273
|
+
if (captures.has("screenshot")) {
|
|
1481
2274
|
try {
|
|
1482
2275
|
const buf = await engine.screenshot(sessionHandle, true);
|
|
1483
2276
|
const p = await ctx.store.writeScreenshot(runDir, buf, "final");
|
|
@@ -1486,11 +2279,42 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1486
2279
|
failureReason ??= `screenshot capture failed: ${describeError(err)}`;
|
|
1487
2280
|
}
|
|
1488
2281
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
2282
|
+
if (captures.has("console") && engine instanceof PlaywrightEngine) {
|
|
2283
|
+
try {
|
|
2284
|
+
const messages = engine.peekBuffers(session.id).console;
|
|
2285
|
+
evidence.console = await ctx.store.writeReport(
|
|
2286
|
+
runDir,
|
|
2287
|
+
"console.json",
|
|
2288
|
+
JSON.stringify(
|
|
2289
|
+
{
|
|
2290
|
+
count: messages.length,
|
|
2291
|
+
by_level: countByLevel(messages),
|
|
2292
|
+
messages
|
|
2293
|
+
},
|
|
2294
|
+
null,
|
|
2295
|
+
2
|
|
2296
|
+
)
|
|
2297
|
+
);
|
|
2298
|
+
} catch (err) {
|
|
2299
|
+
failureReason ??= `console capture failed: ${describeError(err)}`;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (captures.has("a11y_tree") && finalSnapshot) {
|
|
2303
|
+
try {
|
|
2304
|
+
evidence.a11y_tree = await ctx.store.writeReport(
|
|
2305
|
+
runDir,
|
|
2306
|
+
"a11y_tree.json",
|
|
2307
|
+
JSON.stringify(finalSnapshot, null, 2)
|
|
2308
|
+
);
|
|
2309
|
+
} catch (err) {
|
|
2310
|
+
failureReason ??= `a11y_tree capture failed: ${describeError(err)}`;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
try {
|
|
2314
|
+
evidence.replay_bundle = await ctx.store.writeReplayBundle(
|
|
2315
|
+
runDir,
|
|
2316
|
+
{
|
|
2317
|
+
version: 1,
|
|
1494
2318
|
run_id: runDir.split("/").pop() ?? "run",
|
|
1495
2319
|
recorded_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1496
2320
|
open: args.open,
|
|
@@ -1506,12 +2330,32 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1506
2330
|
await ctx.registry.close(sessionHandle).catch(() => void 0);
|
|
1507
2331
|
}
|
|
1508
2332
|
}
|
|
2333
|
+
if (opts.captureEvidence) {
|
|
2334
|
+
if (captureCfg?.har) evidence.har = captureCfg.har.path;
|
|
2335
|
+
if (captureCfg?.trace) {
|
|
2336
|
+
evidence.trace = resolvePath2(captureCfg.trace.artifactDir, "trace.zip");
|
|
2337
|
+
}
|
|
2338
|
+
if (captureCfg?.video) {
|
|
2339
|
+
try {
|
|
2340
|
+
const files = await readdir(captureCfg.video.dir).catch(() => []);
|
|
2341
|
+
evidence.video = files.filter((f) => f.endsWith(".webm")).map((f) => resolvePath2(captureCfg.video.dir, f));
|
|
2342
|
+
} catch {
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
1509
2346
|
const out = { passed, evidence };
|
|
1510
2347
|
if (failedAtStep !== void 0) out.failedAtStep = failedAtStep;
|
|
1511
2348
|
if (failureReason !== void 0) out.failureReason = failureReason;
|
|
1512
2349
|
if (finalSnapshot) out.finalUrl = finalSnapshot.url_or_screen;
|
|
1513
2350
|
return out;
|
|
1514
2351
|
}
|
|
2352
|
+
function countByLevel(messages) {
|
|
2353
|
+
const counts = {};
|
|
2354
|
+
for (const m of messages) {
|
|
2355
|
+
counts[m.level] = (counts[m.level] ?? 0) + 1;
|
|
2356
|
+
}
|
|
2357
|
+
return counts;
|
|
2358
|
+
}
|
|
1515
2359
|
async function minimize(ctx, args, initialSteps, runDir) {
|
|
1516
2360
|
const tagged = initialSteps.map((step, origIndex) => ({ step, origIndex }));
|
|
1517
2361
|
let attempts = 0;
|
|
@@ -1575,9 +2419,84 @@ async function runStep(engine, session, step, snap) {
|
|
|
1575
2419
|
case "navigate":
|
|
1576
2420
|
await engine.navigate(session, step.url);
|
|
1577
2421
|
return;
|
|
2422
|
+
case "hover": {
|
|
2423
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
2424
|
+
if (!ref) throw missingQuery(step.query);
|
|
2425
|
+
await engine.hover(session, ref);
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
case "drag": {
|
|
2429
|
+
const fromRef = findRefByQuery(snap.tree, step.from_query);
|
|
2430
|
+
if (!fromRef) throw missingQuery(step.from_query);
|
|
2431
|
+
const toRef = findRefByQuery(snap.tree, step.to_query);
|
|
2432
|
+
if (!toRef) throw missingQuery(step.to_query);
|
|
2433
|
+
await engine.drag(session, fromRef, toRef);
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
case "fill_form": {
|
|
2437
|
+
const resolved = step.fields.map((f) => {
|
|
2438
|
+
const ref = findRefByQuery(snap.tree, f.query);
|
|
2439
|
+
if (!ref) throw missingQuery(f.query);
|
|
2440
|
+
return f.kind !== void 0 ? { ref, value: f.value, kind: f.kind } : { ref, value: f.value };
|
|
2441
|
+
});
|
|
2442
|
+
await engine.fillForm(session, resolved);
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
case "upload": {
|
|
2446
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
2447
|
+
if (!ref) throw missingQuery(step.query);
|
|
2448
|
+
await engine.uploadFile(session, ref, step.file_path);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
case "dialog": {
|
|
2452
|
+
requirePlaywright(engine, "dialog");
|
|
2453
|
+
void engine.handleDialog(session.id, {
|
|
2454
|
+
action: step.action,
|
|
2455
|
+
...step.text !== void 0 ? { text: step.text } : {}
|
|
2456
|
+
}).catch(() => void 0);
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
case "set_env": {
|
|
2460
|
+
requirePlaywright(engine, "set_env");
|
|
2461
|
+
await engine.setEnv(session.id, {
|
|
2462
|
+
...step.viewport !== void 0 ? { viewport: step.viewport } : {},
|
|
2463
|
+
...step.offline !== void 0 ? { offline: step.offline } : {},
|
|
2464
|
+
...step.geolocation !== void 0 ? { geolocation: step.geolocation } : {},
|
|
2465
|
+
...step.color_scheme !== void 0 ? { colorScheme: step.color_scheme } : {},
|
|
2466
|
+
...step.reduced_motion !== void 0 ? { reducedMotion: step.reduced_motion } : {},
|
|
2467
|
+
...step.extra_headers !== void 0 ? { extraHeaders: step.extra_headers } : {},
|
|
2468
|
+
...step.network_throttle !== void 0 ? { networkThrottle: step.network_throttle } : {},
|
|
2469
|
+
...step.cpu_throttle !== void 0 ? { cpuThrottle: step.cpu_throttle } : {}
|
|
2470
|
+
});
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
case "switch_page": {
|
|
2474
|
+
requirePlaywright(engine, "switch_page");
|
|
2475
|
+
await engine.switchPage(session.id, step.index);
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
case "evaluate": {
|
|
2479
|
+
requirePlaywright(engine, "evaluate");
|
|
2480
|
+
if (process.env.ROLEPOD_ALLOW_EVAL !== "1") {
|
|
2481
|
+
throw new RolepodMcpError(
|
|
2482
|
+
"engine_error",
|
|
2483
|
+
"verify_ui_flow step kind 'evaluate' is disabled. Restart the rolepod-uiproof MCP server with ROLEPOD_ALLOW_EVAL=1 to enable."
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
await engine.evaluate(session.id, step.script);
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
function requirePlaywright(engine, stepKind) {
|
|
2492
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2493
|
+
throw new RolepodMcpError(
|
|
2494
|
+
"unsupported_engine",
|
|
2495
|
+
`verify_ui_flow step kind "${stepKind}" is web-only and requires PlaywrightEngine.`
|
|
2496
|
+
);
|
|
1578
2497
|
}
|
|
1579
2498
|
}
|
|
1580
|
-
function evaluateExpect(exp, snap) {
|
|
2499
|
+
function evaluateExpect(exp, snap, engine, sessionId) {
|
|
1581
2500
|
switch (exp.kind) {
|
|
1582
2501
|
case "text_visible":
|
|
1583
2502
|
return treeHasText(snap.tree, exp.text);
|
|
@@ -1597,6 +2516,49 @@ function evaluateExpect(exp, snap) {
|
|
|
1597
2516
|
return node.state?.focused === true;
|
|
1598
2517
|
}
|
|
1599
2518
|
}
|
|
2519
|
+
case "no_console_errors": {
|
|
2520
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
2521
|
+
const msgs = engine.peekBuffers(sessionId).console.filter(
|
|
2522
|
+
(m) => m.level === "error"
|
|
2523
|
+
);
|
|
2524
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
2525
|
+
const remaining = msgs.filter(
|
|
2526
|
+
(m) => !excludes.some((p) => m.text.includes(p))
|
|
2527
|
+
);
|
|
2528
|
+
return remaining.length === 0;
|
|
2529
|
+
}
|
|
2530
|
+
case "no_failed_requests": {
|
|
2531
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
2532
|
+
const reqs = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
2533
|
+
if (r.failure) return true;
|
|
2534
|
+
if (r.status === void 0) return false;
|
|
2535
|
+
if (exp.allow_4xx) return r.status >= 500;
|
|
2536
|
+
return r.status >= 400;
|
|
2537
|
+
});
|
|
2538
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
2539
|
+
const remaining = reqs.filter(
|
|
2540
|
+
(r) => !excludes.some((p) => r.url.includes(p))
|
|
2541
|
+
);
|
|
2542
|
+
return remaining.length === 0;
|
|
2543
|
+
}
|
|
2544
|
+
case "request_made": {
|
|
2545
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
2546
|
+
const re = new RegExp(exp.url_pattern);
|
|
2547
|
+
const wantMethod = exp.method?.toUpperCase();
|
|
2548
|
+
const matches = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
2549
|
+
if (!re.test(r.url)) return false;
|
|
2550
|
+
if (wantMethod && r.method.toUpperCase() !== wantMethod) return false;
|
|
2551
|
+
return true;
|
|
2552
|
+
});
|
|
2553
|
+
const min = exp.min_count ?? 1;
|
|
2554
|
+
return matches.length >= min;
|
|
2555
|
+
}
|
|
2556
|
+
case "response_status": {
|
|
2557
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
2558
|
+
const re = new RegExp(exp.url_pattern);
|
|
2559
|
+
const match = engine.peekBuffers(sessionId).network.find((r) => re.test(r.url) && r.status === exp.status);
|
|
2560
|
+
return match !== void 0;
|
|
2561
|
+
}
|
|
1600
2562
|
}
|
|
1601
2563
|
}
|
|
1602
2564
|
function describeExpect(exp) {
|
|
@@ -1609,6 +2571,14 @@ function describeExpect(exp) {
|
|
|
1609
2571
|
return `url_matches /${exp.pattern}/`;
|
|
1610
2572
|
case "ref_in_state":
|
|
1611
2573
|
return `ref_in_state "${exp.query}" \u2192 ${exp.state}`;
|
|
2574
|
+
case "no_console_errors":
|
|
2575
|
+
return "no_console_errors";
|
|
2576
|
+
case "no_failed_requests":
|
|
2577
|
+
return "no_failed_requests";
|
|
2578
|
+
case "request_made":
|
|
2579
|
+
return `request_made ${exp.method ?? ""} ${exp.url_pattern}`.trim();
|
|
2580
|
+
case "response_status":
|
|
2581
|
+
return `response_status ${exp.url_pattern} = ${exp.status}`;
|
|
1612
2582
|
}
|
|
1613
2583
|
}
|
|
1614
2584
|
function missingQuery(query) {
|
|
@@ -1655,7 +2625,7 @@ function treeHasText(tree, text) {
|
|
|
1655
2625
|
|
|
1656
2626
|
// src/cli/replay.ts
|
|
1657
2627
|
async function runReplay(bundlePath) {
|
|
1658
|
-
const abs =
|
|
2628
|
+
const abs = resolve4(bundlePath);
|
|
1659
2629
|
const raw = await readFile(abs, "utf8");
|
|
1660
2630
|
const bundle = JSON.parse(raw);
|
|
1661
2631
|
if (bundle.version !== 1) {
|
|
@@ -1725,6 +2695,151 @@ var browserCloseTool = {
|
|
|
1725
2695
|
}
|
|
1726
2696
|
};
|
|
1727
2697
|
|
|
2698
|
+
// src/tools/atomic/browser_console.ts
|
|
2699
|
+
var browserConsoleTool = {
|
|
2700
|
+
name: ToolNames.browserConsole,
|
|
2701
|
+
description: "List console messages emitted by the active page since the session opened (or since the last `clear`). Filters: `levels` (default: errors+warnings), substring `contains`, and `limit` (default 50, max 1000). Set `clear: true` to drain the buffer after returning. Read-only.",
|
|
2702
|
+
inputShape: browserConsoleShape,
|
|
2703
|
+
build(ctx) {
|
|
2704
|
+
return safeHandler(async (args) => {
|
|
2705
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2706
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2707
|
+
throw new RolepodMcpError(
|
|
2708
|
+
"unsupported_engine",
|
|
2709
|
+
"console is web-only and requires PlaywrightEngine."
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
const levels = args.levels && args.levels.length > 0 ? args.levels : ["error", "warning"];
|
|
2713
|
+
const messages = engine.getConsole(args.session_id, {
|
|
2714
|
+
levels,
|
|
2715
|
+
...args.contains !== void 0 ? { contains: args.contains } : {},
|
|
2716
|
+
clear: args.clear,
|
|
2717
|
+
limit: args.limit
|
|
2718
|
+
});
|
|
2719
|
+
return ok({
|
|
2720
|
+
count: messages.length,
|
|
2721
|
+
messages
|
|
2722
|
+
});
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
|
|
2727
|
+
// src/tools/atomic/browser_drag.ts
|
|
2728
|
+
var browserDragTool = {
|
|
2729
|
+
name: ToolNames.browserDrag,
|
|
2730
|
+
description: "Drag the element identified by `from_ref` onto the element identified by `to_ref`. Both refs come from the most recent snapshot. Invalidates all refs on success.",
|
|
2731
|
+
inputShape: browserDragShape,
|
|
2732
|
+
build(ctx) {
|
|
2733
|
+
return safeHandler(async (args) => {
|
|
2734
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2735
|
+
await engine.drag(
|
|
2736
|
+
{
|
|
2737
|
+
id: args.session_id,
|
|
2738
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2739
|
+
},
|
|
2740
|
+
args.from_ref,
|
|
2741
|
+
args.to_ref
|
|
2742
|
+
);
|
|
2743
|
+
return ok({ dragged: true });
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
};
|
|
2747
|
+
|
|
2748
|
+
// src/tools/atomic/browser_evaluate.ts
|
|
2749
|
+
var browserEvaluateTool = {
|
|
2750
|
+
name: ToolNames.browserEvaluate,
|
|
2751
|
+
description: "Execute JavaScript in the active page's context. The `script` is the body of an async function \u2014 use `return` for the result and reference inputs via the implicit `args` array. DISABLED unless the MCP server was launched with `ROLEPOD_ALLOW_EVAL=1`; intended for trusted automation setups only (state seeding, computed-style reads, synthetic event dispatch).",
|
|
2752
|
+
inputShape: browserEvaluateShape,
|
|
2753
|
+
build(ctx) {
|
|
2754
|
+
const allowed = process.env.ROLEPOD_ALLOW_EVAL === "1";
|
|
2755
|
+
return safeHandler(async (args) => {
|
|
2756
|
+
if (!allowed) {
|
|
2757
|
+
throw new RolepodMcpError(
|
|
2758
|
+
"engine_error",
|
|
2759
|
+
"browser_evaluate is disabled. Restart the rolepod-uiproof MCP server with the env var ROLEPOD_ALLOW_EVAL=1 to enable arbitrary JavaScript execution in the page context."
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2763
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2764
|
+
throw new RolepodMcpError(
|
|
2765
|
+
"unsupported_engine",
|
|
2766
|
+
"evaluate is web-only and requires PlaywrightEngine."
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
const result = await engine.evaluate(
|
|
2770
|
+
args.session_id,
|
|
2771
|
+
args.script,
|
|
2772
|
+
args.args
|
|
2773
|
+
);
|
|
2774
|
+
return ok({ result });
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2779
|
+
// src/tools/atomic/browser_fill_form.ts
|
|
2780
|
+
var browserFillFormTool = {
|
|
2781
|
+
name: ToolNames.browserFillForm,
|
|
2782
|
+
description: "Batch-fill multiple form fields (inputs, selects, checkboxes, radios) in one call. Each field needs a `ref` from the latest snapshot and a `value`; pass `kind` to disambiguate non-input controls. Token-efficient alternative to a sequence of `type` calls. Invalidates all refs on success.",
|
|
2783
|
+
inputShape: browserFillFormShape,
|
|
2784
|
+
build(ctx) {
|
|
2785
|
+
return safeHandler(async (args) => {
|
|
2786
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2787
|
+
await engine.fillForm(
|
|
2788
|
+
{
|
|
2789
|
+
id: args.session_id,
|
|
2790
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2791
|
+
},
|
|
2792
|
+
args.fields
|
|
2793
|
+
);
|
|
2794
|
+
return ok({ filled: args.fields.length });
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
|
|
2799
|
+
// src/tools/atomic/browser_handle_dialog.ts
|
|
2800
|
+
var browserHandleDialogTool = {
|
|
2801
|
+
name: ToolNames.browserHandleDialog,
|
|
2802
|
+
description: "Pre-arm a one-shot handler for the NEXT JavaScript dialog (`alert`/`confirm`/`prompt`/`beforeunload`) on the active page. Call this BEFORE the action that triggers the dialog (e.g. before clicking the button that calls `confirm()`). Returns when the dialog fires or the timeout (default 30s) elapses. Un-armed dialogs are auto-dismissed so the page does not hang.",
|
|
2803
|
+
inputShape: browserHandleDialogShape,
|
|
2804
|
+
build(ctx) {
|
|
2805
|
+
return safeHandler(async (args) => {
|
|
2806
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2807
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2808
|
+
throw new RolepodMcpError(
|
|
2809
|
+
"unsupported_engine",
|
|
2810
|
+
"handle_dialog is web-only and requires PlaywrightEngine."
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
const { handled } = await engine.handleDialog(args.session_id, {
|
|
2814
|
+
action: args.action,
|
|
2815
|
+
...args.text !== void 0 ? { text: args.text } : {},
|
|
2816
|
+
...args.timeout_ms !== void 0 ? { timeoutMs: args.timeout_ms } : {}
|
|
2817
|
+
});
|
|
2818
|
+
return ok({ handled, action: args.action });
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
|
|
2823
|
+
// src/tools/atomic/browser_hover.ts
|
|
2824
|
+
var browserHoverTool = {
|
|
2825
|
+
name: ToolNames.browserHover,
|
|
2826
|
+
description: "Hover the pointer over the element identified by `ref`. Refs remain valid afterwards (read-mostly).",
|
|
2827
|
+
inputShape: browserHoverShape,
|
|
2828
|
+
build(ctx) {
|
|
2829
|
+
return safeHandler(async (args) => {
|
|
2830
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2831
|
+
await engine.hover(
|
|
2832
|
+
{
|
|
2833
|
+
id: args.session_id,
|
|
2834
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2835
|
+
},
|
|
2836
|
+
args.ref
|
|
2837
|
+
);
|
|
2838
|
+
return ok({ hovered: true });
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
|
|
1728
2843
|
// src/tools/atomic/browser_key.ts
|
|
1729
2844
|
var browserKeyTool = {
|
|
1730
2845
|
name: ToolNames.browserKey,
|
|
@@ -1753,6 +2868,46 @@ var browserNavigateTool = {
|
|
|
1753
2868
|
}
|
|
1754
2869
|
};
|
|
1755
2870
|
|
|
2871
|
+
// src/tools/atomic/browser_network.ts
|
|
2872
|
+
var browserNetworkTool = {
|
|
2873
|
+
name: ToolNames.browserNetwork,
|
|
2874
|
+
description: 'List network requests captured on the active page. Filters: `url_pattern` (substring or regex via `pattern_kind`), `method`, `status_range`, `only_failed`. Set `export_har: true` to require the session to have been opened with HAR recording \u2014 see `verify_ui_flow` capture=["har"] or call `browser_open` with capture.har=true. Read-only unless `clear: true`.',
|
|
2875
|
+
inputShape: browserNetworkShape,
|
|
2876
|
+
build(ctx) {
|
|
2877
|
+
return safeHandler(async (args) => {
|
|
2878
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2879
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2880
|
+
throw new RolepodMcpError(
|
|
2881
|
+
"unsupported_engine",
|
|
2882
|
+
"network is web-only and requires PlaywrightEngine."
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
const requests = engine.getNetwork(args.session_id, {
|
|
2886
|
+
...args.url_pattern !== void 0 ? { urlPattern: args.url_pattern } : {},
|
|
2887
|
+
patternKind: args.pattern_kind,
|
|
2888
|
+
...args.method !== void 0 ? { method: args.method } : {},
|
|
2889
|
+
...args.status_range !== void 0 ? { statusRange: args.status_range } : {},
|
|
2890
|
+
onlyFailed: args.only_failed,
|
|
2891
|
+
clear: args.clear,
|
|
2892
|
+
limit: args.limit
|
|
2893
|
+
});
|
|
2894
|
+
const failed = requests.filter(
|
|
2895
|
+
(r) => !!r.failure || r.status !== void 0 && r.status >= 400
|
|
2896
|
+
).length;
|
|
2897
|
+
return ok({
|
|
2898
|
+
count: requests.length,
|
|
2899
|
+
failed_count: failed,
|
|
2900
|
+
requests,
|
|
2901
|
+
// HAR file lives wherever the session was opened with
|
|
2902
|
+
// `capture.har.path`. We don't echo it here to avoid leaking
|
|
2903
|
+
// filesystem paths into untrusted logs; the verify_ui_flow run
|
|
2904
|
+
// result surfaces it in `evidence_paths.har`.
|
|
2905
|
+
har_recording: args.export_har ? "HAR is written at session close to the path passed via capture.har at open time." : void 0
|
|
2906
|
+
});
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
};
|
|
2910
|
+
|
|
1756
2911
|
// src/tools/atomic/browser_open.ts
|
|
1757
2912
|
var browserOpenTool = {
|
|
1758
2913
|
name: ToolNames.browserOpen,
|
|
@@ -1766,6 +2921,34 @@ var browserOpenTool = {
|
|
|
1766
2921
|
}
|
|
1767
2922
|
};
|
|
1768
2923
|
|
|
2924
|
+
// src/tools/atomic/browser_pages.ts
|
|
2925
|
+
var browserPagesTool = {
|
|
2926
|
+
name: ToolNames.browserPages,
|
|
2927
|
+
description: "List all pages currently open in the session's browser context \u2014 typically just the main page, plus any popups, OAuth windows, or `target=_blank` tabs that the page itself opened. Each entry carries `{ index, url, title, active }`. Read-only.",
|
|
2928
|
+
inputShape: browserPagesShape,
|
|
2929
|
+
build(ctx) {
|
|
2930
|
+
return safeHandler(async (args) => {
|
|
2931
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2932
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2933
|
+
throw new RolepodMcpError(
|
|
2934
|
+
"unsupported_engine",
|
|
2935
|
+
"pages is web-only and requires PlaywrightEngine."
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
const raw = engine.listPages(args.session_id);
|
|
2939
|
+
const pages = await Promise.all(
|
|
2940
|
+
raw.map(async (p) => ({
|
|
2941
|
+
index: p.index,
|
|
2942
|
+
url: p.url,
|
|
2943
|
+
title: await p.title_promise.catch(() => ""),
|
|
2944
|
+
active: p.active
|
|
2945
|
+
}))
|
|
2946
|
+
);
|
|
2947
|
+
return ok({ count: pages.length, pages });
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
|
|
1769
2952
|
// src/tools/atomic/browser_screenshot.ts
|
|
1770
2953
|
var browserScreenshotTool = {
|
|
1771
2954
|
name: ToolNames.browserScreenshot,
|
|
@@ -1810,6 +2993,35 @@ var browserScrollTool = {
|
|
|
1810
2993
|
}
|
|
1811
2994
|
};
|
|
1812
2995
|
|
|
2996
|
+
// src/tools/atomic/browser_set_env.ts
|
|
2997
|
+
var browserSetEnvTool = {
|
|
2998
|
+
name: ToolNames.browserSetEnv,
|
|
2999
|
+
description: "Mutate session environment at runtime: viewport, offline state, geolocation, color_scheme (`light`/`dark`), reduced_motion, extra HTTP headers, network throttle preset (`slow-3g`/`fast-3g`/`slow-4g`/`fast-4g`/`offline`/`no-throttling`), and CPU throttle multiplier. `network_throttle` and `cpu_throttle` are chromium-only (CDP). `user_agent`, `locale`, and `timezone` cannot be changed mid-session \u2014 set them at `browser_open` time. Invalidates all refs.",
|
|
3000
|
+
inputShape: browserSetEnvShape,
|
|
3001
|
+
build(ctx) {
|
|
3002
|
+
return safeHandler(async (args) => {
|
|
3003
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
3004
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
3005
|
+
throw new RolepodMcpError(
|
|
3006
|
+
"unsupported_engine",
|
|
3007
|
+
"set_env is web-only and requires PlaywrightEngine."
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
await engine.setEnv(args.session_id, {
|
|
3011
|
+
...args.viewport !== void 0 ? { viewport: args.viewport } : {},
|
|
3012
|
+
...args.offline !== void 0 ? { offline: args.offline } : {},
|
|
3013
|
+
...args.geolocation !== void 0 ? { geolocation: args.geolocation } : {},
|
|
3014
|
+
...args.color_scheme !== void 0 ? { colorScheme: args.color_scheme } : {},
|
|
3015
|
+
...args.reduced_motion !== void 0 ? { reducedMotion: args.reduced_motion } : {},
|
|
3016
|
+
...args.extra_headers !== void 0 ? { extraHeaders: args.extra_headers } : {},
|
|
3017
|
+
...args.network_throttle !== void 0 ? { networkThrottle: args.network_throttle } : {},
|
|
3018
|
+
...args.cpu_throttle !== void 0 ? { cpuThrottle: args.cpu_throttle } : {}
|
|
3019
|
+
});
|
|
3020
|
+
return ok({ applied: true });
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
|
|
1813
3025
|
// src/tools/atomic/browser_snapshot.ts
|
|
1814
3026
|
var browserSnapshotTool = {
|
|
1815
3027
|
name: ToolNames.browserSnapshot,
|
|
@@ -1832,6 +3044,26 @@ var browserSnapshotTool = {
|
|
|
1832
3044
|
}
|
|
1833
3045
|
};
|
|
1834
3046
|
|
|
3047
|
+
// src/tools/atomic/browser_switch_page.ts
|
|
3048
|
+
var browserSwitchPageTool = {
|
|
3049
|
+
name: ToolNames.browserSwitchPage,
|
|
3050
|
+
description: "Set the active page for subsequent tool calls. Use `browser_pages` to discover indexes. Switching invalidates all refs because each page has its own DOM. Page 0 is the main page; popups land at higher indexes in the order they opened.",
|
|
3051
|
+
inputShape: browserSwitchPageShape,
|
|
3052
|
+
build(ctx) {
|
|
3053
|
+
return safeHandler(async (args) => {
|
|
3054
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
3055
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
3056
|
+
throw new RolepodMcpError(
|
|
3057
|
+
"unsupported_engine",
|
|
3058
|
+
"switch_page is web-only and requires PlaywrightEngine."
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
await engine.switchPage(args.session_id, args.index);
|
|
3062
|
+
return ok({ active_index: args.index });
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
|
|
1835
3067
|
// src/tools/atomic/browser_type.ts
|
|
1836
3068
|
var browserTypeTool = {
|
|
1837
3069
|
name: ToolNames.browserType,
|
|
@@ -1851,6 +3083,27 @@ var browserTypeTool = {
|
|
|
1851
3083
|
}
|
|
1852
3084
|
};
|
|
1853
3085
|
|
|
3086
|
+
// src/tools/atomic/browser_upload_file.ts
|
|
3087
|
+
var browserUploadFileTool = {
|
|
3088
|
+
name: ToolNames.browserUploadFile,
|
|
3089
|
+
description: "Attach a local file to the `<input type=file>` element identified by `ref`. `file_path` MUST be an absolute path on the host filesystem. Invalidates all refs on success.",
|
|
3090
|
+
inputShape: browserUploadFileShape,
|
|
3091
|
+
build(ctx) {
|
|
3092
|
+
return safeHandler(async (args) => {
|
|
3093
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
3094
|
+
await engine.uploadFile(
|
|
3095
|
+
{
|
|
3096
|
+
id: args.session_id,
|
|
3097
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
3098
|
+
},
|
|
3099
|
+
args.ref,
|
|
3100
|
+
args.file_path
|
|
3101
|
+
);
|
|
3102
|
+
return ok({ uploaded: true, file_path: args.file_path });
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
};
|
|
3106
|
+
|
|
1854
3107
|
// src/tools/atomic/browser_wait_for.ts
|
|
1855
3108
|
var browserWaitForTool = {
|
|
1856
3109
|
name: ToolNames.browserWaitFor,
|
|
@@ -1896,7 +3149,11 @@ var auditA11yTool = {
|
|
|
1896
3149
|
inputShape: auditA11yShape,
|
|
1897
3150
|
build(ctx) {
|
|
1898
3151
|
return safeHandler(async (args) => {
|
|
1899
|
-
const
|
|
3152
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3153
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3154
|
+
"audit",
|
|
3155
|
+
{ skill: "audit-a11y" }
|
|
3156
|
+
);
|
|
1900
3157
|
const session = await ctx.registry.open(args.open);
|
|
1901
3158
|
const engine = ctx.registry.engineFor(session.id);
|
|
1902
3159
|
if (!(engine instanceof PlaywrightEngine)) {
|
|
@@ -1964,15 +3221,45 @@ var auditA11yTool = {
|
|
|
1964
3221
|
await ctx.registry.close(session).catch(() => void 0);
|
|
1965
3222
|
}
|
|
1966
3223
|
}
|
|
3224
|
+
const counts = countBySeverity(issues);
|
|
3225
|
+
const status = a11yStatus(counts);
|
|
3226
|
+
const artifacts = reportPath ? [{ type: "report", path: reportPath }] : [];
|
|
3227
|
+
const manifestPath = await writeManifest({
|
|
3228
|
+
runDir,
|
|
3229
|
+
skill,
|
|
3230
|
+
phase: "verify",
|
|
3231
|
+
status,
|
|
3232
|
+
summary: buildAuditSummary(args.level, counts, status),
|
|
3233
|
+
startedAt,
|
|
3234
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3235
|
+
artifacts,
|
|
3236
|
+
metadata: {
|
|
3237
|
+
level: args.level,
|
|
3238
|
+
scope: args.scope,
|
|
3239
|
+
counts,
|
|
3240
|
+
report_format: args.report_format
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
1967
3243
|
return ok({
|
|
1968
3244
|
run_id: runId,
|
|
1969
|
-
counts
|
|
3245
|
+
counts,
|
|
1970
3246
|
issues,
|
|
1971
|
-
report_path: reportPath
|
|
3247
|
+
report_path: reportPath,
|
|
3248
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
1972
3249
|
});
|
|
1973
3250
|
});
|
|
1974
3251
|
}
|
|
1975
3252
|
};
|
|
3253
|
+
function a11yStatus(counts) {
|
|
3254
|
+
if ((counts.critical ?? 0) + (counts.serious ?? 0) > 0) return "fail";
|
|
3255
|
+
if ((counts.moderate ?? 0) + (counts.minor ?? 0) > 0) return "warn";
|
|
3256
|
+
return "pass";
|
|
3257
|
+
}
|
|
3258
|
+
function buildAuditSummary(level, counts, status) {
|
|
3259
|
+
const total = (counts.critical ?? 0) + (counts.serious ?? 0) + (counts.moderate ?? 0) + (counts.minor ?? 0);
|
|
3260
|
+
if (status === "pass") return `${level}: 0 issues`;
|
|
3261
|
+
return `${level}: ${total} issue(s) \u2014 critical=${counts.critical ?? 0}, serious=${counts.serious ?? 0}, moderate=${counts.moderate ?? 0}, minor=${counts.minor ?? 0}`;
|
|
3262
|
+
}
|
|
1976
3263
|
function pickWcagRef(tags) {
|
|
1977
3264
|
return tags.find((t) => /^wcag\d/.test(t));
|
|
1978
3265
|
}
|
|
@@ -2089,14 +3376,18 @@ function scoreTree(root, tokens) {
|
|
|
2089
3376
|
|
|
2090
3377
|
// src/tools/composite/scaffold_e2e.ts
|
|
2091
3378
|
import { readFile as readFile2 } from "fs/promises";
|
|
2092
|
-
import { resolve as
|
|
3379
|
+
import { resolve as resolve5 } from "path";
|
|
2093
3380
|
var scaffoldE2eTool = {
|
|
2094
3381
|
name: ToolNames.scaffoldE2e,
|
|
2095
3382
|
description: "Generate a runnable e2e test file (playwright-test, vitest+playwright, or pytest+selenium) from a scenario description and optional replay bundle from a prior verify_ui_flow run.",
|
|
2096
3383
|
inputShape: scaffoldE2eShape,
|
|
2097
3384
|
build(ctx) {
|
|
2098
3385
|
return safeHandler(async (args) => {
|
|
2099
|
-
const
|
|
3386
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3387
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3388
|
+
"scaffold",
|
|
3389
|
+
{ skill: "scaffold-e2e" }
|
|
3390
|
+
);
|
|
2100
3391
|
const slug = slugify(args.scenario_nl);
|
|
2101
3392
|
const bundle = args.recorded_bundle ? await loadReplay(args.recorded_bundle) : null;
|
|
2102
3393
|
const ctxObj = { args, slug, bundle };
|
|
@@ -2134,19 +3425,35 @@ var scaffoldE2eTool = {
|
|
|
2134
3425
|
);
|
|
2135
3426
|
}
|
|
2136
3427
|
const path = await ctx.store.writeReport(runDir, filename, body);
|
|
3428
|
+
const manifestPath = await writeManifest({
|
|
3429
|
+
runDir,
|
|
3430
|
+
skill,
|
|
3431
|
+
phase: "build",
|
|
3432
|
+
status: "pass",
|
|
3433
|
+
summary: `generated ${args.framework} test "${filename}" from ${bundle ? "replay bundle" : "scenario"}`,
|
|
3434
|
+
startedAt,
|
|
3435
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3436
|
+
artifacts: [{ type: "test_file", path }],
|
|
3437
|
+
metadata: {
|
|
3438
|
+
framework: args.framework,
|
|
3439
|
+
language,
|
|
3440
|
+
from_replay_bundle: Boolean(bundle)
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
2137
3443
|
return ok({
|
|
2138
3444
|
run_id: runId,
|
|
2139
3445
|
test_file_path: path,
|
|
2140
3446
|
language,
|
|
2141
3447
|
dependencies,
|
|
2142
3448
|
setup_notes: setupNotes,
|
|
2143
|
-
from_replay_bundle: Boolean(bundle)
|
|
3449
|
+
from_replay_bundle: Boolean(bundle),
|
|
3450
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
2144
3451
|
});
|
|
2145
3452
|
});
|
|
2146
3453
|
}
|
|
2147
3454
|
};
|
|
2148
3455
|
async function loadReplay(bundlePath) {
|
|
2149
|
-
const raw = await readFile2(
|
|
3456
|
+
const raw = await readFile2(resolve5(bundlePath), "utf8");
|
|
2150
3457
|
return JSON.parse(raw);
|
|
2151
3458
|
}
|
|
2152
3459
|
function slugify(s) {
|
|
@@ -2223,6 +3530,67 @@ function playwrightStepLine(step) {
|
|
|
2223
3530
|
return ` await page.goto(${JSON.stringify(step.url)});`;
|
|
2224
3531
|
case "wait_for":
|
|
2225
3532
|
return ` // wait_for: ${JSON.stringify(step.condition)} \u2014 translate to page.waitForXxx()`;
|
|
3533
|
+
case "hover":
|
|
3534
|
+
return ` await page.getByText(${JSON.stringify(step.query)}, { exact: false }).first().hover();`;
|
|
3535
|
+
case "drag":
|
|
3536
|
+
return [
|
|
3537
|
+
` await page`,
|
|
3538
|
+
` .getByText(${JSON.stringify(step.from_query)}, { exact: false })`,
|
|
3539
|
+
` .first()`,
|
|
3540
|
+
` .dragTo(page.getByText(${JSON.stringify(step.to_query)}, { exact: false }).first());`
|
|
3541
|
+
].join("\n");
|
|
3542
|
+
case "fill_form": {
|
|
3543
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
3544
|
+
return fields.map((f) => {
|
|
3545
|
+
const q = JSON.stringify(f.query);
|
|
3546
|
+
if (f.kind === "select") {
|
|
3547
|
+
return ` await page.getByLabel(${q}).selectOption(${JSON.stringify(String(f.value))});`;
|
|
3548
|
+
}
|
|
3549
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
3550
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true" || String(f.value) === "on";
|
|
3551
|
+
return ` await page.getByLabel(${q}).setChecked(${checked});`;
|
|
3552
|
+
}
|
|
3553
|
+
return ` await page.getByLabel(${q}).fill(${JSON.stringify(String(f.value))});`;
|
|
3554
|
+
}).join("\n");
|
|
3555
|
+
}
|
|
3556
|
+
case "upload":
|
|
3557
|
+
return ` await page.getByLabel(${JSON.stringify(step.query)}).setInputFiles(${JSON.stringify(step.file_path)});`;
|
|
3558
|
+
case "dialog":
|
|
3559
|
+
return [
|
|
3560
|
+
` page.once("dialog", async (dialog) => {`,
|
|
3561
|
+
step.action === "accept" ? ` await dialog.accept();` : step.action === "accept_with_text" ? ` await dialog.accept(${JSON.stringify(step.text ?? "")});` : ` await dialog.dismiss();`,
|
|
3562
|
+
` });`
|
|
3563
|
+
].join("\n");
|
|
3564
|
+
case "set_env": {
|
|
3565
|
+
const lines = [];
|
|
3566
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
3567
|
+
const v = step.viewport;
|
|
3568
|
+
lines.push(` await page.setViewportSize({ width: ${v.width}, height: ${v.height} });`);
|
|
3569
|
+
}
|
|
3570
|
+
if (step.offline !== void 0) {
|
|
3571
|
+
lines.push(` await page.context().setOffline(${Boolean(step.offline)});`);
|
|
3572
|
+
}
|
|
3573
|
+
if (step.geolocation) {
|
|
3574
|
+
lines.push(` await page.context().setGeolocation(${JSON.stringify(step.geolocation)});`);
|
|
3575
|
+
}
|
|
3576
|
+
if (step.color_scheme || step.reduced_motion) {
|
|
3577
|
+
const opts = {};
|
|
3578
|
+
if (step.color_scheme) opts.colorScheme = step.color_scheme;
|
|
3579
|
+
if (step.reduced_motion) opts.reducedMotion = step.reduced_motion;
|
|
3580
|
+
lines.push(` await page.emulateMedia(${JSON.stringify(opts)});`);
|
|
3581
|
+
}
|
|
3582
|
+
if (step.extra_headers) {
|
|
3583
|
+
lines.push(` await page.context().setExtraHTTPHeaders(${JSON.stringify(step.extra_headers)});`);
|
|
3584
|
+
}
|
|
3585
|
+
if (step.network_throttle || step.cpu_throttle !== void 0) {
|
|
3586
|
+
lines.push(` // network/cpu throttle requires CDP \u2014 see Playwright docs (chromium only)`);
|
|
3587
|
+
}
|
|
3588
|
+
return lines.length > 0 ? lines.join("\n") : ` // set_env: nothing to apply`;
|
|
3589
|
+
}
|
|
3590
|
+
case "switch_page":
|
|
3591
|
+
return ` const allPages = page.context().pages(); /* switch to index ${step.index} */ if (allPages[${step.index}]) await allPages[${step.index}].bringToFront();`;
|
|
3592
|
+
case "evaluate":
|
|
3593
|
+
return ` await page.evaluate(${JSON.stringify(step.script)});`;
|
|
2226
3594
|
default:
|
|
2227
3595
|
return ` // unsupported step kind: ${step.kind}`;
|
|
2228
3596
|
}
|
|
@@ -2237,6 +3605,20 @@ function playwrightExpectLine(exp) {
|
|
|
2237
3605
|
return ` await expect(page).toHaveURL(new RegExp(${JSON.stringify(exp.pattern)}));`;
|
|
2238
3606
|
case "ref_in_state":
|
|
2239
3607
|
return ` // ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)} \u2014 translate as needed`;
|
|
3608
|
+
case "no_console_errors":
|
|
3609
|
+
return [
|
|
3610
|
+
` // no_console_errors \u2014 collect via page.on('console') before the steps, then:`,
|
|
3611
|
+
` // expect(consoleErrors).toEqual([]);`
|
|
3612
|
+
].join("\n");
|
|
3613
|
+
case "no_failed_requests":
|
|
3614
|
+
return [
|
|
3615
|
+
` // no_failed_requests \u2014 collect via page.on('requestfailed'/'response') before the steps, then:`,
|
|
3616
|
+
` // expect(failedRequests).toEqual([]);`
|
|
3617
|
+
].join("\n");
|
|
3618
|
+
case "request_made":
|
|
3619
|
+
return ` await page.waitForRequest(new RegExp(${JSON.stringify(exp.url_pattern)}));`;
|
|
3620
|
+
case "response_status":
|
|
3621
|
+
return ` await page.waitForResponse((r) => new RegExp(${JSON.stringify(exp.url_pattern)}).test(r.url()) && r.status() === ${Number(exp.status)});`;
|
|
2240
3622
|
default:
|
|
2241
3623
|
return ` // unsupported expect kind: ${exp.kind}`;
|
|
2242
3624
|
}
|
|
@@ -2253,6 +3635,56 @@ function seleniumStepLine(step) {
|
|
|
2253
3635
|
return ` driver.get(${JSON.stringify(step.url)})`;
|
|
2254
3636
|
case "wait_for":
|
|
2255
3637
|
return ` # wait_for: ${JSON.stringify(step.condition)} \u2014 translate to WebDriverWait`;
|
|
3638
|
+
case "hover":
|
|
3639
|
+
return [
|
|
3640
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
3641
|
+
` target = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.query))}\\")]")`,
|
|
3642
|
+
` ActionChains(driver).move_to_element(target).perform()`
|
|
3643
|
+
].join("\n");
|
|
3644
|
+
case "drag":
|
|
3645
|
+
return [
|
|
3646
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
3647
|
+
` src = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.from_query))}\\")]")`,
|
|
3648
|
+
` dst = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.to_query))}\\")]")`,
|
|
3649
|
+
` ActionChains(driver).drag_and_drop(src, dst).perform()`
|
|
3650
|
+
].join("\n");
|
|
3651
|
+
case "fill_form": {
|
|
3652
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
3653
|
+
return fields.map((f) => {
|
|
3654
|
+
const q = escapePy(f.query);
|
|
3655
|
+
if (f.kind === "select") {
|
|
3656
|
+
return [
|
|
3657
|
+
` from selenium.webdriver.support.ui import Select`,
|
|
3658
|
+
` Select(driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]")).select_by_visible_text(${JSON.stringify(String(f.value))})`
|
|
3659
|
+
].join("\n");
|
|
3660
|
+
}
|
|
3661
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
3662
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true";
|
|
3663
|
+
return ` el = driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]"); el.click() if el.is_selected() != ${checked ? "True" : "False"} else None`;
|
|
3664
|
+
}
|
|
3665
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]").send_keys(${JSON.stringify(String(f.value))})`;
|
|
3666
|
+
}).join("\n");
|
|
3667
|
+
}
|
|
3668
|
+
case "upload":
|
|
3669
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${escapePy(String(step.query))}\\"]").send_keys(${JSON.stringify(step.file_path)})`;
|
|
3670
|
+
case "dialog":
|
|
3671
|
+
return [
|
|
3672
|
+
` alert = driver.switch_to.alert`,
|
|
3673
|
+
step.action === "accept" ? ` alert.accept()` : step.action === "accept_with_text" ? ` alert.send_keys(${JSON.stringify(step.text ?? "")}); alert.accept()` : ` alert.dismiss()`
|
|
3674
|
+
].join("\n");
|
|
3675
|
+
case "set_env": {
|
|
3676
|
+
const lines = [];
|
|
3677
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
3678
|
+
const v = step.viewport;
|
|
3679
|
+
lines.push(` driver.set_window_size(${v.width}, ${v.height})`);
|
|
3680
|
+
}
|
|
3681
|
+
lines.push(` # set_env partially supported in Selenium \u2014 see selenium docs for offline/geolocation/colorScheme via CDP`);
|
|
3682
|
+
return lines.join("\n");
|
|
3683
|
+
}
|
|
3684
|
+
case "switch_page":
|
|
3685
|
+
return ` driver.switch_to.window(driver.window_handles[${step.index}])`;
|
|
3686
|
+
case "evaluate":
|
|
3687
|
+
return ` driver.execute_script(${JSON.stringify(step.script)})`;
|
|
2256
3688
|
default:
|
|
2257
3689
|
return ` # unsupported step kind: ${step.kind}`;
|
|
2258
3690
|
}
|
|
@@ -2267,6 +3699,18 @@ function seleniumExpectLine(exp) {
|
|
|
2267
3699
|
return ` import re; assert re.search(${JSON.stringify(exp.pattern)}, driver.current_url)`;
|
|
2268
3700
|
case "ref_in_state":
|
|
2269
3701
|
return ` # ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)}`;
|
|
3702
|
+
case "no_console_errors":
|
|
3703
|
+
return [
|
|
3704
|
+
` # no_console_errors \u2014 read browser logs via driver.get_log("browser")`,
|
|
3705
|
+
` errors = [l for l in driver.get_log("browser") if l.get("level") == "SEVERE"]`,
|
|
3706
|
+
` assert errors == [], f"console errors: {errors}"`
|
|
3707
|
+
].join("\n");
|
|
3708
|
+
case "no_failed_requests":
|
|
3709
|
+
return ` # no_failed_requests \u2014 selenium has no built-in network capture. Enable selenium-wire or BiDi for this.`;
|
|
3710
|
+
case "request_made":
|
|
3711
|
+
return ` # request_made ${JSON.stringify(exp.url_pattern)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
3712
|
+
case "response_status":
|
|
3713
|
+
return ` # response_status ${JSON.stringify(exp.url_pattern)} == ${Number(exp.status)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
2270
3714
|
default:
|
|
2271
3715
|
return ` # unsupported expect kind: ${exp.kind}`;
|
|
2272
3716
|
}
|
|
@@ -2298,7 +3742,7 @@ function indent(block, n) {
|
|
|
2298
3742
|
// src/tools/composite/visual_diff.ts
|
|
2299
3743
|
import { existsSync as existsSync2 } from "fs";
|
|
2300
3744
|
import { readFile as readFile3 } from "fs/promises";
|
|
2301
|
-
import { resolve as
|
|
3745
|
+
import { resolve as resolve6 } from "path";
|
|
2302
3746
|
import pixelmatch from "pixelmatch";
|
|
2303
3747
|
import { PNG } from "pngjs";
|
|
2304
3748
|
var visualDiffTool = {
|
|
@@ -2307,7 +3751,11 @@ var visualDiffTool = {
|
|
|
2307
3751
|
inputShape: visualDiffShape,
|
|
2308
3752
|
build(ctx) {
|
|
2309
3753
|
return safeHandler(async (args) => {
|
|
2310
|
-
const
|
|
3754
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3755
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3756
|
+
"vdiff",
|
|
3757
|
+
{ skill: "visual-diff" }
|
|
3758
|
+
);
|
|
2311
3759
|
const session = await ctx.registry.open({
|
|
2312
3760
|
...args.open,
|
|
2313
3761
|
...args.viewport ? { viewport: args.viewport } : {}
|
|
@@ -2326,7 +3774,7 @@ var visualDiffTool = {
|
|
|
2326
3774
|
);
|
|
2327
3775
|
const currentPath = await ctx.store.writeScreenshot(runDir, buf, "current");
|
|
2328
3776
|
await ctx.store.ensureDir(ctx.store.baselineDir);
|
|
2329
|
-
const baselinePath =
|
|
3777
|
+
const baselinePath = resolve6(
|
|
2330
3778
|
ctx.store.baselineDir,
|
|
2331
3779
|
`${args.baseline_id}.png`
|
|
2332
3780
|
);
|
|
@@ -2336,6 +3784,20 @@ var visualDiffTool = {
|
|
|
2336
3784
|
`${args.baseline_id}.png`,
|
|
2337
3785
|
buf
|
|
2338
3786
|
);
|
|
3787
|
+
const manifestPath2 = await writeManifest({
|
|
3788
|
+
runDir,
|
|
3789
|
+
skill,
|
|
3790
|
+
phase: "verify",
|
|
3791
|
+
status: "pass",
|
|
3792
|
+
summary: `baseline "${args.baseline_id}" seeded from current capture`,
|
|
3793
|
+
startedAt,
|
|
3794
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3795
|
+
artifacts: [
|
|
3796
|
+
{ type: "baseline", path: baselinePath },
|
|
3797
|
+
{ type: "screenshot", path: currentPath }
|
|
3798
|
+
],
|
|
3799
|
+
metadata: { baseline_id: args.baseline_id, seeded: true }
|
|
3800
|
+
});
|
|
2339
3801
|
return ok({
|
|
2340
3802
|
run_id: runId,
|
|
2341
3803
|
baseline_id: args.baseline_id,
|
|
@@ -2343,6 +3805,7 @@ var visualDiffTool = {
|
|
|
2343
3805
|
passed: true,
|
|
2344
3806
|
baseline_path: baselinePath,
|
|
2345
3807
|
current_path: currentPath,
|
|
3808
|
+
...manifestPath2 ? { manifest: manifestPath2 } : {},
|
|
2346
3809
|
note: "Baseline did not exist \u2014 current capture saved as the new baseline."
|
|
2347
3810
|
});
|
|
2348
3811
|
}
|
|
@@ -2373,21 +3836,45 @@ var visualDiffTool = {
|
|
|
2373
3836
|
);
|
|
2374
3837
|
const total = baseline.width * baseline.height;
|
|
2375
3838
|
const diffPct = diffPixels / total;
|
|
3839
|
+
const passed = diffPct <= args.threshold_pct;
|
|
2376
3840
|
const diffImagePath = await ctx.store.writeBytes(
|
|
2377
3841
|
runDir,
|
|
2378
3842
|
"diff.png",
|
|
2379
3843
|
PNG.sync.write(diff)
|
|
2380
3844
|
);
|
|
3845
|
+
const artifacts = [
|
|
3846
|
+
{ type: "baseline", path: baselinePath },
|
|
3847
|
+
{ type: "screenshot", path: currentPath },
|
|
3848
|
+
{ type: "diff", path: diffImagePath }
|
|
3849
|
+
];
|
|
3850
|
+
const manifestPath = await writeManifest({
|
|
3851
|
+
runDir,
|
|
3852
|
+
skill,
|
|
3853
|
+
phase: "verify",
|
|
3854
|
+
status: passed ? "pass" : "fail",
|
|
3855
|
+
summary: `diff ${(diffPct * 100).toFixed(3)}% vs baseline "${args.baseline_id}" (threshold ${(args.threshold_pct * 100).toFixed(3)}%)`,
|
|
3856
|
+
startedAt,
|
|
3857
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3858
|
+
artifacts,
|
|
3859
|
+
metadata: {
|
|
3860
|
+
baseline_id: args.baseline_id,
|
|
3861
|
+
diff_pct: Number(diffPct.toFixed(6)),
|
|
3862
|
+
diff_pixels: diffPixels,
|
|
3863
|
+
total_pixels: total,
|
|
3864
|
+
threshold_pct: args.threshold_pct
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
2381
3867
|
return ok({
|
|
2382
3868
|
run_id: runId,
|
|
2383
3869
|
baseline_id: args.baseline_id,
|
|
2384
3870
|
diff_pct: Number(diffPct.toFixed(6)),
|
|
2385
3871
|
diff_pixels: diffPixels,
|
|
2386
3872
|
total_pixels: total,
|
|
2387
|
-
passed
|
|
3873
|
+
passed,
|
|
2388
3874
|
baseline_path: baselinePath,
|
|
2389
3875
|
current_path: currentPath,
|
|
2390
|
-
diff_image_path: diffImagePath
|
|
3876
|
+
diff_image_path: diffImagePath,
|
|
3877
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
2391
3878
|
});
|
|
2392
3879
|
} finally {
|
|
2393
3880
|
if (args.close_on_finish) {
|
|
@@ -2544,13 +4031,131 @@ var toolMetadata = {
|
|
|
2544
4031
|
readOnlyHint: true,
|
|
2545
4032
|
openWorldHint: true
|
|
2546
4033
|
}
|
|
4034
|
+
},
|
|
4035
|
+
// ---------- v0.5 atomic additions ----------
|
|
4036
|
+
[ToolNames.browserHover]: {
|
|
4037
|
+
title: "Hover Element",
|
|
4038
|
+
annotations: {
|
|
4039
|
+
title: "Hover Element",
|
|
4040
|
+
readOnlyHint: false,
|
|
4041
|
+
destructiveHint: false,
|
|
4042
|
+
idempotentHint: true,
|
|
4043
|
+
openWorldHint: true
|
|
4044
|
+
}
|
|
4045
|
+
},
|
|
4046
|
+
[ToolNames.browserDrag]: {
|
|
4047
|
+
title: "Drag Element",
|
|
4048
|
+
annotations: {
|
|
4049
|
+
title: "Drag Element",
|
|
4050
|
+
readOnlyHint: false,
|
|
4051
|
+
destructiveHint: true,
|
|
4052
|
+
idempotentHint: false,
|
|
4053
|
+
openWorldHint: true
|
|
4054
|
+
}
|
|
4055
|
+
},
|
|
4056
|
+
[ToolNames.browserFillForm]: {
|
|
4057
|
+
title: "Fill Form (Batch)",
|
|
4058
|
+
annotations: {
|
|
4059
|
+
title: "Fill Form (Batch)",
|
|
4060
|
+
readOnlyHint: false,
|
|
4061
|
+
destructiveHint: true,
|
|
4062
|
+
idempotentHint: false,
|
|
4063
|
+
openWorldHint: true
|
|
4064
|
+
}
|
|
4065
|
+
},
|
|
4066
|
+
[ToolNames.browserUploadFile]: {
|
|
4067
|
+
title: "Upload File",
|
|
4068
|
+
annotations: {
|
|
4069
|
+
title: "Upload File",
|
|
4070
|
+
readOnlyHint: false,
|
|
4071
|
+
destructiveHint: true,
|
|
4072
|
+
idempotentHint: false,
|
|
4073
|
+
openWorldHint: true
|
|
4074
|
+
}
|
|
4075
|
+
},
|
|
4076
|
+
[ToolNames.browserHandleDialog]: {
|
|
4077
|
+
title: "Pre-arm Dialog Handler",
|
|
4078
|
+
annotations: {
|
|
4079
|
+
title: "Pre-arm Dialog Handler",
|
|
4080
|
+
readOnlyHint: false,
|
|
4081
|
+
destructiveHint: true,
|
|
4082
|
+
idempotentHint: false,
|
|
4083
|
+
openWorldHint: false
|
|
4084
|
+
}
|
|
4085
|
+
},
|
|
4086
|
+
[ToolNames.browserConsole]: {
|
|
4087
|
+
title: "Inspect Console Logs",
|
|
4088
|
+
annotations: {
|
|
4089
|
+
title: "Inspect Console Logs",
|
|
4090
|
+
readOnlyHint: true,
|
|
4091
|
+
openWorldHint: false
|
|
4092
|
+
}
|
|
4093
|
+
},
|
|
4094
|
+
[ToolNames.browserNetwork]: {
|
|
4095
|
+
title: "Inspect Network Requests",
|
|
4096
|
+
annotations: {
|
|
4097
|
+
title: "Inspect Network Requests",
|
|
4098
|
+
readOnlyHint: true,
|
|
4099
|
+
openWorldHint: false
|
|
4100
|
+
}
|
|
4101
|
+
},
|
|
4102
|
+
[ToolNames.browserSetEnv]: {
|
|
4103
|
+
title: "Set Browser Environment",
|
|
4104
|
+
annotations: {
|
|
4105
|
+
title: "Set Browser Environment",
|
|
4106
|
+
readOnlyHint: false,
|
|
4107
|
+
destructiveHint: true,
|
|
4108
|
+
idempotentHint: true,
|
|
4109
|
+
openWorldHint: false
|
|
4110
|
+
}
|
|
4111
|
+
},
|
|
4112
|
+
[ToolNames.browserEvaluate]: {
|
|
4113
|
+
title: "Evaluate JavaScript (gated; arbitrary code execution)",
|
|
4114
|
+
annotations: {
|
|
4115
|
+
title: "Evaluate JavaScript",
|
|
4116
|
+
// Arbitrary code execution in the page context. Gated by
|
|
4117
|
+
// ROLEPOD_ALLOW_EVAL=1 server-side. Always treat as destructive.
|
|
4118
|
+
readOnlyHint: false,
|
|
4119
|
+
destructiveHint: true,
|
|
4120
|
+
idempotentHint: false,
|
|
4121
|
+
openWorldHint: true
|
|
4122
|
+
}
|
|
4123
|
+
},
|
|
4124
|
+
[ToolNames.browserPages]: {
|
|
4125
|
+
title: "List Open Pages",
|
|
4126
|
+
annotations: {
|
|
4127
|
+
title: "List Open Pages",
|
|
4128
|
+
readOnlyHint: true,
|
|
4129
|
+
openWorldHint: false
|
|
4130
|
+
}
|
|
4131
|
+
},
|
|
4132
|
+
[ToolNames.browserSwitchPage]: {
|
|
4133
|
+
title: "Switch Active Page",
|
|
4134
|
+
annotations: {
|
|
4135
|
+
title: "Switch Active Page",
|
|
4136
|
+
readOnlyHint: false,
|
|
4137
|
+
destructiveHint: false,
|
|
4138
|
+
idempotentHint: true,
|
|
4139
|
+
openWorldHint: false
|
|
4140
|
+
}
|
|
2547
4141
|
}
|
|
2548
4142
|
};
|
|
2549
4143
|
|
|
2550
4144
|
// src/server.ts
|
|
2551
4145
|
var SERVER_NAME = "rolepod-uiproof";
|
|
2552
|
-
var SERVER_VERSION = "0.
|
|
4146
|
+
var SERVER_VERSION = "0.6.0";
|
|
4147
|
+
var SUPPORTED_PROTOCOL = "v1";
|
|
4148
|
+
function checkProtocolCompat() {
|
|
4149
|
+
const requested = process.env.ROLEPOD_PROTOCOL;
|
|
4150
|
+
if (!requested) return;
|
|
4151
|
+
if (requested !== SUPPORTED_PROTOCOL) {
|
|
4152
|
+
console.warn(
|
|
4153
|
+
`rolepod protocol mismatch: expected ${SUPPORTED_PROTOCOL}, got ${requested}. Manifest will still be written in ${SUPPORTED_PROTOCOL} shape \u2014 parent may not parse it correctly.`
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
2553
4157
|
function buildServer(opts = {}) {
|
|
4158
|
+
checkProtocolCompat();
|
|
2554
4159
|
const webEngine = createWebEngine();
|
|
2555
4160
|
const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
|
|
2556
4161
|
registry.register("web", webEngine);
|
|
@@ -2566,7 +4171,7 @@ function buildServer(opts = {}) {
|
|
|
2566
4171
|
version: SERVER_VERSION
|
|
2567
4172
|
});
|
|
2568
4173
|
const tools = [
|
|
2569
|
-
// atomic
|
|
4174
|
+
// atomic (v0.1-v0.4)
|
|
2570
4175
|
browserOpenTool,
|
|
2571
4176
|
browserCloseTool,
|
|
2572
4177
|
browserSnapshotTool,
|
|
@@ -2577,6 +4182,18 @@ function buildServer(opts = {}) {
|
|
|
2577
4182
|
browserWaitForTool,
|
|
2578
4183
|
browserScreenshotTool,
|
|
2579
4184
|
browserNavigateTool,
|
|
4185
|
+
// atomic (v0.5)
|
|
4186
|
+
browserHoverTool,
|
|
4187
|
+
browserDragTool,
|
|
4188
|
+
browserFillFormTool,
|
|
4189
|
+
browserUploadFileTool,
|
|
4190
|
+
browserHandleDialogTool,
|
|
4191
|
+
browserConsoleTool,
|
|
4192
|
+
browserNetworkTool,
|
|
4193
|
+
browserSetEnvTool,
|
|
4194
|
+
browserEvaluateTool,
|
|
4195
|
+
browserPagesTool,
|
|
4196
|
+
browserSwitchPageTool,
|
|
2580
4197
|
// composite
|
|
2581
4198
|
verifyUiFlowTool,
|
|
2582
4199
|
auditA11yTool,
|
|
@@ -2599,6 +4216,8 @@ function buildServer(opts = {}) {
|
|
|
2599
4216
|
}
|
|
2600
4217
|
log.info("rolepod-uiproof server built", {
|
|
2601
4218
|
version: SERVER_VERSION,
|
|
4219
|
+
protocol: SUPPORTED_PROTOCOL,
|
|
4220
|
+
mode: store.mode,
|
|
2602
4221
|
tools: tools.map((t) => t.name)
|
|
2603
4222
|
});
|
|
2604
4223
|
return {
|