Files
openclaw/extensions/slack/src/channel.test.ts
2026-05-02 05:34:56 +01:00

1007 lines
28 KiB
TypeScript

import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { slackPlugin } from "./channel.js";
import { slackOutbound } from "./outbound-adapter.js";
import * as probeModule from "./probe.js";
import type { OpenClawConfig } from "./runtime-api.js";
import { clearSlackRuntime, setSlackRuntime } from "./runtime.js";
const { handleSlackActionMock } = vi.hoisted(() => ({
handleSlackActionMock: vi.fn(),
}));
const { sendMessageSlackMock } = vi.hoisted(() => ({
sendMessageSlackMock: vi.fn(),
}));
vi.mock("./action-runtime.js", async () => {
const actual = await vi.importActual<typeof import("./action-runtime.js")>("./action-runtime.js");
return {
...actual,
handleSlackAction: handleSlackActionMock,
};
});
vi.mock("./send.runtime.js", () => ({
sendMessageSlack: sendMessageSlackMock,
}));
beforeEach(async () => {
handleSlackActionMock.mockReset();
sendMessageSlackMock.mockReset();
sendMessageSlackMock.mockResolvedValue({ messageId: "msg-1", channelId: "D123" });
setSlackRuntime({
channel: {
slack: {
handleSlackAction: handleSlackActionMock,
},
},
} as never);
});
async function getSlackConfiguredState(cfg: OpenClawConfig) {
const account = slackPlugin.config.resolveAccount(cfg, "default");
return {
configured: slackPlugin.config.isConfigured?.(account, cfg),
snapshot: await slackPlugin.status?.buildAccountSnapshot?.({
account,
cfg,
runtime: undefined,
}),
};
}
function requireSlackHandleAction() {
const handleAction = slackPlugin.actions?.handleAction;
if (!handleAction) {
throw new Error("slack actions.handleAction unavailable");
}
return handleAction;
}
function requireSlackSendText() {
const sendText = slackPlugin.outbound?.sendText;
if (!sendText) {
throw new Error("slack outbound.sendText unavailable");
}
return sendText;
}
function requireSlackSendMedia() {
const sendMedia = slackPlugin.outbound?.sendMedia;
if (!sendMedia) {
throw new Error("slack outbound.sendMedia unavailable");
}
return sendMedia;
}
function requireSlackSendPayload() {
const sendPayload = slackPlugin.outbound?.sendPayload ?? slackOutbound.sendPayload;
if (!sendPayload) {
throw new Error("slack outbound.sendPayload unavailable");
}
return sendPayload;
}
function requireSlackListPeers() {
const listPeers = slackPlugin.directory?.listPeers;
if (!listPeers) {
throw new Error("slack directory.listPeers unavailable");
}
return listPeers;
}
describe("slackPlugin actions", () => {
it("prefers session lookup for announce target routing", () => {
expect(slackPlugin.meta.preferSessionLookupForAnnounceTarget).toBe(true);
});
it("owns unified message tool discovery", () => {
const discovery = slackPlugin.actions?.describeMessageTool({
cfg: {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
capabilities: { interactiveReplies: true },
},
},
},
});
expect(discovery?.actions).toContain("send");
expect(discovery?.capabilities).toEqual(expect.arrayContaining(["presentation"]));
expect(discovery?.schema).toBeUndefined();
});
it("honors the selected Slack account during message tool discovery", () => {
const cfg: OpenClawConfig = {
channels: {
slack: {
botToken: "xoxb-root",
appToken: "xapp-root",
actions: {
reactions: false,
messages: false,
pins: false,
memberInfo: false,
emojiList: false,
},
capabilities: {
interactiveReplies: false,
},
accounts: {
default: {
botToken: "xoxb-default",
appToken: "xapp-default",
actions: {
reactions: false,
messages: false,
pins: false,
memberInfo: false,
emojiList: false,
},
capabilities: {
interactiveReplies: false,
},
},
work: {
botToken: "xoxb-work",
appToken: "xapp-work",
actions: {
reactions: true,
messages: true,
pins: false,
memberInfo: false,
emojiList: false,
},
capabilities: {
interactiveReplies: true,
},
},
},
},
},
};
expect(slackPlugin.actions?.describeMessageTool?.({ cfg, accountId: "default" })).toMatchObject(
{
actions: ["send"],
capabilities: ["presentation"],
},
);
expect(slackPlugin.actions?.describeMessageTool?.({ cfg, accountId: "work" })).toMatchObject({
actions: [
"send",
"react",
"reactions",
"read",
"edit",
"delete",
"download-file",
"upload-file",
],
capabilities: expect.arrayContaining(["presentation"]),
});
});
it("uses configured defaultAccount for pairing approval notifications", async () => {
const cfg = {
channels: {
slack: {
defaultAccount: "work",
accounts: {
work: {
botToken: "xoxb-work",
},
},
},
},
} as OpenClawConfig;
setSlackRuntime({
config: {
loadConfig: () => cfg,
},
} as never);
const notify = slackPlugin.pairing?.notifyApproval;
if (!notify) {
throw new Error("slack pairing notify unavailable");
}
await notify({
cfg,
id: "U12345678",
});
expect(sendMessageSlackMock).toHaveBeenCalledWith(
"user:U12345678",
expect.stringContaining("approved"),
expect.objectContaining({
accountId: "work",
cfg,
token: "xoxb-work",
}),
);
});
it("does not expose Slack-native message tool schema", () => {
const discovery = slackPlugin.actions?.describeMessageTool({
cfg: {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
},
},
} as OpenClawConfig,
});
expect(discovery?.schema).toBeUndefined();
});
it("treats interactive reply payloads as structured Slack payloads", () => {
const hasStructuredReplyPayload = slackPlugin.messaging?.hasStructuredReplyPayload;
if (!hasStructuredReplyPayload) {
throw new Error("slack messaging.hasStructuredReplyPayload unavailable");
}
expect(
hasStructuredReplyPayload({
payload: {
text: "Choose",
interactive: {
blocks: [{ type: "buttons", buttons: [{ label: "Retry", value: "retry" }] }],
},
},
}),
).toBe(true);
});
it("forwards read threadId to Slack action handler", async () => {
handleSlackActionMock.mockResolvedValueOnce({ messages: [], hasMore: false });
const handleAction = requireSlackHandleAction();
await handleAction({
action: "read",
channel: "slack",
accountId: "default",
cfg: {},
params: {
channelId: "C123",
threadId: "1712345678.123456",
messageId: "1712345678.654321",
},
});
expect(handleSlackActionMock).toHaveBeenCalledWith(
expect.objectContaining({
action: "readMessages",
channelId: "C123",
threadId: "1712345678.123456",
messageId: "1712345678.654321",
}),
{},
undefined,
);
});
it("forwards media access through the bundled Slack action invoke path", async () => {
handleSlackActionMock.mockResolvedValueOnce({ ok: true });
const handleAction = requireSlackHandleAction();
const mediaLocalRoots = ["/tmp/workspace-agent"];
const mediaReadFile = vi.fn(async () => Buffer.from("file"));
await handleAction({
action: "upload-file",
channel: "slack",
accountId: "default",
cfg: {},
params: {
to: "channel:C123",
filePath: "/tmp/workspace-agent/renders/file.wav",
initialComment: "render",
},
mediaLocalRoots,
mediaReadFile,
toolContext: {
currentChannelId: "C123",
replyToMode: "all",
},
} as never);
expect(handleSlackActionMock).toHaveBeenCalledWith(
expect.objectContaining({
action: "uploadFile",
to: "channel:C123",
filePath: "/tmp/workspace-agent/renders/file.wav",
initialComment: "render",
}),
{},
expect.objectContaining({
currentChannelId: "C123",
replyToMode: "all",
mediaLocalRoots,
mediaReadFile,
}),
);
});
});
describe("slackPlugin status", () => {
it("uses the direct Slack probe helper when runtime is not initialized", async () => {
const probeSpy = vi.spyOn(probeModule, "probeSlack").mockResolvedValueOnce({
ok: true,
status: 200,
bot: { id: "B1", name: "openclaw-bot" },
team: { id: "T1", name: "OpenClaw" },
});
clearSlackRuntime();
const cfg = {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
},
},
} as OpenClawConfig;
const account = slackPlugin.config.resolveAccount(cfg, "default");
const result = await slackPlugin.status!.probeAccount!({
account,
timeoutMs: 2500,
cfg,
});
expect(probeSpy).toHaveBeenCalledWith("xoxb-test", 2500);
expect(result).toEqual({
ok: true,
status: 200,
bot: { id: "B1", name: "openclaw-bot" },
team: { id: "T1", name: "OpenClaw" },
});
});
it("recovers thread routing from mixed-case Slack session keys", async () => {
const resolveRoute = slackPlugin.messaging?.resolveOutboundSessionRoute;
if (!resolveRoute) {
throw new Error("slack messaging.resolveOutboundSessionRoute unavailable");
}
const route = await resolveRoute({
cfg: {} as OpenClawConfig,
agentId: "main",
target: "channel:C1",
currentSessionKey: "agent:main:slack:channel:C1:thread:1712345678.123456",
});
expect(route).toMatchObject({
sessionKey: "agent:main:slack:channel:c1:thread:1712345678.123456",
baseSessionKey: "agent:main:slack:channel:c1",
threadId: "1712345678.123456",
});
});
});
describe("slackPlugin security", () => {
it("normalizes dm allowlist entries with trimmed prefixes", () => {
const resolveDmPolicy = slackPlugin.security?.resolveDmPolicy;
if (!resolveDmPolicy) {
throw new Error("resolveDmPolicy unavailable");
}
const result = resolveDmPolicy({
cfg: {
channels: {
slack: {
dm: { policy: "allowlist", allowFrom: [" slack:U123 "] },
},
},
} as OpenClawConfig,
account: slackPlugin.config.resolveAccount(
{
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
dm: { policy: "allowlist", allowFrom: [" slack:U123 "] },
},
},
} as OpenClawConfig,
"default",
),
});
if (!result) {
throw new Error("slack resolveDmPolicy returned null");
}
expect(result.policy).toBe("allowlist");
expect(result.allowFrom).toEqual([" slack:U123 "]);
expect(result.policyPath).toBe("channels.slack.dmPolicy");
expect(result.allowFromPath).toBe("channels.slack.");
expect(result.normalizeEntry?.(" slack:U123 ")).toBe("U123");
expect(result.normalizeEntry?.(" user:U999 ")).toBe("U999");
});
});
describe("slackPlugin outbound", () => {
const cfg = {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
},
},
};
it("treats ACP block text as visible delivered output", () => {
expect(
slackPlugin.outbound?.shouldTreatDeliveredTextAsVisible?.({
kind: "block",
text: "hello",
}),
).toBe(true);
expect(
slackPlugin.outbound?.shouldTreatDeliveredTextAsVisible?.({
kind: "tool",
text: "hello",
}),
).toBe(false);
});
it("advertises the 8000-character Slack default chunk limit", () => {
expect(slackOutbound.textChunkLimit).toBe(8000);
expect(slackPlugin.outbound?.textChunkLimit).toBe(8000);
});
it("uses threadId as threadTs fallback for sendText", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-text" });
const sendText = requireSlackSendText();
const result = await sendText({
cfg,
to: "C123",
text: "hello",
accountId: "default",
threadId: "1712345678.123456",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"C123",
"hello",
expect.objectContaining({
threadTs: "1712345678.123456",
}),
);
expect(result).toEqual({ channel: "slack", messageId: "m-text" });
});
it("prefers replyToId over threadId for sendMedia", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-media" });
const sendMedia = requireSlackSendMedia();
const result = await sendMedia({
cfg,
to: "C999",
text: "caption",
mediaUrl: "https://example.com/image.png",
accountId: "default",
replyToId: "1712000000.000001",
threadId: "1712345678.123456",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"C999",
"caption",
expect.objectContaining({
mediaUrl: "https://example.com/image.png",
threadTs: "1712000000.000001",
}),
);
expect(result).toEqual({ channel: "slack", messageId: "m-media" });
});
it("falls back to threadId when replyToId is not a Slack thread timestamp", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-text" });
const sendText = requireSlackSendText();
const result = await sendText({
cfg,
to: "C123",
text: "hello",
accountId: "default",
replyToId: "msg-internal-1",
threadId: "1712345678.123456",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"C123",
"hello",
expect.objectContaining({
threadTs: "1712345678.123456",
}),
);
expect(result).toEqual({ channel: "slack", messageId: "m-text" });
});
it("does not stringify numeric Slack thread ids", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-text" });
const sendText = requireSlackSendText();
await sendText({
cfg,
to: "C123",
text: "hello",
accountId: "default",
threadId: 1712345678.123456,
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"C123",
"hello",
expect.objectContaining({
threadTs: undefined,
}),
);
});
it("falls back to auto-thread lookup when replyToId is not a Slack thread timestamp", () => {
const resolveAutoThreadId = slackPlugin.threading?.resolveAutoThreadId;
if (!resolveAutoThreadId) {
throw new Error("slack threading.resolveAutoThreadId unavailable");
}
const threadId = resolveAutoThreadId({
cfg,
to: "channel:C123",
replyToId: "msg-internal-1",
toolContext: {
currentChannelId: "C123",
currentThreadTs: "1712345678.123456",
replyToMode: "all",
},
});
expect(threadId).toBe("1712345678.123456");
});
it("does not recover invalid Slack auto-thread anchors", () => {
const resolveAutoThreadId = slackPlugin.threading?.resolveAutoThreadId;
if (!resolveAutoThreadId) {
throw new Error("slack threading.resolveAutoThreadId unavailable");
}
const threadId = resolveAutoThreadId({
cfg,
to: "channel:C123",
replyToId: "msg-internal-1",
toolContext: {
currentChannelId: "C123",
currentThreadTs: "thread-root",
replyToMode: "all",
},
});
expect(threadId).toBeUndefined();
});
it("does not stringify numeric thread ids in tool context", () => {
const buildToolContext = slackPlugin.threading?.buildToolContext;
if (!buildToolContext) {
throw new Error("slack threading.buildToolContext unavailable");
}
const context = buildToolContext({
cfg,
context: {
To: "channel:C123",
MessageThreadId: 1712345678.123456,
},
});
expect(context?.currentThreadTs).toBeUndefined();
});
it("falls back to threadId in reply transport when replyToId is not a Slack thread timestamp", () => {
const resolveReplyTransport = slackPlugin.threading?.resolveReplyTransport;
if (!resolveReplyTransport) {
throw new Error("slack threading.resolveReplyTransport unavailable");
}
expect(
resolveReplyTransport({
cfg,
replyToId: "msg-internal-1",
threadId: "1712345678.123456",
}),
).toEqual({
replyToId: "1712345678.123456",
threadId: null,
});
});
it("forwards mediaLocalRoots for sendMedia", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-media-local" });
const sendMedia = requireSlackSendMedia();
const mediaLocalRoots = ["/tmp/workspace"];
const result = await sendMedia({
cfg,
to: "C999",
text: "caption",
mediaUrl: "/tmp/workspace/image.png",
mediaLocalRoots,
accountId: "default",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"C999",
"caption",
expect.objectContaining({
mediaUrl: "/tmp/workspace/image.png",
mediaLocalRoots,
}),
);
expect(result).toEqual({ channel: "slack", messageId: "m-media-local" });
});
it("sends block payload media first, then the final block message", async () => {
const sendSlack = vi
.fn()
.mockResolvedValueOnce({ messageId: "m-media-1" })
.mockResolvedValueOnce({ messageId: "m-media-2" })
.mockResolvedValueOnce({ messageId: "m-final" });
const sendPayload = requireSlackSendPayload();
const result = await sendPayload({
cfg,
to: "C999",
text: "",
payload: {
text: "hello",
mediaUrls: ["https://example.com/1.png", "https://example.com/2.png"],
presentation: {
blocks: [{ type: "text", text: "Block body" }],
},
},
accountId: "default",
deps: { sendSlack },
mediaLocalRoots: ["/tmp/media"],
});
expect(sendSlack).toHaveBeenCalledTimes(3);
expect(sendSlack).toHaveBeenNthCalledWith(
1,
"C999",
"",
expect.objectContaining({
mediaUrl: "https://example.com/1.png",
mediaLocalRoots: ["/tmp/media"],
}),
);
expect(sendSlack).toHaveBeenNthCalledWith(
2,
"C999",
"",
expect.objectContaining({
mediaUrl: "https://example.com/2.png",
mediaLocalRoots: ["/tmp/media"],
}),
);
expect(sendSlack).toHaveBeenNthCalledWith(
3,
"C999",
"hello",
expect.objectContaining({
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: "Block body",
},
},
],
}),
);
expect(result).toEqual({ channel: "slack", messageId: "m-final" });
});
it("renders shared interactive payloads into Slack Block Kit via plugin outbound", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-interactive" });
const sendPayload = requireSlackSendPayload();
const result = await sendPayload({
cfg,
to: "user:U123",
text: "",
payload: {
text: "Slack interactive smoke.",
interactive: {
blocks: [
{
type: "text",
text: "Slack interactive smoke.",
},
{
type: "buttons",
buttons: [
{ label: "Approve", value: "approve" },
{ label: "Reject", value: "reject" },
],
},
{
type: "select",
placeholder: "Choose a target",
options: [
{ label: "Canary", value: "canary" },
{ label: "Production", value: "production" },
],
},
],
},
},
accountId: "default",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"user:U123",
"Slack interactive smoke.",
expect.objectContaining({
blocks: [
expect.objectContaining({
type: "section",
}),
expect.objectContaining({
type: "actions",
elements: [
expect.objectContaining({ type: "button", value: "approve" }),
expect.objectContaining({ type: "button", value: "reject" }),
],
}),
expect.objectContaining({
type: "actions",
elements: [
expect.objectContaining({
type: "static_select",
options: [
expect.objectContaining({ value: "canary" }),
expect.objectContaining({ value: "production" }),
],
}),
],
}),
],
}),
);
expect(result).toEqual({ channel: "slack", messageId: "m-interactive" });
});
});
describe("slackPlugin directory", () => {
it("lists configured peers without throwing a ReferenceError", async () => {
const listPeers = requireSlackListPeers();
await expect(
listPeers({
cfg: {
channels: {
slack: {
dms: {
U123: {},
},
},
},
},
runtime: createRuntimeEnv(),
}),
).resolves.toEqual([{ id: "user:u123", kind: "user" }]);
});
});
describe("slackPlugin agentPrompt", () => {
it("tells agents interactive replies are disabled by default", () => {
const hints = slackPlugin.agentPrompt?.messageToolHints?.({
cfg: {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
},
},
},
});
expect(hints).toEqual([
"- Slack interactive replies are disabled. If needed, ask to set `channels.slack.capabilities.interactiveReplies=true` (or the same under `channels.slack.accounts.<account>.capabilities`).",
]);
});
it("shows Slack interactive reply directives when enabled", () => {
const hints = slackPlugin.agentPrompt?.messageToolHints?.({
cfg: {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
capabilities: { interactiveReplies: true },
},
},
},
});
expect(hints).toContain(
"- Prefer Slack buttons/selects for 2-5 discrete choices or parameter picks instead of asking the user to type one.",
);
expect(hints).toContain(
"- Slack interactive replies: use `[[slack_buttons: Label:value, Other:other]]` to add action buttons that route clicks back as Slack interaction system events.",
);
expect(hints).toContain(
"- Slack selects: use `[[slack_select: Placeholder | Label:value, Other:other]]` to add a static select menu that routes the chosen value back as a Slack interaction system event.",
);
});
});
describe("slackPlugin outbound new targets", () => {
const cfg = {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
},
},
};
it("sends to a new user target via DM without erroring", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-new-user", channelId: "D999" });
const sendText = requireSlackSendText();
const result = await sendText({
cfg,
to: "user:U99NEW",
text: "hello new user",
accountId: "default",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"user:U99NEW",
"hello new user",
expect.objectContaining({ cfg }),
);
expect(result).toEqual({ channel: "slack", messageId: "m-new-user", channelId: "D999" });
});
it("sends to a new channel target without erroring", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-new-chan", channelId: "C555" });
const sendText = requireSlackSendText();
const result = await sendText({
cfg,
to: "channel:C555NEW",
text: "hello channel",
accountId: "default",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"channel:C555NEW",
"hello channel",
expect.objectContaining({ cfg }),
);
expect(result).toEqual({ channel: "slack", messageId: "m-new-chan", channelId: "C555" });
});
it("sends media to a new user target without erroring", async () => {
const sendSlack = vi.fn().mockResolvedValue({ messageId: "m-new-media", channelId: "D888" });
const sendMedia = requireSlackSendMedia();
const result = await sendMedia({
cfg,
to: "user:U88NEW",
text: "here is a file",
mediaUrl: "https://example.com/file.png",
accountId: "default",
deps: { sendSlack },
});
expect(sendSlack).toHaveBeenCalledWith(
"user:U88NEW",
"here is a file",
expect.objectContaining({
cfg,
mediaUrl: "https://example.com/file.png",
}),
);
expect(result).toEqual({ channel: "slack", messageId: "m-new-media", channelId: "D888" });
});
});
describe("slackPlugin config", () => {
it("treats HTTP mode accounts with bot token + signing secret as configured", async () => {
const cfg: OpenClawConfig = {
channels: {
slack: {
mode: "http",
botToken: "xoxb-http",
signingSecret: "secret-http", // pragma: allowlist secret
},
},
};
const { configured, snapshot } = await getSlackConfiguredState(cfg);
expect(configured).toBe(true);
expect(snapshot?.configured).toBe(true);
});
it("keeps socket mode requiring app token", async () => {
const cfg: OpenClawConfig = {
channels: {
slack: {
mode: "socket",
botToken: "xoxb-socket",
},
},
};
const { configured, snapshot } = await getSlackConfiguredState(cfg);
expect(configured).toBe(false);
expect(snapshot?.configured).toBe(false);
});
it("does not mark partial configured-unavailable token status as configured", async () => {
const snapshot = await slackPlugin.status?.buildAccountSnapshot?.({
account: {
accountId: "default",
name: "Default",
enabled: true,
configured: false,
botTokenStatus: "configured_unavailable",
appTokenStatus: "missing",
botTokenSource: "config",
appTokenSource: "none",
config: {},
} as never,
cfg: {} as OpenClawConfig,
runtime: undefined,
});
expect(snapshot?.configured).toBe(false);
expect(snapshot?.botTokenStatus).toBe("configured_unavailable");
expect(snapshot?.appTokenStatus).toBe("missing");
});
it("keeps HTTP mode signing-secret unavailable accounts configured in snapshots", async () => {
const snapshot = await slackPlugin.status?.buildAccountSnapshot?.({
account: {
accountId: "default",
name: "Default",
enabled: true,
configured: true,
mode: "http",
botTokenStatus: "available",
signingSecretStatus: "configured_unavailable", // pragma: allowlist secret
botTokenSource: "config",
signingSecretSource: "config", // pragma: allowlist secret
config: {
mode: "http",
botToken: "xoxb-http",
signingSecret: { source: "env", provider: "default", id: "SLACK_SIGNING_SECRET" },
},
} as never,
cfg: {} as OpenClawConfig,
runtime: undefined,
});
expect(snapshot?.configured).toBe(true);
expect(snapshot?.botTokenStatus).toBe("available");
expect(snapshot?.signingSecretStatus).toBe("configured_unavailable");
});
});