Files
openclaw/extensions/slack/src/message-action-dispatch.test.ts
2026-05-02 03:58:30 +01:00

339 lines
8.4 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { handleSlackMessageAction } from "./message-action-dispatch.js";
function createInvokeSpy() {
return vi.fn(async (action: Record<string, unknown>) => ({
ok: true,
content: action,
}));
}
describe("handleSlackMessageAction", () => {
it("merges presentation and interactive blocks when sending", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "send",
cfg: {},
params: {
to: "channel:C1",
message: "Deploy?",
presentation: {
blocks: [{ type: "text", text: "Deploy summary" }],
},
interactive: {
blocks: [
{
type: "buttons",
buttons: [{ label: "Approve", value: "approve" }],
},
],
},
},
} as never,
invoke: invoke as never,
});
const action = invoke.mock.calls[0]?.[0] as {
blocks?: Array<{ type?: string; elements?: Array<{ value?: string }> }>;
};
expect(action.blocks).toEqual([
expect.objectContaining({ type: "section" }),
expect.objectContaining({
type: "actions",
elements: [expect.objectContaining({ value: "approve" })],
}),
]);
});
it("keeps generated Slack control ids unique when presentation and interactive controls are merged", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "send",
cfg: {},
params: {
to: "channel:C1",
message: "Deploy?",
presentation: {
blocks: [
{
type: "buttons",
buttons: [{ label: "Stage", value: "stage" }],
},
],
},
interactive: {
blocks: [
{
type: "buttons",
buttons: [{ label: "Approve", value: "approve" }],
},
],
},
},
} as never,
invoke: invoke as never,
});
const action = invoke.mock.calls[0]?.[0] as {
blocks?: Array<{
block_id?: string;
elements?: Array<{ action_id?: string; value?: string }>;
}>;
};
expect(action.blocks).toEqual([
expect.objectContaining({
block_id: "openclaw_reply_buttons_1",
elements: [expect.objectContaining({ action_id: "openclaw:reply_button:1:1" })],
}),
expect.objectContaining({
block_id: "openclaw_reply_buttons_2",
elements: [expect.objectContaining({ action_id: "openclaw:reply_button:2:1" })],
}),
]);
});
it("passes media and rendered interactive blocks through for split Slack delivery", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "send",
cfg: {},
params: {
to: "channel:C1",
message: "Approval required",
media: "https://example.com/report.md",
interactive: {
blocks: [
{
type: "buttons",
buttons: [{ label: "Approve", value: "approve" }],
},
],
},
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledOnce();
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "sendMessage",
to: "channel:C1",
content: "Approval required",
mediaUrl: "https://example.com/report.md",
blocks: [
expect.objectContaining({
type: "actions",
elements: [expect.objectContaining({ value: "approve" })],
}),
],
}),
expect.any(Object),
undefined,
);
});
it("maps upload-file to the internal uploadFile action", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "upload-file",
cfg: {},
params: {
to: "user:U1",
filePath: "/tmp/report.png",
initialComment: "fresh build",
filename: "build.png",
title: "Build Screenshot",
threadId: "111.222",
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "uploadFile",
to: "user:U1",
filePath: "/tmp/report.png",
initialComment: "fresh build",
filename: "build.png",
title: "Build Screenshot",
threadTs: "111.222",
}),
expect.any(Object),
undefined,
);
});
it("maps upload-file aliases to upload params", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "upload-file",
cfg: {},
params: {
channelId: "C1",
media: "/tmp/chart.png",
message: "chart attached",
replyTo: "333.444",
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "uploadFile",
to: "C1",
filePath: "/tmp/chart.png",
initialComment: "chart attached",
threadTs: "333.444",
}),
expect.any(Object),
undefined,
);
});
it("maps upload-file path alias to filePath", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "upload-file",
cfg: {},
params: {
to: "channel:C1",
path: "/tmp/report.txt",
initialComment: "path alias",
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "uploadFile",
to: "channel:C1",
filePath: "/tmp/report.txt",
initialComment: "path alias",
}),
expect.any(Object),
undefined,
);
});
it("forwards messageId for read actions", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "read",
cfg: {},
params: {
channelId: "C1",
messageId: "1712345678.654321",
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "readMessages",
channelId: "C1",
messageId: "1712345678.654321",
}),
{},
);
});
it("requires filePath, path, or media for upload-file", async () => {
await expect(
handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "upload-file",
cfg: {},
params: {
to: "channel:C1",
},
} as never,
invoke: createInvokeSpy() as never,
}),
).rejects.toThrow(/upload-file requires filePath, path, or media/i);
});
it("maps download-file to the internal downloadFile action", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "download-file",
cfg: {},
params: {
channelId: "C1",
fileId: "F123",
threadId: "111.222",
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "downloadFile",
fileId: "F123",
channelId: "C1",
threadId: "111.222",
}),
expect.any(Object),
);
});
it("maps download-file target aliases to scope fields", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "download-file",
cfg: {},
params: {
to: "channel:C2",
fileId: "F999",
replyTo: "333.444",
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "downloadFile",
fileId: "F999",
channelId: "channel:C2",
threadId: "333.444",
}),
expect.any(Object),
);
});
});