mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(browser): thread snapshot timeoutMs through agent tool and helpers (#75702)
Summary: - Threads browser snapshot `timeoutMs` through the agent action, client/proxy request, snapshot route plan, Ch ... Playwright/CDP helpers, regression tests, changelog, and one JSDoc-only shrinkwrap script type annotation. - Reproducibility: yes. source reproduction is high-confidence: current main accepts top-level browser `timeou ... helpers drop it. I did not rerun the original macOS or Browserbase live scenario in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(browser): apply default snapshot timeout to proxy path and add Pl… - PR branch already contained follow-up commit before automerge: docs(changelog): add browser snapshot timeout propagation fix entry - PR branch already contained follow-up commit before automerge: fix(browser): thread snapshot timeoutMs through agent tool and helpers - PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-7570… Validation: - ClawSweeper review passed for head0eec196962. - Required merge gates passed before the squash merge. Prepared head SHA:0eec196962Review: https://github.com/openclaw/openclaw/pull/75702#issuecomment-4359923127 Co-authored-by: masatohoshino <g515hoshino@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
co-authored by
clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
takhoffman
parent
d581415026
commit
069c7b87eb
@@ -3259,6 +3259,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/plugins: refresh persisted plugin registry policy in place for `plugins enable` and `plugins disable`, so routine toggles no longer rebuild and hash every plugin source when the target is already indexed. Thanks @vincentkoc.
|
||||
- Windows/install: run npm from a writable installer temp directory and pin the Bedrock runtime dependency below a Windows ARM Node 24 npm resolver failure, so global OpenClaw installs no longer fail before onboarding. Thanks @mariozechner.
|
||||
- CLI/plugins: scope install and enable slot selection to the selected plugin manifest/runtime fallback, so plugin installs no longer load every plugin runtime or broad status snapshot just to update memory/context slots. Thanks @vincentkoc.
|
||||
- Browser/snapshot: propagate the configured snapshot timeout through the agent tool, Chrome MCP, and Playwright snapshot paths so snapshot actions honor the requested deadline instead of hanging. Fixes #72934. Thanks @masatohoshino.
|
||||
- Plugins/TTS: keep bundled speech-provider discovery available on cold package Gateway paths and add bundled plugin matrix runtime probes for health, readiness, RPC, TTS discovery, and post-ready runtime-deps watchdog coverage. Refs #75283. Thanks @vincentkoc.
|
||||
- Google Meet/Twilio: show delegated voice call ID, DTMF, and intro-greeting state in `googlemeet doctor`, and avoid claiming DTMF was sent when no Meet PIN sequence was configured. Refs #72478. Thanks @DougButdorf.
|
||||
- Plugins/tools: prefer built bundled plugin code during tool discovery and skip channel runtime hydration while preserving companion provider registrations, reducing per-run plugin-tool prep cost without dropping executable plugin tools. Fixes #75290. Thanks @thanos-openclaw.
|
||||
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
resolveRuntimeImageSanitization,
|
||||
wrapExternalContent,
|
||||
} from "./browser-tool.runtime.js";
|
||||
import { DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./browser/constants.js";
|
||||
import {
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS,
|
||||
} from "./browser/constants.js";
|
||||
|
||||
const browserToolActionDeps = {
|
||||
browserAct,
|
||||
@@ -365,6 +368,8 @@ export async function executeSnapshotAction(params: {
|
||||
: hasMaxChars
|
||||
? maxChars
|
||||
: undefined;
|
||||
const snapshotTimeoutMs =
|
||||
normalizePositiveTimeoutMs(input.timeoutMs) ?? DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS;
|
||||
const snapshotQuery = {
|
||||
...(format ? { format } : {}),
|
||||
targetId,
|
||||
@@ -379,6 +384,7 @@ export async function executeSnapshotAction(params: {
|
||||
labels,
|
||||
urls,
|
||||
mode,
|
||||
timeoutMs: snapshotTimeoutMs,
|
||||
};
|
||||
let refsFallback: "role" | undefined;
|
||||
const readSnapshot = async (query: typeof snapshotQuery) =>
|
||||
@@ -388,6 +394,7 @@ export async function executeSnapshotAction(params: {
|
||||
path: "/snapshot",
|
||||
profile,
|
||||
query,
|
||||
timeoutMs: snapshotTimeoutMs,
|
||||
})) as Awaited<ReturnType<typeof browserSnapshot>>)
|
||||
: await browserToolActionDeps.browserSnapshot(baseUrl, {
|
||||
...query,
|
||||
|
||||
@@ -554,6 +554,86 @@ describe("browser tool snapshot maxChars", () => {
|
||||
expect(opts.refs).toBe("aria");
|
||||
});
|
||||
|
||||
it("propagates input.timeoutMs into the direct browser snapshot call", async () => {
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "snapshot",
|
||||
target: "host",
|
||||
snapshotFormat: "ai",
|
||||
timeoutMs: 9000,
|
||||
});
|
||||
|
||||
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
format: "ai",
|
||||
timeoutMs: 9000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the default snapshot timeout in the direct browser snapshot call", async () => {
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "snapshot",
|
||||
target: "host",
|
||||
snapshotFormat: "ai",
|
||||
});
|
||||
|
||||
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
format: "ai",
|
||||
// DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS = 20_000.
|
||||
timeoutMs: 20_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("propagates input.timeoutMs into the proxied browser snapshot request", async () => {
|
||||
mockSingleBrowserProxyNode();
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
gatewayMocks.callGatewayTool.mockResolvedValue({
|
||||
ok: true,
|
||||
payload: {
|
||||
result: {
|
||||
ok: true,
|
||||
format: "ai",
|
||||
targetId: "t1",
|
||||
url: "https://x",
|
||||
snapshot: "ok",
|
||||
},
|
||||
files: [],
|
||||
},
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "snapshot",
|
||||
target: "node",
|
||||
profile: "user",
|
||||
snapshotFormat: "ai",
|
||||
timeoutMs: 7777,
|
||||
});
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
// proxy adds a 5_000 ms slack on top of the per-request timeout.
|
||||
expect.objectContaining({ timeoutMs: 7777 + 5_000 }),
|
||||
expect.objectContaining({
|
||||
command: "browser.proxy",
|
||||
params: expect.objectContaining({
|
||||
method: "GET",
|
||||
path: "/snapshot",
|
||||
profile: "user",
|
||||
query: expect.objectContaining({ timeoutMs: 7777 }),
|
||||
timeoutMs: 7777,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses config snapshot defaults when mode is not provided", async () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: { snapshotDefaults: { mode: "efficient" } },
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
resetChromeMcpSessionsForTest,
|
||||
setChromeMcpSessionFactoryForTest,
|
||||
takeChromeMcpScreenshot,
|
||||
takeChromeMcpSnapshot,
|
||||
} from "./chrome-mcp.js";
|
||||
|
||||
type ToolCall = {
|
||||
@@ -856,6 +857,27 @@ describe("chrome MCP page parsing", () => {
|
||||
expect(tabs).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("forwards an explicit timeoutMs to take_snapshot via the callTool race", async () => {
|
||||
vi.useFakeTimers();
|
||||
const session = createFakeSession();
|
||||
session.client.callTool = vi.fn(
|
||||
async () => new Promise<never>(() => {}),
|
||||
) as typeof session.client.callTool;
|
||||
setChromeMcpSessionFactoryForTest(async () => session);
|
||||
|
||||
const snapshotPromise = takeChromeMcpSnapshot({
|
||||
profileName: "chrome-live",
|
||||
targetId: "1",
|
||||
timeoutMs: 75,
|
||||
});
|
||||
void snapshotPromise.catch(() => {});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(75);
|
||||
|
||||
await expect(snapshotPromise).rejects.toThrow(/Chrome MCP "take_snapshot".*timed out/);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("honors timeoutMs for ephemeral availability probes", async () => {
|
||||
vi.useFakeTimers();
|
||||
const closeMock = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -1019,6 +1019,7 @@ export async function takeChromeMcpSnapshot(params: {
|
||||
profile?: ChromeMcpProfileOptions;
|
||||
userDataDir?: string;
|
||||
targetId: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<ChromeMcpSnapshotNode> {
|
||||
const result = await callTool(
|
||||
params.profileName,
|
||||
@@ -1027,6 +1028,7 @@ export async function takeChromeMcpSnapshot(params: {
|
||||
{
|
||||
pageId: parsePageId(params.targetId),
|
||||
},
|
||||
{ timeoutMs: params.timeoutMs },
|
||||
);
|
||||
return extractSnapshot(result);
|
||||
}
|
||||
|
||||
@@ -112,6 +112,33 @@ describe("browser client", () => {
|
||||
expect(parsed.searchParams.get("refs")).toBe("aria");
|
||||
});
|
||||
|
||||
it("forwards an explicit snapshot timeoutMs into the query string", async () => {
|
||||
const calls: string[] = [];
|
||||
stubSnapshotFetch(calls);
|
||||
|
||||
await browserSnapshot("http://127.0.0.1:18791", {
|
||||
format: "ai",
|
||||
timeoutMs: 4321,
|
||||
});
|
||||
|
||||
const snapshotCall = calls.find((url) => url.includes("/snapshot?"));
|
||||
expect(snapshotCall).toBeTruthy();
|
||||
const parsed = new URL(snapshotCall as string);
|
||||
expect(parsed.searchParams.get("timeoutMs")).toBe("4321");
|
||||
});
|
||||
|
||||
it("falls back to the default snapshot timeout when none is supplied", async () => {
|
||||
const calls: string[] = [];
|
||||
stubSnapshotFetch(calls);
|
||||
|
||||
await browserSnapshot("http://127.0.0.1:18791", { format: "ai" });
|
||||
|
||||
const snapshotCall = calls.find((url) => url.includes("/snapshot?"));
|
||||
expect(snapshotCall).toBeTruthy();
|
||||
const parsed = new URL(snapshotCall as string);
|
||||
expect(parsed.searchParams.get("timeoutMs")).toBe("20000");
|
||||
});
|
||||
|
||||
it("omits format when the caller wants server-side snapshot capability defaults", async () => {
|
||||
const calls: string[] = [];
|
||||
stubSnapshotFetch(calls);
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
BrowserTransport,
|
||||
SnapshotAriaNode,
|
||||
} from "./client.types.js";
|
||||
import { DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS } from "./constants.js";
|
||||
import type { BrowserDoctorReport } from "./doctor.js";
|
||||
|
||||
export type { BrowserStatus, BrowserTab, BrowserTransport } from "./client.types.js";
|
||||
@@ -320,6 +321,7 @@ export async function browserSnapshot(
|
||||
urls?: boolean;
|
||||
mode?: "efficient";
|
||||
profile?: string;
|
||||
timeoutMs?: number;
|
||||
},
|
||||
): Promise<SnapshotResult> {
|
||||
const q = new URLSearchParams();
|
||||
@@ -365,8 +367,13 @@ export async function browserSnapshot(
|
||||
if (opts.profile) {
|
||||
q.set("profile", opts.profile);
|
||||
}
|
||||
const resolvedTimeoutMs =
|
||||
typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0
|
||||
? Math.floor(opts.timeoutMs)
|
||||
: DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS;
|
||||
q.set("timeoutMs", String(resolvedTimeoutMs));
|
||||
return await fetchBrowserJson<SnapshotResult>(withBaseUrl(baseUrl, `/snapshot?${q.toString()}`), {
|
||||
timeoutMs: 20000,
|
||||
timeoutMs: resolvedTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 60_000;
|
||||
export const DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS = 15_000;
|
||||
export const DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS = 8_000;
|
||||
export const DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS = 20_000;
|
||||
export const DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS = 20_000;
|
||||
export const DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES = 120;
|
||||
export const DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION = 8;
|
||||
export const DEFAULT_BROWSER_TAB_CLEANUP_SWEEP_MINUTES = 5;
|
||||
|
||||
@@ -92,6 +92,45 @@ describe("pw-tools-core aria snapshot storage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("races snapshotAriaViaPlaywright against an explicit timeoutMs", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const page = { id: "page-1" };
|
||||
getPageForTargetId.mockResolvedValue(page);
|
||||
withPageScopedCdpClient.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
const mod = await import("./pw-tools-core.snapshot.js");
|
||||
const promise = mod.snapshotAriaViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:9222",
|
||||
targetId: "tab-1",
|
||||
timeoutMs: 750,
|
||||
});
|
||||
void promise.catch(() => {});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(750);
|
||||
|
||||
await expect(promise).rejects.toThrow(/Aria snapshot via Playwright timed out/);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("forwards an explicit timeoutMs into the role-aria Playwright ariaSnapshot call", async () => {
|
||||
const ariaSnapshotMock = vi.fn().mockResolvedValue("");
|
||||
const page = { ariaSnapshot: ariaSnapshotMock };
|
||||
getPageForTargetId.mockResolvedValue(page);
|
||||
|
||||
const mod = await import("./pw-tools-core.snapshot.js");
|
||||
await mod.snapshotRoleViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:9222",
|
||||
targetId: "tab-1",
|
||||
refsMode: "aria",
|
||||
timeoutMs: 8888,
|
||||
});
|
||||
|
||||
expect(ariaSnapshotMock).toHaveBeenCalledWith({ mode: "ai", timeout: 8888 });
|
||||
});
|
||||
|
||||
it("stores role fallback metadata when backend markers are unavailable", async () => {
|
||||
const page = { id: "page-1" };
|
||||
const mod = await import("./pw-tools-core.snapshot.js");
|
||||
|
||||
@@ -140,6 +140,7 @@ export async function snapshotAriaViaPlaywright(opts: {
|
||||
cdpUrl: string;
|
||||
targetId?: string;
|
||||
limit?: number;
|
||||
timeoutMs?: number;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
}): Promise<{ nodes: AriaSnapshotNode[] }> {
|
||||
const limit = Math.max(1, Math.min(2000, Math.floor(opts.limit ?? 500)));
|
||||
@@ -157,7 +158,11 @@ export async function snapshotAriaViaPlaywright(opts: {
|
||||
targetId: opts.targetId,
|
||||
});
|
||||
}
|
||||
const res = (await withPageScopedCdpClient({
|
||||
const ariaTimeoutMs =
|
||||
typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0
|
||||
? Math.max(500, Math.min(60_000, Math.floor(opts.timeoutMs)))
|
||||
: undefined;
|
||||
const collectAxTree = withPageScopedCdpClient({
|
||||
cdpUrl: opts.cdpUrl,
|
||||
page,
|
||||
targetId: opts.targetId,
|
||||
@@ -167,7 +172,23 @@ export async function snapshotAriaViaPlaywright(opts: {
|
||||
nodes?: RawAXNode[];
|
||||
};
|
||||
},
|
||||
})) as {
|
||||
});
|
||||
const res = (await (ariaTimeoutMs === undefined
|
||||
? collectAxTree
|
||||
: (() => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(`Aria snapshot via Playwright timed out after ${ariaTimeoutMs}ms.`));
|
||||
}, ariaTimeoutMs);
|
||||
timer.unref?.();
|
||||
});
|
||||
return Promise.race([collectAxTree, timeout]).finally(() => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
})())) as {
|
||||
nodes?: RawAXNode[];
|
||||
};
|
||||
const nodes = Array.isArray(res?.nodes) ? res.nodes : [];
|
||||
@@ -241,6 +262,7 @@ export async function snapshotRoleViaPlaywright(opts: {
|
||||
refsMode?: "role" | "aria";
|
||||
options?: RoleSnapshotOptions;
|
||||
urls?: boolean;
|
||||
timeoutMs?: number;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
}): Promise<{
|
||||
snapshot: string;
|
||||
@@ -262,13 +284,15 @@ export async function snapshotRoleViaPlaywright(opts: {
|
||||
});
|
||||
}
|
||||
|
||||
const ariaSnapshotTimeout = Math.max(500, Math.min(60_000, Math.floor(opts.timeoutMs ?? 5000)));
|
||||
|
||||
if (opts.refsMode === "aria") {
|
||||
if (normalizeOptionalString(opts.selector) || normalizeOptionalString(opts.frameSelector)) {
|
||||
throw new Error("refs=aria does not support selector/frame snapshots yet.");
|
||||
}
|
||||
const snapshot = await page.ariaSnapshot({
|
||||
mode: "ai",
|
||||
timeout: 5000,
|
||||
timeout: ariaSnapshotTimeout,
|
||||
});
|
||||
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
||||
const snapshotWithUrls = opts.urls
|
||||
@@ -298,7 +322,7 @@ export async function snapshotRoleViaPlaywright(opts: {
|
||||
? page.locator(selector)
|
||||
: page.locator(":root");
|
||||
|
||||
const ariaSnapshot = await locator.ariaSnapshot();
|
||||
const ariaSnapshot = await locator.ariaSnapshot({ timeout: ariaSnapshotTimeout });
|
||||
const built = buildRoleSnapshotFromAriaSnapshot(ariaSnapshot ?? "", opts.options);
|
||||
const snapshotWithUrls = opts.urls
|
||||
? appendSnapshotUrls(built.snapshot, await collectSnapshotUrls(page))
|
||||
|
||||
@@ -47,4 +47,31 @@ describe("resolveSnapshotPlan", () => {
|
||||
expect(plan.urls).toBe(true);
|
||||
expect(plan.wantsRoleSnapshot).toBe(true);
|
||||
});
|
||||
|
||||
it("parses timeoutMs from the snapshot query string", () => {
|
||||
const plan = resolveSnapshotPlan({
|
||||
profile: profile("openclaw"),
|
||||
query: { timeoutMs: "12345" },
|
||||
hasPlaywright: true,
|
||||
});
|
||||
|
||||
expect(plan.timeoutMs).toBe(12345);
|
||||
});
|
||||
|
||||
it("ignores non-positive timeoutMs values", () => {
|
||||
expect(
|
||||
resolveSnapshotPlan({
|
||||
profile: profile("openclaw"),
|
||||
query: { timeoutMs: "0" },
|
||||
hasPlaywright: true,
|
||||
}).timeoutMs,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
resolveSnapshotPlan({
|
||||
profile: profile("openclaw"),
|
||||
query: { timeoutMs: "not-a-number" },
|
||||
hasPlaywright: true,
|
||||
}).timeoutMs,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ type BrowserSnapshotPlan = {
|
||||
refsMode?: "aria" | "role";
|
||||
selectorValue?: string;
|
||||
frameSelectorValue?: string;
|
||||
timeoutMs?: number;
|
||||
wantsRoleSnapshot: boolean;
|
||||
};
|
||||
|
||||
@@ -79,6 +80,11 @@ export function resolveSnapshotPlan(params: {
|
||||
depthRaw ?? (mode === "efficient" ? DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH : undefined);
|
||||
const selectorValue = normalizeOptionalString(toStringOrEmpty(params.query.selector));
|
||||
const frameSelectorValue = normalizeOptionalString(toStringOrEmpty(params.query.frame));
|
||||
const timeoutMsRaw = toNumber(params.query.timeoutMs);
|
||||
const timeoutMs =
|
||||
timeoutMsRaw !== undefined && Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0
|
||||
? Math.max(1, Math.floor(timeoutMsRaw))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
format,
|
||||
@@ -93,6 +99,7 @@ export function resolveSnapshotPlan(params: {
|
||||
refsMode,
|
||||
selectorValue,
|
||||
frameSelectorValue,
|
||||
timeoutMs,
|
||||
wantsRoleSnapshot:
|
||||
labels === true ||
|
||||
urls === true ||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js";
|
||||
|
||||
const cdpMocks = vi.hoisted(() => ({
|
||||
captureScreenshot: vi.fn(),
|
||||
snapshotAria: vi.fn(async () => ({ nodes: [] })),
|
||||
snapshotRoleViaCdp: vi.fn(async () => ({
|
||||
snapshot: "button Continue",
|
||||
refs: {},
|
||||
stats: { lines: 1, chars: 15, refs: 0, interactive: 0 },
|
||||
})),
|
||||
}));
|
||||
|
||||
const profileContext = vi.hoisted(() => ({
|
||||
profile: {
|
||||
name: "openclaw",
|
||||
driver: "openclaw" as const,
|
||||
cdpPort: 18_800,
|
||||
cdpUrl: "http://127.0.0.1:18800",
|
||||
cdpHost: "127.0.0.1",
|
||||
cdpIsLoopback: true,
|
||||
color: "#FF4500",
|
||||
headless: false,
|
||||
attachOnly: false,
|
||||
},
|
||||
ensureTabAvailable: vi.fn(async () => ({
|
||||
targetId: "tab-1",
|
||||
url: "https://example.com",
|
||||
wsUrl: "ws://127.0.0.1:18800/devtools/page/tab-1",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../cdp.js", () => ({
|
||||
captureScreenshot: cdpMocks.captureScreenshot,
|
||||
snapshotAria: cdpMocks.snapshotAria,
|
||||
snapshotRoleViaCdp: cdpMocks.snapshotRoleViaCdp,
|
||||
}));
|
||||
|
||||
vi.mock("../chrome-mcp.js", () => ({
|
||||
evaluateChromeMcpScript: vi.fn(),
|
||||
navigateChromeMcpPage: vi.fn(),
|
||||
takeChromeMcpScreenshot: vi.fn(),
|
||||
takeChromeMcpSnapshot: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../navigation-guard.js", () => ({
|
||||
assertBrowserNavigationAllowed: vi.fn(async () => {}),
|
||||
assertBrowserNavigationResultAllowed: vi.fn(async () => {}),
|
||||
withBrowserNavigationPolicy: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
vi.mock("../screenshot.js", () => ({
|
||||
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES: 128,
|
||||
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE: 64,
|
||||
normalizeBrowserScreenshot: vi.fn(async (buffer: Buffer) => ({
|
||||
buffer,
|
||||
contentType: "image/png",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../media/store.js", () => ({
|
||||
ensureMediaDir: vi.fn(async () => {}),
|
||||
saveMediaBuffer: vi.fn(async () => ({ path: "/tmp/fake.png" })),
|
||||
}));
|
||||
|
||||
vi.mock("./agent.shared.js", () => ({
|
||||
getPwAiModule: vi.fn(async () => null),
|
||||
handleRouteError: vi.fn(),
|
||||
readBody: vi.fn((req: { body?: unknown }) => req.body ?? {}),
|
||||
requirePwAi: vi.fn(async () => null),
|
||||
resolveProfileContext: vi.fn(() => profileContext),
|
||||
withPlaywrightRouteContext: vi.fn(),
|
||||
withRouteTabContext: vi.fn(),
|
||||
}));
|
||||
|
||||
const { registerBrowserAgentSnapshotRoutes } = await import("./agent.snapshot.js");
|
||||
|
||||
function getSnapshotHandler() {
|
||||
const { app, getHandlers } = createBrowserRouteApp();
|
||||
registerBrowserAgentSnapshotRoutes(app, {
|
||||
state: () => ({ resolved: {} }),
|
||||
} as never);
|
||||
const handler = getHandlers.get("/snapshot");
|
||||
expect(handler).toBeTypeOf("function");
|
||||
return handler;
|
||||
}
|
||||
|
||||
describe("browser agent snapshot timeout routing", () => {
|
||||
beforeEach(() => {
|
||||
cdpMocks.captureScreenshot.mockClear();
|
||||
cdpMocks.snapshotAria.mockClear();
|
||||
cdpMocks.snapshotRoleViaCdp.mockClear();
|
||||
profileContext.ensureTabAvailable.mockClear();
|
||||
});
|
||||
|
||||
it("passes timeoutMs to direct CDP aria snapshots", async () => {
|
||||
const handler = getSnapshotHandler();
|
||||
const response = createBrowserRouteResponse();
|
||||
|
||||
await handler?.({ params: {}, query: { format: "aria", timeoutMs: "4321" } }, response.res);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(cdpMocks.snapshotAria).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
wsUrl: "ws://127.0.0.1:18800/devtools/page/tab-1",
|
||||
timeoutMs: 4321,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes timeoutMs to direct CDP role snapshots", async () => {
|
||||
const handler = getSnapshotHandler();
|
||||
const response = createBrowserRouteResponse();
|
||||
|
||||
await handler?.({ params: {}, query: { format: "ai", timeoutMs: "9876" } }, response.res);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(cdpMocks.snapshotRoleViaCdp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
wsUrl: "ws://127.0.0.1:18800/devtools/page/tab-1",
|
||||
timeoutMs: 9876,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -577,6 +577,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
profileName: profileCtx.profile.name,
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
timeoutMs: plan.timeoutMs,
|
||||
});
|
||||
if (plan.format === "aria") {
|
||||
return res.json({
|
||||
@@ -623,6 +624,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
profile: profileCtx.profile,
|
||||
targetId: tab.targetId,
|
||||
format: "png",
|
||||
timeoutMs: plan.timeoutMs,
|
||||
});
|
||||
const normalized = await normalizeBrowserScreenshot(labeled, {
|
||||
maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
||||
@@ -683,6 +685,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
refsMode: plan.refsMode,
|
||||
ssrfPolicy: ctx.state().resolved.ssrfPolicy,
|
||||
urls: plan.urls,
|
||||
timeoutMs: plan.timeoutMs,
|
||||
options: {
|
||||
interactive: plan.interactive ?? undefined,
|
||||
compact: plan.compact ?? undefined,
|
||||
@@ -700,6 +703,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
return await snapshotRoleViaCdp({
|
||||
wsUrl: tab.wsUrl,
|
||||
urls: plan.urls,
|
||||
timeoutMs: plan.timeoutMs,
|
||||
options: {
|
||||
interactive: plan.interactive ?? undefined,
|
||||
compact: plan.compact ?? undefined,
|
||||
@@ -725,6 +729,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
targetId: tab.targetId,
|
||||
ssrfPolicy: ctx.state().resolved.ssrfPolicy,
|
||||
urls: plan.urls,
|
||||
timeoutMs: plan.timeoutMs,
|
||||
...(typeof plan.resolvedMaxChars === "number"
|
||||
? { maxChars: plan.resolvedMaxChars }
|
||||
: {}),
|
||||
@@ -743,6 +748,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
targetId: tab.targetId,
|
||||
refs: "refs" in snap ? snap.refs : {},
|
||||
type: "png",
|
||||
timeoutMs: plan.timeoutMs,
|
||||
});
|
||||
const normalized = await normalizeBrowserScreenshot(labeled.buffer, {
|
||||
maxSide: DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
||||
@@ -797,11 +803,12 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
cdpUrl: profileCtx.profile.cdpUrl,
|
||||
targetId: tab.targetId,
|
||||
limit: plan.limit,
|
||||
timeoutMs: plan.timeoutMs,
|
||||
ssrfPolicy: ctx.state().resolved.ssrfPolicy,
|
||||
});
|
||||
});
|
||||
})()
|
||||
: snapshotAria({ wsUrl: tab.wsUrl ?? "", limit: plan.limit });
|
||||
: snapshotAria({ wsUrl: tab.wsUrl ?? "", limit: plan.limit, timeoutMs: plan.timeoutMs });
|
||||
|
||||
const resolved = await Promise.resolve(snap);
|
||||
if (!resolved) {
|
||||
|
||||
@@ -242,6 +242,7 @@ function disableShrinkwrappedOverrideConflictSources(lockfile, overrideRules) {
|
||||
if (!packages || typeof packages !== "object") {
|
||||
return [];
|
||||
}
|
||||
/** @type {Set<string>} */
|
||||
const disabled = new Set();
|
||||
for (const violation of collectOverrideViolations(lockfile, overrideRules)) {
|
||||
const ancestors = violation.packagePath.slice(0, -1).toReversed();
|
||||
|
||||
Reference in New Issue
Block a user