@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
package/dist/index.js
CHANGED
|
@@ -28,16 +28,49 @@ var log = {
|
|
|
28
28
|
// src/artifact/ArtifactStore.ts
|
|
29
29
|
var ArtifactStore = class {
|
|
30
30
|
rootDir;
|
|
31
|
+
mode;
|
|
32
|
+
baselineRoot;
|
|
31
33
|
constructor(opts = {}) {
|
|
32
|
-
|
|
34
|
+
const detectedParent = process.env.ROLEPOD_PARENT === "1";
|
|
35
|
+
this.mode = opts.mode ?? (detectedParent ? "with-parent" : "standalone");
|
|
36
|
+
if (opts.rootDir !== void 0) {
|
|
37
|
+
this.rootDir = opts.rootDir;
|
|
38
|
+
} else if (this.mode === "with-parent") {
|
|
39
|
+
this.rootDir = resolve(process.cwd(), ".rolepod", "evidence");
|
|
40
|
+
} else {
|
|
41
|
+
this.rootDir = resolve(process.cwd(), ".rolepod-uiproof", "artifacts");
|
|
42
|
+
}
|
|
43
|
+
this.baselineRoot = resolve(process.cwd(), ".rolepod-uiproof", "baselines");
|
|
33
44
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Allocate a fresh run dir and ensure it exists.
|
|
47
|
+
*
|
|
48
|
+
* - standalone: `./.rolepod-uiproof/artifacts/{prefix}_{ts}_{uuid}/`
|
|
49
|
+
* - with-parent: `./.rolepod/evidence/{ts}-rolepod-uiproof-{skill}/`
|
|
50
|
+
*
|
|
51
|
+
* `prefix` is preserved for back-compat with v0.5 callers; new callers
|
|
52
|
+
* should also pass `opts.skill` so the with-parent path can be derived
|
|
53
|
+
* unambiguously and the manifest can be emitted with the canonical
|
|
54
|
+
* skill name.
|
|
55
|
+
*/
|
|
56
|
+
async startRun(prefix = "run", opts = {}) {
|
|
57
|
+
const ts = this.timestampSlug();
|
|
58
|
+
const skill = opts.skill ?? prefix;
|
|
59
|
+
let runId;
|
|
60
|
+
if (this.mode === "with-parent") {
|
|
61
|
+
runId = `${ts}-rolepod-uiproof-${skill}`;
|
|
62
|
+
} else {
|
|
63
|
+
runId = `${prefix}_${ts}_${randomUUID().slice(0, 8)}`;
|
|
64
|
+
}
|
|
37
65
|
const runDir = resolve(this.rootDir, runId);
|
|
38
66
|
await mkdir(runDir, { recursive: true });
|
|
39
|
-
log.debug("artifact run started", {
|
|
40
|
-
|
|
67
|
+
log.debug("artifact run started", {
|
|
68
|
+
run_id: runId,
|
|
69
|
+
dir: runDir,
|
|
70
|
+
mode: this.mode,
|
|
71
|
+
skill
|
|
72
|
+
});
|
|
73
|
+
return { runId, runDir, skill, mode: this.mode };
|
|
41
74
|
}
|
|
42
75
|
async writeScreenshot(runDir, buf, name) {
|
|
43
76
|
const path = resolve(runDir, `${name}.png`);
|
|
@@ -65,7 +98,7 @@ var ArtifactStore = class {
|
|
|
65
98
|
}
|
|
66
99
|
/** Root for stored visual baselines: `./.rolepod-uiproof/baselines/`. */
|
|
67
100
|
get baselineDir() {
|
|
68
|
-
return
|
|
101
|
+
return this.baselineRoot;
|
|
69
102
|
}
|
|
70
103
|
timestampSlug() {
|
|
71
104
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -415,6 +448,34 @@ var AppiumEngine = class {
|
|
|
415
448
|
throw new UnsupportedPlatformError(_session.platform);
|
|
416
449
|
}
|
|
417
450
|
// -------------------------------------------------------------------------
|
|
451
|
+
// v0.5 cross-platform additions — mobile stubs.
|
|
452
|
+
// These ship as `not_implemented_in_v05` until the mobile gesture work lands.
|
|
453
|
+
// -------------------------------------------------------------------------
|
|
454
|
+
async hover(_session, _ref) {
|
|
455
|
+
throw new RolepodMcpError(
|
|
456
|
+
"engine_error",
|
|
457
|
+
"hover is not yet implemented for mobile (Appium). Use long-press via custom gesture if needed."
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
async drag(_session, _fromRef, _toRef) {
|
|
461
|
+
throw new RolepodMcpError(
|
|
462
|
+
"engine_error",
|
|
463
|
+
"drag is not yet implemented for mobile (Appium). Use the W3C Actions API directly if needed."
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
async fillForm(session, fields) {
|
|
467
|
+
for (const f of fields) {
|
|
468
|
+
const v = typeof f.value === "boolean" ? String(f.value) : f.value;
|
|
469
|
+
await this.type(session, f.ref, v);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async uploadFile(_session, _ref, _filePath) {
|
|
473
|
+
throw new RolepodMcpError(
|
|
474
|
+
"engine_error",
|
|
475
|
+
"upload_file is not supported on mobile (Appium)."
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
// -------------------------------------------------------------------------
|
|
418
479
|
// Internals
|
|
419
480
|
// -------------------------------------------------------------------------
|
|
420
481
|
async loadWdio() {
|
|
@@ -529,6 +590,7 @@ function treeIncludesText(node, text) {
|
|
|
529
590
|
|
|
530
591
|
// src/engine/PlaywrightEngine.ts
|
|
531
592
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
593
|
+
import { resolve as resolvePath, isAbsolute } from "path";
|
|
532
594
|
import {
|
|
533
595
|
chromium,
|
|
534
596
|
firefox,
|
|
@@ -638,6 +700,83 @@ function parseAriaSnapshot(snapshotYaml) {
|
|
|
638
700
|
}
|
|
639
701
|
|
|
640
702
|
// src/engine/PlaywrightEngine.ts
|
|
703
|
+
var CONSOLE_BUFFER_CAP = 1e3;
|
|
704
|
+
var NETWORK_BUFFER_CAP = 1e3;
|
|
705
|
+
var NETWORK_PRESETS = {
|
|
706
|
+
offline: { offline: true, downloadThroughput: 0, uploadThroughput: 0, latency: 0 },
|
|
707
|
+
"slow-3g": {
|
|
708
|
+
offline: false,
|
|
709
|
+
// 500 Kbps down / 500 Kbps up / 400ms RTT
|
|
710
|
+
downloadThroughput: 500 * 1024 / 8,
|
|
711
|
+
uploadThroughput: 500 * 1024 / 8,
|
|
712
|
+
latency: 400
|
|
713
|
+
},
|
|
714
|
+
"fast-3g": {
|
|
715
|
+
offline: false,
|
|
716
|
+
downloadThroughput: 1.5 * 1024 * 1024 / 8,
|
|
717
|
+
uploadThroughput: 750 * 1024 / 8,
|
|
718
|
+
latency: 150
|
|
719
|
+
},
|
|
720
|
+
"slow-4g": {
|
|
721
|
+
offline: false,
|
|
722
|
+
downloadThroughput: 4 * 1024 * 1024 / 8,
|
|
723
|
+
uploadThroughput: 3 * 1024 * 1024 / 8,
|
|
724
|
+
latency: 100
|
|
725
|
+
},
|
|
726
|
+
"fast-4g": {
|
|
727
|
+
offline: false,
|
|
728
|
+
downloadThroughput: 9 * 1024 * 1024 / 8,
|
|
729
|
+
uploadThroughput: 4.5 * 1024 * 1024 / 8,
|
|
730
|
+
latency: 60
|
|
731
|
+
},
|
|
732
|
+
"no-throttling": {
|
|
733
|
+
offline: false,
|
|
734
|
+
downloadThroughput: -1,
|
|
735
|
+
uploadThroughput: -1,
|
|
736
|
+
latency: 0
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
function pushRing(buf, entry, cap) {
|
|
740
|
+
buf.push(entry);
|
|
741
|
+
if (buf.length > cap) buf.splice(0, buf.length - cap);
|
|
742
|
+
}
|
|
743
|
+
function mapConsoleLevel(t) {
|
|
744
|
+
switch (t) {
|
|
745
|
+
case "error":
|
|
746
|
+
return "error";
|
|
747
|
+
case "warning":
|
|
748
|
+
return "warning";
|
|
749
|
+
case "info":
|
|
750
|
+
return "info";
|
|
751
|
+
case "debug":
|
|
752
|
+
return "debug";
|
|
753
|
+
case "trace":
|
|
754
|
+
return "trace";
|
|
755
|
+
default:
|
|
756
|
+
return "log";
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function formatConsoleLocation(msg) {
|
|
760
|
+
try {
|
|
761
|
+
const loc = msg.location();
|
|
762
|
+
if (!loc?.url) return void 0;
|
|
763
|
+
return `${loc.url}:${loc.lineNumber}:${loc.columnNumber}`;
|
|
764
|
+
} catch {
|
|
765
|
+
return void 0;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function findNetworkEntry(buf, req) {
|
|
769
|
+
const url = req.url();
|
|
770
|
+
const method = req.method();
|
|
771
|
+
for (let i = buf.length - 1; i >= 0; i--) {
|
|
772
|
+
const e = buf[i];
|
|
773
|
+
if (!e) continue;
|
|
774
|
+
if (e.url === url && e.method === method && e.status === void 0 && !e.failure) {
|
|
775
|
+
return e;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return void 0;
|
|
779
|
+
}
|
|
641
780
|
var PlaywrightEngine = class {
|
|
642
781
|
id = "playwright";
|
|
643
782
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -653,35 +792,80 @@ var PlaywrightEngine = class {
|
|
|
653
792
|
if (opts.viewport) contextOptions.viewport = opts.viewport;
|
|
654
793
|
if (opts.user_agent) contextOptions.userAgent = opts.user_agent;
|
|
655
794
|
if (opts.locale) contextOptions.locale = opts.locale;
|
|
795
|
+
if (opts.capture?.har) {
|
|
796
|
+
contextOptions.recordHar = { path: opts.capture.har.path };
|
|
797
|
+
}
|
|
798
|
+
if (opts.capture?.video) {
|
|
799
|
+
contextOptions.recordVideo = {
|
|
800
|
+
dir: opts.capture.video.dir,
|
|
801
|
+
size: opts.capture.video.sizeWidth && opts.capture.video.sizeHeight ? {
|
|
802
|
+
width: opts.capture.video.sizeWidth,
|
|
803
|
+
height: opts.capture.video.sizeHeight
|
|
804
|
+
} : void 0
|
|
805
|
+
};
|
|
806
|
+
}
|
|
656
807
|
const context = await browser.newContext(contextOptions);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
808
|
+
if (opts.capture?.trace) {
|
|
809
|
+
await context.tracing.start({
|
|
810
|
+
screenshots: true,
|
|
811
|
+
snapshots: true,
|
|
812
|
+
sources: false
|
|
813
|
+
});
|
|
660
814
|
}
|
|
815
|
+
const page = await context.newPage();
|
|
661
816
|
const sessionId = randomUUID3();
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
session,
|
|
817
|
+
const internals = {
|
|
818
|
+
session: {
|
|
819
|
+
id: sessionId,
|
|
820
|
+
platform: "web",
|
|
821
|
+
browser,
|
|
822
|
+
context,
|
|
823
|
+
mainPage: page
|
|
824
|
+
},
|
|
671
825
|
refIndex: /* @__PURE__ */ new Map(),
|
|
672
826
|
snapshotGeneration: 0,
|
|
673
827
|
refGeneration: -1,
|
|
674
|
-
lastSnapshotAt: null
|
|
828
|
+
lastSnapshotAt: null,
|
|
829
|
+
pages: [page],
|
|
830
|
+
activePageIndex: 0,
|
|
831
|
+
consoleBuffer: [],
|
|
832
|
+
networkBuffer: [],
|
|
833
|
+
networkInflight: /* @__PURE__ */ new Map(),
|
|
834
|
+
networkNextId: 1,
|
|
835
|
+
dialogArming: null,
|
|
836
|
+
captureOpts: opts.capture,
|
|
837
|
+
traceStarted: !!opts.capture?.trace
|
|
838
|
+
};
|
|
839
|
+
this.attachPageListeners(internals, page);
|
|
840
|
+
context.on("page", (newPage) => {
|
|
841
|
+
internals.pages.push(newPage);
|
|
842
|
+
this.attachPageListeners(internals, newPage);
|
|
675
843
|
});
|
|
844
|
+
if (opts.url) {
|
|
845
|
+
await page.goto(opts.url, { waitUntil: "domcontentloaded" });
|
|
846
|
+
}
|
|
847
|
+
this.sessions.set(sessionId, internals);
|
|
676
848
|
log.info("session opened", {
|
|
677
849
|
session_id: sessionId,
|
|
678
850
|
browser: browserName,
|
|
679
|
-
url: opts.url ?? null
|
|
851
|
+
url: opts.url ?? null,
|
|
852
|
+
capture: opts.capture ? Object.keys(opts.capture).filter(
|
|
853
|
+
(k) => opts.capture[k]
|
|
854
|
+
) : []
|
|
680
855
|
});
|
|
681
856
|
return { id: sessionId, platform: "web" };
|
|
682
857
|
}
|
|
683
858
|
async close(session) {
|
|
684
859
|
const s = this.requireSession(session.id);
|
|
860
|
+
if (s.traceStarted && s.captureOpts?.trace) {
|
|
861
|
+
const tracePath = resolvePath(s.captureOpts.trace.artifactDir, "trace.zip");
|
|
862
|
+
await s.session.context.tracing.stop({ path: tracePath }).catch((err) => {
|
|
863
|
+
log.warn("trace stop failed", {
|
|
864
|
+
session_id: session.id,
|
|
865
|
+
err: String(err)
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
}
|
|
685
869
|
await s.session.context.close().catch((err) => {
|
|
686
870
|
log.warn("context close failed", { session_id: session.id, err: String(err) });
|
|
687
871
|
});
|
|
@@ -693,7 +877,7 @@ var PlaywrightEngine = class {
|
|
|
693
877
|
}
|
|
694
878
|
async snapshot(session, mode = "visible") {
|
|
695
879
|
const s = this.requireSession(session.id);
|
|
696
|
-
const ariaYaml = await s.
|
|
880
|
+
const ariaYaml = await this.activePage(s).ariaSnapshot({ mode: "ai" });
|
|
697
881
|
const { tree, refIndex } = parseAriaSnapshot(ariaYaml);
|
|
698
882
|
void mode;
|
|
699
883
|
s.snapshotGeneration += 1;
|
|
@@ -703,7 +887,7 @@ var PlaywrightEngine = class {
|
|
|
703
887
|
return {
|
|
704
888
|
session_id: session.id,
|
|
705
889
|
platform: "web",
|
|
706
|
-
url_or_screen: s.
|
|
890
|
+
url_or_screen: this.activePage(s).url(),
|
|
707
891
|
taken_at: s.lastSnapshotAt,
|
|
708
892
|
tree
|
|
709
893
|
};
|
|
@@ -723,7 +907,7 @@ var PlaywrightEngine = class {
|
|
|
723
907
|
}
|
|
724
908
|
async key(session, key) {
|
|
725
909
|
const s = this.requireSession(session.id);
|
|
726
|
-
await s.
|
|
910
|
+
await this.activePage(s).keyboard.press(key);
|
|
727
911
|
this.invalidateRefs(s);
|
|
728
912
|
}
|
|
729
913
|
async scroll(session, dir, amount = 400, ref) {
|
|
@@ -737,13 +921,13 @@ var PlaywrightEngine = class {
|
|
|
737
921
|
[dx, dy]
|
|
738
922
|
);
|
|
739
923
|
} else {
|
|
740
|
-
await s.
|
|
924
|
+
await this.activePage(s).mouse.wheel(dx, dy);
|
|
741
925
|
}
|
|
742
926
|
this.invalidateRefs(s);
|
|
743
927
|
}
|
|
744
928
|
async waitFor(session, cond, timeoutMs = 1e4) {
|
|
745
929
|
const s = this.requireSession(session.id);
|
|
746
|
-
const page = s
|
|
930
|
+
const page = this.activePage(s);
|
|
747
931
|
switch (cond.kind) {
|
|
748
932
|
case "text_visible":
|
|
749
933
|
await page.getByText(cond.text, { exact: false }).first().waitFor({ state: "visible", timeout: timeoutMs });
|
|
@@ -763,14 +947,14 @@ var PlaywrightEngine = class {
|
|
|
763
947
|
}
|
|
764
948
|
async screenshot(session, fullPage = false) {
|
|
765
949
|
const s = this.requireSession(session.id);
|
|
766
|
-
return s.
|
|
950
|
+
return this.activePage(s).screenshot({ fullPage });
|
|
767
951
|
}
|
|
768
952
|
async navigate(session, url) {
|
|
769
953
|
const s = this.requireSession(session.id);
|
|
770
954
|
if (s.session.platform !== "web") {
|
|
771
955
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
772
956
|
}
|
|
773
|
-
await s.
|
|
957
|
+
await this.activePage(s).goto(url, { waitUntil: "domcontentloaded" });
|
|
774
958
|
this.invalidateRefs(s);
|
|
775
959
|
}
|
|
776
960
|
/**
|
|
@@ -784,7 +968,7 @@ var PlaywrightEngine = class {
|
|
|
784
968
|
if (s.session.platform !== "web") {
|
|
785
969
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
786
970
|
}
|
|
787
|
-
return s
|
|
971
|
+
return this.activePage(s);
|
|
788
972
|
}
|
|
789
973
|
/** Increment generation; the next ref-using call will see them as stale. */
|
|
790
974
|
bumpGeneration(sessionId) {
|
|
@@ -792,8 +976,311 @@ var PlaywrightEngine = class {
|
|
|
792
976
|
this.invalidateRefs(s);
|
|
793
977
|
}
|
|
794
978
|
// -------------------------------------------------------------------------
|
|
979
|
+
// v0.5 — input additions
|
|
980
|
+
// -------------------------------------------------------------------------
|
|
981
|
+
async hover(session, ref) {
|
|
982
|
+
const s = this.requireSession(session.id);
|
|
983
|
+
const locator = this.resolveLocator(s, ref);
|
|
984
|
+
await locator.hover();
|
|
985
|
+
}
|
|
986
|
+
async drag(session, fromRef, toRef) {
|
|
987
|
+
const s = this.requireSession(session.id);
|
|
988
|
+
const from = this.resolveLocator(s, fromRef);
|
|
989
|
+
const to = this.resolveLocator(s, toRef);
|
|
990
|
+
await from.dragTo(to);
|
|
991
|
+
this.invalidateRefs(s);
|
|
992
|
+
}
|
|
993
|
+
async fillForm(session, fields) {
|
|
994
|
+
const s = this.requireSession(session.id);
|
|
995
|
+
for (const field of fields) {
|
|
996
|
+
const locator = this.resolveLocator(s, field.ref);
|
|
997
|
+
const kind = field.kind;
|
|
998
|
+
if (kind === "checkbox" || kind === "radio") {
|
|
999
|
+
const checked = typeof field.value === "boolean" ? field.value : field.value === "true" || field.value === "on";
|
|
1000
|
+
await locator.setChecked(checked);
|
|
1001
|
+
} else if (kind === "select") {
|
|
1002
|
+
await locator.selectOption(String(field.value));
|
|
1003
|
+
} else {
|
|
1004
|
+
await locator.fill(String(field.value));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
this.invalidateRefs(s);
|
|
1008
|
+
}
|
|
1009
|
+
async uploadFile(session, ref, filePath) {
|
|
1010
|
+
const s = this.requireSession(session.id);
|
|
1011
|
+
if (!isAbsolute(filePath)) {
|
|
1012
|
+
throw new RolepodMcpError(
|
|
1013
|
+
"invalid_input",
|
|
1014
|
+
`upload_file requires an absolute path; got "${filePath}".`,
|
|
1015
|
+
{ file_path: filePath }
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
const locator = this.resolveLocator(s, ref);
|
|
1019
|
+
await locator.setInputFiles(filePath);
|
|
1020
|
+
this.invalidateRefs(s);
|
|
1021
|
+
}
|
|
1022
|
+
// -------------------------------------------------------------------------
|
|
1023
|
+
// v0.5 — web-only extensions (not on Engine interface; tools cast to
|
|
1024
|
+
// PlaywrightEngine before calling).
|
|
1025
|
+
// -------------------------------------------------------------------------
|
|
1026
|
+
/**
|
|
1027
|
+
* Pre-arm a one-shot dialog handler for the next dialog raised on the
|
|
1028
|
+
* active page. Returns when either the dialog fires (and is handled)
|
|
1029
|
+
* or the timeout elapses. The caller is expected to trigger the
|
|
1030
|
+
* dialog (via click etc.) AFTER arming.
|
|
1031
|
+
*/
|
|
1032
|
+
async handleDialog(sessionId, opts) {
|
|
1033
|
+
const s = this.requireSession(sessionId);
|
|
1034
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1035
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
1036
|
+
if (s.dialogArming) {
|
|
1037
|
+
s.dialogArming.resolve(false);
|
|
1038
|
+
}
|
|
1039
|
+
return new Promise((resolve5) => {
|
|
1040
|
+
const arming = {
|
|
1041
|
+
action: opts.action,
|
|
1042
|
+
text: opts.text,
|
|
1043
|
+
expiresAt,
|
|
1044
|
+
resolve: (handled) => {
|
|
1045
|
+
s.dialogArming = null;
|
|
1046
|
+
resolve5({ handled });
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
s.dialogArming = arming;
|
|
1050
|
+
const timer = setTimeout(() => {
|
|
1051
|
+
if (s.dialogArming === arming) {
|
|
1052
|
+
s.dialogArming = null;
|
|
1053
|
+
resolve5({ handled: false });
|
|
1054
|
+
}
|
|
1055
|
+
}, timeoutMs);
|
|
1056
|
+
timer.unref?.();
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
getConsole(sessionId, opts) {
|
|
1060
|
+
const s = this.requireSession(sessionId);
|
|
1061
|
+
const levels = opts?.levels;
|
|
1062
|
+
const contains = opts?.contains;
|
|
1063
|
+
const limit = opts?.limit ?? 50;
|
|
1064
|
+
let entries = s.consoleBuffer;
|
|
1065
|
+
if (levels && levels.length > 0) {
|
|
1066
|
+
entries = entries.filter((e) => levels.includes(e.level));
|
|
1067
|
+
}
|
|
1068
|
+
if (contains) {
|
|
1069
|
+
entries = entries.filter((e) => e.text.includes(contains));
|
|
1070
|
+
}
|
|
1071
|
+
const result = entries.slice(-limit);
|
|
1072
|
+
if (opts?.clear) s.consoleBuffer = [];
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
getNetwork(sessionId, opts) {
|
|
1076
|
+
const s = this.requireSession(sessionId);
|
|
1077
|
+
let entries = s.networkBuffer;
|
|
1078
|
+
if (opts?.urlPattern) {
|
|
1079
|
+
if (opts.patternKind === "regex") {
|
|
1080
|
+
const re = new RegExp(opts.urlPattern);
|
|
1081
|
+
entries = entries.filter((e) => re.test(e.url));
|
|
1082
|
+
} else {
|
|
1083
|
+
entries = entries.filter((e) => e.url.includes(opts.urlPattern));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (opts?.method) {
|
|
1087
|
+
const m = opts.method.toUpperCase();
|
|
1088
|
+
entries = entries.filter((e) => e.method.toUpperCase() === m);
|
|
1089
|
+
}
|
|
1090
|
+
if (opts?.statusRange) {
|
|
1091
|
+
const { min, max } = opts.statusRange;
|
|
1092
|
+
entries = entries.filter(
|
|
1093
|
+
(e) => e.status !== void 0 && e.status >= min && e.status <= max
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
if (opts?.onlyFailed) {
|
|
1097
|
+
entries = entries.filter(
|
|
1098
|
+
(e) => !!e.failure || e.status !== void 0 && e.status >= 400
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
const limit = opts?.limit ?? 50;
|
|
1102
|
+
const result = entries.slice(-limit);
|
|
1103
|
+
if (opts?.clear) s.networkBuffer = [];
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Read the consoleBuffer/networkBuffer directly without filtering —
|
|
1108
|
+
* used by verify_ui_flow expect evaluators.
|
|
1109
|
+
*/
|
|
1110
|
+
peekBuffers(sessionId) {
|
|
1111
|
+
const s = this.requireSession(sessionId);
|
|
1112
|
+
return { console: s.consoleBuffer, network: s.networkBuffer };
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Runtime mutation of context-level emulation. CPU + network throttle
|
|
1116
|
+
* use CDP and only work on chromium; everything else is cross-browser.
|
|
1117
|
+
*/
|
|
1118
|
+
async setEnv(sessionId, opts) {
|
|
1119
|
+
const s = this.requireSession(sessionId);
|
|
1120
|
+
const page = this.activePage(s);
|
|
1121
|
+
const ctx = s.session.context;
|
|
1122
|
+
if (opts.viewport) {
|
|
1123
|
+
await page.setViewportSize(opts.viewport);
|
|
1124
|
+
}
|
|
1125
|
+
if (opts.offline !== void 0) {
|
|
1126
|
+
await ctx.setOffline(opts.offline);
|
|
1127
|
+
}
|
|
1128
|
+
if (opts.geolocation) {
|
|
1129
|
+
await ctx.setGeolocation(opts.geolocation);
|
|
1130
|
+
}
|
|
1131
|
+
if (opts.extraHeaders) {
|
|
1132
|
+
await ctx.setExtraHTTPHeaders(opts.extraHeaders);
|
|
1133
|
+
}
|
|
1134
|
+
if (opts.colorScheme || opts.reducedMotion) {
|
|
1135
|
+
await page.emulateMedia({
|
|
1136
|
+
...opts.colorScheme ? { colorScheme: opts.colorScheme } : {},
|
|
1137
|
+
...opts.reducedMotion ? { reducedMotion: opts.reducedMotion } : {}
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
if (opts.networkThrottle || opts.cpuThrottle !== void 0) {
|
|
1141
|
+
const browserName = ctx.browser()?.browserType().name();
|
|
1142
|
+
if (browserName !== "chromium") {
|
|
1143
|
+
throw new RolepodMcpError(
|
|
1144
|
+
"unsupported_engine",
|
|
1145
|
+
`networkThrottle / cpuThrottle require chromium (CDP-backed); current browser is "${browserName}".`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
const cdp = await ctx.newCDPSession(page);
|
|
1149
|
+
try {
|
|
1150
|
+
if (opts.networkThrottle) {
|
|
1151
|
+
const preset = NETWORK_PRESETS[opts.networkThrottle];
|
|
1152
|
+
await cdp.send("Network.enable");
|
|
1153
|
+
await cdp.send("Network.emulateNetworkConditions", preset);
|
|
1154
|
+
}
|
|
1155
|
+
if (opts.cpuThrottle !== void 0) {
|
|
1156
|
+
await cdp.send("Emulation.setCPUThrottlingRate", {
|
|
1157
|
+
rate: opts.cpuThrottle
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
} finally {
|
|
1161
|
+
await cdp.detach().catch(() => void 0);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
this.invalidateRefs(s);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Execute a JavaScript function in the page context. ALWAYS gated by
|
|
1168
|
+
* the tool layer (`ROLEPOD_ALLOW_EVAL=1`); this method does not enforce
|
|
1169
|
+
* the env check.
|
|
1170
|
+
*/
|
|
1171
|
+
async evaluate(sessionId, script, args) {
|
|
1172
|
+
const s = this.requireSession(sessionId);
|
|
1173
|
+
const page = this.activePage(s);
|
|
1174
|
+
return page.evaluate(
|
|
1175
|
+
({ src, a }) => (
|
|
1176
|
+
// eslint-disable-next-line no-new-func
|
|
1177
|
+
new Function("args", `return (async () => { ${src} })();`)(a)
|
|
1178
|
+
),
|
|
1179
|
+
{ src: script, a: args ?? [] }
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
listPages(sessionId) {
|
|
1183
|
+
const s = this.requireSession(sessionId);
|
|
1184
|
+
return s.pages.map((p, i) => ({
|
|
1185
|
+
index: i,
|
|
1186
|
+
url: p.url(),
|
|
1187
|
+
title_promise: p.title(),
|
|
1188
|
+
active: i === s.activePageIndex
|
|
1189
|
+
}));
|
|
1190
|
+
}
|
|
1191
|
+
async switchPage(sessionId, index) {
|
|
1192
|
+
const s = this.requireSession(sessionId);
|
|
1193
|
+
if (index < 0 || index >= s.pages.length) {
|
|
1194
|
+
throw new RolepodMcpError(
|
|
1195
|
+
"invalid_input",
|
|
1196
|
+
`Page index ${index} out of range (have ${s.pages.length} page(s)).`,
|
|
1197
|
+
{ index, available: s.pages.length }
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
s.activePageIndex = index;
|
|
1201
|
+
this.invalidateRefs(s);
|
|
1202
|
+
}
|
|
1203
|
+
// -------------------------------------------------------------------------
|
|
795
1204
|
// Internal helpers
|
|
796
1205
|
// -------------------------------------------------------------------------
|
|
1206
|
+
activePage(s) {
|
|
1207
|
+
return s.pages[s.activePageIndex] ?? s.session.mainPage;
|
|
1208
|
+
}
|
|
1209
|
+
attachPageListeners(s, page) {
|
|
1210
|
+
page.on("console", (msg) => {
|
|
1211
|
+
const level = mapConsoleLevel(msg.type());
|
|
1212
|
+
pushRing(
|
|
1213
|
+
s.consoleBuffer,
|
|
1214
|
+
{
|
|
1215
|
+
level,
|
|
1216
|
+
text: msg.text(),
|
|
1217
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1218
|
+
location: formatConsoleLocation(msg)
|
|
1219
|
+
},
|
|
1220
|
+
CONSOLE_BUFFER_CAP
|
|
1221
|
+
);
|
|
1222
|
+
});
|
|
1223
|
+
page.on("request", (req) => {
|
|
1224
|
+
const id = s.networkNextId++;
|
|
1225
|
+
s.networkInflight.set(req.url() + "::" + req.method() + "::" + id, {
|
|
1226
|
+
id,
|
|
1227
|
+
startedAt: Date.now(),
|
|
1228
|
+
resourceType: req.resourceType()
|
|
1229
|
+
});
|
|
1230
|
+
pushRing(
|
|
1231
|
+
s.networkBuffer,
|
|
1232
|
+
{
|
|
1233
|
+
id,
|
|
1234
|
+
url: req.url(),
|
|
1235
|
+
method: req.method(),
|
|
1236
|
+
resource_type: req.resourceType(),
|
|
1237
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1238
|
+
},
|
|
1239
|
+
NETWORK_BUFFER_CAP
|
|
1240
|
+
);
|
|
1241
|
+
});
|
|
1242
|
+
page.on("response", (res) => {
|
|
1243
|
+
const req = res.request();
|
|
1244
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1245
|
+
if (entry) {
|
|
1246
|
+
entry.status = res.status();
|
|
1247
|
+
entry.duration_ms = Date.now() - new Date(entry.ts).getTime();
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
page.on("requestfailed", (req) => {
|
|
1251
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1252
|
+
if (entry) {
|
|
1253
|
+
entry.failure = req.failure()?.errorText ?? "request failed";
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
page.on("dialog", (dialog) => {
|
|
1257
|
+
void this.handlePageDialog(s, dialog);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
async handlePageDialog(s, dialog) {
|
|
1261
|
+
const arm = s.dialogArming;
|
|
1262
|
+
if (!arm || Date.now() > arm.expiresAt) {
|
|
1263
|
+
await dialog.dismiss().catch(() => void 0);
|
|
1264
|
+
if (arm) arm.resolve(false);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
if (arm.action === "accept") {
|
|
1269
|
+
await dialog.accept();
|
|
1270
|
+
} else if (arm.action === "accept_with_text") {
|
|
1271
|
+
await dialog.accept(arm.text ?? "");
|
|
1272
|
+
} else {
|
|
1273
|
+
await dialog.dismiss();
|
|
1274
|
+
}
|
|
1275
|
+
arm.resolve(true);
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
log.warn("dialog handle failed", {
|
|
1278
|
+
session_id: s.session.id,
|
|
1279
|
+
err: String(err)
|
|
1280
|
+
});
|
|
1281
|
+
arm.resolve(false);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
797
1284
|
requireSession(sessionId) {
|
|
798
1285
|
const s = this.sessions.get(sessionId);
|
|
799
1286
|
if (!s) {
|
|
@@ -822,7 +1309,7 @@ var PlaywrightEngine = class {
|
|
|
822
1309
|
if (meta.ref.startsWith("s")) {
|
|
823
1310
|
throw new UnknownRefError(s.session.id, ref);
|
|
824
1311
|
}
|
|
825
|
-
return s.
|
|
1312
|
+
return this.activePage(s).locator(`aria-ref=${meta.ref}`);
|
|
826
1313
|
}
|
|
827
1314
|
invalidateRefs(s) {
|
|
828
1315
|
s.snapshotGeneration += 1;
|
|
@@ -1064,6 +1551,143 @@ var browserNavigateShape = {
|
|
|
1064
1551
|
url: z.string().url()
|
|
1065
1552
|
};
|
|
1066
1553
|
var browserNavigateSchema = z.object(browserNavigateShape);
|
|
1554
|
+
var browserHoverShape = {
|
|
1555
|
+
session_id: z.string().min(1),
|
|
1556
|
+
ref: z.string().min(1)
|
|
1557
|
+
};
|
|
1558
|
+
var browserHoverSchema = z.object(browserHoverShape);
|
|
1559
|
+
var browserDragShape = {
|
|
1560
|
+
session_id: z.string().min(1),
|
|
1561
|
+
from_ref: z.string().min(1),
|
|
1562
|
+
to_ref: z.string().min(1)
|
|
1563
|
+
};
|
|
1564
|
+
var browserDragSchema = z.object(browserDragShape);
|
|
1565
|
+
var fillFieldKindSchema = z.enum([
|
|
1566
|
+
"input",
|
|
1567
|
+
"select",
|
|
1568
|
+
"checkbox",
|
|
1569
|
+
"radio"
|
|
1570
|
+
]);
|
|
1571
|
+
var fillFormFieldSchema = z.object({
|
|
1572
|
+
ref: z.string().min(1),
|
|
1573
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1574
|
+
kind: fillFieldKindSchema.optional()
|
|
1575
|
+
});
|
|
1576
|
+
var browserFillFormShape = {
|
|
1577
|
+
session_id: z.string().min(1),
|
|
1578
|
+
fields: z.array(fillFormFieldSchema).min(1)
|
|
1579
|
+
};
|
|
1580
|
+
var browserFillFormSchema = z.object(browserFillFormShape);
|
|
1581
|
+
var browserUploadFileShape = {
|
|
1582
|
+
session_id: z.string().min(1),
|
|
1583
|
+
ref: z.string().min(1),
|
|
1584
|
+
file_path: z.string().min(1)
|
|
1585
|
+
};
|
|
1586
|
+
var browserUploadFileSchema = z.object(browserUploadFileShape);
|
|
1587
|
+
var dialogActionSchema = z.enum([
|
|
1588
|
+
"accept",
|
|
1589
|
+
"dismiss",
|
|
1590
|
+
"accept_with_text"
|
|
1591
|
+
]);
|
|
1592
|
+
var browserHandleDialogShape = {
|
|
1593
|
+
session_id: z.string().min(1),
|
|
1594
|
+
action: dialogActionSchema,
|
|
1595
|
+
/** Only used when action='accept_with_text'. */
|
|
1596
|
+
text: z.string().optional(),
|
|
1597
|
+
/**
|
|
1598
|
+
* Arming behavior: registers a one-shot handler for the NEXT dialog
|
|
1599
|
+
* raised on the page. Call this BEFORE the action that triggers the
|
|
1600
|
+
* dialog (e.g. before clicking the button that calls `confirm()`).
|
|
1601
|
+
* Default 30s if no dialog appears, handler is auto-removed.
|
|
1602
|
+
*/
|
|
1603
|
+
timeout_ms: z.number().int().positive().optional()
|
|
1604
|
+
};
|
|
1605
|
+
var browserHandleDialogSchema = z.object(browserHandleDialogShape);
|
|
1606
|
+
var consoleLevelSchema = z.enum([
|
|
1607
|
+
"error",
|
|
1608
|
+
"warning",
|
|
1609
|
+
"info",
|
|
1610
|
+
"log",
|
|
1611
|
+
"debug",
|
|
1612
|
+
"trace"
|
|
1613
|
+
]);
|
|
1614
|
+
var browserConsoleShape = {
|
|
1615
|
+
session_id: z.string().min(1),
|
|
1616
|
+
/** Filter to only these levels. Default: errors+warnings. */
|
|
1617
|
+
levels: z.array(consoleLevelSchema).optional(),
|
|
1618
|
+
/** Substring match on message text. */
|
|
1619
|
+
contains: z.string().optional(),
|
|
1620
|
+
/** Drop all buffered messages after returning. */
|
|
1621
|
+
clear: z.boolean().default(false),
|
|
1622
|
+
/** Cap on returned messages (artifact still holds full ring buffer). */
|
|
1623
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1624
|
+
};
|
|
1625
|
+
var browserConsoleSchema = z.object(browserConsoleShape);
|
|
1626
|
+
var browserNetworkShape = {
|
|
1627
|
+
session_id: z.string().min(1),
|
|
1628
|
+
/** Substring or regex (per `pattern_kind`) match on URL. */
|
|
1629
|
+
url_pattern: z.string().optional(),
|
|
1630
|
+
pattern_kind: z.enum(["substring", "regex"]).default("substring"),
|
|
1631
|
+
method: z.string().optional(),
|
|
1632
|
+
/** Inclusive range — e.g. `{min: 400, max: 599}` for any error response. */
|
|
1633
|
+
status_range: z.object({
|
|
1634
|
+
min: z.number().int().min(100).max(599),
|
|
1635
|
+
max: z.number().int().min(100).max(599)
|
|
1636
|
+
}).optional(),
|
|
1637
|
+
only_failed: z.boolean().default(false),
|
|
1638
|
+
/** Write the full HAR file for this session to artifacts/{runId}/network.har. */
|
|
1639
|
+
export_har: z.boolean().default(false),
|
|
1640
|
+
/** Drop buffered entries after returning. */
|
|
1641
|
+
clear: z.boolean().default(false),
|
|
1642
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1643
|
+
};
|
|
1644
|
+
var browserNetworkSchema = z.object(browserNetworkShape);
|
|
1645
|
+
var networkPresetSchema = z.enum([
|
|
1646
|
+
"offline",
|
|
1647
|
+
"slow-3g",
|
|
1648
|
+
"fast-3g",
|
|
1649
|
+
"slow-4g",
|
|
1650
|
+
"fast-4g",
|
|
1651
|
+
"no-throttling"
|
|
1652
|
+
]);
|
|
1653
|
+
var geolocationSchema = z.object({
|
|
1654
|
+
latitude: z.number().min(-90).max(90),
|
|
1655
|
+
longitude: z.number().min(-180).max(180),
|
|
1656
|
+
accuracy: z.number().nonnegative().optional()
|
|
1657
|
+
});
|
|
1658
|
+
var browserSetEnvShape = {
|
|
1659
|
+
session_id: z.string().min(1),
|
|
1660
|
+
viewport: viewportSchema.optional(),
|
|
1661
|
+
offline: z.boolean().optional(),
|
|
1662
|
+
geolocation: geolocationSchema.optional(),
|
|
1663
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1664
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1665
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1666
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1667
|
+
/** CPU slowdown multiplier (1 = no throttle, 4 = 4x slower). Chromium only. */
|
|
1668
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1669
|
+
};
|
|
1670
|
+
var browserSetEnvSchema = z.object(browserSetEnvShape);
|
|
1671
|
+
var browserEvaluateShape = {
|
|
1672
|
+
session_id: z.string().min(1),
|
|
1673
|
+
script: z.string().min(1),
|
|
1674
|
+
args: z.array(z.unknown()).optional()
|
|
1675
|
+
};
|
|
1676
|
+
var browserEvaluateSchema = z.object(browserEvaluateShape);
|
|
1677
|
+
var browserPagesShape = {
|
|
1678
|
+
session_id: z.string().min(1)
|
|
1679
|
+
};
|
|
1680
|
+
var browserPagesSchema = z.object(browserPagesShape);
|
|
1681
|
+
var browserSwitchPageShape = {
|
|
1682
|
+
session_id: z.string().min(1),
|
|
1683
|
+
index: z.number().int().nonnegative()
|
|
1684
|
+
};
|
|
1685
|
+
var browserSwitchPageSchema = z.object(browserSwitchPageShape);
|
|
1686
|
+
var verifyFillFieldSchema = z.object({
|
|
1687
|
+
query: z.string(),
|
|
1688
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1689
|
+
kind: fillFieldKindSchema.optional()
|
|
1690
|
+
});
|
|
1067
1691
|
var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
1068
1692
|
z.object({ kind: z.literal("click"), query: z.string() }),
|
|
1069
1693
|
z.object({
|
|
@@ -1074,7 +1698,44 @@ var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
|
1074
1698
|
}),
|
|
1075
1699
|
z.object({ kind: z.literal("key"), key: z.string() }),
|
|
1076
1700
|
z.object({ kind: z.literal("wait_for"), condition: waitConditionSchema }),
|
|
1077
|
-
z.object({ kind: z.literal("navigate"), url: z.string().url() })
|
|
1701
|
+
z.object({ kind: z.literal("navigate"), url: z.string().url() }),
|
|
1702
|
+
// v0.5 additions
|
|
1703
|
+
z.object({ kind: z.literal("hover"), query: z.string() }),
|
|
1704
|
+
z.object({
|
|
1705
|
+
kind: z.literal("drag"),
|
|
1706
|
+
from_query: z.string(),
|
|
1707
|
+
to_query: z.string()
|
|
1708
|
+
}),
|
|
1709
|
+
z.object({
|
|
1710
|
+
kind: z.literal("fill_form"),
|
|
1711
|
+
fields: z.array(verifyFillFieldSchema).min(1)
|
|
1712
|
+
}),
|
|
1713
|
+
z.object({
|
|
1714
|
+
kind: z.literal("upload"),
|
|
1715
|
+
query: z.string(),
|
|
1716
|
+
file_path: z.string().min(1)
|
|
1717
|
+
}),
|
|
1718
|
+
z.object({
|
|
1719
|
+
kind: z.literal("dialog"),
|
|
1720
|
+
action: dialogActionSchema,
|
|
1721
|
+
text: z.string().optional()
|
|
1722
|
+
}),
|
|
1723
|
+
z.object({
|
|
1724
|
+
kind: z.literal("set_env"),
|
|
1725
|
+
viewport: viewportSchema.optional(),
|
|
1726
|
+
offline: z.boolean().optional(),
|
|
1727
|
+
geolocation: geolocationSchema.optional(),
|
|
1728
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1729
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1730
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1731
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1732
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1733
|
+
}),
|
|
1734
|
+
z.object({
|
|
1735
|
+
kind: z.literal("switch_page"),
|
|
1736
|
+
index: z.number().int().nonnegative()
|
|
1737
|
+
}),
|
|
1738
|
+
z.object({ kind: z.literal("evaluate"), script: z.string().min(1) })
|
|
1078
1739
|
]);
|
|
1079
1740
|
var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
1080
1741
|
z.object({ kind: z.literal("text_visible"), text: z.string() }),
|
|
@@ -1084,6 +1745,28 @@ var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
|
1084
1745
|
kind: z.literal("ref_in_state"),
|
|
1085
1746
|
query: z.string(),
|
|
1086
1747
|
state: z.enum(["visible", "enabled", "focused"])
|
|
1748
|
+
}),
|
|
1749
|
+
// v0.5 additions
|
|
1750
|
+
z.object({
|
|
1751
|
+
kind: z.literal("no_console_errors"),
|
|
1752
|
+
exclude_patterns: z.array(z.string()).optional()
|
|
1753
|
+
}),
|
|
1754
|
+
z.object({
|
|
1755
|
+
kind: z.literal("no_failed_requests"),
|
|
1756
|
+
exclude_patterns: z.array(z.string()).optional(),
|
|
1757
|
+
/** When true, only 5xx counts as a failure. Default false (4xx + 5xx). */
|
|
1758
|
+
allow_4xx: z.boolean().optional()
|
|
1759
|
+
}),
|
|
1760
|
+
z.object({
|
|
1761
|
+
kind: z.literal("request_made"),
|
|
1762
|
+
url_pattern: z.string(),
|
|
1763
|
+
method: z.string().optional(),
|
|
1764
|
+
min_count: z.number().int().positive().optional()
|
|
1765
|
+
}),
|
|
1766
|
+
z.object({
|
|
1767
|
+
kind: z.literal("response_status"),
|
|
1768
|
+
url_pattern: z.string(),
|
|
1769
|
+
status: z.number().int().min(100).max(599)
|
|
1087
1770
|
})
|
|
1088
1771
|
]);
|
|
1089
1772
|
var captureKindSchema = z.enum([
|
|
@@ -1091,7 +1774,8 @@ var captureKindSchema = z.enum([
|
|
|
1091
1774
|
"har",
|
|
1092
1775
|
"console",
|
|
1093
1776
|
"a11y_tree",
|
|
1094
|
-
"video"
|
|
1777
|
+
"video",
|
|
1778
|
+
"trace"
|
|
1095
1779
|
]);
|
|
1096
1780
|
var verifyUiFlowShape = {
|
|
1097
1781
|
mode: z.enum(["assert", "reproduce"]).default("assert"),
|
|
@@ -1169,6 +1853,19 @@ var ToolNames = {
|
|
|
1169
1853
|
browserWaitFor: "rolepod_browser_wait_for",
|
|
1170
1854
|
browserScreenshot: "rolepod_browser_screenshot",
|
|
1171
1855
|
browserNavigate: "rolepod_browser_navigate",
|
|
1856
|
+
// v0.5 atomics
|
|
1857
|
+
browserHover: "rolepod_browser_hover",
|
|
1858
|
+
browserDrag: "rolepod_browser_drag",
|
|
1859
|
+
browserFillForm: "rolepod_browser_fill_form",
|
|
1860
|
+
browserUploadFile: "rolepod_browser_upload_file",
|
|
1861
|
+
browserHandleDialog: "rolepod_browser_handle_dialog",
|
|
1862
|
+
browserConsole: "rolepod_browser_console",
|
|
1863
|
+
browserNetwork: "rolepod_browser_network",
|
|
1864
|
+
browserSetEnv: "rolepod_browser_set_env",
|
|
1865
|
+
browserEvaluate: "rolepod_browser_evaluate",
|
|
1866
|
+
browserPages: "rolepod_browser_pages",
|
|
1867
|
+
browserSwitchPage: "rolepod_browser_switch_page",
|
|
1868
|
+
// composite
|
|
1172
1869
|
verifyUiFlow: "rolepod_verify_ui_flow",
|
|
1173
1870
|
auditA11y: "rolepod_audit_a11y",
|
|
1174
1871
|
visualDiff: "rolepod_visual_diff",
|
|
@@ -1242,43 +1939,256 @@ var browserCloseTool = {
|
|
|
1242
1939
|
}
|
|
1243
1940
|
};
|
|
1244
1941
|
|
|
1245
|
-
// src/tools/atomic/
|
|
1246
|
-
var
|
|
1247
|
-
name: ToolNames.
|
|
1248
|
-
description: "
|
|
1249
|
-
inputShape:
|
|
1942
|
+
// src/tools/atomic/browser_console.ts
|
|
1943
|
+
var browserConsoleTool = {
|
|
1944
|
+
name: ToolNames.browserConsole,
|
|
1945
|
+
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.",
|
|
1946
|
+
inputShape: browserConsoleShape,
|
|
1250
1947
|
build(ctx) {
|
|
1251
1948
|
return safeHandler(async (args) => {
|
|
1252
1949
|
const engine = ctx.registry.engineFor(args.session_id);
|
|
1253
|
-
|
|
1254
|
-
|
|
1950
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
1951
|
+
throw new RolepodMcpError(
|
|
1952
|
+
"unsupported_engine",
|
|
1953
|
+
"console is web-only and requires PlaywrightEngine."
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
const levels = args.levels && args.levels.length > 0 ? args.levels : ["error", "warning"];
|
|
1957
|
+
const messages = engine.getConsole(args.session_id, {
|
|
1958
|
+
levels,
|
|
1959
|
+
...args.contains !== void 0 ? { contains: args.contains } : {},
|
|
1960
|
+
clear: args.clear,
|
|
1961
|
+
limit: args.limit
|
|
1962
|
+
});
|
|
1963
|
+
return ok({
|
|
1964
|
+
count: messages.length,
|
|
1965
|
+
messages
|
|
1966
|
+
});
|
|
1255
1967
|
});
|
|
1256
1968
|
}
|
|
1257
1969
|
};
|
|
1258
1970
|
|
|
1259
|
-
// src/tools/atomic/
|
|
1260
|
-
var
|
|
1261
|
-
name: ToolNames.
|
|
1262
|
-
description: "
|
|
1263
|
-
inputShape:
|
|
1971
|
+
// src/tools/atomic/browser_drag.ts
|
|
1972
|
+
var browserDragTool = {
|
|
1973
|
+
name: ToolNames.browserDrag,
|
|
1974
|
+
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.",
|
|
1975
|
+
inputShape: browserDragShape,
|
|
1264
1976
|
build(ctx) {
|
|
1265
1977
|
return safeHandler(async (args) => {
|
|
1266
1978
|
const engine = ctx.registry.engineFor(args.session_id);
|
|
1267
|
-
await engine.
|
|
1268
|
-
|
|
1979
|
+
await engine.drag(
|
|
1980
|
+
{
|
|
1981
|
+
id: args.session_id,
|
|
1982
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
1983
|
+
},
|
|
1984
|
+
args.from_ref,
|
|
1985
|
+
args.to_ref
|
|
1986
|
+
);
|
|
1987
|
+
return ok({ dragged: true });
|
|
1269
1988
|
});
|
|
1270
1989
|
}
|
|
1271
1990
|
};
|
|
1272
1991
|
|
|
1273
|
-
// src/tools/atomic/
|
|
1274
|
-
var
|
|
1275
|
-
name: ToolNames.
|
|
1276
|
-
description: "
|
|
1277
|
-
inputShape:
|
|
1992
|
+
// src/tools/atomic/browser_evaluate.ts
|
|
1993
|
+
var browserEvaluateTool = {
|
|
1994
|
+
name: ToolNames.browserEvaluate,
|
|
1995
|
+
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).",
|
|
1996
|
+
inputShape: browserEvaluateShape,
|
|
1278
1997
|
build(ctx) {
|
|
1998
|
+
const allowed = process.env.ROLEPOD_ALLOW_EVAL === "1";
|
|
1279
1999
|
return safeHandler(async (args) => {
|
|
1280
|
-
|
|
1281
|
-
|
|
2000
|
+
if (!allowed) {
|
|
2001
|
+
throw new RolepodMcpError(
|
|
2002
|
+
"engine_error",
|
|
2003
|
+
"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."
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2007
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2008
|
+
throw new RolepodMcpError(
|
|
2009
|
+
"unsupported_engine",
|
|
2010
|
+
"evaluate is web-only and requires PlaywrightEngine."
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
const result = await engine.evaluate(
|
|
2014
|
+
args.session_id,
|
|
2015
|
+
args.script,
|
|
2016
|
+
args.args
|
|
2017
|
+
);
|
|
2018
|
+
return ok({ result });
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
|
|
2023
|
+
// src/tools/atomic/browser_fill_form.ts
|
|
2024
|
+
var browserFillFormTool = {
|
|
2025
|
+
name: ToolNames.browserFillForm,
|
|
2026
|
+
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.",
|
|
2027
|
+
inputShape: browserFillFormShape,
|
|
2028
|
+
build(ctx) {
|
|
2029
|
+
return safeHandler(async (args) => {
|
|
2030
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2031
|
+
await engine.fillForm(
|
|
2032
|
+
{
|
|
2033
|
+
id: args.session_id,
|
|
2034
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2035
|
+
},
|
|
2036
|
+
args.fields
|
|
2037
|
+
);
|
|
2038
|
+
return ok({ filled: args.fields.length });
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
// src/tools/atomic/browser_handle_dialog.ts
|
|
2044
|
+
var browserHandleDialogTool = {
|
|
2045
|
+
name: ToolNames.browserHandleDialog,
|
|
2046
|
+
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.",
|
|
2047
|
+
inputShape: browserHandleDialogShape,
|
|
2048
|
+
build(ctx) {
|
|
2049
|
+
return safeHandler(async (args) => {
|
|
2050
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2051
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2052
|
+
throw new RolepodMcpError(
|
|
2053
|
+
"unsupported_engine",
|
|
2054
|
+
"handle_dialog is web-only and requires PlaywrightEngine."
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
const { handled } = await engine.handleDialog(args.session_id, {
|
|
2058
|
+
action: args.action,
|
|
2059
|
+
...args.text !== void 0 ? { text: args.text } : {},
|
|
2060
|
+
...args.timeout_ms !== void 0 ? { timeoutMs: args.timeout_ms } : {}
|
|
2061
|
+
});
|
|
2062
|
+
return ok({ handled, action: args.action });
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
|
|
2067
|
+
// src/tools/atomic/browser_hover.ts
|
|
2068
|
+
var browserHoverTool = {
|
|
2069
|
+
name: ToolNames.browserHover,
|
|
2070
|
+
description: "Hover the pointer over the element identified by `ref`. Refs remain valid afterwards (read-mostly).",
|
|
2071
|
+
inputShape: browserHoverShape,
|
|
2072
|
+
build(ctx) {
|
|
2073
|
+
return safeHandler(async (args) => {
|
|
2074
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2075
|
+
await engine.hover(
|
|
2076
|
+
{
|
|
2077
|
+
id: args.session_id,
|
|
2078
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2079
|
+
},
|
|
2080
|
+
args.ref
|
|
2081
|
+
);
|
|
2082
|
+
return ok({ hovered: true });
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
// src/tools/atomic/browser_key.ts
|
|
2088
|
+
var browserKeyTool = {
|
|
2089
|
+
name: ToolNames.browserKey,
|
|
2090
|
+
description: "Press a single key (e.g. 'Enter', 'Tab', 'Escape'). Invalidates all refs on success.",
|
|
2091
|
+
inputShape: browserKeyShape,
|
|
2092
|
+
build(ctx) {
|
|
2093
|
+
return safeHandler(async (args) => {
|
|
2094
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2095
|
+
await engine.key({ id: args.session_id, platform: ctx.registry.platformOf(args.session_id) }, args.key);
|
|
2096
|
+
return ok({ pressed: true });
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
// src/tools/atomic/browser_navigate.ts
|
|
2102
|
+
var browserNavigateTool = {
|
|
2103
|
+
name: ToolNames.browserNavigate,
|
|
2104
|
+
description: "Navigate the session to a new URL (web only). Invalidates all refs on success.",
|
|
2105
|
+
inputShape: browserNavigateShape,
|
|
2106
|
+
build(ctx) {
|
|
2107
|
+
return safeHandler(async (args) => {
|
|
2108
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2109
|
+
await engine.navigate({ id: args.session_id, platform: ctx.registry.platformOf(args.session_id) }, args.url);
|
|
2110
|
+
return ok({ navigated: true, url: args.url });
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
// src/tools/atomic/browser_network.ts
|
|
2116
|
+
var browserNetworkTool = {
|
|
2117
|
+
name: ToolNames.browserNetwork,
|
|
2118
|
+
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`.',
|
|
2119
|
+
inputShape: browserNetworkShape,
|
|
2120
|
+
build(ctx) {
|
|
2121
|
+
return safeHandler(async (args) => {
|
|
2122
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2123
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2124
|
+
throw new RolepodMcpError(
|
|
2125
|
+
"unsupported_engine",
|
|
2126
|
+
"network is web-only and requires PlaywrightEngine."
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
const requests = engine.getNetwork(args.session_id, {
|
|
2130
|
+
...args.url_pattern !== void 0 ? { urlPattern: args.url_pattern } : {},
|
|
2131
|
+
patternKind: args.pattern_kind,
|
|
2132
|
+
...args.method !== void 0 ? { method: args.method } : {},
|
|
2133
|
+
...args.status_range !== void 0 ? { statusRange: args.status_range } : {},
|
|
2134
|
+
onlyFailed: args.only_failed,
|
|
2135
|
+
clear: args.clear,
|
|
2136
|
+
limit: args.limit
|
|
2137
|
+
});
|
|
2138
|
+
const failed = requests.filter(
|
|
2139
|
+
(r) => !!r.failure || r.status !== void 0 && r.status >= 400
|
|
2140
|
+
).length;
|
|
2141
|
+
return ok({
|
|
2142
|
+
count: requests.length,
|
|
2143
|
+
failed_count: failed,
|
|
2144
|
+
requests,
|
|
2145
|
+
// HAR file lives wherever the session was opened with
|
|
2146
|
+
// `capture.har.path`. We don't echo it here to avoid leaking
|
|
2147
|
+
// filesystem paths into untrusted logs; the verify_ui_flow run
|
|
2148
|
+
// result surfaces it in `evidence_paths.har`.
|
|
2149
|
+
har_recording: args.export_har ? "HAR is written at session close to the path passed via capture.har at open time." : void 0
|
|
2150
|
+
});
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
|
|
2155
|
+
// src/tools/atomic/browser_open.ts
|
|
2156
|
+
var browserOpenTool = {
|
|
2157
|
+
name: ToolNames.browserOpen,
|
|
2158
|
+
description: "Open a new browser or mobile session against a target. v0.1 supports platform='web' only; mobile lands in v0.3.",
|
|
2159
|
+
inputShape: browserOpenShape,
|
|
2160
|
+
build(ctx) {
|
|
2161
|
+
return safeHandler(async (args) => {
|
|
2162
|
+
const session = await ctx.registry.open(args);
|
|
2163
|
+
return ok({ session_id: session.id, platform: session.platform });
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2168
|
+
// src/tools/atomic/browser_pages.ts
|
|
2169
|
+
var browserPagesTool = {
|
|
2170
|
+
name: ToolNames.browserPages,
|
|
2171
|
+
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.",
|
|
2172
|
+
inputShape: browserPagesShape,
|
|
2173
|
+
build(ctx) {
|
|
2174
|
+
return safeHandler(async (args) => {
|
|
2175
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2176
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2177
|
+
throw new RolepodMcpError(
|
|
2178
|
+
"unsupported_engine",
|
|
2179
|
+
"pages is web-only and requires PlaywrightEngine."
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
const raw = engine.listPages(args.session_id);
|
|
2183
|
+
const pages = await Promise.all(
|
|
2184
|
+
raw.map(async (p) => ({
|
|
2185
|
+
index: p.index,
|
|
2186
|
+
url: p.url,
|
|
2187
|
+
title: await p.title_promise.catch(() => ""),
|
|
2188
|
+
active: p.active
|
|
2189
|
+
}))
|
|
2190
|
+
);
|
|
2191
|
+
return ok({ count: pages.length, pages });
|
|
1282
2192
|
});
|
|
1283
2193
|
}
|
|
1284
2194
|
};
|
|
@@ -1327,6 +2237,35 @@ var browserScrollTool = {
|
|
|
1327
2237
|
}
|
|
1328
2238
|
};
|
|
1329
2239
|
|
|
2240
|
+
// src/tools/atomic/browser_set_env.ts
|
|
2241
|
+
var browserSetEnvTool = {
|
|
2242
|
+
name: ToolNames.browserSetEnv,
|
|
2243
|
+
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.",
|
|
2244
|
+
inputShape: browserSetEnvShape,
|
|
2245
|
+
build(ctx) {
|
|
2246
|
+
return safeHandler(async (args) => {
|
|
2247
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2248
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2249
|
+
throw new RolepodMcpError(
|
|
2250
|
+
"unsupported_engine",
|
|
2251
|
+
"set_env is web-only and requires PlaywrightEngine."
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
await engine.setEnv(args.session_id, {
|
|
2255
|
+
...args.viewport !== void 0 ? { viewport: args.viewport } : {},
|
|
2256
|
+
...args.offline !== void 0 ? { offline: args.offline } : {},
|
|
2257
|
+
...args.geolocation !== void 0 ? { geolocation: args.geolocation } : {},
|
|
2258
|
+
...args.color_scheme !== void 0 ? { colorScheme: args.color_scheme } : {},
|
|
2259
|
+
...args.reduced_motion !== void 0 ? { reducedMotion: args.reduced_motion } : {},
|
|
2260
|
+
...args.extra_headers !== void 0 ? { extraHeaders: args.extra_headers } : {},
|
|
2261
|
+
...args.network_throttle !== void 0 ? { networkThrottle: args.network_throttle } : {},
|
|
2262
|
+
...args.cpu_throttle !== void 0 ? { cpuThrottle: args.cpu_throttle } : {}
|
|
2263
|
+
});
|
|
2264
|
+
return ok({ applied: true });
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
|
|
1330
2269
|
// src/tools/atomic/browser_snapshot.ts
|
|
1331
2270
|
var browserSnapshotTool = {
|
|
1332
2271
|
name: ToolNames.browserSnapshot,
|
|
@@ -1349,6 +2288,26 @@ var browserSnapshotTool = {
|
|
|
1349
2288
|
}
|
|
1350
2289
|
};
|
|
1351
2290
|
|
|
2291
|
+
// src/tools/atomic/browser_switch_page.ts
|
|
2292
|
+
var browserSwitchPageTool = {
|
|
2293
|
+
name: ToolNames.browserSwitchPage,
|
|
2294
|
+
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.",
|
|
2295
|
+
inputShape: browserSwitchPageShape,
|
|
2296
|
+
build(ctx) {
|
|
2297
|
+
return safeHandler(async (args) => {
|
|
2298
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2299
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2300
|
+
throw new RolepodMcpError(
|
|
2301
|
+
"unsupported_engine",
|
|
2302
|
+
"switch_page is web-only and requires PlaywrightEngine."
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
await engine.switchPage(args.session_id, args.index);
|
|
2306
|
+
return ok({ active_index: args.index });
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
|
|
1352
2311
|
// src/tools/atomic/browser_type.ts
|
|
1353
2312
|
var browserTypeTool = {
|
|
1354
2313
|
name: ToolNames.browserType,
|
|
@@ -1368,6 +2327,27 @@ var browserTypeTool = {
|
|
|
1368
2327
|
}
|
|
1369
2328
|
};
|
|
1370
2329
|
|
|
2330
|
+
// src/tools/atomic/browser_upload_file.ts
|
|
2331
|
+
var browserUploadFileTool = {
|
|
2332
|
+
name: ToolNames.browserUploadFile,
|
|
2333
|
+
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.",
|
|
2334
|
+
inputShape: browserUploadFileShape,
|
|
2335
|
+
build(ctx) {
|
|
2336
|
+
return safeHandler(async (args) => {
|
|
2337
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2338
|
+
await engine.uploadFile(
|
|
2339
|
+
{
|
|
2340
|
+
id: args.session_id,
|
|
2341
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2342
|
+
},
|
|
2343
|
+
args.ref,
|
|
2344
|
+
args.file_path
|
|
2345
|
+
);
|
|
2346
|
+
return ok({ uploaded: true, file_path: args.file_path });
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
|
|
1371
2351
|
// src/tools/atomic/browser_wait_for.ts
|
|
1372
2352
|
var browserWaitForTool = {
|
|
1373
2353
|
name: ToolNames.browserWaitFor,
|
|
@@ -1389,6 +2369,39 @@ var browserWaitForTool = {
|
|
|
1389
2369
|
|
|
1390
2370
|
// src/tools/composite/audit_a11y.ts
|
|
1391
2371
|
import AxeBuilder from "@axe-core/playwright";
|
|
2372
|
+
|
|
2373
|
+
// src/util/manifest.ts
|
|
2374
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
2375
|
+
import { resolve as resolve2 } from "path";
|
|
2376
|
+
var ROLEPOD_PROTOCOL_VERSION = "rolepod/v1";
|
|
2377
|
+
async function writeManifest(input) {
|
|
2378
|
+
const manifest = {
|
|
2379
|
+
protocol: ROLEPOD_PROTOCOL_VERSION,
|
|
2380
|
+
plugin: "rolepod-uiproof",
|
|
2381
|
+
skill: input.skill,
|
|
2382
|
+
phase: input.phase,
|
|
2383
|
+
status: input.status,
|
|
2384
|
+
summary: input.summary,
|
|
2385
|
+
started_at: input.startedAt,
|
|
2386
|
+
finished_at: input.finishedAt,
|
|
2387
|
+
artifacts: input.artifacts,
|
|
2388
|
+
metadata: input.metadata ?? {}
|
|
2389
|
+
};
|
|
2390
|
+
const path = resolve2(input.runDir, "manifest.json");
|
|
2391
|
+
try {
|
|
2392
|
+
await writeFile2(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
2393
|
+
return path;
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
log.warn("manifest write failed", {
|
|
2396
|
+
run_dir: input.runDir,
|
|
2397
|
+
skill: input.skill,
|
|
2398
|
+
err: String(err)
|
|
2399
|
+
});
|
|
2400
|
+
return void 0;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/tools/composite/audit_a11y.ts
|
|
1392
2405
|
var TAGS_BY_LEVEL = {
|
|
1393
2406
|
"wcag-a": ["wcag2a", "wcag21a"],
|
|
1394
2407
|
"wcag-aa": ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
|
|
@@ -1413,7 +2426,11 @@ var auditA11yTool = {
|
|
|
1413
2426
|
inputShape: auditA11yShape,
|
|
1414
2427
|
build(ctx) {
|
|
1415
2428
|
return safeHandler(async (args) => {
|
|
1416
|
-
const
|
|
2429
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2430
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
2431
|
+
"audit",
|
|
2432
|
+
{ skill: "audit-a11y" }
|
|
2433
|
+
);
|
|
1417
2434
|
const session = await ctx.registry.open(args.open);
|
|
1418
2435
|
const engine = ctx.registry.engineFor(session.id);
|
|
1419
2436
|
if (!(engine instanceof PlaywrightEngine)) {
|
|
@@ -1481,15 +2498,45 @@ var auditA11yTool = {
|
|
|
1481
2498
|
await ctx.registry.close(session).catch(() => void 0);
|
|
1482
2499
|
}
|
|
1483
2500
|
}
|
|
2501
|
+
const counts = countBySeverity(issues);
|
|
2502
|
+
const status = a11yStatus(counts);
|
|
2503
|
+
const artifacts = reportPath ? [{ type: "report", path: reportPath }] : [];
|
|
2504
|
+
const manifestPath = await writeManifest({
|
|
2505
|
+
runDir,
|
|
2506
|
+
skill,
|
|
2507
|
+
phase: "verify",
|
|
2508
|
+
status,
|
|
2509
|
+
summary: buildAuditSummary(args.level, counts, status),
|
|
2510
|
+
startedAt,
|
|
2511
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2512
|
+
artifacts,
|
|
2513
|
+
metadata: {
|
|
2514
|
+
level: args.level,
|
|
2515
|
+
scope: args.scope,
|
|
2516
|
+
counts,
|
|
2517
|
+
report_format: args.report_format
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
1484
2520
|
return ok({
|
|
1485
2521
|
run_id: runId,
|
|
1486
|
-
counts
|
|
2522
|
+
counts,
|
|
1487
2523
|
issues,
|
|
1488
|
-
report_path: reportPath
|
|
2524
|
+
report_path: reportPath,
|
|
2525
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
1489
2526
|
});
|
|
1490
2527
|
});
|
|
1491
2528
|
}
|
|
1492
2529
|
};
|
|
2530
|
+
function a11yStatus(counts) {
|
|
2531
|
+
if ((counts.critical ?? 0) + (counts.serious ?? 0) > 0) return "fail";
|
|
2532
|
+
if ((counts.moderate ?? 0) + (counts.minor ?? 0) > 0) return "warn";
|
|
2533
|
+
return "pass";
|
|
2534
|
+
}
|
|
2535
|
+
function buildAuditSummary(level, counts, status) {
|
|
2536
|
+
const total = (counts.critical ?? 0) + (counts.serious ?? 0) + (counts.moderate ?? 0) + (counts.minor ?? 0);
|
|
2537
|
+
if (status === "pass") return `${level}: 0 issues`;
|
|
2538
|
+
return `${level}: ${total} issue(s) \u2014 critical=${counts.critical ?? 0}, serious=${counts.serious ?? 0}, moderate=${counts.moderate ?? 0}, minor=${counts.minor ?? 0}`;
|
|
2539
|
+
}
|
|
1493
2540
|
function pickWcagRef(tags) {
|
|
1494
2541
|
return tags.find((t) => /^wcag\d/.test(t));
|
|
1495
2542
|
}
|
|
@@ -1606,14 +2653,18 @@ function scoreTree(root, tokens) {
|
|
|
1606
2653
|
|
|
1607
2654
|
// src/tools/composite/scaffold_e2e.ts
|
|
1608
2655
|
import { readFile } from "fs/promises";
|
|
1609
|
-
import { resolve as
|
|
2656
|
+
import { resolve as resolve3 } from "path";
|
|
1610
2657
|
var scaffoldE2eTool = {
|
|
1611
2658
|
name: ToolNames.scaffoldE2e,
|
|
1612
2659
|
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.",
|
|
1613
2660
|
inputShape: scaffoldE2eShape,
|
|
1614
2661
|
build(ctx) {
|
|
1615
2662
|
return safeHandler(async (args) => {
|
|
1616
|
-
const
|
|
2663
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2664
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
2665
|
+
"scaffold",
|
|
2666
|
+
{ skill: "scaffold-e2e" }
|
|
2667
|
+
);
|
|
1617
2668
|
const slug = slugify(args.scenario_nl);
|
|
1618
2669
|
const bundle = args.recorded_bundle ? await loadReplay(args.recorded_bundle) : null;
|
|
1619
2670
|
const ctxObj = { args, slug, bundle };
|
|
@@ -1651,19 +2702,35 @@ var scaffoldE2eTool = {
|
|
|
1651
2702
|
);
|
|
1652
2703
|
}
|
|
1653
2704
|
const path = await ctx.store.writeReport(runDir, filename, body);
|
|
2705
|
+
const manifestPath = await writeManifest({
|
|
2706
|
+
runDir,
|
|
2707
|
+
skill,
|
|
2708
|
+
phase: "build",
|
|
2709
|
+
status: "pass",
|
|
2710
|
+
summary: `generated ${args.framework} test "${filename}" from ${bundle ? "replay bundle" : "scenario"}`,
|
|
2711
|
+
startedAt,
|
|
2712
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2713
|
+
artifacts: [{ type: "test_file", path }],
|
|
2714
|
+
metadata: {
|
|
2715
|
+
framework: args.framework,
|
|
2716
|
+
language,
|
|
2717
|
+
from_replay_bundle: Boolean(bundle)
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
1654
2720
|
return ok({
|
|
1655
2721
|
run_id: runId,
|
|
1656
2722
|
test_file_path: path,
|
|
1657
2723
|
language,
|
|
1658
2724
|
dependencies,
|
|
1659
2725
|
setup_notes: setupNotes,
|
|
1660
|
-
from_replay_bundle: Boolean(bundle)
|
|
2726
|
+
from_replay_bundle: Boolean(bundle),
|
|
2727
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
1661
2728
|
});
|
|
1662
2729
|
});
|
|
1663
2730
|
}
|
|
1664
2731
|
};
|
|
1665
2732
|
async function loadReplay(bundlePath) {
|
|
1666
|
-
const raw = await readFile(
|
|
2733
|
+
const raw = await readFile(resolve3(bundlePath), "utf8");
|
|
1667
2734
|
return JSON.parse(raw);
|
|
1668
2735
|
}
|
|
1669
2736
|
function slugify(s) {
|
|
@@ -1740,6 +2807,67 @@ function playwrightStepLine(step) {
|
|
|
1740
2807
|
return ` await page.goto(${JSON.stringify(step.url)});`;
|
|
1741
2808
|
case "wait_for":
|
|
1742
2809
|
return ` // wait_for: ${JSON.stringify(step.condition)} \u2014 translate to page.waitForXxx()`;
|
|
2810
|
+
case "hover":
|
|
2811
|
+
return ` await page.getByText(${JSON.stringify(step.query)}, { exact: false }).first().hover();`;
|
|
2812
|
+
case "drag":
|
|
2813
|
+
return [
|
|
2814
|
+
` await page`,
|
|
2815
|
+
` .getByText(${JSON.stringify(step.from_query)}, { exact: false })`,
|
|
2816
|
+
` .first()`,
|
|
2817
|
+
` .dragTo(page.getByText(${JSON.stringify(step.to_query)}, { exact: false }).first());`
|
|
2818
|
+
].join("\n");
|
|
2819
|
+
case "fill_form": {
|
|
2820
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
2821
|
+
return fields.map((f) => {
|
|
2822
|
+
const q = JSON.stringify(f.query);
|
|
2823
|
+
if (f.kind === "select") {
|
|
2824
|
+
return ` await page.getByLabel(${q}).selectOption(${JSON.stringify(String(f.value))});`;
|
|
2825
|
+
}
|
|
2826
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
2827
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true" || String(f.value) === "on";
|
|
2828
|
+
return ` await page.getByLabel(${q}).setChecked(${checked});`;
|
|
2829
|
+
}
|
|
2830
|
+
return ` await page.getByLabel(${q}).fill(${JSON.stringify(String(f.value))});`;
|
|
2831
|
+
}).join("\n");
|
|
2832
|
+
}
|
|
2833
|
+
case "upload":
|
|
2834
|
+
return ` await page.getByLabel(${JSON.stringify(step.query)}).setInputFiles(${JSON.stringify(step.file_path)});`;
|
|
2835
|
+
case "dialog":
|
|
2836
|
+
return [
|
|
2837
|
+
` page.once("dialog", async (dialog) => {`,
|
|
2838
|
+
step.action === "accept" ? ` await dialog.accept();` : step.action === "accept_with_text" ? ` await dialog.accept(${JSON.stringify(step.text ?? "")});` : ` await dialog.dismiss();`,
|
|
2839
|
+
` });`
|
|
2840
|
+
].join("\n");
|
|
2841
|
+
case "set_env": {
|
|
2842
|
+
const lines = [];
|
|
2843
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
2844
|
+
const v = step.viewport;
|
|
2845
|
+
lines.push(` await page.setViewportSize({ width: ${v.width}, height: ${v.height} });`);
|
|
2846
|
+
}
|
|
2847
|
+
if (step.offline !== void 0) {
|
|
2848
|
+
lines.push(` await page.context().setOffline(${Boolean(step.offline)});`);
|
|
2849
|
+
}
|
|
2850
|
+
if (step.geolocation) {
|
|
2851
|
+
lines.push(` await page.context().setGeolocation(${JSON.stringify(step.geolocation)});`);
|
|
2852
|
+
}
|
|
2853
|
+
if (step.color_scheme || step.reduced_motion) {
|
|
2854
|
+
const opts = {};
|
|
2855
|
+
if (step.color_scheme) opts.colorScheme = step.color_scheme;
|
|
2856
|
+
if (step.reduced_motion) opts.reducedMotion = step.reduced_motion;
|
|
2857
|
+
lines.push(` await page.emulateMedia(${JSON.stringify(opts)});`);
|
|
2858
|
+
}
|
|
2859
|
+
if (step.extra_headers) {
|
|
2860
|
+
lines.push(` await page.context().setExtraHTTPHeaders(${JSON.stringify(step.extra_headers)});`);
|
|
2861
|
+
}
|
|
2862
|
+
if (step.network_throttle || step.cpu_throttle !== void 0) {
|
|
2863
|
+
lines.push(` // network/cpu throttle requires CDP \u2014 see Playwright docs (chromium only)`);
|
|
2864
|
+
}
|
|
2865
|
+
return lines.length > 0 ? lines.join("\n") : ` // set_env: nothing to apply`;
|
|
2866
|
+
}
|
|
2867
|
+
case "switch_page":
|
|
2868
|
+
return ` const allPages = page.context().pages(); /* switch to index ${step.index} */ if (allPages[${step.index}]) await allPages[${step.index}].bringToFront();`;
|
|
2869
|
+
case "evaluate":
|
|
2870
|
+
return ` await page.evaluate(${JSON.stringify(step.script)});`;
|
|
1743
2871
|
default:
|
|
1744
2872
|
return ` // unsupported step kind: ${step.kind}`;
|
|
1745
2873
|
}
|
|
@@ -1754,6 +2882,20 @@ function playwrightExpectLine(exp) {
|
|
|
1754
2882
|
return ` await expect(page).toHaveURL(new RegExp(${JSON.stringify(exp.pattern)}));`;
|
|
1755
2883
|
case "ref_in_state":
|
|
1756
2884
|
return ` // ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)} \u2014 translate as needed`;
|
|
2885
|
+
case "no_console_errors":
|
|
2886
|
+
return [
|
|
2887
|
+
` // no_console_errors \u2014 collect via page.on('console') before the steps, then:`,
|
|
2888
|
+
` // expect(consoleErrors).toEqual([]);`
|
|
2889
|
+
].join("\n");
|
|
2890
|
+
case "no_failed_requests":
|
|
2891
|
+
return [
|
|
2892
|
+
` // no_failed_requests \u2014 collect via page.on('requestfailed'/'response') before the steps, then:`,
|
|
2893
|
+
` // expect(failedRequests).toEqual([]);`
|
|
2894
|
+
].join("\n");
|
|
2895
|
+
case "request_made":
|
|
2896
|
+
return ` await page.waitForRequest(new RegExp(${JSON.stringify(exp.url_pattern)}));`;
|
|
2897
|
+
case "response_status":
|
|
2898
|
+
return ` await page.waitForResponse((r) => new RegExp(${JSON.stringify(exp.url_pattern)}).test(r.url()) && r.status() === ${Number(exp.status)});`;
|
|
1757
2899
|
default:
|
|
1758
2900
|
return ` // unsupported expect kind: ${exp.kind}`;
|
|
1759
2901
|
}
|
|
@@ -1770,6 +2912,56 @@ function seleniumStepLine(step) {
|
|
|
1770
2912
|
return ` driver.get(${JSON.stringify(step.url)})`;
|
|
1771
2913
|
case "wait_for":
|
|
1772
2914
|
return ` # wait_for: ${JSON.stringify(step.condition)} \u2014 translate to WebDriverWait`;
|
|
2915
|
+
case "hover":
|
|
2916
|
+
return [
|
|
2917
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
2918
|
+
` target = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.query))}\\")]")`,
|
|
2919
|
+
` ActionChains(driver).move_to_element(target).perform()`
|
|
2920
|
+
].join("\n");
|
|
2921
|
+
case "drag":
|
|
2922
|
+
return [
|
|
2923
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
2924
|
+
` src = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.from_query))}\\")]")`,
|
|
2925
|
+
` dst = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.to_query))}\\")]")`,
|
|
2926
|
+
` ActionChains(driver).drag_and_drop(src, dst).perform()`
|
|
2927
|
+
].join("\n");
|
|
2928
|
+
case "fill_form": {
|
|
2929
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
2930
|
+
return fields.map((f) => {
|
|
2931
|
+
const q = escapePy(f.query);
|
|
2932
|
+
if (f.kind === "select") {
|
|
2933
|
+
return [
|
|
2934
|
+
` from selenium.webdriver.support.ui import Select`,
|
|
2935
|
+
` Select(driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]")).select_by_visible_text(${JSON.stringify(String(f.value))})`
|
|
2936
|
+
].join("\n");
|
|
2937
|
+
}
|
|
2938
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
2939
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true";
|
|
2940
|
+
return ` el = driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]"); el.click() if el.is_selected() != ${checked ? "True" : "False"} else None`;
|
|
2941
|
+
}
|
|
2942
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]").send_keys(${JSON.stringify(String(f.value))})`;
|
|
2943
|
+
}).join("\n");
|
|
2944
|
+
}
|
|
2945
|
+
case "upload":
|
|
2946
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${escapePy(String(step.query))}\\"]").send_keys(${JSON.stringify(step.file_path)})`;
|
|
2947
|
+
case "dialog":
|
|
2948
|
+
return [
|
|
2949
|
+
` alert = driver.switch_to.alert`,
|
|
2950
|
+
step.action === "accept" ? ` alert.accept()` : step.action === "accept_with_text" ? ` alert.send_keys(${JSON.stringify(step.text ?? "")}); alert.accept()` : ` alert.dismiss()`
|
|
2951
|
+
].join("\n");
|
|
2952
|
+
case "set_env": {
|
|
2953
|
+
const lines = [];
|
|
2954
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
2955
|
+
const v = step.viewport;
|
|
2956
|
+
lines.push(` driver.set_window_size(${v.width}, ${v.height})`);
|
|
2957
|
+
}
|
|
2958
|
+
lines.push(` # set_env partially supported in Selenium \u2014 see selenium docs for offline/geolocation/colorScheme via CDP`);
|
|
2959
|
+
return lines.join("\n");
|
|
2960
|
+
}
|
|
2961
|
+
case "switch_page":
|
|
2962
|
+
return ` driver.switch_to.window(driver.window_handles[${step.index}])`;
|
|
2963
|
+
case "evaluate":
|
|
2964
|
+
return ` driver.execute_script(${JSON.stringify(step.script)})`;
|
|
1773
2965
|
default:
|
|
1774
2966
|
return ` # unsupported step kind: ${step.kind}`;
|
|
1775
2967
|
}
|
|
@@ -1784,6 +2976,18 @@ function seleniumExpectLine(exp) {
|
|
|
1784
2976
|
return ` import re; assert re.search(${JSON.stringify(exp.pattern)}, driver.current_url)`;
|
|
1785
2977
|
case "ref_in_state":
|
|
1786
2978
|
return ` # ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)}`;
|
|
2979
|
+
case "no_console_errors":
|
|
2980
|
+
return [
|
|
2981
|
+
` # no_console_errors \u2014 read browser logs via driver.get_log("browser")`,
|
|
2982
|
+
` errors = [l for l in driver.get_log("browser") if l.get("level") == "SEVERE"]`,
|
|
2983
|
+
` assert errors == [], f"console errors: {errors}"`
|
|
2984
|
+
].join("\n");
|
|
2985
|
+
case "no_failed_requests":
|
|
2986
|
+
return ` # no_failed_requests \u2014 selenium has no built-in network capture. Enable selenium-wire or BiDi for this.`;
|
|
2987
|
+
case "request_made":
|
|
2988
|
+
return ` # request_made ${JSON.stringify(exp.url_pattern)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
2989
|
+
case "response_status":
|
|
2990
|
+
return ` # response_status ${JSON.stringify(exp.url_pattern)} == ${Number(exp.status)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
1787
2991
|
default:
|
|
1788
2992
|
return ` # unsupported expect kind: ${exp.kind}`;
|
|
1789
2993
|
}
|
|
@@ -1812,6 +3016,10 @@ function indent(block, n) {
|
|
|
1812
3016
|
return block.split("\n").map((l) => l.length > 0 ? pad + l : l).join("\n");
|
|
1813
3017
|
}
|
|
1814
3018
|
|
|
3019
|
+
// src/tools/composite/verify_ui_flow.ts
|
|
3020
|
+
import { readdir } from "fs/promises";
|
|
3021
|
+
import { resolve as resolvePath2 } from "path";
|
|
3022
|
+
|
|
1815
3023
|
// src/replay/minimize.ts
|
|
1816
3024
|
async function ddmin(input, reproduces) {
|
|
1817
3025
|
let current = [...input];
|
|
@@ -1846,7 +3054,11 @@ var verifyUiFlowTool = {
|
|
|
1846
3054
|
inputShape: verifyUiFlowShape,
|
|
1847
3055
|
build(ctx) {
|
|
1848
3056
|
return safeHandler(async (args) => {
|
|
1849
|
-
const
|
|
3057
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3058
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3059
|
+
"verify",
|
|
3060
|
+
{ skill: "verify-ui" }
|
|
3061
|
+
);
|
|
1850
3062
|
const initial = await runFlow(ctx, args, args.steps, runDir, {
|
|
1851
3063
|
captureEvidence: true,
|
|
1852
3064
|
bundleName: "replay.json"
|
|
@@ -1871,17 +3083,75 @@ var verifyUiFlowTool = {
|
|
|
1871
3083
|
attempts: min.attempts
|
|
1872
3084
|
};
|
|
1873
3085
|
}
|
|
3086
|
+
const manifestPath = await writeManifest({
|
|
3087
|
+
runDir,
|
|
3088
|
+
skill,
|
|
3089
|
+
phase: "verify",
|
|
3090
|
+
status: initial.passed ? "pass" : "fail",
|
|
3091
|
+
summary: buildVerifySummary(args, initial),
|
|
3092
|
+
startedAt,
|
|
3093
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3094
|
+
artifacts: flattenVerifyEvidence(initial.evidence),
|
|
3095
|
+
metadata: {
|
|
3096
|
+
mode: args.mode,
|
|
3097
|
+
step_count: args.steps.length,
|
|
3098
|
+
expect_count: args.expect.length,
|
|
3099
|
+
...initial.finalUrl !== void 0 ? { final_url: initial.finalUrl } : {}
|
|
3100
|
+
}
|
|
3101
|
+
});
|
|
3102
|
+
if (manifestPath) result.manifest = manifestPath;
|
|
1874
3103
|
return ok(result);
|
|
1875
3104
|
});
|
|
1876
3105
|
}
|
|
1877
3106
|
};
|
|
3107
|
+
function buildVerifySummary(args, outcome) {
|
|
3108
|
+
const stepCount = args.steps.length;
|
|
3109
|
+
const expectCount = args.expect.length;
|
|
3110
|
+
if (outcome.passed) {
|
|
3111
|
+
return `${stepCount} step(s), ${expectCount} expect(s) passed`;
|
|
3112
|
+
}
|
|
3113
|
+
if (outcome.failedAtStep !== void 0) {
|
|
3114
|
+
return `failed at step ${outcome.failedAtStep}: ${outcome.failureReason ?? "unknown"}`;
|
|
3115
|
+
}
|
|
3116
|
+
return `failed: ${outcome.failureReason ?? "unknown"}`;
|
|
3117
|
+
}
|
|
3118
|
+
function flattenVerifyEvidence(ev) {
|
|
3119
|
+
const out = [];
|
|
3120
|
+
for (const s of ev.screenshots) out.push({ type: "screenshot", path: s });
|
|
3121
|
+
if (ev.replay_bundle) out.push({ type: "replay_bundle", path: ev.replay_bundle });
|
|
3122
|
+
if (ev.console) out.push({ type: "console", path: ev.console });
|
|
3123
|
+
if (ev.a11y_tree) out.push({ type: "a11y_tree", path: ev.a11y_tree });
|
|
3124
|
+
if (ev.har) out.push({ type: "har", path: ev.har });
|
|
3125
|
+
if (ev.trace) out.push({ type: "trace", path: ev.trace });
|
|
3126
|
+
if (ev.video) for (const v of ev.video) out.push({ type: "video", path: v });
|
|
3127
|
+
return out;
|
|
3128
|
+
}
|
|
3129
|
+
function buildCaptureOptions(captures, runDir) {
|
|
3130
|
+
const cap = {};
|
|
3131
|
+
if (captures.has("har")) {
|
|
3132
|
+
cap.har = { path: resolvePath2(runDir, "network.har") };
|
|
3133
|
+
}
|
|
3134
|
+
if (captures.has("video")) {
|
|
3135
|
+
cap.video = { dir: resolvePath2(runDir, "videos") };
|
|
3136
|
+
}
|
|
3137
|
+
if (captures.has("trace")) {
|
|
3138
|
+
cap.trace = { artifactDir: runDir };
|
|
3139
|
+
}
|
|
3140
|
+
return Object.keys(cap).length > 0 ? cap : void 0;
|
|
3141
|
+
}
|
|
1878
3142
|
async function runFlow(ctx, args, steps, runDir, opts) {
|
|
1879
3143
|
const evidence = { screenshots: [] };
|
|
3144
|
+
const captures = new Set(args.capture ?? ["screenshot"]);
|
|
1880
3145
|
let passed = false;
|
|
1881
3146
|
let failedAtStep;
|
|
1882
3147
|
let failureReason;
|
|
1883
3148
|
let finalSnapshot;
|
|
1884
|
-
const
|
|
3149
|
+
const openOpts = { ...args.open };
|
|
3150
|
+
const captureCfg = buildCaptureOptions(captures, runDir);
|
|
3151
|
+
if (captureCfg) {
|
|
3152
|
+
openOpts.capture = captureCfg;
|
|
3153
|
+
}
|
|
3154
|
+
const session = await ctx.registry.open(openOpts);
|
|
1885
3155
|
const engine = ctx.registry.engineFor(session.id);
|
|
1886
3156
|
const sessionHandle = { id: session.id, platform: session.platform };
|
|
1887
3157
|
try {
|
|
@@ -1900,7 +3170,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1900
3170
|
const failures = [];
|
|
1901
3171
|
for (let i = 0; i < args.expect.length; i++) {
|
|
1902
3172
|
const expectation = args.expect[i];
|
|
1903
|
-
if (!evaluateExpect(expectation, finalSnapshot)) {
|
|
3173
|
+
if (!evaluateExpect(expectation, finalSnapshot, engine, session.id)) {
|
|
1904
3174
|
failures.push(`expect[${i}] ${describeExpect(expectation)}`);
|
|
1905
3175
|
}
|
|
1906
3176
|
}
|
|
@@ -1914,8 +3184,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1914
3184
|
passed = false;
|
|
1915
3185
|
} finally {
|
|
1916
3186
|
if (opts.captureEvidence) {
|
|
1917
|
-
|
|
1918
|
-
if (wantScreenshot) {
|
|
3187
|
+
if (captures.has("screenshot")) {
|
|
1919
3188
|
try {
|
|
1920
3189
|
const buf = await engine.screenshot(sessionHandle, true);
|
|
1921
3190
|
const p = await ctx.store.writeScreenshot(runDir, buf, "final");
|
|
@@ -1924,6 +3193,37 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1924
3193
|
failureReason ??= `screenshot capture failed: ${describeError(err)}`;
|
|
1925
3194
|
}
|
|
1926
3195
|
}
|
|
3196
|
+
if (captures.has("console") && engine instanceof PlaywrightEngine) {
|
|
3197
|
+
try {
|
|
3198
|
+
const messages = engine.peekBuffers(session.id).console;
|
|
3199
|
+
evidence.console = await ctx.store.writeReport(
|
|
3200
|
+
runDir,
|
|
3201
|
+
"console.json",
|
|
3202
|
+
JSON.stringify(
|
|
3203
|
+
{
|
|
3204
|
+
count: messages.length,
|
|
3205
|
+
by_level: countByLevel(messages),
|
|
3206
|
+
messages
|
|
3207
|
+
},
|
|
3208
|
+
null,
|
|
3209
|
+
2
|
|
3210
|
+
)
|
|
3211
|
+
);
|
|
3212
|
+
} catch (err) {
|
|
3213
|
+
failureReason ??= `console capture failed: ${describeError(err)}`;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
if (captures.has("a11y_tree") && finalSnapshot) {
|
|
3217
|
+
try {
|
|
3218
|
+
evidence.a11y_tree = await ctx.store.writeReport(
|
|
3219
|
+
runDir,
|
|
3220
|
+
"a11y_tree.json",
|
|
3221
|
+
JSON.stringify(finalSnapshot, null, 2)
|
|
3222
|
+
);
|
|
3223
|
+
} catch (err) {
|
|
3224
|
+
failureReason ??= `a11y_tree capture failed: ${describeError(err)}`;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
1927
3227
|
try {
|
|
1928
3228
|
evidence.replay_bundle = await ctx.store.writeReplayBundle(
|
|
1929
3229
|
runDir,
|
|
@@ -1944,12 +3244,32 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1944
3244
|
await ctx.registry.close(sessionHandle).catch(() => void 0);
|
|
1945
3245
|
}
|
|
1946
3246
|
}
|
|
3247
|
+
if (opts.captureEvidence) {
|
|
3248
|
+
if (captureCfg?.har) evidence.har = captureCfg.har.path;
|
|
3249
|
+
if (captureCfg?.trace) {
|
|
3250
|
+
evidence.trace = resolvePath2(captureCfg.trace.artifactDir, "trace.zip");
|
|
3251
|
+
}
|
|
3252
|
+
if (captureCfg?.video) {
|
|
3253
|
+
try {
|
|
3254
|
+
const files = await readdir(captureCfg.video.dir).catch(() => []);
|
|
3255
|
+
evidence.video = files.filter((f) => f.endsWith(".webm")).map((f) => resolvePath2(captureCfg.video.dir, f));
|
|
3256
|
+
} catch {
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
1947
3260
|
const out = { passed, evidence };
|
|
1948
3261
|
if (failedAtStep !== void 0) out.failedAtStep = failedAtStep;
|
|
1949
3262
|
if (failureReason !== void 0) out.failureReason = failureReason;
|
|
1950
3263
|
if (finalSnapshot) out.finalUrl = finalSnapshot.url_or_screen;
|
|
1951
3264
|
return out;
|
|
1952
3265
|
}
|
|
3266
|
+
function countByLevel(messages) {
|
|
3267
|
+
const counts = {};
|
|
3268
|
+
for (const m of messages) {
|
|
3269
|
+
counts[m.level] = (counts[m.level] ?? 0) + 1;
|
|
3270
|
+
}
|
|
3271
|
+
return counts;
|
|
3272
|
+
}
|
|
1953
3273
|
async function minimize(ctx, args, initialSteps, runDir) {
|
|
1954
3274
|
const tagged = initialSteps.map((step, origIndex) => ({ step, origIndex }));
|
|
1955
3275
|
let attempts = 0;
|
|
@@ -2013,9 +3333,84 @@ async function runStep(engine, session, step, snap) {
|
|
|
2013
3333
|
case "navigate":
|
|
2014
3334
|
await engine.navigate(session, step.url);
|
|
2015
3335
|
return;
|
|
3336
|
+
case "hover": {
|
|
3337
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
3338
|
+
if (!ref) throw missingQuery(step.query);
|
|
3339
|
+
await engine.hover(session, ref);
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
case "drag": {
|
|
3343
|
+
const fromRef = findRefByQuery(snap.tree, step.from_query);
|
|
3344
|
+
if (!fromRef) throw missingQuery(step.from_query);
|
|
3345
|
+
const toRef = findRefByQuery(snap.tree, step.to_query);
|
|
3346
|
+
if (!toRef) throw missingQuery(step.to_query);
|
|
3347
|
+
await engine.drag(session, fromRef, toRef);
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
case "fill_form": {
|
|
3351
|
+
const resolved = step.fields.map((f) => {
|
|
3352
|
+
const ref = findRefByQuery(snap.tree, f.query);
|
|
3353
|
+
if (!ref) throw missingQuery(f.query);
|
|
3354
|
+
return f.kind !== void 0 ? { ref, value: f.value, kind: f.kind } : { ref, value: f.value };
|
|
3355
|
+
});
|
|
3356
|
+
await engine.fillForm(session, resolved);
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
case "upload": {
|
|
3360
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
3361
|
+
if (!ref) throw missingQuery(step.query);
|
|
3362
|
+
await engine.uploadFile(session, ref, step.file_path);
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
case "dialog": {
|
|
3366
|
+
requirePlaywright(engine, "dialog");
|
|
3367
|
+
void engine.handleDialog(session.id, {
|
|
3368
|
+
action: step.action,
|
|
3369
|
+
...step.text !== void 0 ? { text: step.text } : {}
|
|
3370
|
+
}).catch(() => void 0);
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
case "set_env": {
|
|
3374
|
+
requirePlaywright(engine, "set_env");
|
|
3375
|
+
await engine.setEnv(session.id, {
|
|
3376
|
+
...step.viewport !== void 0 ? { viewport: step.viewport } : {},
|
|
3377
|
+
...step.offline !== void 0 ? { offline: step.offline } : {},
|
|
3378
|
+
...step.geolocation !== void 0 ? { geolocation: step.geolocation } : {},
|
|
3379
|
+
...step.color_scheme !== void 0 ? { colorScheme: step.color_scheme } : {},
|
|
3380
|
+
...step.reduced_motion !== void 0 ? { reducedMotion: step.reduced_motion } : {},
|
|
3381
|
+
...step.extra_headers !== void 0 ? { extraHeaders: step.extra_headers } : {},
|
|
3382
|
+
...step.network_throttle !== void 0 ? { networkThrottle: step.network_throttle } : {},
|
|
3383
|
+
...step.cpu_throttle !== void 0 ? { cpuThrottle: step.cpu_throttle } : {}
|
|
3384
|
+
});
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
case "switch_page": {
|
|
3388
|
+
requirePlaywright(engine, "switch_page");
|
|
3389
|
+
await engine.switchPage(session.id, step.index);
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
case "evaluate": {
|
|
3393
|
+
requirePlaywright(engine, "evaluate");
|
|
3394
|
+
if (process.env.ROLEPOD_ALLOW_EVAL !== "1") {
|
|
3395
|
+
throw new RolepodMcpError(
|
|
3396
|
+
"engine_error",
|
|
3397
|
+
"verify_ui_flow step kind 'evaluate' is disabled. Restart the rolepod-uiproof MCP server with ROLEPOD_ALLOW_EVAL=1 to enable."
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3400
|
+
await engine.evaluate(session.id, step.script);
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
2016
3403
|
}
|
|
2017
3404
|
}
|
|
2018
|
-
function
|
|
3405
|
+
function requirePlaywright(engine, stepKind) {
|
|
3406
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
3407
|
+
throw new RolepodMcpError(
|
|
3408
|
+
"unsupported_engine",
|
|
3409
|
+
`verify_ui_flow step kind "${stepKind}" is web-only and requires PlaywrightEngine.`
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
function evaluateExpect(exp, snap, engine, sessionId) {
|
|
2019
3414
|
switch (exp.kind) {
|
|
2020
3415
|
case "text_visible":
|
|
2021
3416
|
return treeHasText(snap.tree, exp.text);
|
|
@@ -2035,6 +3430,49 @@ function evaluateExpect(exp, snap) {
|
|
|
2035
3430
|
return node.state?.focused === true;
|
|
2036
3431
|
}
|
|
2037
3432
|
}
|
|
3433
|
+
case "no_console_errors": {
|
|
3434
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
3435
|
+
const msgs = engine.peekBuffers(sessionId).console.filter(
|
|
3436
|
+
(m) => m.level === "error"
|
|
3437
|
+
);
|
|
3438
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
3439
|
+
const remaining = msgs.filter(
|
|
3440
|
+
(m) => !excludes.some((p) => m.text.includes(p))
|
|
3441
|
+
);
|
|
3442
|
+
return remaining.length === 0;
|
|
3443
|
+
}
|
|
3444
|
+
case "no_failed_requests": {
|
|
3445
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
3446
|
+
const reqs = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
3447
|
+
if (r.failure) return true;
|
|
3448
|
+
if (r.status === void 0) return false;
|
|
3449
|
+
if (exp.allow_4xx) return r.status >= 500;
|
|
3450
|
+
return r.status >= 400;
|
|
3451
|
+
});
|
|
3452
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
3453
|
+
const remaining = reqs.filter(
|
|
3454
|
+
(r) => !excludes.some((p) => r.url.includes(p))
|
|
3455
|
+
);
|
|
3456
|
+
return remaining.length === 0;
|
|
3457
|
+
}
|
|
3458
|
+
case "request_made": {
|
|
3459
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
3460
|
+
const re = new RegExp(exp.url_pattern);
|
|
3461
|
+
const wantMethod = exp.method?.toUpperCase();
|
|
3462
|
+
const matches = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
3463
|
+
if (!re.test(r.url)) return false;
|
|
3464
|
+
if (wantMethod && r.method.toUpperCase() !== wantMethod) return false;
|
|
3465
|
+
return true;
|
|
3466
|
+
});
|
|
3467
|
+
const min = exp.min_count ?? 1;
|
|
3468
|
+
return matches.length >= min;
|
|
3469
|
+
}
|
|
3470
|
+
case "response_status": {
|
|
3471
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
3472
|
+
const re = new RegExp(exp.url_pattern);
|
|
3473
|
+
const match = engine.peekBuffers(sessionId).network.find((r) => re.test(r.url) && r.status === exp.status);
|
|
3474
|
+
return match !== void 0;
|
|
3475
|
+
}
|
|
2038
3476
|
}
|
|
2039
3477
|
}
|
|
2040
3478
|
function describeExpect(exp) {
|
|
@@ -2047,6 +3485,14 @@ function describeExpect(exp) {
|
|
|
2047
3485
|
return `url_matches /${exp.pattern}/`;
|
|
2048
3486
|
case "ref_in_state":
|
|
2049
3487
|
return `ref_in_state "${exp.query}" \u2192 ${exp.state}`;
|
|
3488
|
+
case "no_console_errors":
|
|
3489
|
+
return "no_console_errors";
|
|
3490
|
+
case "no_failed_requests":
|
|
3491
|
+
return "no_failed_requests";
|
|
3492
|
+
case "request_made":
|
|
3493
|
+
return `request_made ${exp.method ?? ""} ${exp.url_pattern}`.trim();
|
|
3494
|
+
case "response_status":
|
|
3495
|
+
return `response_status ${exp.url_pattern} = ${exp.status}`;
|
|
2050
3496
|
}
|
|
2051
3497
|
}
|
|
2052
3498
|
function missingQuery(query) {
|
|
@@ -2094,7 +3540,7 @@ function treeHasText(tree, text) {
|
|
|
2094
3540
|
// src/tools/composite/visual_diff.ts
|
|
2095
3541
|
import { existsSync } from "fs";
|
|
2096
3542
|
import { readFile as readFile2 } from "fs/promises";
|
|
2097
|
-
import { resolve as
|
|
3543
|
+
import { resolve as resolve4 } from "path";
|
|
2098
3544
|
import pixelmatch from "pixelmatch";
|
|
2099
3545
|
import { PNG } from "pngjs";
|
|
2100
3546
|
var visualDiffTool = {
|
|
@@ -2103,7 +3549,11 @@ var visualDiffTool = {
|
|
|
2103
3549
|
inputShape: visualDiffShape,
|
|
2104
3550
|
build(ctx) {
|
|
2105
3551
|
return safeHandler(async (args) => {
|
|
2106
|
-
const
|
|
3552
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3553
|
+
const { runId, runDir, skill } = await ctx.store.startRun(
|
|
3554
|
+
"vdiff",
|
|
3555
|
+
{ skill: "visual-diff" }
|
|
3556
|
+
);
|
|
2107
3557
|
const session = await ctx.registry.open({
|
|
2108
3558
|
...args.open,
|
|
2109
3559
|
...args.viewport ? { viewport: args.viewport } : {}
|
|
@@ -2122,7 +3572,7 @@ var visualDiffTool = {
|
|
|
2122
3572
|
);
|
|
2123
3573
|
const currentPath = await ctx.store.writeScreenshot(runDir, buf, "current");
|
|
2124
3574
|
await ctx.store.ensureDir(ctx.store.baselineDir);
|
|
2125
|
-
const baselinePath =
|
|
3575
|
+
const baselinePath = resolve4(
|
|
2126
3576
|
ctx.store.baselineDir,
|
|
2127
3577
|
`${args.baseline_id}.png`
|
|
2128
3578
|
);
|
|
@@ -2132,6 +3582,20 @@ var visualDiffTool = {
|
|
|
2132
3582
|
`${args.baseline_id}.png`,
|
|
2133
3583
|
buf
|
|
2134
3584
|
);
|
|
3585
|
+
const manifestPath2 = await writeManifest({
|
|
3586
|
+
runDir,
|
|
3587
|
+
skill,
|
|
3588
|
+
phase: "verify",
|
|
3589
|
+
status: "pass",
|
|
3590
|
+
summary: `baseline "${args.baseline_id}" seeded from current capture`,
|
|
3591
|
+
startedAt,
|
|
3592
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3593
|
+
artifacts: [
|
|
3594
|
+
{ type: "baseline", path: baselinePath },
|
|
3595
|
+
{ type: "screenshot", path: currentPath }
|
|
3596
|
+
],
|
|
3597
|
+
metadata: { baseline_id: args.baseline_id, seeded: true }
|
|
3598
|
+
});
|
|
2135
3599
|
return ok({
|
|
2136
3600
|
run_id: runId,
|
|
2137
3601
|
baseline_id: args.baseline_id,
|
|
@@ -2139,6 +3603,7 @@ var visualDiffTool = {
|
|
|
2139
3603
|
passed: true,
|
|
2140
3604
|
baseline_path: baselinePath,
|
|
2141
3605
|
current_path: currentPath,
|
|
3606
|
+
...manifestPath2 ? { manifest: manifestPath2 } : {},
|
|
2142
3607
|
note: "Baseline did not exist \u2014 current capture saved as the new baseline."
|
|
2143
3608
|
});
|
|
2144
3609
|
}
|
|
@@ -2169,21 +3634,45 @@ var visualDiffTool = {
|
|
|
2169
3634
|
);
|
|
2170
3635
|
const total = baseline.width * baseline.height;
|
|
2171
3636
|
const diffPct = diffPixels / total;
|
|
3637
|
+
const passed = diffPct <= args.threshold_pct;
|
|
2172
3638
|
const diffImagePath = await ctx.store.writeBytes(
|
|
2173
3639
|
runDir,
|
|
2174
3640
|
"diff.png",
|
|
2175
3641
|
PNG.sync.write(diff)
|
|
2176
3642
|
);
|
|
3643
|
+
const artifacts = [
|
|
3644
|
+
{ type: "baseline", path: baselinePath },
|
|
3645
|
+
{ type: "screenshot", path: currentPath },
|
|
3646
|
+
{ type: "diff", path: diffImagePath }
|
|
3647
|
+
];
|
|
3648
|
+
const manifestPath = await writeManifest({
|
|
3649
|
+
runDir,
|
|
3650
|
+
skill,
|
|
3651
|
+
phase: "verify",
|
|
3652
|
+
status: passed ? "pass" : "fail",
|
|
3653
|
+
summary: `diff ${(diffPct * 100).toFixed(3)}% vs baseline "${args.baseline_id}" (threshold ${(args.threshold_pct * 100).toFixed(3)}%)`,
|
|
3654
|
+
startedAt,
|
|
3655
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3656
|
+
artifacts,
|
|
3657
|
+
metadata: {
|
|
3658
|
+
baseline_id: args.baseline_id,
|
|
3659
|
+
diff_pct: Number(diffPct.toFixed(6)),
|
|
3660
|
+
diff_pixels: diffPixels,
|
|
3661
|
+
total_pixels: total,
|
|
3662
|
+
threshold_pct: args.threshold_pct
|
|
3663
|
+
}
|
|
3664
|
+
});
|
|
2177
3665
|
return ok({
|
|
2178
3666
|
run_id: runId,
|
|
2179
3667
|
baseline_id: args.baseline_id,
|
|
2180
3668
|
diff_pct: Number(diffPct.toFixed(6)),
|
|
2181
3669
|
diff_pixels: diffPixels,
|
|
2182
3670
|
total_pixels: total,
|
|
2183
|
-
passed
|
|
3671
|
+
passed,
|
|
2184
3672
|
baseline_path: baselinePath,
|
|
2185
3673
|
current_path: currentPath,
|
|
2186
|
-
diff_image_path: diffImagePath
|
|
3674
|
+
diff_image_path: diffImagePath,
|
|
3675
|
+
...manifestPath ? { manifest: manifestPath } : {}
|
|
2187
3676
|
});
|
|
2188
3677
|
} finally {
|
|
2189
3678
|
if (args.close_on_finish) {
|
|
@@ -2340,13 +3829,131 @@ var toolMetadata = {
|
|
|
2340
3829
|
readOnlyHint: true,
|
|
2341
3830
|
openWorldHint: true
|
|
2342
3831
|
}
|
|
3832
|
+
},
|
|
3833
|
+
// ---------- v0.5 atomic additions ----------
|
|
3834
|
+
[ToolNames.browserHover]: {
|
|
3835
|
+
title: "Hover Element",
|
|
3836
|
+
annotations: {
|
|
3837
|
+
title: "Hover Element",
|
|
3838
|
+
readOnlyHint: false,
|
|
3839
|
+
destructiveHint: false,
|
|
3840
|
+
idempotentHint: true,
|
|
3841
|
+
openWorldHint: true
|
|
3842
|
+
}
|
|
3843
|
+
},
|
|
3844
|
+
[ToolNames.browserDrag]: {
|
|
3845
|
+
title: "Drag Element",
|
|
3846
|
+
annotations: {
|
|
3847
|
+
title: "Drag Element",
|
|
3848
|
+
readOnlyHint: false,
|
|
3849
|
+
destructiveHint: true,
|
|
3850
|
+
idempotentHint: false,
|
|
3851
|
+
openWorldHint: true
|
|
3852
|
+
}
|
|
3853
|
+
},
|
|
3854
|
+
[ToolNames.browserFillForm]: {
|
|
3855
|
+
title: "Fill Form (Batch)",
|
|
3856
|
+
annotations: {
|
|
3857
|
+
title: "Fill Form (Batch)",
|
|
3858
|
+
readOnlyHint: false,
|
|
3859
|
+
destructiveHint: true,
|
|
3860
|
+
idempotentHint: false,
|
|
3861
|
+
openWorldHint: true
|
|
3862
|
+
}
|
|
3863
|
+
},
|
|
3864
|
+
[ToolNames.browserUploadFile]: {
|
|
3865
|
+
title: "Upload File",
|
|
3866
|
+
annotations: {
|
|
3867
|
+
title: "Upload File",
|
|
3868
|
+
readOnlyHint: false,
|
|
3869
|
+
destructiveHint: true,
|
|
3870
|
+
idempotentHint: false,
|
|
3871
|
+
openWorldHint: true
|
|
3872
|
+
}
|
|
3873
|
+
},
|
|
3874
|
+
[ToolNames.browserHandleDialog]: {
|
|
3875
|
+
title: "Pre-arm Dialog Handler",
|
|
3876
|
+
annotations: {
|
|
3877
|
+
title: "Pre-arm Dialog Handler",
|
|
3878
|
+
readOnlyHint: false,
|
|
3879
|
+
destructiveHint: true,
|
|
3880
|
+
idempotentHint: false,
|
|
3881
|
+
openWorldHint: false
|
|
3882
|
+
}
|
|
3883
|
+
},
|
|
3884
|
+
[ToolNames.browserConsole]: {
|
|
3885
|
+
title: "Inspect Console Logs",
|
|
3886
|
+
annotations: {
|
|
3887
|
+
title: "Inspect Console Logs",
|
|
3888
|
+
readOnlyHint: true,
|
|
3889
|
+
openWorldHint: false
|
|
3890
|
+
}
|
|
3891
|
+
},
|
|
3892
|
+
[ToolNames.browserNetwork]: {
|
|
3893
|
+
title: "Inspect Network Requests",
|
|
3894
|
+
annotations: {
|
|
3895
|
+
title: "Inspect Network Requests",
|
|
3896
|
+
readOnlyHint: true,
|
|
3897
|
+
openWorldHint: false
|
|
3898
|
+
}
|
|
3899
|
+
},
|
|
3900
|
+
[ToolNames.browserSetEnv]: {
|
|
3901
|
+
title: "Set Browser Environment",
|
|
3902
|
+
annotations: {
|
|
3903
|
+
title: "Set Browser Environment",
|
|
3904
|
+
readOnlyHint: false,
|
|
3905
|
+
destructiveHint: true,
|
|
3906
|
+
idempotentHint: true,
|
|
3907
|
+
openWorldHint: false
|
|
3908
|
+
}
|
|
3909
|
+
},
|
|
3910
|
+
[ToolNames.browserEvaluate]: {
|
|
3911
|
+
title: "Evaluate JavaScript (gated; arbitrary code execution)",
|
|
3912
|
+
annotations: {
|
|
3913
|
+
title: "Evaluate JavaScript",
|
|
3914
|
+
// Arbitrary code execution in the page context. Gated by
|
|
3915
|
+
// ROLEPOD_ALLOW_EVAL=1 server-side. Always treat as destructive.
|
|
3916
|
+
readOnlyHint: false,
|
|
3917
|
+
destructiveHint: true,
|
|
3918
|
+
idempotentHint: false,
|
|
3919
|
+
openWorldHint: true
|
|
3920
|
+
}
|
|
3921
|
+
},
|
|
3922
|
+
[ToolNames.browserPages]: {
|
|
3923
|
+
title: "List Open Pages",
|
|
3924
|
+
annotations: {
|
|
3925
|
+
title: "List Open Pages",
|
|
3926
|
+
readOnlyHint: true,
|
|
3927
|
+
openWorldHint: false
|
|
3928
|
+
}
|
|
3929
|
+
},
|
|
3930
|
+
[ToolNames.browserSwitchPage]: {
|
|
3931
|
+
title: "Switch Active Page",
|
|
3932
|
+
annotations: {
|
|
3933
|
+
title: "Switch Active Page",
|
|
3934
|
+
readOnlyHint: false,
|
|
3935
|
+
destructiveHint: false,
|
|
3936
|
+
idempotentHint: true,
|
|
3937
|
+
openWorldHint: false
|
|
3938
|
+
}
|
|
2343
3939
|
}
|
|
2344
3940
|
};
|
|
2345
3941
|
|
|
2346
3942
|
// src/server.ts
|
|
2347
3943
|
var SERVER_NAME = "rolepod-uiproof";
|
|
2348
|
-
var SERVER_VERSION = "0.
|
|
3944
|
+
var SERVER_VERSION = "0.6.0";
|
|
3945
|
+
var SUPPORTED_PROTOCOL = "v1";
|
|
3946
|
+
function checkProtocolCompat() {
|
|
3947
|
+
const requested = process.env.ROLEPOD_PROTOCOL;
|
|
3948
|
+
if (!requested) return;
|
|
3949
|
+
if (requested !== SUPPORTED_PROTOCOL) {
|
|
3950
|
+
console.warn(
|
|
3951
|
+
`rolepod protocol mismatch: expected ${SUPPORTED_PROTOCOL}, got ${requested}. Manifest will still be written in ${SUPPORTED_PROTOCOL} shape \u2014 parent may not parse it correctly.`
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
2349
3955
|
function buildServer(opts = {}) {
|
|
3956
|
+
checkProtocolCompat();
|
|
2350
3957
|
const webEngine = createWebEngine();
|
|
2351
3958
|
const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
|
|
2352
3959
|
registry.register("web", webEngine);
|
|
@@ -2362,7 +3969,7 @@ function buildServer(opts = {}) {
|
|
|
2362
3969
|
version: SERVER_VERSION
|
|
2363
3970
|
});
|
|
2364
3971
|
const tools = [
|
|
2365
|
-
// atomic
|
|
3972
|
+
// atomic (v0.1-v0.4)
|
|
2366
3973
|
browserOpenTool,
|
|
2367
3974
|
browserCloseTool,
|
|
2368
3975
|
browserSnapshotTool,
|
|
@@ -2373,6 +3980,18 @@ function buildServer(opts = {}) {
|
|
|
2373
3980
|
browserWaitForTool,
|
|
2374
3981
|
browserScreenshotTool,
|
|
2375
3982
|
browserNavigateTool,
|
|
3983
|
+
// atomic (v0.5)
|
|
3984
|
+
browserHoverTool,
|
|
3985
|
+
browserDragTool,
|
|
3986
|
+
browserFillFormTool,
|
|
3987
|
+
browserUploadFileTool,
|
|
3988
|
+
browserHandleDialogTool,
|
|
3989
|
+
browserConsoleTool,
|
|
3990
|
+
browserNetworkTool,
|
|
3991
|
+
browserSetEnvTool,
|
|
3992
|
+
browserEvaluateTool,
|
|
3993
|
+
browserPagesTool,
|
|
3994
|
+
browserSwitchPageTool,
|
|
2376
3995
|
// composite
|
|
2377
3996
|
verifyUiFlowTool,
|
|
2378
3997
|
auditA11yTool,
|
|
@@ -2395,6 +4014,8 @@ function buildServer(opts = {}) {
|
|
|
2395
4014
|
}
|
|
2396
4015
|
log.info("rolepod-uiproof server built", {
|
|
2397
4016
|
version: SERVER_VERSION,
|
|
4017
|
+
protocol: SUPPORTED_PROTOCOL,
|
|
4018
|
+
mode: store.mode,
|
|
2398
4019
|
tools: tools.map((t) => t.name)
|
|
2399
4020
|
});
|
|
2400
4021
|
return {
|
|
@@ -2426,20 +4047,42 @@ export {
|
|
|
2426
4047
|
browserClickShape,
|
|
2427
4048
|
browserCloseSchema,
|
|
2428
4049
|
browserCloseShape,
|
|
4050
|
+
browserConsoleSchema,
|
|
4051
|
+
browserConsoleShape,
|
|
4052
|
+
browserDragSchema,
|
|
4053
|
+
browserDragShape,
|
|
4054
|
+
browserEvaluateSchema,
|
|
4055
|
+
browserEvaluateShape,
|
|
4056
|
+
browserFillFormSchema,
|
|
4057
|
+
browserFillFormShape,
|
|
4058
|
+
browserHandleDialogSchema,
|
|
4059
|
+
browserHandleDialogShape,
|
|
4060
|
+
browserHoverSchema,
|
|
4061
|
+
browserHoverShape,
|
|
2429
4062
|
browserKeySchema,
|
|
2430
4063
|
browserKeyShape,
|
|
2431
4064
|
browserNavigateSchema,
|
|
2432
4065
|
browserNavigateShape,
|
|
4066
|
+
browserNetworkSchema,
|
|
4067
|
+
browserNetworkShape,
|
|
2433
4068
|
browserOpenSchema,
|
|
2434
4069
|
browserOpenShape,
|
|
4070
|
+
browserPagesSchema,
|
|
4071
|
+
browserPagesShape,
|
|
2435
4072
|
browserScreenshotSchema,
|
|
2436
4073
|
browserScreenshotShape,
|
|
2437
4074
|
browserScrollSchema,
|
|
2438
4075
|
browserScrollShape,
|
|
4076
|
+
browserSetEnvSchema,
|
|
4077
|
+
browserSetEnvShape,
|
|
2439
4078
|
browserSnapshotSchema,
|
|
2440
4079
|
browserSnapshotShape,
|
|
4080
|
+
browserSwitchPageSchema,
|
|
4081
|
+
browserSwitchPageShape,
|
|
2441
4082
|
browserTypeSchema,
|
|
2442
4083
|
browserTypeShape,
|
|
4084
|
+
browserUploadFileSchema,
|
|
4085
|
+
browserUploadFileShape,
|
|
2443
4086
|
browserWaitForSchema,
|
|
2444
4087
|
browserWaitForShape,
|
|
2445
4088
|
buildServer,
|