mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 01:41:22 +00:00
refactor(channels): dedupe transport and gateway test scaffolds
This commit is contained in:
@@ -8,6 +8,12 @@ describe("cdp", () => {
|
||||
let httpServer: ReturnType<typeof createServer> | null = null;
|
||||
let wsServer: WebSocketServer | null = null;
|
||||
|
||||
const startWsServer = async () => {
|
||||
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
|
||||
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
|
||||
return (wsServer.address() as { port: number }).port;
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (!httpServer) {
|
||||
@@ -26,9 +32,7 @@ describe("cdp", () => {
|
||||
});
|
||||
|
||||
it("creates a target via the browser websocket", async () => {
|
||||
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
|
||||
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
|
||||
const wsPort = (wsServer.address() as { port: number }).port;
|
||||
const wsPort = await startWsServer();
|
||||
|
||||
wsServer.on("connection", (socket) => {
|
||||
socket.on("message", (data) => {
|
||||
@@ -75,9 +79,7 @@ describe("cdp", () => {
|
||||
});
|
||||
|
||||
it("evaluates javascript via CDP", async () => {
|
||||
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
|
||||
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
|
||||
const wsPort = (wsServer.address() as { port: number }).port;
|
||||
const wsPort = await startWsServer();
|
||||
|
||||
wsServer.on("connection", (socket) => {
|
||||
socket.on("message", (data) => {
|
||||
@@ -112,9 +114,7 @@ describe("cdp", () => {
|
||||
});
|
||||
|
||||
it("captures an aria snapshot via CDP", async () => {
|
||||
wsServer = new WebSocketServer({ port: 0, host: "127.0.0.1" });
|
||||
await new Promise<void>((resolve) => wsServer?.once("listening", resolve));
|
||||
const wsPort = (wsServer.address() as { port: number }).port;
|
||||
const wsPort = await startWsServer();
|
||||
|
||||
wsServer.on("connection", (socket) => {
|
||||
socket.on("message", (data) => {
|
||||
|
||||
@@ -7,21 +7,30 @@ import type {
|
||||
import { buildProfileQuery, withBaseUrl } from "./client-actions-url.js";
|
||||
import { fetchBrowserJson } from "./client-fetch.js";
|
||||
|
||||
function buildQuerySuffix(params: Array<[string, string | boolean | undefined]>): string {
|
||||
const query = new URLSearchParams();
|
||||
for (const [key, value] of params) {
|
||||
if (typeof value === "boolean") {
|
||||
query.set(key, String(value));
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
query.set(key, value);
|
||||
}
|
||||
}
|
||||
const encoded = query.toString();
|
||||
return encoded.length > 0 ? `?${encoded}` : "";
|
||||
}
|
||||
|
||||
export async function browserConsoleMessages(
|
||||
baseUrl: string | undefined,
|
||||
opts: { level?: string; targetId?: string; profile?: string } = {},
|
||||
): Promise<{ ok: true; messages: BrowserConsoleMessage[]; targetId: string }> {
|
||||
const q = new URLSearchParams();
|
||||
if (opts.level) {
|
||||
q.set("level", opts.level);
|
||||
}
|
||||
if (opts.targetId) {
|
||||
q.set("targetId", opts.targetId);
|
||||
}
|
||||
if (opts.profile) {
|
||||
q.set("profile", opts.profile);
|
||||
}
|
||||
const suffix = q.toString() ? `?${q.toString()}` : "";
|
||||
const suffix = buildQuerySuffix([
|
||||
["level", opts.level],
|
||||
["targetId", opts.targetId],
|
||||
["profile", opts.profile],
|
||||
]);
|
||||
return await fetchBrowserJson<{
|
||||
ok: true;
|
||||
messages: BrowserConsoleMessage[];
|
||||
@@ -46,17 +55,11 @@ export async function browserPageErrors(
|
||||
baseUrl: string | undefined,
|
||||
opts: { targetId?: string; clear?: boolean; profile?: string } = {},
|
||||
): Promise<{ ok: true; targetId: string; errors: BrowserPageError[] }> {
|
||||
const q = new URLSearchParams();
|
||||
if (opts.targetId) {
|
||||
q.set("targetId", opts.targetId);
|
||||
}
|
||||
if (typeof opts.clear === "boolean") {
|
||||
q.set("clear", String(opts.clear));
|
||||
}
|
||||
if (opts.profile) {
|
||||
q.set("profile", opts.profile);
|
||||
}
|
||||
const suffix = q.toString() ? `?${q.toString()}` : "";
|
||||
const suffix = buildQuerySuffix([
|
||||
["targetId", opts.targetId],
|
||||
["clear", typeof opts.clear === "boolean" ? opts.clear : undefined],
|
||||
["profile", opts.profile],
|
||||
]);
|
||||
return await fetchBrowserJson<{
|
||||
ok: true;
|
||||
targetId: string;
|
||||
@@ -73,20 +76,12 @@ export async function browserRequests(
|
||||
profile?: string;
|
||||
} = {},
|
||||
): Promise<{ ok: true; targetId: string; requests: BrowserNetworkRequest[] }> {
|
||||
const q = new URLSearchParams();
|
||||
if (opts.targetId) {
|
||||
q.set("targetId", opts.targetId);
|
||||
}
|
||||
if (opts.filter) {
|
||||
q.set("filter", opts.filter);
|
||||
}
|
||||
if (typeof opts.clear === "boolean") {
|
||||
q.set("clear", String(opts.clear));
|
||||
}
|
||||
if (opts.profile) {
|
||||
q.set("profile", opts.profile);
|
||||
}
|
||||
const suffix = q.toString() ? `?${q.toString()}` : "";
|
||||
const suffix = buildQuerySuffix([
|
||||
["targetId", opts.targetId],
|
||||
["filter", opts.filter],
|
||||
["clear", typeof opts.clear === "boolean" ? opts.clear : undefined],
|
||||
["profile", opts.profile],
|
||||
]);
|
||||
return await fetchBrowserJson<{
|
||||
ok: true;
|
||||
targetId: string;
|
||||
|
||||
@@ -11,6 +11,25 @@ import {
|
||||
import { browserOpenTab, browserSnapshot, browserStatus, browserTabs } from "./client.js";
|
||||
|
||||
describe("browser client", () => {
|
||||
function stubSnapshotFetch(calls: string[]) {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
calls.push(url);
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
format: "ai",
|
||||
targetId: "t1",
|
||||
url: "https://x",
|
||||
snapshot: "ok",
|
||||
}),
|
||||
} as unknown as Response;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
@@ -50,22 +69,7 @@ describe("browser client", () => {
|
||||
|
||||
it("adds labels + efficient mode query params to snapshots", async () => {
|
||||
const calls: string[] = [];
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
calls.push(url);
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
format: "ai",
|
||||
targetId: "t1",
|
||||
url: "https://x",
|
||||
snapshot: "ok",
|
||||
}),
|
||||
} as unknown as Response;
|
||||
}),
|
||||
);
|
||||
stubSnapshotFetch(calls);
|
||||
|
||||
await expect(
|
||||
browserSnapshot("http://127.0.0.1:18791", {
|
||||
@@ -84,22 +88,7 @@ describe("browser client", () => {
|
||||
|
||||
it("adds refs=aria to snapshots when requested", async () => {
|
||||
const calls: string[] = [];
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
calls.push(url);
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
format: "ai",
|
||||
targetId: "t1",
|
||||
url: "https://x",
|
||||
snapshot: "ok",
|
||||
}),
|
||||
} as unknown as Response;
|
||||
}),
|
||||
);
|
||||
stubSnapshotFetch(calls);
|
||||
|
||||
await browserSnapshot("http://127.0.0.1:18791", {
|
||||
format: "ai",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { createServer } from "node:http";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import WebSocket from "ws";
|
||||
import {
|
||||
@@ -7,22 +5,7 @@ import {
|
||||
getChromeExtensionRelayAuthHeaders,
|
||||
stopChromeExtensionRelayServer,
|
||||
} from "./extension-relay.js";
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
while (true) {
|
||||
const port = await new Promise<number>((resolve, reject) => {
|
||||
const s = createServer();
|
||||
s.once("error", reject);
|
||||
s.listen(0, "127.0.0.1", () => {
|
||||
const assigned = (s.address() as AddressInfo).port;
|
||||
s.close((err) => (err ? reject(err) : resolve(assigned)));
|
||||
});
|
||||
});
|
||||
if (port < 65535) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { getFreePort } from "./test-port.js";
|
||||
|
||||
function waitForOpen(ws: WebSocket) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
@@ -23,6 +23,38 @@ describe("pw-tools-core", () => {
|
||||
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw");
|
||||
});
|
||||
|
||||
async function waitForImplicitDownloadOutput(params: {
|
||||
downloadUrl: string;
|
||||
suggestedFilename: string;
|
||||
}) {
|
||||
let downloadHandler: ((download: unknown) => void) | undefined;
|
||||
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
|
||||
if (event === "download") {
|
||||
downloadHandler = handler;
|
||||
}
|
||||
});
|
||||
const off = vi.fn();
|
||||
const saveAs = vi.fn(async () => {});
|
||||
setPwToolsCoreCurrentPage({ on, off });
|
||||
|
||||
const p = mod.waitForDownloadViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
downloadHandler?.({
|
||||
url: () => params.downloadUrl,
|
||||
suggestedFilename: () => params.suggestedFilename,
|
||||
saveAs,
|
||||
});
|
||||
|
||||
const res = await p;
|
||||
const outPath = vi.mocked(saveAs).mock.calls[0]?.[0];
|
||||
return { res, outPath };
|
||||
}
|
||||
|
||||
it("waits for the next download and saves it", async () => {
|
||||
let downloadHandler: ((download: unknown) => void) | undefined;
|
||||
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
|
||||
@@ -98,35 +130,11 @@ describe("pw-tools-core", () => {
|
||||
expect(res.path).toBe(targetPath);
|
||||
});
|
||||
it("uses preferred tmp dir when waiting for download without explicit path", async () => {
|
||||
let downloadHandler: ((download: unknown) => void) | undefined;
|
||||
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
|
||||
if (event === "download") {
|
||||
downloadHandler = handler;
|
||||
}
|
||||
});
|
||||
const off = vi.fn();
|
||||
|
||||
const saveAs = vi.fn(async () => {});
|
||||
const download = {
|
||||
url: () => "https://example.com/file.bin",
|
||||
suggestedFilename: () => "file.bin",
|
||||
saveAs,
|
||||
};
|
||||
|
||||
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
|
||||
setPwToolsCoreCurrentPage({ on, off });
|
||||
|
||||
const p = mod.waitForDownloadViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
timeoutMs: 1000,
|
||||
const { res, outPath } = await waitForImplicitDownloadOutput({
|
||||
downloadUrl: "https://example.com/file.bin",
|
||||
suggestedFilename: "file.bin",
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
downloadHandler?.(download);
|
||||
|
||||
const res = await p;
|
||||
const outPath = vi.mocked(saveAs).mock.calls[0]?.[0];
|
||||
expect(typeof outPath).toBe("string");
|
||||
const expectedRootedDownloadsDir = path.join(
|
||||
path.sep,
|
||||
@@ -142,35 +150,11 @@ describe("pw-tools-core", () => {
|
||||
});
|
||||
|
||||
it("sanitizes suggested download filenames to prevent traversal escapes", async () => {
|
||||
let downloadHandler: ((download: unknown) => void) | undefined;
|
||||
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
|
||||
if (event === "download") {
|
||||
downloadHandler = handler;
|
||||
}
|
||||
});
|
||||
const off = vi.fn();
|
||||
|
||||
const saveAs = vi.fn(async () => {});
|
||||
const download = {
|
||||
url: () => "https://example.com/evil",
|
||||
suggestedFilename: () => "../../../../etc/passwd",
|
||||
saveAs,
|
||||
};
|
||||
|
||||
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
|
||||
setPwToolsCoreCurrentPage({ on, off });
|
||||
|
||||
const p = mod.waitForDownloadViaPlaywright({
|
||||
cdpUrl: "http://127.0.0.1:18792",
|
||||
targetId: "T1",
|
||||
timeoutMs: 1000,
|
||||
const { res, outPath } = await waitForImplicitDownloadOutput({
|
||||
downloadUrl: "https://example.com/evil",
|
||||
suggestedFilename: "../../../../etc/passwd",
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
downloadHandler?.(download);
|
||||
|
||||
const res = await p;
|
||||
const outPath = vi.mocked(saveAs).mock.calls[0]?.[0];
|
||||
expect(typeof outPath).toBe("string");
|
||||
expect(path.dirname(String(outPath))).toBe(
|
||||
path.join(path.sep, "tmp", "openclaw-preferred", "downloads"),
|
||||
|
||||
@@ -3,6 +3,23 @@ import type { BrowserRouteRegistrar } from "./types.js";
|
||||
import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js";
|
||||
import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
|
||||
|
||||
type StorageKind = "local" | "session";
|
||||
|
||||
function resolveBodyTargetId(body: unknown): string | undefined {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||
return undefined;
|
||||
}
|
||||
const targetId = toStringOrEmpty((body as Record<string, unknown>).targetId);
|
||||
return targetId || undefined;
|
||||
}
|
||||
|
||||
function parseStorageKind(raw: string): StorageKind | null {
|
||||
if (raw === "local" || raw === "session") {
|
||||
return raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function registerBrowserAgentStorageRoutes(
|
||||
app: BrowserRouteRegistrar,
|
||||
ctx: BrowserRouteContext,
|
||||
@@ -35,7 +52,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const cookie =
|
||||
body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie)
|
||||
? (body.cookie as Record<string, unknown>)
|
||||
@@ -79,7 +96,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
try {
|
||||
const tab = await profileCtx.ensureTabAvailable(targetId);
|
||||
const pw = await requirePwAi(res, "cookies clear");
|
||||
@@ -101,8 +118,8 @@ export function registerBrowserAgentStorageRoutes(
|
||||
if (!profileCtx) {
|
||||
return;
|
||||
}
|
||||
const kind = toStringOrEmpty(req.params.kind);
|
||||
if (kind !== "local" && kind !== "session") {
|
||||
const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
|
||||
if (!kind) {
|
||||
return jsonError(res, 400, "kind must be local|session");
|
||||
}
|
||||
const targetId = typeof req.query.targetId === "string" ? req.query.targetId.trim() : "";
|
||||
@@ -130,12 +147,12 @@ export function registerBrowserAgentStorageRoutes(
|
||||
if (!profileCtx) {
|
||||
return;
|
||||
}
|
||||
const kind = toStringOrEmpty(req.params.kind);
|
||||
if (kind !== "local" && kind !== "session") {
|
||||
const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
|
||||
if (!kind) {
|
||||
return jsonError(res, 400, "kind must be local|session");
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const key = toStringOrEmpty(body.key);
|
||||
if (!key) {
|
||||
return jsonError(res, 400, "key is required");
|
||||
@@ -165,12 +182,12 @@ export function registerBrowserAgentStorageRoutes(
|
||||
if (!profileCtx) {
|
||||
return;
|
||||
}
|
||||
const kind = toStringOrEmpty(req.params.kind);
|
||||
if (kind !== "local" && kind !== "session") {
|
||||
const kind = parseStorageKind(toStringOrEmpty(req.params.kind));
|
||||
if (!kind) {
|
||||
return jsonError(res, 400, "kind must be local|session");
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
try {
|
||||
const tab = await profileCtx.ensureTabAvailable(targetId);
|
||||
const pw = await requirePwAi(res, "storage clear");
|
||||
@@ -194,7 +211,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const offline = toBoolean(body.offline);
|
||||
if (offline === undefined) {
|
||||
return jsonError(res, 400, "offline is required");
|
||||
@@ -222,7 +239,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const headers =
|
||||
body.headers && typeof body.headers === "object" && !Array.isArray(body.headers)
|
||||
? (body.headers as Record<string, unknown>)
|
||||
@@ -259,7 +276,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const clear = toBoolean(body.clear) ?? false;
|
||||
const username = toStringOrEmpty(body.username) || undefined;
|
||||
const password = typeof body.password === "string" ? body.password : undefined;
|
||||
@@ -288,7 +305,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const clear = toBoolean(body.clear) ?? false;
|
||||
const latitude = toNumber(body.latitude);
|
||||
const longitude = toNumber(body.longitude);
|
||||
@@ -321,7 +338,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const schemeRaw = toStringOrEmpty(body.colorScheme);
|
||||
const colorScheme =
|
||||
schemeRaw === "dark" || schemeRaw === "light" || schemeRaw === "no-preference"
|
||||
@@ -355,7 +372,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const timezoneId = toStringOrEmpty(body.timezoneId);
|
||||
if (!timezoneId) {
|
||||
return jsonError(res, 400, "timezoneId is required");
|
||||
@@ -383,7 +400,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const locale = toStringOrEmpty(body.locale);
|
||||
if (!locale) {
|
||||
return jsonError(res, 400, "locale is required");
|
||||
@@ -411,7 +428,7 @@ export function registerBrowserAgentStorageRoutes(
|
||||
return;
|
||||
}
|
||||
const body = readBody(req);
|
||||
const targetId = toStringOrEmpty(body.targetId) || undefined;
|
||||
const targetId = resolveBodyTargetId(body);
|
||||
const name = toStringOrEmpty(body.name);
|
||||
if (!name) {
|
||||
return jsonError(res, 400, "name is required");
|
||||
|
||||
24
src/browser/server-context.chrome-test-harness.ts
Normal file
24
src/browser/server-context.chrome-test-harness.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, vi } from "vitest";
|
||||
|
||||
const chromeUserDataDir = { dir: "/tmp/openclaw" };
|
||||
|
||||
beforeAll(async () => {
|
||||
chromeUserDataDir.dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-user-data-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(chromeUserDataDir.dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
vi.mock("./chrome.js", () => ({
|
||||
isChromeCdpReady: vi.fn(async () => true),
|
||||
isChromeReachable: vi.fn(async () => true),
|
||||
launchOpenClawChrome: vi.fn(async () => {
|
||||
throw new Error("unexpected launch");
|
||||
}),
|
||||
resolveOpenClawUserDataDir: vi.fn(() => chromeUserDataDir.dir),
|
||||
stopOpenClawChrome: vi.fn(async () => {}),
|
||||
}));
|
||||
@@ -1,30 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { BrowserServerState } from "./server-context.js";
|
||||
import "./server-context.chrome-test-harness.js";
|
||||
import { createBrowserRouteContext } from "./server-context.js";
|
||||
|
||||
const chromeUserDataDir = vi.hoisted(() => ({ dir: "/tmp/openclaw" }));
|
||||
|
||||
beforeAll(async () => {
|
||||
chromeUserDataDir.dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-user-data-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(chromeUserDataDir.dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
vi.mock("./chrome.js", () => ({
|
||||
isChromeCdpReady: vi.fn(async () => true),
|
||||
isChromeReachable: vi.fn(async () => true),
|
||||
launchOpenClawChrome: vi.fn(async () => {
|
||||
throw new Error("unexpected launch");
|
||||
}),
|
||||
resolveOpenClawUserDataDir: vi.fn(() => chromeUserDataDir.dir),
|
||||
stopOpenClawChrome: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
function makeBrowserState(): BrowserServerState {
|
||||
return {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
|
||||
@@ -1,32 +1,10 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BrowserServerState } from "./server-context.js";
|
||||
import * as cdpModule from "./cdp.js";
|
||||
import * as pwAiModule from "./pw-ai-module.js";
|
||||
import "./server-context.chrome-test-harness.js";
|
||||
import { createBrowserRouteContext } from "./server-context.js";
|
||||
|
||||
const chromeUserDataDir = vi.hoisted(() => ({ dir: "/tmp/openclaw" }));
|
||||
|
||||
beforeAll(async () => {
|
||||
chromeUserDataDir.dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-user-data-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(chromeUserDataDir.dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
vi.mock("./chrome.js", () => ({
|
||||
isChromeCdpReady: vi.fn(async () => true),
|
||||
isChromeReachable: vi.fn(async () => true),
|
||||
launchOpenClawChrome: vi.fn(async () => {
|
||||
throw new Error("unexpected launch");
|
||||
}),
|
||||
resolveOpenClawUserDataDir: vi.fn(() => chromeUserDataDir.dir),
|
||||
stopOpenClawChrome: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -3,35 +3,21 @@ import { fetch as realFetch } from "undici";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_UPLOAD_DIR } from "./paths.js";
|
||||
import {
|
||||
getBrowserControlServerBaseUrl,
|
||||
installAgentContractHooks,
|
||||
postJson,
|
||||
startServerAndBase,
|
||||
} from "./server.agent-contract.test-harness.js";
|
||||
import {
|
||||
getBrowserControlServerTestState,
|
||||
getPwMocks,
|
||||
installBrowserControlServerHooks,
|
||||
setBrowserControlServerEvaluateEnabled,
|
||||
startBrowserControlServerFromConfig,
|
||||
} from "./server.control-server.test-harness.js";
|
||||
|
||||
const state = getBrowserControlServerTestState();
|
||||
const pwMocks = getPwMocks();
|
||||
|
||||
describe("browser control server", () => {
|
||||
installBrowserControlServerHooks();
|
||||
|
||||
const startServerAndBase = async () => {
|
||||
await startBrowserControlServerFromConfig();
|
||||
const base = getBrowserControlServerBaseUrl();
|
||||
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
|
||||
return base;
|
||||
};
|
||||
|
||||
const postJson = async <T>(url: string, body?: unknown): Promise<T> => {
|
||||
const res = await realFetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
return (await res.json()) as T;
|
||||
};
|
||||
installAgentContractHooks();
|
||||
|
||||
const slowTimeoutMs = process.platform === "win32" ? 40_000 : 20_000;
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ import { fetch as realFetch } from "undici";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants.js";
|
||||
import {
|
||||
getBrowserControlServerBaseUrl,
|
||||
installAgentContractHooks,
|
||||
postJson,
|
||||
startServerAndBase,
|
||||
} from "./server.agent-contract.test-harness.js";
|
||||
import {
|
||||
getBrowserControlServerTestState,
|
||||
getCdpMocks,
|
||||
getPwMocks,
|
||||
installBrowserControlServerHooks,
|
||||
startBrowserControlServerFromConfig,
|
||||
} from "./server.control-server.test-harness.js";
|
||||
|
||||
const state = getBrowserControlServerTestState();
|
||||
@@ -15,23 +17,7 @@ const cdpMocks = getCdpMocks();
|
||||
const pwMocks = getPwMocks();
|
||||
|
||||
describe("browser control server", () => {
|
||||
installBrowserControlServerHooks();
|
||||
|
||||
const startServerAndBase = async () => {
|
||||
await startBrowserControlServerFromConfig();
|
||||
const base = getBrowserControlServerBaseUrl();
|
||||
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
|
||||
return base;
|
||||
};
|
||||
|
||||
const postJson = async <T>(url: string, body?: unknown): Promise<T> => {
|
||||
const res = await realFetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
return (await res.json()) as T;
|
||||
};
|
||||
installAgentContractHooks();
|
||||
|
||||
it("agent contract: snapshot endpoints", async () => {
|
||||
const base = await startServerAndBase();
|
||||
|
||||
26
src/browser/server.agent-contract.test-harness.ts
Normal file
26
src/browser/server.agent-contract.test-harness.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { fetch as realFetch } from "undici";
|
||||
import {
|
||||
getBrowserControlServerBaseUrl,
|
||||
installBrowserControlServerHooks,
|
||||
startBrowserControlServerFromConfig,
|
||||
} from "./server.control-server.test-harness.js";
|
||||
|
||||
export function installAgentContractHooks() {
|
||||
installBrowserControlServerHooks();
|
||||
}
|
||||
|
||||
export async function startServerAndBase(): Promise<string> {
|
||||
await startBrowserControlServerFromConfig();
|
||||
const base = getBrowserControlServerBaseUrl();
|
||||
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
|
||||
return base;
|
||||
}
|
||||
|
||||
export async function postJson<T>(url: string, body?: unknown): Promise<T> {
|
||||
const res = await realFetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { type AddressInfo, createServer } from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
|
||||
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
||||
import { getFreePort } from "./test-port.js";
|
||||
|
||||
export { getFreePort } from "./test-port.js";
|
||||
|
||||
type HarnessState = {
|
||||
testPort: number;
|
||||
@@ -226,22 +228,6 @@ const server = await import("./server.js");
|
||||
export const startBrowserControlServerFromConfig = server.startBrowserControlServerFromConfig;
|
||||
export const stopBrowserControlServer = server.stopBrowserControlServer;
|
||||
|
||||
export async function getFreePort(): Promise<number> {
|
||||
while (true) {
|
||||
const port = await new Promise<number>((resolve, reject) => {
|
||||
const s = createServer();
|
||||
s.once("error", reject);
|
||||
s.listen(0, "127.0.0.1", () => {
|
||||
const assigned = (s.address() as AddressInfo).port;
|
||||
s.close((err) => (err ? reject(err) : resolve(assigned)));
|
||||
});
|
||||
});
|
||||
if (port < 65535) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function makeResponse(
|
||||
body: unknown,
|
||||
init?: { ok?: boolean; status?: number; text?: string },
|
||||
|
||||
18
src/browser/test-port.ts
Normal file
18
src/browser/test-port.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { createServer } from "node:http";
|
||||
|
||||
export async function getFreePort(): Promise<number> {
|
||||
while (true) {
|
||||
const port = await new Promise<number>((resolve, reject) => {
|
||||
const s = createServer();
|
||||
s.once("error", reject);
|
||||
s.listen(0, "127.0.0.1", () => {
|
||||
const assigned = (s.address() as AddressInfo).port;
|
||||
s.close((err) => (err ? reject(err) : resolve(assigned)));
|
||||
});
|
||||
});
|
||||
if (port < 65535) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user