mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:21:23 +00:00
perf(test): optimize heavy suites and stabilize lock timing
This commit is contained in:
@@ -25,6 +25,8 @@ installGatewayTestHooks({ scope: "suite" });
|
||||
let server: Awaited<ReturnType<typeof startServerWithClient>>["server"];
|
||||
let ws: WebSocket;
|
||||
let port: number;
|
||||
let nodeWs: WebSocket;
|
||||
let nodeId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const token = "test-gateway-token-1234567890";
|
||||
@@ -33,94 +35,60 @@ beforeAll(async () => {
|
||||
ws = started.ws;
|
||||
port = started.port;
|
||||
await connectOk(ws, { token });
|
||||
|
||||
nodeWs = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
await new Promise<void>((resolve) => nodeWs.once("open", resolve));
|
||||
|
||||
const identity = loadOrCreateDeviceIdentity();
|
||||
nodeId = identity.deviceId;
|
||||
await connectOk(nodeWs, {
|
||||
role: "node",
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
||||
version: "1.0.0",
|
||||
platform: "darwin",
|
||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||
},
|
||||
commands: ["canvas.snapshot"],
|
||||
token,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
nodeWs.close();
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
describe("late-arriving invoke results", () => {
|
||||
test("returns success for unknown invoke id (late arrival after timeout)", async () => {
|
||||
// Create a node client WebSocket
|
||||
const nodeWs = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
await new Promise<void>((resolve) => nodeWs.once("open", resolve));
|
||||
test("returns success for unknown invoke ids for both success and error payloads", async () => {
|
||||
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;
|
||||
|
||||
try {
|
||||
// Connect as a node with device identity
|
||||
const identity = loadOrCreateDeviceIdentity();
|
||||
const nodeId = identity.deviceId;
|
||||
|
||||
await connectOk(nodeWs, {
|
||||
role: "node",
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
||||
version: "1.0.0",
|
||||
platform: "ios",
|
||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||
},
|
||||
commands: ["canvas.snapshot"],
|
||||
token: "test-gateway-token-1234567890",
|
||||
});
|
||||
|
||||
// Send an invoke result with an unknown ID (simulating late arrival after timeout)
|
||||
for (const params of cases) {
|
||||
const result = await rpcReq<{ ok?: boolean; ignored?: boolean }>(
|
||||
nodeWs,
|
||||
"node.invoke.result",
|
||||
{
|
||||
id: "unknown-invoke-id-12345",
|
||||
...params,
|
||||
nodeId,
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({ result: "late" }),
|
||||
},
|
||||
);
|
||||
|
||||
// Late-arriving results return success instead of error to reduce log noise
|
||||
// Late-arriving results return success instead of error to reduce log noise.
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.payload?.ok).toBe(true);
|
||||
expect(result.payload?.ignored).toBe(true);
|
||||
} finally {
|
||||
nodeWs.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("returns success for unknown invoke id with error payload", async () => {
|
||||
// Verifies late results are accepted regardless of their ok/error status
|
||||
const nodeWs = new WebSocket(`ws://127.0.0.1:${port}`);
|
||||
await new Promise<void>((resolve) => nodeWs.once("open", resolve));
|
||||
|
||||
try {
|
||||
await connectOk(nodeWs, {
|
||||
role: "node",
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
||||
version: "1.0.0",
|
||||
platform: "darwin",
|
||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||
},
|
||||
commands: [],
|
||||
});
|
||||
|
||||
const identity = loadOrCreateDeviceIdentity();
|
||||
const nodeId = identity.deviceId;
|
||||
|
||||
// Late invoke result with error payload - should still return success
|
||||
const result = await rpcReq<{ ok?: boolean; ignored?: boolean }>(
|
||||
nodeWs,
|
||||
"node.invoke.result",
|
||||
{
|
||||
id: "another-unknown-invoke-id",
|
||||
nodeId,
|
||||
ok: false,
|
||||
error: { code: "FAILED", message: "test error" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.payload?.ok).toBe(true);
|
||||
expect(result.payload?.ignored).toBe(true);
|
||||
} finally {
|
||||
nodeWs.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { resetTestPluginRegistry, setTestPluginRegistry, testState } from "./test-helpers.mocks.js";
|
||||
import { installGatewayTestHooks, getFreePort, startGatewayServer } from "./test-helpers.server.js";
|
||||
@@ -22,134 +20,137 @@ const resolveGatewayToken = (): string => {
|
||||
return token;
|
||||
};
|
||||
|
||||
describe("POST /tools/invoke", () => {
|
||||
it("invokes a tool and returns {ok:true,result}", async () => {
|
||||
// Allow the agents_list tool for main agent.
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["agents_list"],
|
||||
},
|
||||
const allowAgentsListForMain = () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["agents_list"],
|
||||
},
|
||||
],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
},
|
||||
],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
};
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, {
|
||||
const invokeAgentsList = async (params: {
|
||||
port: number;
|
||||
headers?: Record<string, string>;
|
||||
sessionKey?: string;
|
||||
}) => {
|
||||
const body: Record<string, unknown> = { tool: "agents_list", action: "json", args: {} };
|
||||
if (params.sessionKey) {
|
||||
body.sessionKey = params.sessionKey;
|
||||
}
|
||||
return await fetch(`http://127.0.0.1:${params.port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", ...params.headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
};
|
||||
|
||||
describe("POST /tools/invoke", () => {
|
||||
let sharedPort = 0;
|
||||
let sharedServer: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
sharedPort = await getFreePort();
|
||||
sharedServer = await startGatewayServer(sharedPort, {
|
||||
bind: "loopback",
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await sharedServer.close();
|
||||
});
|
||||
|
||||
it("invokes a tool and returns {ok:true,result}", async () => {
|
||||
allowAgentsListForMain();
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
const res = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
sessionKey: "main",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body).toHaveProperty("result");
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("supports tools.alsoAllow as additive allowlist (profile stage)", async () => {
|
||||
// No explicit tool allowlist; rely on profile + alsoAllow.
|
||||
it("supports tools.alsoAllow in profile and implicit modes", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [{ id: "main" }],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
|
||||
// minimal profile does NOT include agents_list, but alsoAllow should.
|
||||
const { writeConfigFile } = await import("../config/config.js");
|
||||
await writeConfigFile({
|
||||
tools: { profile: "minimal", alsoAllow: ["agents_list"] },
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
const resProfile = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
sessionKey: "main",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.ok).toBe(true);
|
||||
expect(resProfile.status).toBe(200);
|
||||
const profileBody = await resProfile.json();
|
||||
expect(profileBody.ok).toBe(true);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("supports tools.alsoAllow without allow/profile (implicit allow-all)", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [{ id: "main" }],
|
||||
await writeConfigFile({
|
||||
tools: { alsoAllow: ["agents_list"] },
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
|
||||
const { CONFIG_PATH } = await import("../config/config.js");
|
||||
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
|
||||
await fs.writeFile(
|
||||
CONFIG_PATH,
|
||||
JSON.stringify({ tools: { alsoAllow: ["agents_list"] } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
} as any);
|
||||
const resImplicit = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
sessionKey: "main",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.ok).toBe(true);
|
||||
|
||||
await server.close();
|
||||
expect(resImplicit.status).toBe(200);
|
||||
const implicitBody = await resImplicit.json();
|
||||
expect(implicitBody.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts password auth when bearer token matches", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["agents_list"],
|
||||
},
|
||||
},
|
||||
],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
it("handles dedicated auth modes for password accept and token reject", async () => {
|
||||
allowAgentsListForMain();
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, {
|
||||
const passwordPort = await getFreePort();
|
||||
const passwordServer = await startGatewayServer(passwordPort, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "password", password: "secret" },
|
||||
});
|
||||
try {
|
||||
const passwordRes = await invokeAgentsList({
|
||||
port: passwordPort,
|
||||
headers: { authorization: "Bearer secret" },
|
||||
sessionKey: "main",
|
||||
});
|
||||
expect(passwordRes.status).toBe(200);
|
||||
} finally {
|
||||
await passwordServer.close();
|
||||
}
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: "Bearer secret",
|
||||
},
|
||||
body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
const tokenPort = await getFreePort();
|
||||
const tokenServer = await startGatewayServer(tokenPort, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token: "t" },
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
await server.close();
|
||||
try {
|
||||
const tokenRes = await invokeAgentsList({
|
||||
port: tokenPort,
|
||||
sessionKey: "main",
|
||||
});
|
||||
expect(tokenRes.status).toBe(401);
|
||||
} finally {
|
||||
await tokenServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("routes tools invoke before plugin HTTP handlers", async () => {
|
||||
@@ -171,72 +172,23 @@ describe("POST /tools/invoke", () => {
|
||||
];
|
||||
setTestPluginRegistry(registry);
|
||||
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["agents_list"],
|
||||
},
|
||||
},
|
||||
],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
allowAgentsListForMain();
|
||||
try {
|
||||
const token = resolveGatewayToken();
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({
|
||||
tool: "agents_list",
|
||||
action: "json",
|
||||
args: {},
|
||||
sessionKey: "main",
|
||||
}),
|
||||
const res = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
sessionKey: "main",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(pluginHandler).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await server.close();
|
||||
resetTestPluginRegistry();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects unauthorized when auth mode is token and header is missing", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["agents_list"],
|
||||
},
|
||||
},
|
||||
],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token: "t" },
|
||||
});
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("returns 404 when tool is not allowlisted", async () => {
|
||||
it("returns 404 when denylisted or blocked by tools.profile", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
@@ -248,34 +200,16 @@ describe("POST /tools/invoke", () => {
|
||||
],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
const denyRes = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
sessionKey: "main",
|
||||
});
|
||||
expect(denyRes.status).toBe(404);
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("respects tools.profile allowlist", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["agents_list"],
|
||||
},
|
||||
},
|
||||
],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
allowAgentsListForMain();
|
||||
|
||||
const { writeConfigFile } = await import("../config/config.js");
|
||||
await writeConfigFile({
|
||||
@@ -283,19 +217,12 @@ describe("POST /tools/invoke", () => {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "agents_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
const profileRes = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
sessionKey: "main",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
|
||||
await server.close();
|
||||
expect(profileRes.status).toBe(404);
|
||||
});
|
||||
|
||||
it("uses the configured main session key when sessionKey is missing or main", async () => {
|
||||
@@ -319,26 +246,19 @@ describe("POST /tools/invoke", () => {
|
||||
} as any;
|
||||
testState.sessionConfig = { mainKey: "primary" };
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
|
||||
const payload = { tool: "agents_list", action: "json", args: {} };
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const resDefault = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify(payload),
|
||||
const resDefault = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(resDefault.status).toBe(200);
|
||||
|
||||
const resMain = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ ...payload, sessionKey: "main" }),
|
||||
const resMain = await invokeAgentsList({
|
||||
port: sharedPort,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
sessionKey: "main",
|
||||
});
|
||||
expect(resMain.status).toBe(200);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user