mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:31:24 +00:00
perf(test): cut gateway unit suite overhead
This commit is contained in:
283
src/gateway/gateway-misc.test.ts
Normal file
283
src/gateway/gateway-misc.test.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, test, vi } from "vitest";
|
||||
import type { RequestFrame } from "./protocol/index.js";
|
||||
import type { GatewayClient as GatewayMethodClient } from "./server-methods/types.js";
|
||||
import type { GatewayRequestContext, RespondFn } from "./server-methods/types.js";
|
||||
import type { GatewayWsClient } from "./server/ws-types.js";
|
||||
import { GatewayClient } from "./client.js";
|
||||
import { handleControlUiHttpRequest } from "./control-ui.js";
|
||||
import {
|
||||
DEFAULT_DANGEROUS_NODE_COMMANDS,
|
||||
resolveNodeCommandAllowlist,
|
||||
} from "./node-command-policy.js";
|
||||
import { createGatewayBroadcaster } from "./server-broadcast.js";
|
||||
import { createChatRunRegistry } from "./server-chat.js";
|
||||
import { handleNodeInvokeResult } from "./server-methods/nodes.handlers.invoke-result.js";
|
||||
import { createNodeSubscriptionManager } from "./server-node-subscriptions.js";
|
||||
|
||||
const wsMockState = vi.hoisted(() => ({
|
||||
last: null as { url: unknown; opts: unknown } | null,
|
||||
}));
|
||||
|
||||
vi.mock("ws", () => ({
|
||||
WebSocket: class MockWebSocket {
|
||||
on = vi.fn();
|
||||
close = vi.fn();
|
||||
send = vi.fn();
|
||||
|
||||
constructor(url: unknown, opts: unknown) {
|
||||
wsMockState.last = { url, opts };
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe("GatewayClient", () => {
|
||||
test("uses a large maxPayload for node snapshots", () => {
|
||||
wsMockState.last = null;
|
||||
const client = new GatewayClient({ url: "ws://127.0.0.1:1" });
|
||||
client.start();
|
||||
|
||||
expect(wsMockState.last?.url).toBe("ws://127.0.0.1:1");
|
||||
expect(wsMockState.last?.opts).toEqual(
|
||||
expect.objectContaining({ maxPayload: 25 * 1024 * 1024 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const makeControlUiResponse = (): {
|
||||
res: ServerResponse;
|
||||
setHeader: ReturnType<typeof vi.fn>;
|
||||
end: ReturnType<typeof vi.fn>;
|
||||
} => {
|
||||
const setHeader = vi.fn();
|
||||
const end = vi.fn();
|
||||
const res = {
|
||||
headersSent: false,
|
||||
statusCode: 200,
|
||||
setHeader,
|
||||
end,
|
||||
} as unknown as ServerResponse;
|
||||
return { res, setHeader, end };
|
||||
};
|
||||
|
||||
describe("handleControlUiHttpRequest", () => {
|
||||
it("sets anti-clickjacking headers for Control UI responses", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ui-"));
|
||||
try {
|
||||
await fs.writeFile(path.join(tmp, "index.html"), "<html></html>\n");
|
||||
const { res, setHeader } = makeControlUiResponse();
|
||||
const handled = handleControlUiHttpRequest(
|
||||
{ url: "/", method: "GET" } as IncomingMessage,
|
||||
res,
|
||||
{
|
||||
root: { kind: "resolved", path: tmp },
|
||||
},
|
||||
);
|
||||
expect(handled).toBe(true);
|
||||
expect(setHeader).toHaveBeenCalledWith("X-Frame-Options", "DENY");
|
||||
expect(setHeader).toHaveBeenCalledWith("Content-Security-Policy", "frame-ancestors 'none'");
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
type TestSocket = {
|
||||
bufferedAmount: number;
|
||||
send: (payload: string) => void;
|
||||
close: (code: number, reason: string) => void;
|
||||
};
|
||||
|
||||
describe("gateway broadcaster", () => {
|
||||
it("filters approval and pairing events by scope", () => {
|
||||
const approvalsSocket: TestSocket = {
|
||||
bufferedAmount: 0,
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
};
|
||||
const pairingSocket: TestSocket = {
|
||||
bufferedAmount: 0,
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
};
|
||||
const readSocket: TestSocket = {
|
||||
bufferedAmount: 0,
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
};
|
||||
|
||||
const clients = new Set<GatewayWsClient>([
|
||||
{
|
||||
socket: approvalsSocket as unknown as GatewayWsClient["socket"],
|
||||
connect: { role: "operator", scopes: ["operator.approvals"] } as GatewayWsClient["connect"],
|
||||
connId: "c-approvals",
|
||||
},
|
||||
{
|
||||
socket: pairingSocket as unknown as GatewayWsClient["socket"],
|
||||
connect: { role: "operator", scopes: ["operator.pairing"] } as GatewayWsClient["connect"],
|
||||
connId: "c-pairing",
|
||||
},
|
||||
{
|
||||
socket: readSocket as unknown as GatewayWsClient["socket"],
|
||||
connect: { role: "operator", scopes: ["operator.read"] } as GatewayWsClient["connect"],
|
||||
connId: "c-read",
|
||||
},
|
||||
]);
|
||||
|
||||
const { broadcast, broadcastToConnIds } = createGatewayBroadcaster({ clients });
|
||||
|
||||
broadcast("exec.approval.requested", { id: "1" });
|
||||
broadcast("device.pair.requested", { requestId: "r1" });
|
||||
|
||||
expect(approvalsSocket.send).toHaveBeenCalledTimes(1);
|
||||
expect(pairingSocket.send).toHaveBeenCalledTimes(1);
|
||||
expect(readSocket.send).toHaveBeenCalledTimes(0);
|
||||
|
||||
broadcastToConnIds("tick", { ts: 1 }, new Set(["c-read"]));
|
||||
expect(readSocket.send).toHaveBeenCalledTimes(1);
|
||||
expect(approvalsSocket.send).toHaveBeenCalledTimes(1);
|
||||
expect(pairingSocket.send).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chat run registry", () => {
|
||||
test("queues and removes runs per session", () => {
|
||||
const registry = createChatRunRegistry();
|
||||
|
||||
registry.add("s1", { sessionKey: "main", clientRunId: "c1" });
|
||||
registry.add("s1", { sessionKey: "main", clientRunId: "c2" });
|
||||
|
||||
expect(registry.peek("s1")?.clientRunId).toBe("c1");
|
||||
expect(registry.shift("s1")?.clientRunId).toBe("c1");
|
||||
expect(registry.peek("s1")?.clientRunId).toBe("c2");
|
||||
|
||||
expect(registry.remove("s1", "c2")?.clientRunId).toBe("c2");
|
||||
expect(registry.peek("s1")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("late-arriving invoke results", () => {
|
||||
test("returns success for unknown invoke ids for both success and error payloads", async () => {
|
||||
const nodeId = "node-123";
|
||||
const cases = [
|
||||
{
|
||||
id: "unknown-invoke-id-12345",
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({ result: "late" }),
|
||||
},
|
||||
{
|
||||
id: "another-unknown-invoke-id",
|
||||
ok: false,
|
||||
error: { code: "FAILED", message: "test error" },
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const params of cases) {
|
||||
const respond = vi.fn<RespondFn>();
|
||||
const context = {
|
||||
nodeRegistry: { handleInvokeResult: () => false },
|
||||
logGateway: { debug: vi.fn() },
|
||||
} as unknown as GatewayRequestContext;
|
||||
const client = {
|
||||
connect: { device: { id: nodeId } },
|
||||
} as unknown as GatewayMethodClient;
|
||||
|
||||
await handleNodeInvokeResult({
|
||||
req: { method: "node.invoke.result" } as unknown as RequestFrame,
|
||||
params: { ...params, nodeId } as unknown as Record<string, unknown>,
|
||||
client,
|
||||
isWebchatConnect: () => false,
|
||||
respond,
|
||||
context,
|
||||
});
|
||||
|
||||
const [ok, payload, error] = respond.mock.lastCall ?? [];
|
||||
|
||||
// Late-arriving results return success instead of error to reduce log noise.
|
||||
expect(ok).toBe(true);
|
||||
expect(error).toBeUndefined();
|
||||
expect(payload?.ok).toBe(true);
|
||||
expect(payload?.ignored).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("node subscription manager", () => {
|
||||
test("routes events to subscribed nodes", () => {
|
||||
const manager = createNodeSubscriptionManager();
|
||||
const sent: Array<{
|
||||
nodeId: string;
|
||||
event: string;
|
||||
payloadJSON?: string | null;
|
||||
}> = [];
|
||||
const sendEvent = (evt: { nodeId: string; event: string; payloadJSON?: string | null }) =>
|
||||
sent.push(evt);
|
||||
|
||||
manager.subscribe("node-a", "main");
|
||||
manager.subscribe("node-b", "main");
|
||||
manager.sendToSession("main", "chat", { ok: true }, sendEvent);
|
||||
|
||||
expect(sent).toHaveLength(2);
|
||||
expect(sent.map((s) => s.nodeId).toSorted()).toEqual(["node-a", "node-b"]);
|
||||
expect(sent[0].event).toBe("chat");
|
||||
});
|
||||
|
||||
test("unsubscribeAll clears session mappings", () => {
|
||||
const manager = createNodeSubscriptionManager();
|
||||
const sent: string[] = [];
|
||||
const sendEvent = (evt: { nodeId: string; event: string }) =>
|
||||
sent.push(`${evt.nodeId}:${evt.event}`);
|
||||
|
||||
manager.subscribe("node-a", "main");
|
||||
manager.subscribe("node-a", "secondary");
|
||||
manager.unsubscribeAll("node-a");
|
||||
manager.sendToSession("main", "tick", {}, sendEvent);
|
||||
manager.sendToSession("secondary", "tick", {}, sendEvent);
|
||||
|
||||
expect(sent).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveNodeCommandAllowlist", () => {
|
||||
it("includes iOS service commands by default", () => {
|
||||
const allow = resolveNodeCommandAllowlist(
|
||||
{},
|
||||
{
|
||||
platform: "ios 26.0",
|
||||
deviceFamily: "iPhone",
|
||||
},
|
||||
);
|
||||
|
||||
expect(allow.has("device.info")).toBe(true);
|
||||
expect(allow.has("device.status")).toBe(true);
|
||||
expect(allow.has("system.notify")).toBe(true);
|
||||
expect(allow.has("contacts.search")).toBe(true);
|
||||
expect(allow.has("calendar.events")).toBe(true);
|
||||
expect(allow.has("reminders.list")).toBe(true);
|
||||
expect(allow.has("photos.latest")).toBe(true);
|
||||
expect(allow.has("motion.activity")).toBe(true);
|
||||
|
||||
for (const cmd of DEFAULT_DANGEROUS_NODE_COMMANDS) {
|
||||
expect(allow.has(cmd)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("can explicitly allow dangerous commands via allowCommands", () => {
|
||||
const allow = resolveNodeCommandAllowlist(
|
||||
{
|
||||
gateway: {
|
||||
nodes: {
|
||||
allowCommands: ["camera.snap", "screen.record"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ platform: "ios", deviceFamily: "iPhone" },
|
||||
);
|
||||
expect(allow.has("camera.snap")).toBe(true);
|
||||
expect(allow.has("screen.record")).toBe(true);
|
||||
expect(allow.has("camera.clip")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user