test: merge browser contract server suites

This commit is contained in:
Peter Steinberger
2026-04-17 05:54:25 +01:00
parent a0d9598425
commit 5817a76236
4 changed files with 577 additions and 600 deletions

View File

@@ -1,176 +0,0 @@
import { describe, expect, it } from "vitest";
import {
installAgentContractHooks,
startServerAndBase,
} from "./server.agent-contract.test-harness.js";
import {
setBrowserControlServerEvaluateEnabled,
setBrowserControlServerProfiles,
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch } from "./test-fetch.js";
type ActErrorResponse = {
error?: string;
code?: string;
};
type ActErrorHttpResponse = {
status: number;
body: ActErrorResponse;
};
async function postActAndReadError(base: string, body?: unknown): Promise<ActErrorHttpResponse> {
const realFetch = getBrowserTestFetch();
const response = await realFetch(`${base}/act`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
return {
status: response.status,
body: (await response.json()) as ActErrorResponse,
};
}
describe("browser control server", () => {
installAgentContractHooks();
const slowTimeoutMs = process.platform === "win32" ? 40_000 : 20_000;
it(
"returns ACT_KIND_REQUIRED when kind is missing",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_KIND_REQUIRED");
expect(response.body.error).toContain("kind is required");
},
slowTimeoutMs,
);
it(
"returns ACT_INVALID_REQUEST for malformed action payloads",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "click",
ref: {},
});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_INVALID_REQUEST");
expect(response.body.error).toContain("click requires ref or selector");
},
slowTimeoutMs,
);
it(
"returns ACT_EXISTING_SESSION_UNSUPPORTED for unsupported existing-session actions",
async () => {
setBrowserControlServerProfiles({
openclaw: {
color: "#FF4500",
driver: "existing-session",
},
});
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "batch",
actions: [{ kind: "press", key: "Enter" }],
});
expect(response.status).toBe(501);
expect(response.body.code).toBe("ACT_EXISTING_SESSION_UNSUPPORTED");
expect(response.body.error).toContain("batch");
},
slowTimeoutMs,
);
it(
"returns ACT_TARGET_ID_MISMATCH for batched action targetId overrides",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "batch",
actions: [{ kind: "click", ref: "5", targetId: "other-tab" }],
});
expect(response.status).toBe(403);
expect(response.body.code).toBe("ACT_TARGET_ID_MISMATCH");
expect(response.body.error).toContain("batched action targetId must match request targetId");
},
slowTimeoutMs,
);
it(
"returns ACT_TARGET_ID_MISMATCH for top-level action targetId overrides",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "click",
ref: "5",
// Intentionally non-string: route-level target selection ignores this,
// while action normalization stringifies it.
targetId: 12345,
});
expect(response.status).toBe(403);
expect(response.body.code).toBe("ACT_TARGET_ID_MISMATCH");
expect(response.body.error).toContain("action targetId must match request targetId");
},
slowTimeoutMs,
);
it(
"returns ACT_SELECTOR_UNSUPPORTED for selector on unsupported action kinds",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "evaluate",
fn: "() => 1",
selector: "#submit",
});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_SELECTOR_UNSUPPORTED");
expect(response.body.error).toContain("'selector' is not supported");
},
slowTimeoutMs,
);
it(
"returns ACT_INVALID_REQUEST for malformed unsupported selector actions before selector gating",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "press",
selector: "#submit",
});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_INVALID_REQUEST");
expect(response.body.error).toContain("press requires key");
},
slowTimeoutMs,
);
it(
"returns ACT_EVALUATE_DISABLED when evaluate is blocked by config",
async () => {
setBrowserControlServerEvaluateEnabled(false);
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "evaluate",
fn: "() => 1",
});
expect(response.status).toBe(403);
expect(response.body.code).toBe("ACT_EVALUATE_DISABLED");
expect(response.body.error).toContain("browser.evaluateEnabled=false");
},
slowTimeoutMs,
);
});

View File

@@ -0,0 +1,577 @@
import fs from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants.js";
import {
installAgentContractHooks,
postJson,
startServerAndBase,
} from "./server.agent-contract.test-harness.js";
import {
cleanupBrowserControlServerTestContext,
getBrowserControlServerBaseUrl,
getBrowserControlServerTestState,
getCdpMocks,
getPwMocks,
makeResponse,
resetBrowserControlServerTestContext,
setBrowserControlServerEvaluateEnabled,
setBrowserControlServerProfiles,
setBrowserControlServerReachable,
startBrowserControlServerFromConfig,
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch } from "./test-fetch.js";
type ActErrorResponse = {
error?: string;
code?: string;
};
type ActErrorHttpResponse = {
status: number;
body: ActErrorResponse;
};
async function postActAndReadError(base: string, body?: unknown): Promise<ActErrorHttpResponse> {
const realFetch = getBrowserTestFetch();
const response = await realFetch(`${base}/act`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
return {
status: response.status,
body: (await response.json()) as ActErrorResponse,
};
}
const state = getBrowserControlServerTestState();
const cdpMocks = getCdpMocks();
const pwMocks = getPwMocks();
describe("browser control server", () => {
installAgentContractHooks();
const slowTimeoutMs = process.platform === "win32" ? 40_000 : 20_000;
it(
"returns ACT_KIND_REQUIRED when kind is missing",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_KIND_REQUIRED");
expect(response.body.error).toContain("kind is required");
},
slowTimeoutMs,
);
it(
"returns ACT_INVALID_REQUEST for malformed action payloads",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "click",
ref: {},
});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_INVALID_REQUEST");
expect(response.body.error).toContain("click requires ref or selector");
},
slowTimeoutMs,
);
it(
"returns ACT_EXISTING_SESSION_UNSUPPORTED for unsupported existing-session actions",
async () => {
setBrowserControlServerProfiles({
openclaw: {
color: "#FF4500",
driver: "existing-session",
},
});
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "batch",
actions: [{ kind: "press", key: "Enter" }],
});
expect(response.status).toBe(501);
expect(response.body.code).toBe("ACT_EXISTING_SESSION_UNSUPPORTED");
expect(response.body.error).toContain("batch");
},
slowTimeoutMs,
);
it(
"returns ACT_TARGET_ID_MISMATCH for batched action targetId overrides",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "batch",
actions: [{ kind: "click", ref: "5", targetId: "other-tab" }],
});
expect(response.status).toBe(403);
expect(response.body.code).toBe("ACT_TARGET_ID_MISMATCH");
expect(response.body.error).toContain("batched action targetId must match request targetId");
},
slowTimeoutMs,
);
it(
"returns ACT_TARGET_ID_MISMATCH for top-level action targetId overrides",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "click",
ref: "5",
// Intentionally non-string: route-level target selection ignores this,
// while action normalization stringifies it.
targetId: 12345,
});
expect(response.status).toBe(403);
expect(response.body.code).toBe("ACT_TARGET_ID_MISMATCH");
expect(response.body.error).toContain("action targetId must match request targetId");
},
slowTimeoutMs,
);
it(
"returns ACT_SELECTOR_UNSUPPORTED for selector on unsupported action kinds",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "evaluate",
fn: "() => 1",
selector: "#submit",
});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_SELECTOR_UNSUPPORTED");
expect(response.body.error).toContain("'selector' is not supported");
},
slowTimeoutMs,
);
it(
"returns ACT_INVALID_REQUEST for malformed unsupported selector actions before selector gating",
async () => {
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "press",
selector: "#submit",
});
expect(response.status).toBe(400);
expect(response.body.code).toBe("ACT_INVALID_REQUEST");
expect(response.body.error).toContain("press requires key");
},
slowTimeoutMs,
);
it(
"returns ACT_EVALUATE_DISABLED when evaluate is blocked by config",
async () => {
setBrowserControlServerEvaluateEnabled(false);
const base = await startServerAndBase();
const response = await postActAndReadError(base, {
kind: "evaluate",
fn: "() => 1",
});
expect(response.status).toBe(403);
expect(response.body.code).toBe("ACT_EVALUATE_DISABLED");
expect(response.body.error).toContain("browser.evaluateEnabled=false");
},
slowTimeoutMs,
);
it("agent contract: snapshot endpoints", async () => {
const base = await startServerAndBase();
const realFetch = getBrowserTestFetch();
const snapAria = (await realFetch(`${base}/snapshot?format=aria&limit=1`).then((r) =>
r.json(),
)) as { ok: boolean; format?: string };
expect(snapAria.ok).toBe(true);
expect(snapAria.format).toBe("aria");
expect(cdpMocks.snapshotAria).toHaveBeenCalledWith({
wsUrl: "ws://127.0.0.1/devtools/page/abcd1234",
limit: 1,
});
const snapAi = (await realFetch(`${base}/snapshot?format=ai`).then((r) => r.json())) as {
ok: boolean;
format?: string;
};
expect(snapAi.ok).toBe(true);
expect(snapAi.format).toBe("ai");
expect(pwMocks.snapshotAiViaPlaywright).toHaveBeenCalledWith({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS,
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
});
const snapAiZero = (await realFetch(`${base}/snapshot?format=ai&maxChars=0`).then((r) =>
r.json(),
)) as { ok: boolean; format?: string };
expect(snapAiZero.ok).toBe(true);
expect(snapAiZero.format).toBe("ai");
const [lastCall] = pwMocks.snapshotAiViaPlaywright.mock.calls.at(-1) ?? [];
expect(lastCall).toEqual({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
});
});
it("agent contract: navigation + common act commands", async () => {
const base = await startServerAndBase();
const realFetch = getBrowserTestFetch();
const nav = await postJson<{ ok: boolean; targetId?: string }>(`${base}/navigate`, {
url: "https://example.com",
});
expect(nav.ok).toBe(true);
expect(typeof nav.targetId).toBe("string");
expect(pwMocks.navigateViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
url: "https://example.com",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const click = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "click",
ref: "1",
button: "left",
modifiers: ["Shift"],
});
expect(click.ok).toBe(true);
expect(pwMocks.clickViaPlaywright).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "1",
button: "left",
modifiers: ["Shift"],
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [clickArgs] = pwMocks.clickViaPlaywright.mock.calls[0] ?? [];
expect((clickArgs as { doubleClick?: boolean }).doubleClick).toBeUndefined();
const clickSelector = await realFetch(`${base}/act`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind: "click", selector: "button.save" }),
});
expect(clickSelector.status).toBe(200);
expect(((await clickSelector.json()) as { ok?: boolean }).ok).toBe(true);
expect(pwMocks.clickViaPlaywright).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
selector: "button.save",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [clickSelectorArgs] = pwMocks.clickViaPlaywright.mock.calls[1] ?? [];
expect((clickSelectorArgs as { doubleClick?: boolean }).doubleClick).toBeUndefined();
const type = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "type",
ref: "1",
text: "",
});
expect(type.ok).toBe(true);
expect(pwMocks.typeViaPlaywright).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "1",
text: "",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [typeArgs] = pwMocks.typeViaPlaywright.mock.calls[0] ?? [];
expect((typeArgs as { submit?: boolean }).submit).toBeUndefined();
expect((typeArgs as { slowly?: boolean }).slowly).toBeUndefined();
const press = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "press",
key: "Enter",
});
expect(press.ok).toBe(true);
expect(pwMocks.pressKeyViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
key: "Enter",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [pressArgs] = pwMocks.pressKeyViaPlaywright.mock.calls[0] ?? [];
expect((pressArgs as { delayMs?: number }).delayMs).toBeUndefined();
const hover = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "hover",
ref: "2",
});
expect(hover.ok).toBe(true);
expect(pwMocks.hoverViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "2",
}),
);
const [hoverArgs] = pwMocks.hoverViaPlaywright.mock.calls[0] ?? [];
expect((hoverArgs as { timeoutMs?: number }).timeoutMs).toBeUndefined();
const scroll = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "scrollIntoView",
ref: "2",
});
expect(scroll.ok).toBe(true);
expect(pwMocks.scrollIntoViewViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "2",
}),
);
const [scrollArgs] = pwMocks.scrollIntoViewViaPlaywright.mock.calls[0] ?? [];
expect((scrollArgs as { timeoutMs?: number }).timeoutMs).toBeUndefined();
const drag = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "drag",
startRef: "3",
endRef: "4",
});
expect(drag.ok).toBe(true);
expect(pwMocks.dragViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
startRef: "3",
endRef: "4",
}),
);
const [dragArgs] = pwMocks.dragViaPlaywright.mock.calls[0] ?? [];
expect((dragArgs as { timeoutMs?: number }).timeoutMs).toBeUndefined();
});
it("POST /tabs/open?profile=unknown returns 404", async () => {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
const realFetch = getBrowserTestFetch();
const result = await realFetch(`${base}/tabs/open?profile=unknown`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://example.com" }),
});
expect(result.status).toBe(404);
const body = (await result.json()) as { error: string };
expect(body.error).toContain("not found");
});
it("POST /tabs/open returns 400 for invalid URLs", async () => {
setBrowserControlServerReachable(true);
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
const realFetch = getBrowserTestFetch();
const result = await realFetch(`${base}/tabs/open`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: "not a url" }),
});
expect(result.status).toBe(400);
const body = (await result.json()) as { error: string };
expect(body.error).toContain("Invalid URL:");
});
});
describe("profile CRUD endpoints", () => {
beforeEach(async () => {
await resetBrowserControlServerTestContext();
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
const u = url;
if (u.includes("/json/list")) {
return makeResponse([]);
}
return makeResponse({}, { ok: false, status: 500, text: "unexpected" });
}),
);
});
afterEach(async () => {
await cleanupBrowserControlServerTestContext();
});
it("validates profile create/delete endpoints", async () => {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
const realFetch = getBrowserTestFetch();
const createMissingName = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
expect(createMissingName.status).toBe(400);
const createMissingNameBody = (await createMissingName.json()) as { error: string };
expect(createMissingNameBody.error).toContain("name is required");
const createInvalidName = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Invalid Name!" }),
});
expect(createInvalidName.status).toBe(400);
const createInvalidNameBody = (await createInvalidName.json()) as { error: string };
expect(createInvalidNameBody.error).toContain("invalid profile name");
const createDuplicate = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "openclaw" }),
});
expect(createDuplicate.status).toBe(409);
const createDuplicateBody = (await createDuplicate.json()) as { error: string };
expect(createDuplicateBody.error).toContain("already exists");
const createRemote = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "remote", cdpUrl: "http://10.0.0.42:9222" }),
});
expect(createRemote.status).toBe(200);
const createRemoteBody = (await createRemote.json()) as {
profile?: string;
cdpUrl?: string;
isRemote?: boolean;
};
expect(createRemoteBody.profile).toBe("remote");
expect(createRemoteBody.cdpUrl).toBe("http://10.0.0.42:9222");
expect(createRemoteBody.isRemote).toBe(true);
const createBadRemote = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "badremote", cdpUrl: "ftp://bad" }),
});
expect(createBadRemote.status).toBe(400);
const createBadRemoteBody = (await createBadRemote.json()) as { error: string };
expect(createBadRemoteBody.error).toContain("cdpUrl");
const createClawd = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "legacyclawd", driver: "clawd" }),
});
expect(createClawd.status).toBe(200);
const createClawdBody = (await createClawd.json()) as {
profile?: string;
transport?: string;
cdpPort?: number | null;
userDataDir?: string | null;
};
expect(createClawdBody.profile).toBe("legacyclawd");
expect(createClawdBody.transport).toBe("cdp");
expect(createClawdBody.cdpPort).toBeTypeOf("number");
expect(createClawdBody.userDataDir).toBeNull();
const explicitUserDataDir = "/tmp/openclaw-brave-profile";
await fs.promises.mkdir(explicitUserDataDir, { recursive: true });
const createExistingSession = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "brave-live",
driver: "existing-session",
userDataDir: explicitUserDataDir,
}),
});
expect(createExistingSession.status).toBe(200);
const createExistingSessionBody = (await createExistingSession.json()) as {
profile?: string;
transport?: string;
userDataDir?: string | null;
};
expect(createExistingSessionBody.profile).toBe("brave-live");
expect(createExistingSessionBody.transport).toBe("chrome-mcp");
expect(createExistingSessionBody.userDataDir).toBe(explicitUserDataDir);
const createBadExistingSession = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "bad-live",
userDataDir: explicitUserDataDir,
}),
});
expect(createBadExistingSession.status).toBe(400);
const createBadExistingSessionBody = (await createBadExistingSession.json()) as {
error: string;
};
expect(createBadExistingSessionBody.error).toContain("driver=existing-session is required");
const createLegacyDriver = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "legacy", driver: "extension" }),
});
expect(createLegacyDriver.status).toBe(400);
const createLegacyDriverBody = (await createLegacyDriver.json()) as { error: string };
expect(createLegacyDriverBody.error).toContain('unsupported profile driver "extension"');
const deleteMissing = await realFetch(`${base}/profiles/nonexistent`, {
method: "DELETE",
});
expect(deleteMissing.status).toBe(404);
const deleteMissingBody = (await deleteMissing.json()) as { error: string };
expect(deleteMissingBody.error).toContain("not found");
const deleteDefault = await realFetch(`${base}/profiles/openclaw`, {
method: "DELETE",
});
expect(deleteDefault.status).toBe(400);
const deleteDefaultBody = (await deleteDefault.json()) as { error: string };
expect(deleteDefaultBody.error).toContain("cannot delete the default profile");
const deleteInvalid = await realFetch(`${base}/profiles/Invalid-Name!`, {
method: "DELETE",
});
expect(deleteInvalid.status).toBe(400);
const deleteInvalidBody = (await deleteInvalid.json()) as { error: string };
expect(deleteInvalidBody.error).toContain("invalid profile name");
});
});

View File

@@ -1,217 +0,0 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants.js";
import {
installAgentContractHooks,
postJson,
startServerAndBase,
} from "./server.agent-contract.test-harness.js";
import {
getBrowserControlServerTestState,
getCdpMocks,
getPwMocks,
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch } from "./test-fetch.js";
const state = getBrowserControlServerTestState();
const cdpMocks = getCdpMocks();
const pwMocks = getPwMocks();
describe("browser control server", () => {
installAgentContractHooks();
it("agent contract: snapshot endpoints", async () => {
const base = await startServerAndBase();
const realFetch = getBrowserTestFetch();
const snapAria = (await realFetch(`${base}/snapshot?format=aria&limit=1`).then((r) =>
r.json(),
)) as { ok: boolean; format?: string };
expect(snapAria.ok).toBe(true);
expect(snapAria.format).toBe("aria");
expect(cdpMocks.snapshotAria).toHaveBeenCalledWith({
wsUrl: "ws://127.0.0.1/devtools/page/abcd1234",
limit: 1,
});
const snapAi = (await realFetch(`${base}/snapshot?format=ai`).then((r) => r.json())) as {
ok: boolean;
format?: string;
};
expect(snapAi.ok).toBe(true);
expect(snapAi.format).toBe("ai");
expect(pwMocks.snapshotAiViaPlaywright).toHaveBeenCalledWith({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS,
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
});
const snapAiZero = (await realFetch(`${base}/snapshot?format=ai&maxChars=0`).then((r) =>
r.json(),
)) as { ok: boolean; format?: string };
expect(snapAiZero.ok).toBe(true);
expect(snapAiZero.format).toBe("ai");
const [lastCall] = pwMocks.snapshotAiViaPlaywright.mock.calls.at(-1) ?? [];
expect(lastCall).toEqual({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
});
});
it("agent contract: navigation + common act commands", async () => {
const base = await startServerAndBase();
const realFetch = getBrowserTestFetch();
const nav = await postJson<{ ok: boolean; targetId?: string }>(`${base}/navigate`, {
url: "https://example.com",
});
expect(nav.ok).toBe(true);
expect(typeof nav.targetId).toBe("string");
expect(pwMocks.navigateViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
url: "https://example.com",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const click = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "click",
ref: "1",
button: "left",
modifiers: ["Shift"],
});
expect(click.ok).toBe(true);
expect(pwMocks.clickViaPlaywright).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "1",
button: "left",
modifiers: ["Shift"],
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [clickArgs] = pwMocks.clickViaPlaywright.mock.calls[0] ?? [];
expect((clickArgs as { doubleClick?: boolean }).doubleClick).toBeUndefined();
const clickSelector = await realFetch(`${base}/act`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind: "click", selector: "button.save" }),
});
expect(clickSelector.status).toBe(200);
expect(((await clickSelector.json()) as { ok?: boolean }).ok).toBe(true);
expect(pwMocks.clickViaPlaywright).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
selector: "button.save",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [clickSelectorArgs] = pwMocks.clickViaPlaywright.mock.calls[1] ?? [];
expect((clickSelectorArgs as { doubleClick?: boolean }).doubleClick).toBeUndefined();
const type = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "type",
ref: "1",
text: "",
});
expect(type.ok).toBe(true);
expect(pwMocks.typeViaPlaywright).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "1",
text: "",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [typeArgs] = pwMocks.typeViaPlaywright.mock.calls[0] ?? [];
expect((typeArgs as { submit?: boolean }).submit).toBeUndefined();
expect((typeArgs as { slowly?: boolean }).slowly).toBeUndefined();
const press = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "press",
key: "Enter",
});
expect(press.ok).toBe(true);
expect(pwMocks.pressKeyViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
key: "Enter",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
}),
);
const [pressArgs] = pwMocks.pressKeyViaPlaywright.mock.calls[0] ?? [];
expect((pressArgs as { delayMs?: number }).delayMs).toBeUndefined();
const hover = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "hover",
ref: "2",
});
expect(hover.ok).toBe(true);
expect(pwMocks.hoverViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "2",
}),
);
const [hoverArgs] = pwMocks.hoverViaPlaywright.mock.calls[0] ?? [];
expect((hoverArgs as { timeoutMs?: number }).timeoutMs).toBeUndefined();
const scroll = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "scrollIntoView",
ref: "2",
});
expect(scroll.ok).toBe(true);
expect(pwMocks.scrollIntoViewViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
ref: "2",
}),
);
const [scrollArgs] = pwMocks.scrollIntoViewViaPlaywright.mock.calls[0] ?? [];
expect((scrollArgs as { timeoutMs?: number }).timeoutMs).toBeUndefined();
const drag = await postJson<{ ok: boolean }>(`${base}/act`, {
kind: "drag",
startRef: "3",
endRef: "4",
});
expect(drag.ok).toBe(true);
expect(pwMocks.dragViaPlaywright).toHaveBeenCalledWith(
expect.objectContaining({
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
startRef: "3",
endRef: "4",
}),
);
const [dragArgs] = pwMocks.dragViaPlaywright.mock.calls[0] ?? [];
expect((dragArgs as { timeoutMs?: number }).timeoutMs).toBeUndefined();
});
});

View File

@@ -1,207 +0,0 @@
import fs from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
cleanupBrowserControlServerTestContext,
getBrowserControlServerBaseUrl,
installBrowserControlServerHooks,
makeResponse,
resetBrowserControlServerTestContext,
setBrowserControlServerReachable,
startBrowserControlServerFromConfig,
} from "./server.control-server.test-harness.js";
import { getBrowserTestFetch } from "./test-fetch.js";
describe("browser control server", () => {
installBrowserControlServerHooks();
it("POST /tabs/open?profile=unknown returns 404", async () => {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
const realFetch = getBrowserTestFetch();
const result = await realFetch(`${base}/tabs/open?profile=unknown`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://example.com" }),
});
expect(result.status).toBe(404);
const body = (await result.json()) as { error: string };
expect(body.error).toContain("not found");
});
it("POST /tabs/open returns 400 for invalid URLs", async () => {
setBrowserControlServerReachable(true);
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
const realFetch = getBrowserTestFetch();
const result = await realFetch(`${base}/tabs/open`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: "not a url" }),
});
expect(result.status).toBe(400);
const body = (await result.json()) as { error: string };
expect(body.error).toContain("Invalid URL:");
});
});
describe("profile CRUD endpoints", () => {
beforeEach(async () => {
await resetBrowserControlServerTestContext();
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
const u = url;
if (u.includes("/json/list")) {
return makeResponse([]);
}
return makeResponse({}, { ok: false, status: 500, text: "unexpected" });
}),
);
});
afterEach(async () => {
await cleanupBrowserControlServerTestContext();
});
it("validates profile create/delete endpoints", async () => {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
const realFetch = getBrowserTestFetch();
const createMissingName = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
expect(createMissingName.status).toBe(400);
const createMissingNameBody = (await createMissingName.json()) as { error: string };
expect(createMissingNameBody.error).toContain("name is required");
const createInvalidName = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Invalid Name!" }),
});
expect(createInvalidName.status).toBe(400);
const createInvalidNameBody = (await createInvalidName.json()) as { error: string };
expect(createInvalidNameBody.error).toContain("invalid profile name");
const createDuplicate = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "openclaw" }),
});
expect(createDuplicate.status).toBe(409);
const createDuplicateBody = (await createDuplicate.json()) as { error: string };
expect(createDuplicateBody.error).toContain("already exists");
const createRemote = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "remote", cdpUrl: "http://10.0.0.42:9222" }),
});
expect(createRemote.status).toBe(200);
const createRemoteBody = (await createRemote.json()) as {
profile?: string;
cdpUrl?: string;
isRemote?: boolean;
};
expect(createRemoteBody.profile).toBe("remote");
expect(createRemoteBody.cdpUrl).toBe("http://10.0.0.42:9222");
expect(createRemoteBody.isRemote).toBe(true);
const createBadRemote = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "badremote", cdpUrl: "ftp://bad" }),
});
expect(createBadRemote.status).toBe(400);
const createBadRemoteBody = (await createBadRemote.json()) as { error: string };
expect(createBadRemoteBody.error).toContain("cdpUrl");
const createClawd = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "legacyclawd", driver: "clawd" }),
});
expect(createClawd.status).toBe(200);
const createClawdBody = (await createClawd.json()) as {
profile?: string;
transport?: string;
cdpPort?: number | null;
userDataDir?: string | null;
};
expect(createClawdBody.profile).toBe("legacyclawd");
expect(createClawdBody.transport).toBe("cdp");
expect(createClawdBody.cdpPort).toBeTypeOf("number");
expect(createClawdBody.userDataDir).toBeNull();
const explicitUserDataDir = "/tmp/openclaw-brave-profile";
await fs.promises.mkdir(explicitUserDataDir, { recursive: true });
const createExistingSession = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "brave-live",
driver: "existing-session",
userDataDir: explicitUserDataDir,
}),
});
expect(createExistingSession.status).toBe(200);
const createExistingSessionBody = (await createExistingSession.json()) as {
profile?: string;
transport?: string;
userDataDir?: string | null;
};
expect(createExistingSessionBody.profile).toBe("brave-live");
expect(createExistingSessionBody.transport).toBe("chrome-mcp");
expect(createExistingSessionBody.userDataDir).toBe(explicitUserDataDir);
const createBadExistingSession = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "bad-live",
userDataDir: explicitUserDataDir,
}),
});
expect(createBadExistingSession.status).toBe(400);
const createBadExistingSessionBody = (await createBadExistingSession.json()) as {
error: string;
};
expect(createBadExistingSessionBody.error).toContain("driver=existing-session is required");
const createLegacyDriver = await realFetch(`${base}/profiles/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "legacy", driver: "extension" }),
});
expect(createLegacyDriver.status).toBe(400);
const createLegacyDriverBody = (await createLegacyDriver.json()) as { error: string };
expect(createLegacyDriverBody.error).toContain('unsupported profile driver "extension"');
const deleteMissing = await realFetch(`${base}/profiles/nonexistent`, {
method: "DELETE",
});
expect(deleteMissing.status).toBe(404);
const deleteMissingBody = (await deleteMissing.json()) as { error: string };
expect(deleteMissingBody.error).toContain("not found");
const deleteDefault = await realFetch(`${base}/profiles/openclaw`, {
method: "DELETE",
});
expect(deleteDefault.status).toBe(400);
const deleteDefaultBody = (await deleteDefault.json()) as { error: string };
expect(deleteDefaultBody.error).toContain("cannot delete the default profile");
const deleteInvalid = await realFetch(`${base}/profiles/Invalid-Name!`, {
method: "DELETE",
});
expect(deleteInvalid.status).toBe(400);
const deleteInvalidBody = (await deleteInvalid.json()) as { error: string };
expect(deleteInvalidBody.error).toContain("invalid profile name");
});
});