From 0eebae44f6d316d5d1637691548aa1f334dfc710 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 06:24:16 +0000 Subject: [PATCH] fix: test browser.request profile body fallback (#28852) (thanks @Sid-Qin) --- CHANGELOG.md | 1 + .../browser.profile-from-body.test.ts | 103 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/gateway/server-methods/browser.profile-from-body.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e2dccf3ffdd..cbe93dae883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like `[System Message]` and line-leading `System:` in untrusted message content. (#30448) +- Gateway/Node browser proxy routing: honor `profile` from `browser.request` JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin. - Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc. - Docker/Image health checks: add Dockerfile `HEALTHCHECK` that probes gateway `GET /healthz` so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc. - Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc. diff --git a/src/gateway/server-methods/browser.profile-from-body.test.ts b/src/gateway/server-methods/browser.profile-from-body.test.ts new file mode 100644 index 00000000000..972fca9f848 --- /dev/null +++ b/src/gateway/server-methods/browser.profile-from-body.test.ts @@ -0,0 +1,103 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { loadConfigMock, isNodeCommandAllowedMock, resolveNodeCommandAllowlistMock } = vi.hoisted( + () => ({ + loadConfigMock: vi.fn(), + isNodeCommandAllowedMock: vi.fn(), + resolveNodeCommandAllowlistMock: vi.fn(), + }), +); + +vi.mock("../../config/config.js", () => ({ + loadConfig: loadConfigMock, +})); + +vi.mock("../node-command-policy.js", () => ({ + isNodeCommandAllowed: isNodeCommandAllowedMock, + resolveNodeCommandAllowlist: resolveNodeCommandAllowlistMock, +})); + +import { browserHandlers } from "./browser.js"; + +type RespondCall = [boolean, unknown?, { code: number; message: string }?]; + +function createContext() { + const invoke = vi.fn(async () => ({ + ok: true, + payload: { + result: { ok: true }, + }, + })); + const listConnected = vi.fn(() => [ + { + nodeId: "node-1", + caps: ["browser"], + commands: ["browser.proxy"], + platform: "linux", + }, + ]); + return { + invoke, + listConnected, + }; +} + +async function runBrowserRequest(params: Record) { + const respond = vi.fn(); + const nodeRegistry = createContext(); + await browserHandlers["browser.request"]({ + params, + respond: respond as never, + context: { nodeRegistry } as never, + client: null, + req: { type: "req", id: "req-1", method: "browser.request" }, + isWebchatConnect: () => false, + }); + return { respond, nodeRegistry }; +} + +describe("browser.request profile selection", () => { + beforeEach(() => { + loadConfigMock.mockReturnValue({ + gateway: { nodes: { browser: { mode: "auto" } } }, + }); + resolveNodeCommandAllowlistMock.mockReturnValue([]); + isNodeCommandAllowedMock.mockReturnValue({ ok: true }); + }); + + it("uses profile from request body when query profile is missing", async () => { + const { respond, nodeRegistry } = await runBrowserRequest({ + method: "POST", + path: "/act", + body: { profile: "work", request: { action: "click", ref: "btn1" } }, + }); + + expect(nodeRegistry.invoke).toHaveBeenCalledWith( + expect.objectContaining({ + command: "browser.proxy", + params: expect.objectContaining({ + profile: "work", + }), + }), + ); + const call = respond.mock.calls[0] as RespondCall | undefined; + expect(call?.[0]).toBe(true); + }); + + it("prefers query profile over body profile when both are present", async () => { + const { nodeRegistry } = await runBrowserRequest({ + method: "POST", + path: "/act", + query: { profile: "chrome" }, + body: { profile: "work", request: { action: "click", ref: "btn1" } }, + }); + + expect(nodeRegistry.invoke).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + profile: "chrome", + }), + }), + ); + }); +});