test(discord): cover additional utility surfaces

This commit is contained in:
Vincent Koc 2026-03-22 15:51:37 -07:00
parent 82508e3931
commit 59be2c8679
4 changed files with 318 additions and 0 deletions

View File

@ -0,0 +1,116 @@
import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
const handleDiscordMessageActionMock = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
vi.mock("./actions/handle-action.js", () => ({
handleDiscordMessageAction: handleDiscordMessageActionMock,
}));
import { discordMessageActions } from "./channel-actions.js";
describe("discordMessageActions", () => {
it("returns no tool actions when no token-sourced Discord accounts are enabled", () => {
const discovery = discordMessageActions.describeMessageTool?.({
cfg: {
channels: {
discord: {
enabled: true,
},
},
} as OpenClawConfig,
});
expect(discovery).toEqual({
actions: [],
capabilities: [],
schema: null,
});
});
it("describes enabled Discord actions for token-backed accounts", () => {
const discovery = discordMessageActions.describeMessageTool?.({
cfg: {
channels: {
discord: {
token: "Bot token-main",
actions: {
polls: true,
reactions: true,
permissions: true,
channels: false,
roles: false,
},
},
},
} as OpenClawConfig,
});
expect(discovery?.capabilities).toEqual(["interactive", "components"]);
expect(discovery?.schema).not.toBeNull();
expect(discovery?.actions).toEqual(
expect.arrayContaining([
"send",
"poll",
"react",
"reactions",
"emoji-list",
"permissions",
]),
);
expect(discovery?.actions).not.toContain("channel-create");
expect(discovery?.actions).not.toContain("role-add");
});
it("extracts send targets for message and thread reply actions", () => {
expect(
discordMessageActions.extractToolSend?.({
args: { action: "sendMessage", to: "channel:123" },
}),
).toEqual({ to: "channel:123" });
expect(
discordMessageActions.extractToolSend?.({
args: { action: "threadReply", channelId: "987" },
}),
).toEqual({ to: "channel:987" });
expect(
discordMessageActions.extractToolSend?.({
args: { action: "threadReply", channelId: " " },
}),
).toBeNull();
});
it("delegates action handling to the Discord action handler", async () => {
const cfg = {
channels: {
discord: {
token: "Bot token-main",
},
},
} as OpenClawConfig;
const toolContext = { sessionId: "s1" };
const mediaLocalRoots = ["/tmp/media"];
await discordMessageActions.handleAction?.({
action: "send",
params: { to: "channel:123", text: "hello" },
cfg,
accountId: "ops",
requesterSenderId: "user-1",
toolContext,
mediaLocalRoots,
});
expect(handleDiscordMessageActionMock).toHaveBeenCalledWith({
action: "send",
params: { to: "channel:123", text: "hello" },
cfg,
accountId: "ops",
requesterSenderId: "user-1",
toolContext,
mediaLocalRoots,
});
});
});

View File

@ -0,0 +1,83 @@
import { Routes } from "discord-api-types/v10";
import { describe, expect, it, vi } from "vitest";
import { createDiscordDraftStream } from "./draft-stream.js";
describe("createDiscordDraftStream", () => {
it("holds the first preview until minInitialChars is reached", async () => {
const rest = {
post: vi.fn(async () => ({ id: "m1" })),
patch: vi.fn(async () => undefined),
delete: vi.fn(async () => undefined),
};
const stream = createDiscordDraftStream({
rest: rest as never,
channelId: "c1",
throttleMs: 250,
minInitialChars: 5,
});
stream.update("hey");
await stream.flush();
expect(rest.post).not.toHaveBeenCalled();
expect(stream.messageId()).toBeUndefined();
});
it("sends a reply preview, then edits the same message on later flushes", async () => {
const rest = {
post: vi.fn(async () => ({ id: "m1" })),
patch: vi.fn(async () => undefined),
delete: vi.fn(async () => undefined),
};
const stream = createDiscordDraftStream({
rest: rest as never,
channelId: "c1",
throttleMs: 250,
replyToMessageId: () => " parent-1 ",
});
stream.update("first draft");
await stream.flush();
stream.update("second draft");
await stream.flush();
expect(rest.post).toHaveBeenCalledWith(Routes.channelMessages("c1"), {
body: {
content: "first draft",
message_reference: {
message_id: "parent-1",
fail_if_not_exists: false,
},
},
});
expect(rest.patch).toHaveBeenCalledWith(Routes.channelMessage("c1", "m1"), {
body: { content: "second draft" },
});
expect(stream.messageId()).toBe("m1");
});
it("stops previewing and warns once text exceeds the configured limit", async () => {
const rest = {
post: vi.fn(async () => ({ id: "m1" })),
patch: vi.fn(async () => undefined),
delete: vi.fn(async () => undefined),
};
const warn = vi.fn();
const stream = createDiscordDraftStream({
rest: rest as never,
channelId: "c1",
maxChars: 5,
throttleMs: 250,
warn,
});
stream.update("123456");
await stream.flush();
expect(rest.post).not.toHaveBeenCalled();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("discord stream preview stopped"),
);
expect(stream.messageId()).toBeUndefined();
});
});

View File

@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import {
looksLikeDiscordTargetId,
normalizeDiscordMessagingTarget,
normalizeDiscordOutboundTarget,
} from "./normalize.js";
describe("discord target normalization", () => {
it("normalizes bare messaging target ids to channel targets", () => {
expect(normalizeDiscordMessagingTarget("1234567890")).toBe("channel:1234567890");
});
it("keeps explicit outbound targets and rejects missing recipients", () => {
expect(normalizeDiscordOutboundTarget("1234567890")).toEqual({
ok: true,
to: "channel:1234567890",
});
expect(normalizeDiscordOutboundTarget("user:42")).toEqual({
ok: true,
to: "user:42",
});
const result = normalizeDiscordOutboundTarget(" ");
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.message).toContain("Discord recipient is required");
}
});
it("detects Discord-style target identifiers", () => {
expect(looksLikeDiscordTargetId("<@!123456>")).toBe(true);
expect(looksLikeDiscordTargetId("user:123456")).toBe(true);
expect(looksLikeDiscordTargetId("discord:123456")).toBe(true);
expect(looksLikeDiscordTargetId("123456")).toBe(true);
expect(looksLikeDiscordTargetId("hello world")).toBe(false);
});
});

View File

@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
import { collectDiscordStatusIssues } from "./status-issues.js";
describe("collectDiscordStatusIssues", () => {
it("reports disabled message content intent and unresolved channel ids", () => {
const issues = collectDiscordStatusIssues([
{
accountId: "ops",
enabled: true,
configured: true,
application: {
intents: {
messageContent: "disabled",
},
},
audit: {
unresolvedChannels: 2,
},
} as ChannelAccountSnapshot,
]);
expect(issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
channel: "discord",
accountId: "ops",
kind: "intent",
}),
expect.objectContaining({
channel: "discord",
accountId: "ops",
kind: "config",
}),
]),
);
});
it("reports channel permission failures with match metadata", () => {
const issues = collectDiscordStatusIssues([
{
accountId: "ops",
enabled: true,
configured: true,
audit: {
channels: [
{
channelId: "123",
ok: false,
missing: ["ViewChannel", "SendMessages"],
error: "403",
matchKey: "alerts",
matchSource: "guilds.ops.channels",
},
],
},
} as ChannelAccountSnapshot,
]);
expect(issues).toHaveLength(1);
expect(issues[0]).toMatchObject({
channel: "discord",
accountId: "ops",
kind: "permissions",
});
expect(issues[0]?.message).toContain("Channel 123 permission check failed");
expect(issues[0]?.message).toContain("alerts");
expect(issues[0]?.message).toContain("guilds.ops.channels");
});
it("ignores accounts that are not enabled and configured", () => {
expect(
collectDiscordStatusIssues([
{
accountId: "ops",
enabled: false,
configured: true,
} as ChannelAccountSnapshot,
]),
).toEqual([]);
});
});