mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
perf: reduce hotspot test startup and timeout costs
This commit is contained in:
@@ -146,7 +146,7 @@ describe("exec tool backgrounding", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses default timeout when timeout is omitted", async () => {
|
it("uses default timeout when timeout is omitted", async () => {
|
||||||
const customBash = createExecTool({ timeoutSec: 1, backgroundMs: 10 });
|
const customBash = createExecTool({ timeoutSec: 0.2, backgroundMs: 10 });
|
||||||
const customProcess = createProcessTool();
|
const customProcess = createProcessTool();
|
||||||
|
|
||||||
const result = await customBash.execute("call1", {
|
const result = await customBash.execute("call1", {
|
||||||
@@ -165,7 +165,7 @@ describe("exec tool backgrounding", () => {
|
|||||||
});
|
});
|
||||||
status = (poll.details as { status: string }).status;
|
status = (poll.details as { status: string }).status;
|
||||||
if (status === "running") {
|
if (status === "running") {
|
||||||
await sleep(50);
|
await sleep(20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
src/discord/monitor/gateway-plugin.ts
Normal file
63
src/discord/monitor/gateway-plugin.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
||||||
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
import type { DiscordAccountConfig } from "../../config/types.js";
|
||||||
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
|
import { danger } from "../../globals.js";
|
||||||
|
|
||||||
|
export function resolveDiscordGatewayIntents(
|
||||||
|
intentsConfig?: import("../../config/types.discord.js").DiscordIntentsConfig,
|
||||||
|
): number {
|
||||||
|
let intents =
|
||||||
|
GatewayIntents.Guilds |
|
||||||
|
GatewayIntents.GuildMessages |
|
||||||
|
GatewayIntents.MessageContent |
|
||||||
|
GatewayIntents.DirectMessages |
|
||||||
|
GatewayIntents.GuildMessageReactions |
|
||||||
|
GatewayIntents.DirectMessageReactions;
|
||||||
|
if (intentsConfig?.presence) {
|
||||||
|
intents |= GatewayIntents.GuildPresences;
|
||||||
|
}
|
||||||
|
if (intentsConfig?.guildMembers) {
|
||||||
|
intents |= GatewayIntents.GuildMembers;
|
||||||
|
}
|
||||||
|
return intents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDiscordGatewayPlugin(params: {
|
||||||
|
discordConfig: DiscordAccountConfig;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
}): GatewayPlugin {
|
||||||
|
const intents = resolveDiscordGatewayIntents(params.discordConfig?.intents);
|
||||||
|
const proxy = params.discordConfig?.proxy?.trim();
|
||||||
|
const options = {
|
||||||
|
reconnect: { maxAttempts: 50 },
|
||||||
|
intents,
|
||||||
|
autoInteractions: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!proxy) {
|
||||||
|
return new GatewayPlugin(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = new HttpsProxyAgent<string>(proxy);
|
||||||
|
|
||||||
|
params.runtime.log?.("discord: gateway proxy enabled");
|
||||||
|
|
||||||
|
class ProxyGatewayPlugin extends GatewayPlugin {
|
||||||
|
constructor() {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
createWebSocket(url: string) {
|
||||||
|
return new WebSocket(url, { agent });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProxyGatewayPlugin();
|
||||||
|
} catch (err) {
|
||||||
|
params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`));
|
||||||
|
return new GatewayPlugin(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ describe("createDiscordGatewayPlugin", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses proxy agent for gateway WebSocket when configured", async () => {
|
it("uses proxy agent for gateway WebSocket when configured", async () => {
|
||||||
const { __testing } = await import("./provider.js");
|
const { createDiscordGatewayPlugin } = await import("./gateway-plugin.js");
|
||||||
const { GatewayPlugin } = await import("@buape/carbon/gateway");
|
const { GatewayPlugin } = await import("@buape/carbon/gateway");
|
||||||
|
|
||||||
const runtime = {
|
const runtime = {
|
||||||
@@ -61,7 +61,7 @@ describe("createDiscordGatewayPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const plugin = __testing.createDiscordGatewayPlugin({
|
const plugin = createDiscordGatewayPlugin({
|
||||||
discordConfig: { proxy: "http://proxy.test:8080" },
|
discordConfig: { proxy: "http://proxy.test:8080" },
|
||||||
runtime,
|
runtime,
|
||||||
});
|
});
|
||||||
@@ -82,7 +82,7 @@ describe("createDiscordGatewayPlugin", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to the default gateway plugin when proxy is invalid", async () => {
|
it("falls back to the default gateway plugin when proxy is invalid", async () => {
|
||||||
const { __testing } = await import("./provider.js");
|
const { createDiscordGatewayPlugin } = await import("./gateway-plugin.js");
|
||||||
const { GatewayPlugin } = await import("@buape/carbon/gateway");
|
const { GatewayPlugin } = await import("@buape/carbon/gateway");
|
||||||
|
|
||||||
const runtime = {
|
const runtime = {
|
||||||
@@ -93,7 +93,7 @@ describe("createDiscordGatewayPlugin", () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const plugin = __testing.createDiscordGatewayPlugin({
|
const plugin = createDiscordGatewayPlugin({
|
||||||
discordConfig: { proxy: "bad-proxy" },
|
discordConfig: { proxy: "bad-proxy" },
|
||||||
runtime,
|
runtime,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
|
import type { GatewayPlugin } from "@buape/carbon/gateway";
|
||||||
import { Client, ReadyListener, type BaseMessageInteractiveComponent } from "@buape/carbon";
|
import { Client, ReadyListener, type BaseMessageInteractiveComponent } from "@buape/carbon";
|
||||||
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
|
||||||
import { Routes } from "discord-api-types/v10";
|
import { Routes } from "discord-api-types/v10";
|
||||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
import WebSocket from "ws";
|
|
||||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||||
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
|
import type { OpenClawConfig, ReplyToMode } from "../../config/config.js";
|
||||||
import type { DiscordAccountConfig } from "../../config/types.js";
|
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||||
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
|
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
|
||||||
@@ -31,6 +28,7 @@ import { resolveDiscordUserAllowlist } from "../resolve-users.js";
|
|||||||
import { normalizeDiscordToken } from "../token.js";
|
import { normalizeDiscordToken } from "../token.js";
|
||||||
import { createAgentComponentButton, createAgentSelectMenu } from "./agent-components.js";
|
import { createAgentComponentButton, createAgentSelectMenu } from "./agent-components.js";
|
||||||
import { createExecApprovalButton, DiscordExecApprovalHandler } from "./exec-approvals.js";
|
import { createExecApprovalButton, DiscordExecApprovalHandler } from "./exec-approvals.js";
|
||||||
|
import { createDiscordGatewayPlugin } from "./gateway-plugin.js";
|
||||||
import { registerGateway, unregisterGateway } from "./gateway-registry.js";
|
import { registerGateway, unregisterGateway } from "./gateway-registry.js";
|
||||||
import {
|
import {
|
||||||
DiscordMessageListener,
|
DiscordMessageListener,
|
||||||
@@ -57,44 +55,6 @@ export type MonitorDiscordOpts = {
|
|||||||
replyToMode?: ReplyToMode;
|
replyToMode?: ReplyToMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createDiscordGatewayPlugin(params: {
|
|
||||||
discordConfig: DiscordAccountConfig;
|
|
||||||
runtime: RuntimeEnv;
|
|
||||||
}): GatewayPlugin {
|
|
||||||
const intents = resolveDiscordGatewayIntents(params.discordConfig?.intents);
|
|
||||||
const proxy = params.discordConfig?.proxy?.trim();
|
|
||||||
const options = {
|
|
||||||
reconnect: { maxAttempts: 50 },
|
|
||||||
intents,
|
|
||||||
autoInteractions: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!proxy) {
|
|
||||||
return new GatewayPlugin(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const agent = new HttpsProxyAgent<string>(proxy);
|
|
||||||
|
|
||||||
params.runtime.log?.("discord: gateway proxy enabled");
|
|
||||||
|
|
||||||
class ProxyGatewayPlugin extends GatewayPlugin {
|
|
||||||
constructor() {
|
|
||||||
super(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
createWebSocket(url: string) {
|
|
||||||
return new WebSocket(url, { agent });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProxyGatewayPlugin();
|
|
||||||
} catch (err) {
|
|
||||||
params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`));
|
|
||||||
return new GatewayPlugin(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeAllowList(list?: Array<string | number>) {
|
function summarizeAllowList(list?: Array<string | number>) {
|
||||||
if (!list || list.length === 0) {
|
if (!list || list.length === 0) {
|
||||||
return "any";
|
return "any";
|
||||||
@@ -164,25 +124,6 @@ function formatDiscordDeployErrorDetails(err: unknown): string {
|
|||||||
return details.length > 0 ? ` (${details.join(", ")})` : "";
|
return details.length > 0 ? ` (${details.join(", ")})` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDiscordGatewayIntents(
|
|
||||||
intentsConfig?: import("../../config/types.discord.js").DiscordIntentsConfig,
|
|
||||||
): number {
|
|
||||||
let intents =
|
|
||||||
GatewayIntents.Guilds |
|
|
||||||
GatewayIntents.GuildMessages |
|
|
||||||
GatewayIntents.MessageContent |
|
|
||||||
GatewayIntents.DirectMessages |
|
|
||||||
GatewayIntents.GuildMessageReactions |
|
|
||||||
GatewayIntents.DirectMessageReactions;
|
|
||||||
if (intentsConfig?.presence) {
|
|
||||||
intents |= GatewayIntents.GuildPresences;
|
|
||||||
}
|
|
||||||
if (intentsConfig?.guildMembers) {
|
|
||||||
intents |= GatewayIntents.GuildMembers;
|
|
||||||
}
|
|
||||||
return intents;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||||
const cfg = opts.config ?? loadConfig();
|
const cfg = opts.config ?? loadConfig();
|
||||||
const account = resolveDiscordAccount({
|
const account = resolveDiscordAccount({
|
||||||
|
|||||||
@@ -262,22 +262,20 @@ describe("POST /tools/invoke", () => {
|
|||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const port = await getFreePort();
|
|
||||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
|
||||||
const token = resolveGatewayToken();
|
const token = resolveGatewayToken();
|
||||||
|
|
||||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
const res = await invokeTool({
|
||||||
method: "POST",
|
port: sharedPort,
|
||||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
tool: "sessions_spawn",
|
||||||
body: JSON.stringify({ tool: "sessions_spawn", args: { task: "test" }, sessionKey: "main" }),
|
args: { task: "test" },
|
||||||
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
sessionKey: "main",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.ok).toBe(false);
|
expect(body.ok).toBe(false);
|
||||||
expect(body.error.type).toBe("not_found");
|
expect(body.error.type).toBe("not_found");
|
||||||
|
|
||||||
await server.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("denies sessions_send via HTTP gateway", async () => {
|
it("denies sessions_send via HTTP gateway", async () => {
|
||||||
@@ -286,18 +284,16 @@ describe("POST /tools/invoke", () => {
|
|||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const port = await getFreePort();
|
|
||||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
|
||||||
const token = resolveGatewayToken();
|
const token = resolveGatewayToken();
|
||||||
|
|
||||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
const res = await invokeTool({
|
||||||
method: "POST",
|
port: sharedPort,
|
||||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
tool: "sessions_send",
|
||||||
body: JSON.stringify({ tool: "sessions_send", args: {}, sessionKey: "main" }),
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
sessionKey: "main",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
await server.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("denies gateway tool via HTTP", async () => {
|
it("denies gateway tool via HTTP", async () => {
|
||||||
@@ -306,18 +302,16 @@ describe("POST /tools/invoke", () => {
|
|||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const port = await getFreePort();
|
|
||||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
|
||||||
const token = resolveGatewayToken();
|
const token = resolveGatewayToken();
|
||||||
|
|
||||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
const res = await invokeTool({
|
||||||
method: "POST",
|
port: sharedPort,
|
||||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
tool: "gateway",
|
||||||
body: JSON.stringify({ tool: "gateway", args: {}, sessionKey: "main" }),
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
sessionKey: "main",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
await server.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the configured main session key when sessionKey is missing or main", async () => {
|
it("uses the configured main session key when sessionKey is missing or main", async () => {
|
||||||
|
|||||||
@@ -387,10 +387,8 @@ describe("gateway multi-instance e2e", () => {
|
|||||||
"spins up two gateways and exercises WS + HTTP + node pairing",
|
"spins up two gateways and exercises WS + HTTP + node pairing",
|
||||||
{ timeout: E2E_TIMEOUT_MS },
|
{ timeout: E2E_TIMEOUT_MS },
|
||||||
async () => {
|
async () => {
|
||||||
const gwA = await spawnGatewayInstance("a");
|
const [gwA, gwB] = await Promise.all([spawnGatewayInstance("a"), spawnGatewayInstance("b")]);
|
||||||
instances.push(gwA);
|
instances.push(gwA, gwB);
|
||||||
const gwB = await spawnGatewayInstance("b");
|
|
||||||
instances.push(gwB);
|
|
||||||
|
|
||||||
const [hookResA, hookResB] = await Promise.all([
|
const [hookResA, hookResB] = await Promise.all([
|
||||||
postJson(
|
postJson(
|
||||||
@@ -415,8 +413,10 @@ describe("gateway multi-instance e2e", () => {
|
|||||||
expect(hookResB.status).toBe(200);
|
expect(hookResB.status).toBe(200);
|
||||||
expect((hookResB.json as { ok?: boolean } | undefined)?.ok).toBe(true);
|
expect((hookResB.json as { ok?: boolean } | undefined)?.ok).toBe(true);
|
||||||
|
|
||||||
const nodeA = await connectNode(gwA, "node-a");
|
const [nodeA, nodeB] = await Promise.all([
|
||||||
const nodeB = await connectNode(gwB, "node-b");
|
connectNode(gwA, "node-a"),
|
||||||
|
connectNode(gwB, "node-b"),
|
||||||
|
]);
|
||||||
nodeClients.push(nodeA.client, nodeB.client);
|
nodeClients.push(nodeA.client, nodeB.client);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
Reference in New Issue
Block a user