Files
openclaw/src/node-host/invoke-browser.test.ts
2026-03-23 00:56:44 -07:00

317 lines
9.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const controlServiceMocks = vi.hoisted(() => ({
createBrowserControlContext: vi.fn(() => ({ control: true })),
startBrowserControlServiceFromConfig: vi.fn(async () => true),
}));
const dispatcherMocks = vi.hoisted(() => ({
dispatch: vi.fn(),
createBrowserRouteDispatcher: vi.fn(() => ({
dispatch: dispatcherMocks.dispatch,
})),
}));
const configMocks = vi.hoisted(() => ({
loadConfig: vi.fn(() => ({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: [] as string[] } },
})),
}));
const browserConfigMocks = vi.hoisted(() => ({
resolveBrowserConfig: vi.fn(() => ({
enabled: true,
defaultProfile: "openclaw",
})),
}));
vi.mock("../browser/control-service.js", () => controlServiceMocks);
vi.mock("../browser/routes/dispatcher.js", () => dispatcherMocks);
vi.mock("../config/config.js", () => configMocks);
vi.mock("../browser/config.js", () => browserConfigMocks);
vi.mock("../media/mime.js", () => ({
detectMime: vi.fn(async () => "image/png"),
}));
let runBrowserProxyCommand: typeof import("./invoke-browser.js").runBrowserProxyCommand;
describe("runBrowserProxyCommand", () => {
beforeEach(async () => {
// No-isolate runs can reuse a cached invoke-browser module that was loaded
// via node-host entrypoints before this file's mocks were declared.
vi.useRealTimers();
vi.resetModules();
dispatcherMocks.dispatch.mockReset();
dispatcherMocks.createBrowserRouteDispatcher.mockReset().mockImplementation(() => ({
dispatch: dispatcherMocks.dispatch,
}));
controlServiceMocks.createBrowserControlContext.mockReset().mockReturnValue({ control: true });
controlServiceMocks.startBrowserControlServiceFromConfig.mockReset().mockResolvedValue(true);
configMocks.loadConfig.mockReset().mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: [] as string[] } },
});
browserConfigMocks.resolveBrowserConfig.mockReset().mockReturnValue({
enabled: true,
defaultProfile: "openclaw",
});
({ runBrowserProxyCommand } = await import("./invoke-browser.js"));
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: [] as string[] } },
});
browserConfigMocks.resolveBrowserConfig.mockReturnValue({
enabled: true,
defaultProfile: "openclaw",
});
controlServiceMocks.startBrowserControlServiceFromConfig.mockResolvedValue(true);
});
it("adds profile and browser status details on ws-backed timeouts", async () => {
vi.useFakeTimers();
dispatcherMocks.dispatch
.mockImplementationOnce(async () => {
await new Promise(() => {});
})
.mockResolvedValueOnce({
status: 200,
body: {
running: true,
cdpHttp: true,
cdpReady: false,
cdpUrl: "http://127.0.0.1:18792",
},
});
const result = expect(
runBrowserProxyCommand(
JSON.stringify({
method: "GET",
path: "/snapshot",
profile: "openclaw",
timeoutMs: 5,
}),
),
).rejects.toThrow(
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=openclaw; status\(running=true, cdpHttp=true, cdpReady=false, cdpUrl=http:\/\/127\.0\.0\.1:18792\)/,
);
await vi.advanceTimersByTimeAsync(10);
await result;
});
it("includes chrome-mcp transport in timeout diagnostics when no CDP URL exists", async () => {
vi.useFakeTimers();
dispatcherMocks.dispatch
.mockImplementationOnce(async () => {
await new Promise(() => {});
})
.mockResolvedValueOnce({
status: 200,
body: {
running: true,
transport: "chrome-mcp",
cdpHttp: true,
cdpReady: false,
cdpUrl: null,
},
});
const result = expect(
runBrowserProxyCommand(
JSON.stringify({
method: "GET",
path: "/snapshot",
profile: "user",
timeoutMs: 5,
}),
),
).rejects.toThrow(
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=user; status\(running=true, cdpHttp=true, cdpReady=false, transport=chrome-mcp\)/,
);
await vi.advanceTimersByTimeAsync(10);
await result;
});
it("redacts sensitive cdpUrl details in timeout diagnostics", async () => {
vi.useFakeTimers();
dispatcherMocks.dispatch
.mockImplementationOnce(async () => {
await new Promise(() => {});
})
.mockResolvedValueOnce({
status: 200,
body: {
running: true,
cdpHttp: true,
cdpReady: false,
cdpUrl:
"https://alice:supersecretpasswordvalue1234@example.com/chrome?token=supersecrettokenvalue1234567890",
},
});
const result = expect(
runBrowserProxyCommand(
JSON.stringify({
method: "GET",
path: "/snapshot",
profile: "remote",
timeoutMs: 5,
}),
),
).rejects.toThrow(
/status\(running=true, cdpHttp=true, cdpReady=false, cdpUrl=https:\/\/example\.com\/chrome\?token=supers…7890\)/,
);
await vi.advanceTimersByTimeAsync(10);
await result;
});
it("keeps non-timeout browser errors intact", async () => {
dispatcherMocks.dispatch.mockResolvedValue({
status: 500,
body: { error: "tab not found" },
});
await expect(
runBrowserProxyCommand(
JSON.stringify({
method: "POST",
path: "/act",
profile: "openclaw",
timeoutMs: 50,
}),
),
).rejects.toThrow("tab not found");
});
it("rejects unauthorized query.profile when allowProfiles is configured", async () => {
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
});
await expect(
runBrowserProxyCommand(
JSON.stringify({
method: "GET",
path: "/snapshot",
query: { profile: "user" },
timeoutMs: 50,
}),
),
).rejects.toThrow("INVALID_REQUEST: browser profile not allowed");
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
});
it("rejects unauthorized body.profile when allowProfiles is configured", async () => {
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
});
await expect(
runBrowserProxyCommand(
JSON.stringify({
method: "POST",
path: "/stop",
body: { profile: "user" },
timeoutMs: 50,
}),
),
).rejects.toThrow("INVALID_REQUEST: browser profile not allowed");
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
});
it("rejects persistent profile creation when allowProfiles is configured", async () => {
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
});
await expect(
runBrowserProxyCommand(
JSON.stringify({
method: "POST",
path: "/profiles/create",
body: { name: "poc", cdpUrl: "http://127.0.0.1:9222" },
timeoutMs: 50,
}),
),
).rejects.toThrow(
"INVALID_REQUEST: browser.proxy cannot create or delete persistent browser profiles when allowProfiles is configured",
);
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
});
it("rejects persistent profile deletion when allowProfiles is configured", async () => {
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
});
await expect(
runBrowserProxyCommand(
JSON.stringify({
method: "DELETE",
path: "/profiles/poc",
timeoutMs: 50,
}),
),
).rejects.toThrow(
"INVALID_REQUEST: browser.proxy cannot create or delete persistent browser profiles when allowProfiles is configured",
);
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
});
it("canonicalizes an allowlisted body profile into the dispatched query", async () => {
configMocks.loadConfig.mockReturnValue({
browser: {},
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
});
dispatcherMocks.dispatch.mockResolvedValue({
status: 200,
body: { ok: true },
});
await runBrowserProxyCommand(
JSON.stringify({
method: "POST",
path: "/stop",
body: { profile: "openclaw" },
timeoutMs: 50,
}),
);
expect(dispatcherMocks.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
path: "/stop",
query: { profile: "openclaw" },
}),
);
});
it("preserves legacy proxy behavior when allowProfiles is empty", async () => {
dispatcherMocks.dispatch.mockResolvedValue({
status: 200,
body: { ok: true },
});
await runBrowserProxyCommand(
JSON.stringify({
method: "POST",
path: "/profiles/create",
body: { name: "poc", cdpUrl: "http://127.0.0.1:9222" },
timeoutMs: 50,
}),
);
expect(dispatcherMocks.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
method: "POST",
path: "/profiles/create",
body: { name: "poc", cdpUrl: "http://127.0.0.1:9222" },
}),
);
});
});