mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 02:41:24 +00:00
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
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 };
|
|
channel?: { id?: 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;
|
|
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;
|
|
state?: { values?: Record<string, Record<string, Record<string, unknown>>> };
|
|
};
|
|
is_cleared?: boolean;
|
|
};
|
|
}) => Promise<void>;
|
|
|
|
function createContext() {
|
|
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 ctx = {
|
|
app,
|
|
runtime: { log: runtimeLog },
|
|
resolveSlackSystemEventSessionKey: resolveSessionKey,
|
|
};
|
|
return {
|
|
ctx,
|
|
app,
|
|
runtimeLog,
|
|
resolveSessionKey,
|
|
getHandler: () => handler,
|
|
getViewHandler: () => viewHandler,
|
|
getViewClosedHandler: () => viewClosedHandler,
|
|
};
|
|
}
|
|
|
|
describe("registerSlackInteractionEvents", () => {
|
|
it("enqueues structured events and updates button rows", async () => {
|
|
enqueueSystemEventMock.mockReset();
|
|
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" },
|
|
channel: { id: "C1" },
|
|
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;
|
|
channelId: string;
|
|
messageTs: string;
|
|
};
|
|
expect(payload).toMatchObject({
|
|
actionId: "openclaw:verify",
|
|
actionType: "button",
|
|
value: "approved",
|
|
userId: "U123",
|
|
channelId: "C1",
|
|
messageTs: "100.200",
|
|
});
|
|
expect(resolveSessionKey).toHaveBeenCalledWith({
|
|
channelId: "C1",
|
|
channelType: undefined,
|
|
});
|
|
expect(app.client.chat.update).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("captures select values and skips chat.update for non-button actions", async () => {
|
|
enqueueSystemEventMock.mockReset();
|
|
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[];
|
|
};
|
|
expect(payload.actionType).toBe("static_select");
|
|
expect(payload.selectedValues).toEqual(["canary"]);
|
|
expect(app.client.chat.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("captures modal submissions and enqueues view submission event", async () => {
|
|
enqueueSystemEventMock.mockReset();
|
|
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",
|
|
private_metadata: JSON.stringify({ channelId: "D123", channelType: "im" }),
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(ack).toHaveBeenCalled();
|
|
expect(resolveSessionKey).toHaveBeenCalledWith({
|
|
channelId: "D123",
|
|
channelType: "im",
|
|
});
|
|
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;
|
|
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",
|
|
});
|
|
expect(payload.inputs).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ actionId: "env_select", selectedValues: ["prod"] }),
|
|
expect.objectContaining({ actionId: "notes_input", inputValue: "ship now" }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("captures modal close events and enqueues view closed event", async () => {
|
|
enqueueSystemEventMock.mockReset();
|
|
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",
|
|
private_metadata: JSON.stringify({ sessionKey: "agent:main:slack:channel:C99" }),
|
|
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;
|
|
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: JSON.stringify({ sessionKey: "agent:main:slack:channel:C99" }),
|
|
});
|
|
expect(payload.inputs).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ actionId: "env_select", selectedValues: ["canary"] }),
|
|
]),
|
|
);
|
|
expect(options.sessionKey).toBe("agent:main:slack:channel:C99");
|
|
});
|
|
});
|