openskynet / src /slack /monitor /events /interactions.test.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import { describe, expect, it, vi } from "vitest";
import { registerSlackInteractionEvents } from "./interactions.js";
const enqueueSystemEventMock = vi.fn();
vi.mock("../../../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
}));
type RegisteredHandler = (args: {
ack: () => Promise<void>;
body: {
user: { id: string };
team?: { id?: string };
trigger_id?: string;
response_url?: string;
channel?: { id?: string };
container?: { channel_id?: string; message_ts?: string; thread_ts?: string };
message?: { ts?: string; text?: string; blocks?: unknown[] };
};
action: Record<string, unknown>;
respond?: (payload: { text: string; response_type: string }) => Promise<void>;
}) => Promise<void>;
type RegisteredViewHandler = (args: {
ack: () => Promise<void>;
body: {
user?: { id?: string };
team?: { id?: string };
view?: {
id?: string;
callback_id?: string;
private_metadata?: string;
root_view_id?: string;
previous_view_id?: string;
external_id?: string;
hash?: string;
state?: { values?: Record<string, Record<string, Record<string, unknown>>> };
};
};
}) => Promise<void>;
type RegisteredViewClosedHandler = (args: {
ack: () => Promise<void>;
body: {
user?: { id?: string };
team?: { id?: string };
view?: {
id?: string;
callback_id?: string;
private_metadata?: string;
root_view_id?: string;
previous_view_id?: string;
external_id?: string;
hash?: string;
state?: { values?: Record<string, Record<string, Record<string, unknown>>> };
};
is_cleared?: boolean;
};
}) => Promise<void>;
function createContext(overrides?: {
dmEnabled?: boolean;
dmPolicy?: "open" | "allowlist" | "pairing" | "disabled";
allowFrom?: string[];
allowNameMatching?: boolean;
channelsConfig?: Record<string, { users?: string[] }>;
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
isChannelAllowed?: (params: {
channelId?: string;
channelName?: string;
channelType?: "im" | "mpim" | "channel" | "group";
}) => boolean;
resolveUserName?: (userId: string) => Promise<{ name?: string }>;
resolveChannelName?: (channelId: string) => Promise<{
name?: string;
type?: "im" | "mpim" | "channel" | "group";
}>;
}) {
let handler: RegisteredHandler | null = null;
let viewHandler: RegisteredViewHandler | null = null;
let viewClosedHandler: RegisteredViewClosedHandler | null = null;
const app = {
action: vi.fn((_matcher: RegExp, next: RegisteredHandler) => {
handler = next;
}),
view: vi.fn((_matcher: RegExp, next: RegisteredViewHandler) => {
viewHandler = next;
}),
viewClosed: vi.fn((_matcher: RegExp, next: RegisteredViewClosedHandler) => {
viewClosedHandler = next;
}),
client: {
chat: {
update: vi.fn().mockResolvedValue(undefined),
},
},
};
const runtimeLog = vi.fn();
const resolveSessionKey = vi.fn().mockReturnValue("agent:ops:slack:channel:C1");
const isChannelAllowed = vi
.fn<
(params: {
channelId?: string;
channelName?: string;
channelType?: "im" | "mpim" | "channel" | "group";
}) => boolean
>()
.mockImplementation((params) => overrides?.isChannelAllowed?.(params) ?? true);
const resolveUserName = vi
.fn<(userId: string) => Promise<{ name?: string }>>()
.mockImplementation((userId) => overrides?.resolveUserName?.(userId) ?? Promise.resolve({}));
const resolveChannelName = vi
.fn<
(channelId: string) => Promise<{
name?: string;
type?: "im" | "mpim" | "channel" | "group";
}>
>()
.mockImplementation(
(channelId) => overrides?.resolveChannelName?.(channelId) ?? Promise.resolve({}),
);
const ctx = {
app,
runtime: { log: runtimeLog },
dmEnabled: overrides?.dmEnabled ?? true,
dmPolicy: overrides?.dmPolicy ?? ("open" as const),
allowFrom: overrides?.allowFrom ?? [],
allowNameMatching: overrides?.allowNameMatching ?? false,
channelsConfig: overrides?.channelsConfig ?? {},
defaultRequireMention: true,
shouldDropMismatchedSlackEvent: (body: unknown) =>
overrides?.shouldDropMismatchedSlackEvent?.(body) ?? false,
isChannelAllowed,
resolveUserName,
resolveChannelName,
resolveSlackSystemEventSessionKey: resolveSessionKey,
};
return {
ctx,
app,
runtimeLog,
resolveSessionKey,
isChannelAllowed,
resolveUserName,
resolveChannelName,
getHandler: () => handler,
getViewHandler: () => viewHandler,
getViewClosedHandler: () => viewClosedHandler,
};
}
describe("registerSlackInteractionEvents", () => {
it("enqueues structured events and updates button rows", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler, resolveSessionKey } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
const respond = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
respond,
body: {
user: { id: "U123" },
team: { id: "T9" },
trigger_id: "123.trigger",
response_url: "https://hooks.slack.test/response",
channel: { id: "C1" },
container: { channel_id: "C1", message_ts: "100.200", thread_ts: "100.100" },
message: {
ts: "100.200",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "verify_block",
elements: [{ type: "button", action_id: "openclaw:verify" }],
},
],
},
},
action: {
type: "button",
action_id: "openclaw:verify",
block_id: "verify_block",
value: "approved",
text: { type: "plain_text", text: "Approve" },
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
expect(eventText.startsWith("Slack interaction: ")).toBe(true);
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
actionId: string;
actionType: string;
value: string;
userId: string;
teamId?: string;
triggerId?: string;
responseUrl?: string;
channelId: string;
messageTs: string;
threadTs?: string;
};
expect(payload).toMatchObject({
actionId: "openclaw:verify",
actionType: "button",
value: "approved",
userId: "U123",
teamId: "T9",
triggerId: "[redacted]",
responseUrl: "[redacted]",
channelId: "C1",
messageTs: "100.200",
threadTs: "100.100",
});
expect(resolveSessionKey).toHaveBeenCalledWith({
channelId: "C1",
channelType: "channel",
senderId: "U123",
});
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
});
it("drops block actions when mismatch guard triggers", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler } = createContext({
shouldDropMismatchedSlackEvent: () => true,
});
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
const respond = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
respond,
body: {
user: { id: "U123" },
team: { id: "T9" },
channel: { id: "C1" },
container: { channel_id: "C1", message_ts: "100.200" },
message: {
ts: "100.200",
text: "fallback",
blocks: [],
},
},
action: {
type: "button",
action_id: "openclaw:verify",
},
});
expect(ack).toHaveBeenCalledTimes(1);
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expect(app.client.chat.update).not.toHaveBeenCalled();
expect(respond).not.toHaveBeenCalled();
});
it("drops modal lifecycle payloads when mismatch guard triggers", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewHandler, getViewClosedHandler } = createContext({
shouldDropMismatchedSlackEvent: () => true,
});
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
const viewClosedHandler = getViewClosedHandler();
expect(viewHandler).toBeTruthy();
expect(viewClosedHandler).toBeTruthy();
const ackSubmit = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack: ackSubmit,
body: {
user: { id: "U123" },
team: { id: "T9" },
view: {
id: "V123",
callback_id: "openclaw:deploy_form",
private_metadata: JSON.stringify({ userId: "U123" }),
},
},
});
expect(ackSubmit).toHaveBeenCalledTimes(1);
const ackClosed = vi.fn().mockResolvedValue(undefined);
await viewClosedHandler!({
ack: ackClosed,
body: {
user: { id: "U123" },
team: { id: "T9" },
view: {
id: "V123",
callback_id: "openclaw:deploy_form",
private_metadata: JSON.stringify({ userId: "U123" }),
},
},
});
expect(ackClosed).toHaveBeenCalledTimes(1);
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("captures select values and updates action rows for non-button actions", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U555" },
channel: { id: "C1" },
message: {
ts: "111.222",
blocks: [{ type: "actions", block_id: "select_block", elements: [] }],
},
},
action: {
type: "static_select",
action_id: "openclaw:pick",
block_id: "select_block",
selected_option: {
text: { type: "plain_text", text: "Canary" },
value: "canary",
},
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
actionType: string;
selectedValues?: string[];
selectedLabels?: string[];
};
expect(payload.actionType).toBe("static_select");
expect(payload.selectedValues).toEqual(["canary"]);
expect(payload.selectedLabels).toEqual(["Canary"]);
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
expect(app.client.chat.update).toHaveBeenCalledWith(
expect.objectContaining({
channel: "C1",
ts: "111.222",
blocks: [
{
type: "context",
elements: [{ type: "mrkdwn", text: ":white_check_mark: *Canary* selected by <@U555>" }],
},
],
}),
);
});
it("blocks block actions from users outside configured channel users allowlist", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler } = createContext({
channelsConfig: {
C1: { users: ["U_ALLOWED"] },
},
});
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
const respond = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
respond,
body: {
user: { id: "U_DENIED" },
channel: { id: "C1" },
message: {
ts: "201.202",
blocks: [{ type: "actions", block_id: "verify_block", elements: [] }],
},
},
action: {
type: "button",
action_id: "openclaw:verify",
block_id: "verify_block",
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expect(app.client.chat.update).not.toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith({
text: "You are not authorized to use this control.",
response_type: "ephemeral",
});
});
it("blocks DM block actions when sender is not in allowFrom", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler } = createContext({
dmPolicy: "allowlist",
allowFrom: ["U_OWNER"],
});
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
const respond = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
respond,
body: {
user: { id: "U_ATTACKER" },
channel: { id: "D222" },
message: {
ts: "301.302",
blocks: [{ type: "actions", block_id: "verify_block", elements: [] }],
},
},
action: {
type: "button",
action_id: "openclaw:verify",
block_id: "verify_block",
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expect(app.client.chat.update).not.toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith({
text: "You are not authorized to use this control.",
response_type: "ephemeral",
});
});
it("ignores malformed action payloads after ack and logs warning", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler, runtimeLog } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U666" },
channel: { id: "C1" },
message: {
ts: "777.888",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "verify_block",
elements: [{ type: "button", action_id: "openclaw:verify" }],
},
],
},
},
action: "not-an-action-object" as unknown as Record<string, unknown>,
});
expect(ack).toHaveBeenCalled();
expect(app.client.chat.update).not.toHaveBeenCalled();
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expect(runtimeLog).toHaveBeenCalledWith(expect.stringContaining("slack:interaction malformed"));
});
it("escapes mrkdwn characters in confirmation labels", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U556" },
channel: { id: "C1" },
message: {
ts: "111.223",
blocks: [{ type: "actions", block_id: "select_block", elements: [] }],
},
},
action: {
type: "static_select",
action_id: "openclaw:pick",
block_id: "select_block",
selected_option: {
text: { type: "plain_text", text: "Canary_*`~<&>" },
value: "canary",
},
},
});
expect(ack).toHaveBeenCalled();
expect(app.client.chat.update).toHaveBeenCalledWith(
expect.objectContaining({
channel: "C1",
ts: "111.223",
blocks: [
{
type: "context",
elements: [
{
type: "mrkdwn",
text: ":white_check_mark: *Canary\\_\\*\\`\\~&lt;&amp;&gt;* selected by <@U556>",
},
],
},
],
}),
);
});
it("falls back to container channel and message timestamps", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler, resolveSessionKey } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U111" },
team: { id: "T111" },
container: { channel_id: "C222", message_ts: "222.333", thread_ts: "222.111" },
},
action: {
type: "button",
action_id: "openclaw:container",
block_id: "container_block",
value: "ok",
text: { type: "plain_text", text: "Container" },
},
});
expect(ack).toHaveBeenCalled();
expect(resolveSessionKey).toHaveBeenCalledWith({
channelId: "C222",
channelType: "channel",
senderId: "U111",
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
channelId?: string;
messageTs?: string;
threadTs?: string;
teamId?: string;
};
expect(payload).toMatchObject({
channelId: "C222",
messageTs: "222.333",
threadTs: "222.111",
teamId: "T111",
});
expect(app.client.chat.update).not.toHaveBeenCalled();
});
it("summarizes multi-select confirmations in updated message rows", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U222" },
channel: { id: "C2" },
message: {
ts: "333.444",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "multi_block",
elements: [{ type: "multi_static_select", action_id: "openclaw:multi" }],
},
],
},
},
action: {
type: "multi_static_select",
action_id: "openclaw:multi",
block_id: "multi_block",
selected_options: [
{ text: { type: "plain_text", text: "Alpha" }, value: "alpha" },
{ text: { type: "plain_text", text: "Beta" }, value: "beta" },
{ text: { type: "plain_text", text: "Gamma" }, value: "gamma" },
{ text: { type: "plain_text", text: "Delta" }, value: "delta" },
],
},
});
expect(ack).toHaveBeenCalled();
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
expect(app.client.chat.update).toHaveBeenCalledWith(
expect.objectContaining({
channel: "C2",
ts: "333.444",
blocks: [
{
type: "context",
elements: [
{
type: "mrkdwn",
text: ":white_check_mark: *Alpha, Beta, Gamma +1* selected by <@U222>",
},
],
},
],
}),
);
});
it("renders date/time/datetime picker selections in confirmation rows", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, app, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U333" },
channel: { id: "C3" },
message: {
ts: "555.666",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "date_block",
elements: [{ type: "datepicker", action_id: "openclaw:date" }],
},
{
type: "actions",
block_id: "time_block",
elements: [{ type: "timepicker", action_id: "openclaw:time" }],
},
{
type: "actions",
block_id: "datetime_block",
elements: [{ type: "datetimepicker", action_id: "openclaw:datetime" }],
},
],
},
},
action: {
type: "datepicker",
action_id: "openclaw:date",
block_id: "date_block",
selected_date: "2026-02-16",
},
});
await handler!({
ack,
body: {
user: { id: "U333" },
channel: { id: "C3" },
message: {
ts: "555.667",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "time_block",
elements: [{ type: "timepicker", action_id: "openclaw:time" }],
},
],
},
},
action: {
type: "timepicker",
action_id: "openclaw:time",
block_id: "time_block",
selected_time: "14:30",
},
});
await handler!({
ack,
body: {
user: { id: "U333" },
channel: { id: "C3" },
message: {
ts: "555.668",
text: "fallback",
blocks: [
{
type: "actions",
block_id: "datetime_block",
elements: [{ type: "datetimepicker", action_id: "openclaw:datetime" }],
},
],
},
},
action: {
type: "datetimepicker",
action_id: "openclaw:datetime",
block_id: "datetime_block",
selected_date_time: selectedDateTimeEpoch,
},
});
expect(app.client.chat.update).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
channel: "C3",
ts: "555.666",
blocks: [
{
type: "context",
elements: [
{ type: "mrkdwn", text: ":white_check_mark: *2026-02-16* selected by <@U333>" },
],
},
expect.anything(),
expect.anything(),
],
}),
);
expect(app.client.chat.update).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
channel: "C3",
ts: "555.667",
blocks: [
{
type: "context",
elements: [{ type: "mrkdwn", text: ":white_check_mark: *14:30* selected by <@U333>" }],
},
],
}),
);
expect(app.client.chat.update).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
channel: "C3",
ts: "555.668",
blocks: [
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `:white_check_mark: *${new Date(
selectedDateTimeEpoch * 1000,
).toISOString()}* selected by <@U333>`,
},
],
},
],
}),
);
});
it("captures expanded selection and temporal payload fields", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U321" },
channel: { id: "C2" },
message: { ts: "222.333" },
},
action: {
type: "multi_conversations_select",
action_id: "openclaw:route",
selected_user: "U777",
selected_users: ["U777", "U888"],
selected_channel: "C777",
selected_channels: ["C777", "C888"],
selected_conversation: "G777",
selected_conversations: ["G777", "G888"],
selected_options: [
{ text: { type: "plain_text", text: "Alpha" }, value: "alpha" },
{ text: { type: "plain_text", text: "Alpha" }, value: "alpha" },
{ text: { type: "plain_text", text: "Beta" }, value: "beta" },
],
selected_date: "2026-02-16",
selected_time: "14:30",
selected_date_time: 1_771_700_200,
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
actionType: string;
selectedValues?: string[];
selectedUsers?: string[];
selectedChannels?: string[];
selectedConversations?: string[];
selectedLabels?: string[];
selectedDate?: string;
selectedTime?: string;
selectedDateTime?: number;
};
expect(payload.actionType).toBe("multi_conversations_select");
expect(payload.selectedValues).toEqual([
"alpha",
"beta",
"U777",
"U888",
"C777",
"C888",
"G777",
"G888",
]);
expect(payload.selectedUsers).toEqual(["U777", "U888"]);
expect(payload.selectedChannels).toEqual(["C777", "C888"]);
expect(payload.selectedConversations).toEqual(["G777", "G888"]);
expect(payload.selectedLabels).toEqual(["Alpha", "Beta"]);
expect(payload.selectedDate).toBe("2026-02-16");
expect(payload.selectedTime).toBe("14:30");
expect(payload.selectedDateTime).toBe(1_771_700_200);
});
it("captures workflow button trigger metadata", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const handler = getHandler();
expect(handler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await handler!({
ack,
body: {
user: { id: "U420" },
team: { id: "T420" },
channel: { id: "C420" },
message: { ts: "420.420" },
},
action: {
type: "workflow_button",
action_id: "openclaw:workflow",
block_id: "workflow_block",
text: { type: "plain_text", text: "Launch workflow" },
workflow: {
trigger_url: "https://slack.com/workflows/triggers/T420/12345",
workflow_id: "Wf12345",
},
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
actionType?: string;
workflowTriggerUrl?: string;
workflowId?: string;
teamId?: string;
channelId?: string;
};
expect(payload).toMatchObject({
actionType: "workflow_button",
workflowTriggerUrl: "[redacted]",
workflowId: "Wf12345",
teamId: "T420",
channelId: "C420",
});
});
it("captures modal submissions and enqueues view submission event", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewHandler, resolveSessionKey } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
expect(viewHandler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack,
body: {
user: { id: "U777" },
team: { id: "T1" },
view: {
id: "V123",
callback_id: "openclaw:deploy_form",
root_view_id: "VROOT",
previous_view_id: "VPREV",
external_id: "deploy-ext-1",
hash: "view-hash-1",
private_metadata: JSON.stringify({
channelId: "D123",
channelType: "im",
userId: "U777",
}),
state: {
values: {
env_block: {
env_select: {
type: "static_select",
selected_option: {
text: { type: "plain_text", text: "Production" },
value: "prod",
},
},
},
notes_block: {
notes_input: {
type: "plain_text_input",
value: "ship now",
},
},
},
},
} as unknown as {
id?: string;
callback_id?: string;
root_view_id?: string;
previous_view_id?: string;
external_id?: string;
hash?: string;
state?: { values: Record<string, unknown> };
},
},
} as never);
expect(ack).toHaveBeenCalled();
expect(resolveSessionKey).toHaveBeenCalledWith({
channelId: "D123",
channelType: "im",
senderId: "U777",
});
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
interactionType: string;
actionId: string;
callbackId: string;
viewId: string;
userId: string;
routedChannelId?: string;
rootViewId?: string;
previousViewId?: string;
externalId?: string;
viewHash?: string;
isStackedView?: boolean;
inputs: Array<{ actionId: string; selectedValues?: string[]; inputValue?: string }>;
};
expect(payload).toMatchObject({
interactionType: "view_submission",
actionId: "view:openclaw:deploy_form",
callbackId: "openclaw:deploy_form",
viewId: "V123",
userId: "U777",
routedChannelId: "D123",
rootViewId: "VROOT",
previousViewId: "VPREV",
externalId: "deploy-ext-1",
viewHash: "[redacted]",
isStackedView: true,
});
expect(payload.inputs).toEqual(
expect.arrayContaining([
expect.objectContaining({ actionId: "env_select", selectedValues: ["prod"] }),
expect.objectContaining({ actionId: "notes_input", inputValue: "ship now" }),
]),
);
});
it("blocks modal events when private metadata userId does not match submitter", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
expect(viewHandler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack,
body: {
user: { id: "U222" },
view: {
callback_id: "openclaw:deploy_form",
private_metadata: JSON.stringify({
channelId: "D123",
channelType: "im",
userId: "U111",
}),
},
},
} as never);
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("blocks modal events when private metadata is missing userId", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
expect(viewHandler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack,
body: {
user: { id: "U222" },
view: {
callback_id: "openclaw:deploy_form",
private_metadata: JSON.stringify({
channelId: "D123",
channelType: "im",
}),
},
},
} as never);
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
});
it("captures modal input labels and picker values across block types", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
expect(viewHandler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack,
body: {
user: { id: "U444" },
view: {
id: "V400",
callback_id: "openclaw:routing_form",
private_metadata: JSON.stringify({ userId: "U444" }),
state: {
values: {
env_block: {
env_select: {
type: "static_select",
selected_option: {
text: { type: "plain_text", text: "Production" },
value: "prod",
},
},
},
assignee_block: {
assignee_select: {
type: "users_select",
selected_user: "U900",
},
},
channel_block: {
channel_select: {
type: "channels_select",
selected_channel: "C900",
},
},
convo_block: {
convo_select: {
type: "conversations_select",
selected_conversation: "G900",
},
},
date_block: {
date_select: {
type: "datepicker",
selected_date: "2026-02-16",
},
},
time_block: {
time_select: {
type: "timepicker",
selected_time: "12:45",
},
},
datetime_block: {
datetime_select: {
type: "datetimepicker",
selected_date_time: 1_771_632_300,
},
},
radio_block: {
radio_select: {
type: "radio_buttons",
selected_option: {
text: { type: "plain_text", text: "Blue" },
value: "blue",
},
},
},
checks_block: {
checks_select: {
type: "checkboxes",
selected_options: [
{ text: { type: "plain_text", text: "A" }, value: "a" },
{ text: { type: "plain_text", text: "B" }, value: "b" },
],
},
},
number_block: {
number_input: {
type: "number_input",
value: "42.5",
},
},
email_block: {
email_input: {
type: "email_text_input",
value: "team@openclaw.ai",
},
},
url_block: {
url_input: {
type: "url_text_input",
value: "https://docs.openclaw.ai",
},
},
richtext_block: {
richtext_input: {
type: "rich_text_input",
rich_text_value: {
type: "rich_text",
elements: [
{
type: "rich_text_section",
elements: [
{ type: "text", text: "Ship this now" },
{ type: "text", text: "with canary metrics" },
],
},
],
},
},
},
},
},
},
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
inputs: Array<{
actionId: string;
inputKind?: string;
selectedValues?: string[];
selectedUsers?: string[];
selectedChannels?: string[];
selectedConversations?: string[];
selectedLabels?: string[];
selectedDate?: string;
selectedTime?: string;
selectedDateTime?: number;
inputNumber?: number;
inputEmail?: string;
inputUrl?: string;
richTextValue?: unknown;
richTextPreview?: string;
}>;
};
expect(payload.inputs).toEqual(
expect.arrayContaining([
expect.objectContaining({
actionId: "env_select",
selectedValues: ["prod"],
selectedLabels: ["Production"],
}),
expect.objectContaining({
actionId: "assignee_select",
selectedValues: ["U900"],
selectedUsers: ["U900"],
}),
expect.objectContaining({
actionId: "channel_select",
selectedValues: ["C900"],
selectedChannels: ["C900"],
}),
expect.objectContaining({
actionId: "convo_select",
selectedValues: ["G900"],
selectedConversations: ["G900"],
}),
expect.objectContaining({ actionId: "date_select", selectedDate: "2026-02-16" }),
expect.objectContaining({ actionId: "time_select", selectedTime: "12:45" }),
expect.objectContaining({ actionId: "datetime_select", selectedDateTime: 1_771_632_300 }),
expect.objectContaining({
actionId: "radio_select",
selectedValues: ["blue"],
selectedLabels: ["Blue"],
}),
expect.objectContaining({
actionId: "checks_select",
selectedValues: ["a", "b"],
selectedLabels: ["A", "B"],
}),
expect.objectContaining({
actionId: "number_input",
inputKind: "number",
inputNumber: 42.5,
}),
expect.objectContaining({
actionId: "email_input",
inputKind: "email",
inputEmail: "team@openclaw.ai",
}),
expect.objectContaining({
actionId: "url_input",
inputKind: "url",
inputUrl: "https://docs.openclaw.ai/",
}),
expect.objectContaining({
actionId: "richtext_input",
inputKind: "rich_text",
richTextPreview: "Ship this now with canary metrics",
richTextValue: {
type: "rich_text",
elements: [
{
type: "rich_text_section",
elements: [
{ type: "text", text: "Ship this now" },
{ type: "text", text: "with canary metrics" },
],
},
],
},
}),
]),
);
});
it("truncates rich text preview to keep payload summaries compact", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
expect(viewHandler).toBeTruthy();
const longText = "deploy ".repeat(40).trim();
const ack = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack,
body: {
user: { id: "U555" },
view: {
id: "V555",
callback_id: "openclaw:long_richtext",
private_metadata: JSON.stringify({ userId: "U555" }),
state: {
values: {
richtext_block: {
richtext_input: {
type: "rich_text_input",
rich_text_value: {
type: "rich_text",
elements: [
{
type: "rich_text_section",
elements: [{ type: "text", text: longText }],
},
],
},
},
},
},
},
},
},
});
expect(ack).toHaveBeenCalled();
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
inputs: Array<{ actionId: string; richTextPreview?: string }>;
};
const richInput = payload.inputs.find((input) => input.actionId === "richtext_input");
expect(richInput?.richTextPreview).toBeTruthy();
expect((richInput?.richTextPreview ?? "").length).toBeLessThanOrEqual(120);
});
it("captures modal close events and enqueues view closed event", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewClosedHandler, resolveSessionKey } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewClosedHandler = getViewClosedHandler();
expect(viewClosedHandler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await viewClosedHandler!({
ack,
body: {
user: { id: "U900" },
team: { id: "T1" },
is_cleared: true,
view: {
id: "V900",
callback_id: "openclaw:deploy_form",
root_view_id: "VROOT900",
previous_view_id: "VPREV900",
external_id: "deploy-ext-900",
hash: "view-hash-900",
private_metadata: JSON.stringify({
sessionKey: "agent:main:slack:channel:C99",
userId: "U900",
}),
state: {
values: {
env_block: {
env_select: {
type: "static_select",
selected_option: {
text: { type: "plain_text", text: "Canary" },
value: "canary",
},
},
},
},
},
},
},
});
expect(ack).toHaveBeenCalled();
expect(resolveSessionKey).not.toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText, options] = enqueueSystemEventMock.mock.calls[0] as [
string,
{ sessionKey?: string },
];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
interactionType: string;
actionId: string;
callbackId: string;
viewId: string;
userId: string;
isCleared: boolean;
privateMetadata: string;
rootViewId?: string;
previousViewId?: string;
externalId?: string;
viewHash?: string;
isStackedView?: boolean;
inputs: Array<{ actionId: string; selectedValues?: string[] }>;
};
expect(payload).toMatchObject({
interactionType: "view_closed",
actionId: "view:openclaw:deploy_form",
callbackId: "openclaw:deploy_form",
viewId: "V900",
userId: "U900",
isCleared: true,
privateMetadata: "[redacted]",
rootViewId: "VROOT900",
previousViewId: "VPREV900",
externalId: "deploy-ext-900",
viewHash: "[redacted]",
isStackedView: true,
});
expect(payload.inputs).toEqual(
expect.arrayContaining([
expect.objectContaining({ actionId: "env_select", selectedValues: ["canary"] }),
]),
);
expect(options.sessionKey).toBe("agent:main:slack:channel:C99");
});
it("defaults modal close isCleared to false when Slack omits the flag", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewClosedHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewClosedHandler = getViewClosedHandler();
expect(viewClosedHandler).toBeTruthy();
const ack = vi.fn().mockResolvedValue(undefined);
await viewClosedHandler!({
ack,
body: {
user: { id: "U901" },
view: {
id: "V901",
callback_id: "openclaw:deploy_form",
private_metadata: JSON.stringify({ userId: "U901" }),
},
},
});
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
interactionType: string;
isCleared?: boolean;
};
expect(payload.interactionType).toBe("view_closed");
expect(payload.isCleared).toBe(false);
});
it("caps oversized interaction payloads with compact summaries", async () => {
enqueueSystemEventMock.mockClear();
const { ctx, getViewHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
expect(viewHandler).toBeTruthy();
const richTextValue = {
type: "rich_text",
elements: Array.from({ length: 20 }, (_, index) => ({
type: "rich_text_section",
elements: [{ type: "text", text: `chunk-${index}-${"x".repeat(400)}` }],
})),
};
const values: Record<string, Record<string, unknown>> = {};
for (let index = 0; index < 20; index += 1) {
values[`block_${index}`] = {
[`input_${index}`]: {
type: "rich_text_input",
rich_text_value: richTextValue,
},
};
}
const ack = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack,
body: {
user: { id: "U915" },
team: { id: "T1" },
view: {
id: "V915",
callback_id: "openclaw:oversize",
private_metadata: JSON.stringify({
channelId: "D915",
channelType: "im",
userId: "U915",
}),
state: {
values,
},
},
},
} as never);
expect(ack).toHaveBeenCalled();
expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
expect(eventText.length).toBeLessThanOrEqual(2400);
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
payloadTruncated?: boolean;
inputs?: unknown[];
inputsOmitted?: number;
};
expect(payload.payloadTruncated).toBe(true);
expect(Array.isArray(payload.inputs) ? payload.inputs.length : 0).toBeLessThanOrEqual(3);
expect((payload.inputsOmitted ?? 0) >= 1).toBe(true);
});
});
const selectedDateTimeEpoch = 1_771_632_300;