diff --git a/extensions/browser/src/browser/cdp-reachability-policy.test.ts b/extensions/browser/src/browser/cdp-reachability-policy.test.ts deleted file mode 100644 index a258b0ec49f..00000000000 --- a/extensions/browser/src/browser/cdp-reachability-policy.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveCdpReachabilityPolicy } from "./cdp-reachability-policy.js"; -import type { ResolvedBrowserProfile } from "./config.js"; -import { assertBrowserNavigationAllowed } from "./navigation-guard.js"; - -function createProfile(overrides: Partial): ResolvedBrowserProfile { - return { - name: "remote", - cdpPort: 9223, - cdpUrl: "http://172.29.128.1:9223", - cdpHost: "172.29.128.1", - cdpIsLoopback: false, - color: "#123456", - driver: "openclaw", - attachOnly: false, - ...overrides, - }; -} - -describe("CDP reachability policy", () => { - it("allows the selected remote profile CDP host without widening browser navigation policy", async () => { - const browserPolicy = {}; - const profile = createProfile({}); - - expect(resolveCdpReachabilityPolicy(profile, browserPolicy)).toEqual({ - allowedHostnames: ["172.29.128.1"], - }); - expect(browserPolicy).toEqual({}); - await expect( - assertBrowserNavigationAllowed({ - url: "http://172.29.128.1/", - ssrfPolicy: browserPolicy, - }), - ).rejects.toThrow(/private\/internal\/special-use ip address/i); - }); - - it("merges the selected remote profile CDP host with existing CDP policy hostnames", () => { - const profile = createProfile({}); - - expect( - resolveCdpReachabilityPolicy(profile, { - allowedHostnames: ["metadata.internal"], - }), - ).toEqual({ - allowedHostnames: ["metadata.internal", "172.29.128.1"], - }); - }); - - it("keeps local managed loopback CDP control outside browser SSRF policy", () => { - const profile = createProfile({ - cdpUrl: "http://127.0.0.1:18800", - cdpHost: "127.0.0.1", - cdpIsLoopback: true, - }); - - expect(resolveCdpReachabilityPolicy(profile, {})).toBeUndefined(); - }); -}); diff --git a/extensions/browser/src/browser/cdp-timeouts.test.ts b/extensions/browser/src/browser/cdp-timeouts.test.ts deleted file mode 100644 index 178915dc78a..00000000000 --- a/extensions/browser/src/browser/cdp-timeouts.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - PROFILE_HTTP_REACHABILITY_TIMEOUT_MS, - PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS, - PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS, - resolveCdpReachabilityTimeouts, -} from "./cdp-timeouts.js"; - -describe("resolveCdpReachabilityTimeouts", () => { - it("uses loopback defaults when timeout is omitted", () => { - expect( - resolveCdpReachabilityTimeouts({ - profileIsLoopback: true, - timeoutMs: undefined, - remoteHttpTimeoutMs: 1500, - remoteHandshakeTimeoutMs: 3000, - }), - ).toEqual({ - httpTimeoutMs: PROFILE_HTTP_REACHABILITY_TIMEOUT_MS, - wsTimeoutMs: PROFILE_HTTP_REACHABILITY_TIMEOUT_MS * 2, - }); - }); - - it("clamps loopback websocket timeout range", () => { - const low = resolveCdpReachabilityTimeouts({ - profileIsLoopback: true, - timeoutMs: 1, - remoteHttpTimeoutMs: 1500, - remoteHandshakeTimeoutMs: 3000, - }); - const high = resolveCdpReachabilityTimeouts({ - profileIsLoopback: true, - timeoutMs: 5000, - remoteHttpTimeoutMs: 1500, - remoteHandshakeTimeoutMs: 3000, - }); - - expect(low.wsTimeoutMs).toBe(PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS); - expect(high.wsTimeoutMs).toBe(PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS); - }); - - it("enforces remote minimums even when caller passes lower timeout", () => { - expect( - resolveCdpReachabilityTimeouts({ - profileIsLoopback: false, - timeoutMs: 200, - remoteHttpTimeoutMs: 1500, - remoteHandshakeTimeoutMs: 3000, - }), - ).toEqual({ - httpTimeoutMs: 1500, - wsTimeoutMs: 3000, - }); - }); - - it("uses remote defaults when timeout is omitted", () => { - expect( - resolveCdpReachabilityTimeouts({ - profileIsLoopback: false, - timeoutMs: undefined, - remoteHttpTimeoutMs: 1750, - remoteHandshakeTimeoutMs: 3250, - }), - ).toEqual({ - httpTimeoutMs: 1750, - wsTimeoutMs: 3250, - }); - }); -}); diff --git a/extensions/browser/src/browser/cdp.helpers.test.ts b/extensions/browser/src/browser/cdp.helpers.test.ts index a275fa5b546..7f9518975c3 100644 --- a/extensions/browser/src/browser/cdp.helpers.test.ts +++ b/extensions/browser/src/browser/cdp.helpers.test.ts @@ -1,4 +1,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { resolveCdpReachabilityPolicy } from "./cdp-reachability-policy.js"; +import { + PROFILE_HTTP_REACHABILITY_TIMEOUT_MS, + PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS, + PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS, + resolveCdpReachabilityTimeouts, +} from "./cdp-timeouts.js"; +import type { ResolvedBrowserProfile } from "./config.js"; +import { assertBrowserNavigationAllowed } from "./navigation-guard.js"; const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn()); @@ -138,3 +147,119 @@ describe("cdp helpers", () => { expect(release).toHaveBeenCalledTimes(1); }); }); + +function createProfile(overrides: Partial): ResolvedBrowserProfile { + return { + name: "remote", + cdpPort: 9223, + cdpUrl: "http://172.29.128.1:9223", + cdpHost: "172.29.128.1", + cdpIsLoopback: false, + color: "#123456", + driver: "openclaw", + attachOnly: false, + ...overrides, + }; +} + +describe("resolveCdpReachabilityTimeouts", () => { + it("uses loopback defaults when timeout is omitted", () => { + expect( + resolveCdpReachabilityTimeouts({ + profileIsLoopback: true, + timeoutMs: undefined, + remoteHttpTimeoutMs: 1500, + remoteHandshakeTimeoutMs: 3000, + }), + ).toEqual({ + httpTimeoutMs: PROFILE_HTTP_REACHABILITY_TIMEOUT_MS, + wsTimeoutMs: PROFILE_HTTP_REACHABILITY_TIMEOUT_MS * 2, + }); + }); + + it("clamps loopback websocket timeout range", () => { + const low = resolveCdpReachabilityTimeouts({ + profileIsLoopback: true, + timeoutMs: 1, + remoteHttpTimeoutMs: 1500, + remoteHandshakeTimeoutMs: 3000, + }); + const high = resolveCdpReachabilityTimeouts({ + profileIsLoopback: true, + timeoutMs: 5000, + remoteHttpTimeoutMs: 1500, + remoteHandshakeTimeoutMs: 3000, + }); + + expect(low.wsTimeoutMs).toBe(PROFILE_WS_REACHABILITY_MIN_TIMEOUT_MS); + expect(high.wsTimeoutMs).toBe(PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS); + }); + + it("enforces remote minimums even when caller passes lower timeout", () => { + expect( + resolveCdpReachabilityTimeouts({ + profileIsLoopback: false, + timeoutMs: 200, + remoteHttpTimeoutMs: 1500, + remoteHandshakeTimeoutMs: 3000, + }), + ).toEqual({ + httpTimeoutMs: 1500, + wsTimeoutMs: 3000, + }); + }); + + it("uses remote defaults when timeout is omitted", () => { + expect( + resolveCdpReachabilityTimeouts({ + profileIsLoopback: false, + timeoutMs: undefined, + remoteHttpTimeoutMs: 1750, + remoteHandshakeTimeoutMs: 3250, + }), + ).toEqual({ + httpTimeoutMs: 1750, + wsTimeoutMs: 3250, + }); + }); +}); + +describe("CDP reachability policy", () => { + it("allows the selected remote profile CDP host without widening browser navigation policy", async () => { + const browserPolicy = {}; + const profile = createProfile({}); + + expect(resolveCdpReachabilityPolicy(profile, browserPolicy)).toEqual({ + allowedHostnames: ["172.29.128.1"], + }); + expect(browserPolicy).toEqual({}); + await expect( + assertBrowserNavigationAllowed({ + url: "http://172.29.128.1/", + ssrfPolicy: browserPolicy, + }), + ).rejects.toThrow(/private\/internal\/special-use ip address/i); + }); + + it("merges the selected remote profile CDP host with existing CDP policy hostnames", () => { + const profile = createProfile({}); + + expect( + resolveCdpReachabilityPolicy(profile, { + allowedHostnames: ["metadata.internal"], + }), + ).toEqual({ + allowedHostnames: ["metadata.internal", "172.29.128.1"], + }); + }); + + it("keeps local managed loopback CDP control outside browser SSRF policy", () => { + const profile = createProfile({ + cdpUrl: "http://127.0.0.1:18800", + cdpHost: "127.0.0.1", + cdpIsLoopback: true, + }); + + expect(resolveCdpReachabilityPolicy(profile, {})).toBeUndefined(); + }); +}); diff --git a/extensions/browser/src/browser/cdp.test.ts b/extensions/browser/src/browser/cdp.test.ts index 7c0accb7bd3..aa0d7767c7e 100644 --- a/extensions/browser/src/browser/cdp.test.ts +++ b/extensions/browser/src/browser/cdp.test.ts @@ -7,7 +7,13 @@ import "../../test-support/browser-security-runtime.mock.js"; import { isDirectCdpWebSocketEndpoint, isWebSocketUrl } from "./cdp.helpers.js"; import { createTargetViaCdp, evaluateJavaScript, normalizeCdpWsUrl, snapshotAria } from "./cdp.js"; import { parseHttpUrl } from "./config.js"; -import { BrowserCdpEndpointBlockedError } from "./errors.js"; +import { + BROWSER_ENDPOINT_BLOCKED_MESSAGE, + BROWSER_NAVIGATION_BLOCKED_MESSAGE, + BrowserCdpEndpointBlockedError, + BrowserValidationError, + toBrowserErrorResponse, +} from "./errors.js"; import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js"; describe("cdp", () => { @@ -460,6 +466,45 @@ describe("cdp", () => { }); }); +describe("browser error mapping", () => { + it("maps blocked browser targets to conflict responses", () => { + const err = new Error( + "Browser target is unavailable after SSRF policy blocked its navigation.", + ); + err.name = "BlockedBrowserTargetError"; + + expect(toBrowserErrorResponse(err)).toEqual({ + status: 409, + message: "Browser target is unavailable after SSRF policy blocked its navigation.", + }); + }); + + it("preserves BrowserError mappings", () => { + expect(toBrowserErrorResponse(new BrowserValidationError("bad input"))).toEqual({ + status: 400, + message: "bad input", + }); + }); + + it("sanitizes navigation-target SSRF policy errors without leaking raw policy details", () => { + expect( + toBrowserErrorResponse( + new SsrFBlockedError("Blocked hostname or private/internal/special-use IP address"), + ), + ).toEqual({ + status: 400, + message: BROWSER_NAVIGATION_BLOCKED_MESSAGE, + }); + }); + + it("maps CDP endpoint policy blocks to a distinct endpoint-scoped message", () => { + expect(toBrowserErrorResponse(new BrowserCdpEndpointBlockedError())).toEqual({ + status: 400, + message: BROWSER_ENDPOINT_BLOCKED_MESSAGE, + }); + }); +}); + describe("isWebSocketUrl", () => { it("returns true for ws:// URLs", () => { expect(isWebSocketUrl("ws://127.0.0.1:9222")).toBe(true); diff --git a/extensions/browser/src/browser/chrome.executables.test.ts b/extensions/browser/src/browser/chrome.executables.test.ts deleted file mode 100644 index 2a5344bd158..00000000000 --- a/extensions/browser/src/browser/chrome.executables.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import fs from "node:fs"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - parseBrowserMajorVersion, - resolveGoogleChromeExecutableForPlatform, -} from "./chrome.executables.js"; - -describe("chrome executables", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("parses odd dotted browser version tokens using the last match", () => { - expect(parseBrowserMajorVersion("Chromium 3.0/1.2.3")).toBe(1); - }); - - it("returns null when no dotted version token exists", () => { - expect(parseBrowserMajorVersion("no version here")).toBeNull(); - }); - - it("classifies beta Linux Google Chrome builds as canary", () => { - vi.spyOn(fs, "existsSync").mockImplementation((candidate) => { - return String(candidate) === "/usr/bin/google-chrome-beta"; - }); - - expect(resolveGoogleChromeExecutableForPlatform("linux")).toEqual({ - kind: "canary", - path: "/usr/bin/google-chrome-beta", - }); - }); - - it("classifies unstable Linux Google Chrome builds as canary", () => { - vi.spyOn(fs, "existsSync").mockImplementation((candidate) => { - return String(candidate) === "/usr/bin/google-chrome-unstable"; - }); - - expect(resolveGoogleChromeExecutableForPlatform("linux")).toEqual({ - kind: "canary", - path: "/usr/bin/google-chrome-unstable", - }); - }); -}); diff --git a/extensions/browser/src/browser/chrome.launch-args.test.ts b/extensions/browser/src/browser/chrome.launch-args.test.ts deleted file mode 100644 index a3a13004f41..00000000000 --- a/extensions/browser/src/browser/chrome.launch-args.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildOpenClawChromeLaunchArgs } from "./chrome.js"; - -describe("browser chrome launch args", () => { - it("does not force an about:blank tab at startup", () => { - const args = buildOpenClawChromeLaunchArgs({ - resolved: { - enabled: true, - controlPort: 18791, - cdpProtocol: "http", - cdpHost: "127.0.0.1", - cdpIsLoopback: true, - cdpPortRangeStart: 18800, - cdpPortRangeEnd: 18810, - evaluateEnabled: false, - remoteCdpTimeoutMs: 1500, - remoteCdpHandshakeTimeoutMs: 3000, - extraArgs: [], - color: "#FF4500", - headless: false, - noSandbox: false, - attachOnly: false, - ssrfPolicy: { allowPrivateNetwork: true }, - defaultProfile: "openclaw", - profiles: { - openclaw: { cdpPort: 18800, color: "#FF4500" }, - }, - }, - profile: { - name: "openclaw", - cdpUrl: "http://127.0.0.1:18800", - cdpPort: 18800, - cdpHost: "127.0.0.1", - cdpIsLoopback: true, - color: "#FF4500", - driver: "openclaw", - attachOnly: false, - }, - userDataDir: "/tmp/openclaw-test-user-data", - }); - - expect(args).not.toContain("about:blank"); - expect(args).toContain("--remote-debugging-port=18800"); - expect(args).toContain("--user-data-dir=/tmp/openclaw-test-user-data"); - }); -}); diff --git a/extensions/browser/src/browser/chrome.test.ts b/extensions/browser/src/browser/chrome.test.ts index f4b93d2b65b..9a5909b5bbf 100644 --- a/extensions/browser/src/browser/chrome.test.ts +++ b/extensions/browser/src/browser/chrome.test.ts @@ -6,6 +6,10 @@ import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { WebSocketServer } from "ws"; +import { + parseBrowserMajorVersion, + resolveGoogleChromeExecutableForPlatform, +} from "./chrome.executables.js"; import { decorateOpenClawProfile, diagnoseChromeCdp, @@ -13,6 +17,7 @@ import { findChromeExecutableMac, findChromeExecutableWindows, formatChromeCdpDiagnostic, + buildOpenClawChromeLaunchArgs, getChromeWebSocketUrl, isChromeCdpReady, isChromeReachable, @@ -575,3 +580,83 @@ describe("browser chrome helpers", () => { expect(proc.kill).toHaveBeenNthCalledWith(2, "SIGKILL"); }); }); + +describe("chrome executables", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("parses odd dotted browser version tokens using the last match", () => { + expect(parseBrowserMajorVersion("Chromium 3.0/1.2.3")).toBe(1); + }); + + it("returns null when no dotted version token exists", () => { + expect(parseBrowserMajorVersion("no version here")).toBeNull(); + }); + + it("classifies beta Linux Google Chrome builds as canary", () => { + vi.spyOn(fs, "existsSync").mockImplementation((candidate) => { + return String(candidate) === "/usr/bin/google-chrome-beta"; + }); + + expect(resolveGoogleChromeExecutableForPlatform("linux")).toEqual({ + kind: "canary", + path: "/usr/bin/google-chrome-beta", + }); + }); + + it("classifies unstable Linux Google Chrome builds as canary", () => { + vi.spyOn(fs, "existsSync").mockImplementation((candidate) => { + return String(candidate) === "/usr/bin/google-chrome-unstable"; + }); + + expect(resolveGoogleChromeExecutableForPlatform("linux")).toEqual({ + kind: "canary", + path: "/usr/bin/google-chrome-unstable", + }); + }); +}); + +describe("browser chrome launch args", () => { + it("does not force an about:blank tab at startup", () => { + const args = buildOpenClawChromeLaunchArgs({ + resolved: { + enabled: true, + controlPort: 18791, + cdpProtocol: "http", + cdpHost: "127.0.0.1", + cdpIsLoopback: true, + cdpPortRangeStart: 18800, + cdpPortRangeEnd: 18810, + evaluateEnabled: false, + remoteCdpTimeoutMs: 1500, + remoteCdpHandshakeTimeoutMs: 3000, + extraArgs: [], + color: "#FF4500", + headless: false, + noSandbox: false, + attachOnly: false, + ssrfPolicy: { allowPrivateNetwork: true }, + defaultProfile: "openclaw", + profiles: { + openclaw: { cdpPort: 18800, color: "#FF4500" }, + }, + }, + profile: { + name: "openclaw", + cdpUrl: "http://127.0.0.1:18800", + cdpPort: 18800, + cdpHost: "127.0.0.1", + cdpIsLoopback: true, + color: "#FF4500", + driver: "openclaw", + attachOnly: false, + }, + userDataDir: "/tmp/openclaw-test-user-data", + }); + + expect(args).not.toContain("about:blank"); + expect(args).toContain("--remote-debugging-port=18800"); + expect(args).toContain("--user-data-dir=/tmp/openclaw-test-user-data"); + }); +}); diff --git a/extensions/browser/src/browser/errors.test.ts b/extensions/browser/src/browser/errors.test.ts deleted file mode 100644 index 8a1507d69a1..00000000000 --- a/extensions/browser/src/browser/errors.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { SsrFBlockedError } from "../infra/net/ssrf.js"; -import { - BROWSER_ENDPOINT_BLOCKED_MESSAGE, - BROWSER_NAVIGATION_BLOCKED_MESSAGE, - BrowserCdpEndpointBlockedError, - BrowserValidationError, - toBrowserErrorResponse, -} from "./errors.js"; - -describe("browser error mapping", () => { - it("maps blocked browser targets to conflict responses", () => { - const err = new Error( - "Browser target is unavailable after SSRF policy blocked its navigation.", - ); - err.name = "BlockedBrowserTargetError"; - - expect(toBrowserErrorResponse(err)).toEqual({ - status: 409, - message: "Browser target is unavailable after SSRF policy blocked its navigation.", - }); - }); - - it("preserves BrowserError mappings", () => { - expect(toBrowserErrorResponse(new BrowserValidationError("bad input"))).toEqual({ - status: 400, - message: "bad input", - }); - }); - - it("sanitizes navigation-target SSRF policy errors without leaking raw policy details", () => { - expect( - toBrowserErrorResponse( - new SsrFBlockedError("Blocked hostname or private/internal/special-use IP address"), - ), - ).toEqual({ - status: 400, - message: BROWSER_NAVIGATION_BLOCKED_MESSAGE, - }); - }); - - it("maps CDP endpoint policy blocks to a distinct endpoint-scoped message", () => { - expect(toBrowserErrorResponse(new BrowserCdpEndpointBlockedError())).toEqual({ - status: 400, - message: BROWSER_ENDPOINT_BLOCKED_MESSAGE, - }); - }); -}); diff --git a/extensions/browser/src/browser/plugin-enabled.test.ts b/extensions/browser/src/browser/plugin-enabled.test.ts deleted file mode 100644 index ccc518223fa..00000000000 --- a/extensions/browser/src/browser/plugin-enabled.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { OpenClawConfig } from "../config/config.js"; -import { isDefaultBrowserPluginEnabled } from "../plugin-enabled.js"; - -describe("isDefaultBrowserPluginEnabled", () => { - it("defaults to enabled", () => { - expect(isDefaultBrowserPluginEnabled({} as OpenClawConfig)).toBe(true); - }); - - it("respects explicit plugin disablement", () => { - expect( - isDefaultBrowserPluginEnabled({ - plugins: { - entries: { - browser: { - enabled: false, - }, - }, - }, - } as OpenClawConfig), - ).toBe(false); - }); -}); diff --git a/extensions/browser/src/browser/request-policy.test.ts b/extensions/browser/src/browser/request-policy.test.ts index 0920ad416cb..1cf17d75556 100644 --- a/extensions/browser/src/browser/request-policy.test.ts +++ b/extensions/browser/src/browser/request-policy.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { isPersistentBrowserProfileMutation } from "./request-policy.js"; +import { matchBrowserUrlPattern } from "./url-pattern.js"; describe("isPersistentBrowserProfileMutation", () => { it.each([ @@ -23,3 +24,27 @@ describe("isPersistentBrowserProfileMutation", () => { expect(isPersistentBrowserProfileMutation(method, path)).toBe(false); }); }); + +describe("browser url pattern matching", () => { + it("matches exact URLs", () => { + expect(matchBrowserUrlPattern("https://example.com/a", "https://example.com/a")).toBe(true); + expect(matchBrowserUrlPattern("https://example.com/a", "https://example.com/b")).toBe(false); + }); + + it("matches substring patterns without wildcards", () => { + expect(matchBrowserUrlPattern("example.com", "https://example.com/a")).toBe(true); + expect(matchBrowserUrlPattern("/dash", "https://example.com/app/dash")).toBe(true); + expect(matchBrowserUrlPattern("nope", "https://example.com/a")).toBe(false); + }); + + it("matches glob patterns", () => { + expect(matchBrowserUrlPattern("**/dash", "https://example.com/app/dash")).toBe(true); + expect(matchBrowserUrlPattern("https://example.com/*", "https://example.com/a")).toBe(true); + expect(matchBrowserUrlPattern("https://example.com/*", "https://other.com/a")).toBe(false); + }); + + it("rejects empty patterns", () => { + expect(matchBrowserUrlPattern("", "https://example.com")).toBe(false); + expect(matchBrowserUrlPattern(" ", "https://example.com")).toBe(false); + }); +}); diff --git a/extensions/browser/src/browser/url-pattern.test.ts b/extensions/browser/src/browser/url-pattern.test.ts deleted file mode 100644 index 1cfdc06c36f..00000000000 --- a/extensions/browser/src/browser/url-pattern.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { matchBrowserUrlPattern } from "./url-pattern.js"; - -describe("browser url pattern matching", () => { - it("matches exact URLs", () => { - expect(matchBrowserUrlPattern("https://example.com/a", "https://example.com/a")).toBe(true); - expect(matchBrowserUrlPattern("https://example.com/a", "https://example.com/b")).toBe(false); - }); - - it("matches substring patterns without wildcards", () => { - expect(matchBrowserUrlPattern("example.com", "https://example.com/a")).toBe(true); - expect(matchBrowserUrlPattern("/dash", "https://example.com/app/dash")).toBe(true); - expect(matchBrowserUrlPattern("nope", "https://example.com/a")).toBe(false); - }); - - it("matches glob patterns", () => { - expect(matchBrowserUrlPattern("**/dash", "https://example.com/app/dash")).toBe(true); - expect(matchBrowserUrlPattern("https://example.com/*", "https://example.com/a")).toBe(true); - expect(matchBrowserUrlPattern("https://example.com/*", "https://other.com/a")).toBe(false); - }); - - it("rejects empty patterns", () => { - expect(matchBrowserUrlPattern("", "https://example.com")).toBe(false); - expect(matchBrowserUrlPattern(" ", "https://example.com")).toBe(false); - }); -}); diff --git a/extensions/browser/src/plugin-service.test.ts b/extensions/browser/src/plugin-service.test.ts index 714a6b64224..b6d150a0af3 100644 --- a/extensions/browser/src/plugin-service.test.ts +++ b/extensions/browser/src/plugin-service.test.ts @@ -1,4 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "./config/config.js"; +import { isDefaultBrowserPluginEnabled } from "./plugin-enabled.js"; import { createBrowserPluginService } from "./plugin-service.js"; const SERVICE_CONTEXT = { @@ -61,3 +63,23 @@ describe("createBrowserPluginService", () => { ); }); }); + +describe("isDefaultBrowserPluginEnabled", () => { + it("defaults to enabled", () => { + expect(isDefaultBrowserPluginEnabled({} as OpenClawConfig)).toBe(true); + }); + + it("respects explicit plugin disablement", () => { + expect( + isDefaultBrowserPluginEnabled({ + plugins: { + entries: { + browser: { + enabled: false, + }, + }, + }, + } as OpenClawConfig), + ).toBe(false); + }); +});