mirror of https://github.com/openclaw/openclaw.git
340 lines
12 KiB
TypeScript
340 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { withEnv } from "../test-utils/env.js";
|
|
import { resolveBrowserConfig, resolveProfile, shouldStartLocalBrowserServer } from "./config.js";
|
|
|
|
describe("browser config", () => {
|
|
it("defaults to enabled with loopback defaults and lobster-orange color", () => {
|
|
const resolved = resolveBrowserConfig(undefined);
|
|
expect(resolved.enabled).toBe(true);
|
|
expect(resolved.controlPort).toBe(18791);
|
|
expect(resolved.color).toBe("#FF4500");
|
|
expect(shouldStartLocalBrowserServer(resolved)).toBe(true);
|
|
expect(resolved.cdpHost).toBe("127.0.0.1");
|
|
expect(resolved.cdpProtocol).toBe("http");
|
|
const profile = resolveProfile(resolved, resolved.defaultProfile);
|
|
expect(profile?.name).toBe("openclaw");
|
|
expect(profile?.driver).toBe("openclaw");
|
|
expect(profile?.cdpPort).toBe(18800);
|
|
expect(profile?.cdpUrl).toBe("http://127.0.0.1:18800");
|
|
|
|
const openclaw = resolveProfile(resolved, "openclaw");
|
|
expect(openclaw?.driver).toBe("openclaw");
|
|
expect(openclaw?.cdpPort).toBe(18800);
|
|
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:18800");
|
|
const chrome = resolveProfile(resolved, "chrome");
|
|
expect(chrome?.driver).toBe("extension");
|
|
expect(chrome?.cdpPort).toBe(18792);
|
|
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:18792");
|
|
expect(resolved.remoteCdpTimeoutMs).toBe(1500);
|
|
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000);
|
|
});
|
|
|
|
it("derives default ports from OPENCLAW_GATEWAY_PORT when unset", () => {
|
|
withEnv({ OPENCLAW_GATEWAY_PORT: "19001" }, () => {
|
|
const resolved = resolveBrowserConfig(undefined);
|
|
expect(resolved.controlPort).toBe(19003);
|
|
const chrome = resolveProfile(resolved, "chrome");
|
|
expect(chrome?.driver).toBe("extension");
|
|
expect(chrome?.cdpPort).toBe(19004);
|
|
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:19004");
|
|
|
|
const openclaw = resolveProfile(resolved, "openclaw");
|
|
expect(openclaw?.cdpPort).toBe(19012);
|
|
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:19012");
|
|
});
|
|
});
|
|
|
|
it("derives default ports from gateway.port when env is unset", () => {
|
|
withEnv({ OPENCLAW_GATEWAY_PORT: undefined }, () => {
|
|
const resolved = resolveBrowserConfig(undefined, { gateway: { port: 19011 } });
|
|
expect(resolved.controlPort).toBe(19013);
|
|
const chrome = resolveProfile(resolved, "chrome");
|
|
expect(chrome?.driver).toBe("extension");
|
|
expect(chrome?.cdpPort).toBe(19014);
|
|
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:19014");
|
|
|
|
const openclaw = resolveProfile(resolved, "openclaw");
|
|
expect(openclaw?.cdpPort).toBe(19022);
|
|
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:19022");
|
|
});
|
|
});
|
|
|
|
it("supports overriding the local CDP auto-allocation range start", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
cdpPortRangeStart: 19000,
|
|
});
|
|
const openclaw = resolveProfile(resolved, "openclaw");
|
|
expect(resolved.cdpPortRangeStart).toBe(19000);
|
|
expect(openclaw?.cdpPort).toBe(19000);
|
|
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:19000");
|
|
});
|
|
|
|
it("rejects cdpPortRangeStart values that overflow the CDP range window", () => {
|
|
expect(() => resolveBrowserConfig({ cdpPortRangeStart: 65535 })).toThrow(
|
|
/cdpPortRangeStart .* too high/i,
|
|
);
|
|
});
|
|
|
|
it("normalizes hex colors", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
color: "ff4500",
|
|
});
|
|
expect(resolved.color).toBe("#FF4500");
|
|
});
|
|
|
|
it("supports custom remote CDP timeouts", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
remoteCdpTimeoutMs: 2200,
|
|
remoteCdpHandshakeTimeoutMs: 5000,
|
|
});
|
|
expect(resolved.remoteCdpTimeoutMs).toBe(2200);
|
|
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(5000);
|
|
});
|
|
|
|
it("falls back to default color for invalid hex", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
color: "#GGGGGG",
|
|
});
|
|
expect(resolved.color).toBe("#FF4500");
|
|
});
|
|
|
|
it("treats non-loopback cdpUrl as remote", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
cdpUrl: "http://example.com:9222",
|
|
});
|
|
const profile = resolveProfile(resolved, "openclaw");
|
|
expect(profile?.cdpIsLoopback).toBe(false);
|
|
});
|
|
|
|
it("supports explicit CDP URLs for the default profile", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
cdpUrl: "http://example.com:9222",
|
|
});
|
|
const profile = resolveProfile(resolved, "openclaw");
|
|
expect(profile?.cdpPort).toBe(9222);
|
|
expect(profile?.cdpUrl).toBe("http://example.com:9222");
|
|
expect(profile?.cdpIsLoopback).toBe(false);
|
|
});
|
|
|
|
it("uses profile cdpUrl when provided", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
profiles: {
|
|
remote: { cdpUrl: "http://10.0.0.42:9222", color: "#0066CC" },
|
|
},
|
|
});
|
|
|
|
const remote = resolveProfile(resolved, "remote");
|
|
expect(remote?.cdpUrl).toBe("http://10.0.0.42:9222");
|
|
expect(remote?.cdpHost).toBe("10.0.0.42");
|
|
expect(remote?.cdpIsLoopback).toBe(false);
|
|
});
|
|
|
|
it("inherits attachOnly from global browser config when profile override is not set", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
attachOnly: true,
|
|
profiles: {
|
|
remote: { cdpUrl: "http://127.0.0.1:9222", color: "#0066CC" },
|
|
},
|
|
});
|
|
|
|
const remote = resolveProfile(resolved, "remote");
|
|
expect(remote?.attachOnly).toBe(true);
|
|
});
|
|
|
|
it("allows profile attachOnly to override global browser attachOnly", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
attachOnly: false,
|
|
profiles: {
|
|
remote: { cdpUrl: "http://127.0.0.1:9222", attachOnly: true, color: "#0066CC" },
|
|
},
|
|
});
|
|
|
|
const remote = resolveProfile(resolved, "remote");
|
|
expect(remote?.attachOnly).toBe(true);
|
|
});
|
|
|
|
it("uses base protocol for profiles with only cdpPort", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
cdpUrl: "https://example.com:9443",
|
|
profiles: {
|
|
work: { cdpPort: 18801, color: "#0066CC" },
|
|
},
|
|
});
|
|
|
|
const work = resolveProfile(resolved, "work");
|
|
expect(work?.cdpUrl).toBe("https://example.com:18801");
|
|
});
|
|
|
|
it("preserves wss:// cdpUrl with query params for the default profile", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
cdpUrl: "wss://connect.browserbase.com?apiKey=test-key",
|
|
});
|
|
const profile = resolveProfile(resolved, "openclaw");
|
|
expect(profile?.cdpUrl).toBe("wss://connect.browserbase.com/?apiKey=test-key");
|
|
expect(profile?.cdpHost).toBe("connect.browserbase.com");
|
|
expect(profile?.cdpPort).toBe(443);
|
|
expect(profile?.cdpIsLoopback).toBe(false);
|
|
});
|
|
|
|
it("preserves loopback direct WebSocket cdpUrl for explicit profiles", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
profiles: {
|
|
localws: {
|
|
cdpUrl: "ws://127.0.0.1:9222/devtools/browser/ABC?token=test-key",
|
|
color: "#0066CC",
|
|
},
|
|
},
|
|
});
|
|
const profile = resolveProfile(resolved, "localws");
|
|
expect(profile?.cdpUrl).toBe("ws://127.0.0.1:9222/devtools/browser/ABC?token=test-key");
|
|
expect(profile?.cdpPort).toBe(9222);
|
|
expect(profile?.cdpIsLoopback).toBe(true);
|
|
});
|
|
|
|
it("trims relayBindHost when configured", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
relayBindHost: " 0.0.0.0 ",
|
|
});
|
|
expect(resolved.relayBindHost).toBe("0.0.0.0");
|
|
});
|
|
|
|
it("rejects unsupported protocols", () => {
|
|
expect(() => resolveBrowserConfig({ cdpUrl: "ftp://127.0.0.1:18791" })).toThrow(
|
|
"must be http(s) or ws(s)",
|
|
);
|
|
});
|
|
|
|
it("does not add the built-in chrome extension profile if the derived relay port is already used", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
profiles: {
|
|
openclaw: { cdpPort: 18792, color: "#FF4500" },
|
|
},
|
|
});
|
|
expect(resolveProfile(resolved, "chrome")).toBe(null);
|
|
expect(resolved.defaultProfile).toBe("openclaw");
|
|
});
|
|
|
|
it("defaults extraArgs to empty array when not provided", () => {
|
|
const resolved = resolveBrowserConfig(undefined);
|
|
expect(resolved.extraArgs).toEqual([]);
|
|
});
|
|
|
|
it("passes through valid extraArgs strings", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
extraArgs: ["--no-sandbox", "--disable-gpu"],
|
|
});
|
|
expect(resolved.extraArgs).toEqual(["--no-sandbox", "--disable-gpu"]);
|
|
});
|
|
|
|
it("filters out empty strings and whitespace-only entries from extraArgs", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
extraArgs: ["--flag", "", " ", "--other"],
|
|
});
|
|
expect(resolved.extraArgs).toEqual(["--flag", "--other"]);
|
|
});
|
|
|
|
it("filters out non-string entries from extraArgs", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
extraArgs: ["--flag", 42, null, undefined, true, "--other"] as unknown as string[],
|
|
});
|
|
expect(resolved.extraArgs).toEqual(["--flag", "--other"]);
|
|
});
|
|
|
|
it("defaults extraArgs to empty array when set to non-array", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
extraArgs: "not-an-array" as unknown as string[],
|
|
});
|
|
expect(resolved.extraArgs).toEqual([]);
|
|
});
|
|
|
|
it("resolves browser SSRF policy when configured", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
ssrfPolicy: {
|
|
allowPrivateNetwork: true,
|
|
allowedHostnames: [" localhost ", ""],
|
|
hostnameAllowlist: [" *.trusted.example ", " "],
|
|
},
|
|
});
|
|
expect(resolved.ssrfPolicy).toEqual({
|
|
dangerouslyAllowPrivateNetwork: true,
|
|
allowedHostnames: ["localhost"],
|
|
hostnameAllowlist: ["*.trusted.example"],
|
|
});
|
|
});
|
|
|
|
it("defaults browser SSRF policy to trusted-network mode", () => {
|
|
const resolved = resolveBrowserConfig({});
|
|
expect(resolved.ssrfPolicy).toEqual({
|
|
dangerouslyAllowPrivateNetwork: true,
|
|
});
|
|
});
|
|
|
|
it("supports explicit strict mode by disabling private network access", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
ssrfPolicy: {
|
|
dangerouslyAllowPrivateNetwork: false,
|
|
},
|
|
});
|
|
expect(resolved.ssrfPolicy).toEqual({});
|
|
});
|
|
|
|
describe("default profile preference", () => {
|
|
it("defaults to openclaw profile when defaultProfile is not configured", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
headless: false,
|
|
noSandbox: false,
|
|
});
|
|
expect(resolved.defaultProfile).toBe("openclaw");
|
|
});
|
|
|
|
it("keeps openclaw default when headless=true", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
headless: true,
|
|
});
|
|
expect(resolved.defaultProfile).toBe("openclaw");
|
|
});
|
|
|
|
it("keeps openclaw default when noSandbox=true", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
noSandbox: true,
|
|
});
|
|
expect(resolved.defaultProfile).toBe("openclaw");
|
|
});
|
|
|
|
it("keeps openclaw default when both headless and noSandbox are true", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
headless: true,
|
|
noSandbox: true,
|
|
});
|
|
expect(resolved.defaultProfile).toBe("openclaw");
|
|
});
|
|
|
|
it("explicit defaultProfile config overrides defaults in headless mode", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
headless: true,
|
|
defaultProfile: "chrome",
|
|
});
|
|
expect(resolved.defaultProfile).toBe("chrome");
|
|
});
|
|
|
|
it("explicit defaultProfile config overrides defaults in noSandbox mode", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
noSandbox: true,
|
|
defaultProfile: "chrome",
|
|
});
|
|
expect(resolved.defaultProfile).toBe("chrome");
|
|
});
|
|
|
|
it("allows custom profile as default even in headless mode", () => {
|
|
const resolved = resolveBrowserConfig({
|
|
headless: true,
|
|
defaultProfile: "custom",
|
|
profiles: {
|
|
custom: { cdpPort: 19999, color: "#00FF00" },
|
|
},
|
|
});
|
|
expect(resolved.defaultProfile).toBe("custom");
|
|
});
|
|
});
|
|
});
|