openclaw/src/discord/monitor/agent-components.test.ts
Robby 078642b308
fix(discord): defer component interactions to prevent timeout (#16287)
* fix(discord): defer component interactions to prevent timeout

Discord requires interaction responses within 3 seconds. Button clicks
were routed through the LLM pipeline before responding, exceeding this
window and showing 'This interaction failed' to users.

Now immediately defers the interaction, then processes the agent
response asynchronously.

Fixes #16262

* fix: harden deferred interaction replies and silent chat finals (#16287) (thanks @robbyczgw-cla)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 17:38:01 +01:00

107 lines
4.0 KiB
TypeScript

import type { ButtonInteraction, ComponentData, StringSelectMenuInteraction } from "@buape/carbon";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { createAgentComponentButton, createAgentSelectMenu } from "./agent-components.js";
const readAllowFromStoreMock = vi.hoisted(() => vi.fn());
const upsertPairingRequestMock = vi.hoisted(() => vi.fn());
const enqueueSystemEventMock = vi.hoisted(() => vi.fn());
vi.mock("../../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
}));
vi.mock("../../infra/system-events.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../infra/system-events.js")>();
return {
...actual,
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
};
});
const createCfg = (): OpenClawConfig => ({}) as OpenClawConfig;
const createDmButtonInteraction = (overrides: Partial<ButtonInteraction> = {}) => {
const reply = vi.fn().mockResolvedValue(undefined);
const defer = vi.fn().mockResolvedValue(undefined);
const interaction = {
rawData: { channel_id: "dm-channel" },
user: { id: "123456789", username: "Alice", discriminator: "1234" },
defer,
reply,
...overrides,
} as unknown as ButtonInteraction;
return { interaction, defer, reply };
};
const createDmSelectInteraction = (overrides: Partial<StringSelectMenuInteraction> = {}) => {
const reply = vi.fn().mockResolvedValue(undefined);
const defer = vi.fn().mockResolvedValue(undefined);
const interaction = {
rawData: { channel_id: "dm-channel" },
user: { id: "123456789", username: "Alice", discriminator: "1234" },
values: ["alpha"],
defer,
reply,
...overrides,
} as unknown as StringSelectMenuInteraction;
return { interaction, defer, reply };
};
beforeEach(() => {
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
enqueueSystemEventMock.mockReset();
});
describe("agent components", () => {
it("sends pairing reply when DM sender is not allowlisted", async () => {
const button = createAgentComponentButton({
cfg: createCfg(),
accountId: "default",
dmPolicy: "pairing",
});
const { interaction, defer, reply } = createDmButtonInteraction();
await button.run(interaction, { componentId: "hello" } as ComponentData);
expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledTimes(1);
expect(reply.mock.calls[0]?.[0]?.content).toContain("Pairing code: PAIRCODE");
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("allows DM interactions when pairing store allowlist matches", async () => {
readAllowFromStoreMock.mockResolvedValue(["123456789"]);
const button = createAgentComponentButton({
cfg: createCfg(),
accountId: "default",
dmPolicy: "allowlist",
});
const { interaction, defer, reply } = createDmButtonInteraction();
await button.run(interaction, { componentId: "hello" } as ComponentData);
expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledWith({ content: "✓" });
expect(enqueueSystemEventMock).toHaveBeenCalled();
});
it("matches tag-based allowlist entries for DM select menus", async () => {
const select = createAgentSelectMenu({
cfg: createCfg(),
accountId: "default",
dmPolicy: "allowlist",
allowFrom: ["Alice#1234"],
});
const { interaction, defer, reply } = createDmSelectInteraction();
await select.run(interaction, { componentId: "hello" } as ComponentData);
expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledWith({ content: "✓" });
expect(enqueueSystemEventMock).toHaveBeenCalled();
});
});