Files
openclaw/extensions/slack/src/approval-handler.runtime.test.ts
2026-05-12 01:19:20 +01:00

252 lines
7.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { slackApprovalNativeRuntime } from "./approval-handler.runtime.js";
type SlackPayload = {
text: string;
blocks?: unknown;
};
type ChatUpdatePayload = {
channel?: string;
ts?: string;
text?: string;
blocks?: unknown;
};
const SLACK_CHAT_UPDATE_TEXT_LIMIT = 4000;
function findSlackActionsBlock(blocks: Array<{ type?: string; elements?: unknown[] }>) {
return blocks.find((block) => block.type === "actions");
}
function readChatUpdatePayload(
chatUpdate: { mock: { calls: unknown[][] } },
index: number,
): ChatUpdatePayload {
const call = chatUpdate.mock.calls[index];
if (!call) {
throw new Error(`Expected Slack chat.update call #${index + 1}`);
}
const [payload] = call;
if (!payload || typeof payload !== "object") {
throw new Error(`Expected Slack chat.update payload #${index + 1}`);
}
return payload as ChatUpdatePayload;
}
describe("slackApprovalNativeRuntime", () => {
it("renders only the allowed pending actions", async () => {
const payload = (await slackApprovalNativeRuntime.presentation.buildPendingPayload({
cfg: {} as never,
accountId: "default",
context: {
app: {} as never,
config: {} as never,
},
request: {
id: "req-1",
request: {
command: "echo hi",
},
createdAtMs: 0,
expiresAtMs: 60_000,
},
approvalKind: "exec",
nowMs: 0,
view: {
approvalKind: "exec",
approvalId: "req-1",
commandText: "echo hi",
metadata: [],
actions: [
{
decision: "allow-once",
label: "Allow Once",
command: "/approve req-1 allow-once",
style: "success",
},
{
decision: "deny",
label: "Deny",
command: "/approve req-1 deny",
style: "danger",
},
],
} as never,
})) as SlackPayload;
expect(payload.text).toContain("*Exec approval required*");
const actionsBlock = findSlackActionsBlock(
payload.blocks as Array<{ type?: string; elements?: unknown[] }>,
);
const labels = (actionsBlock?.elements ?? []).map((element) =>
typeof element === "object" &&
element &&
typeof (element as { text?: { text?: unknown } }).text?.text === "string"
? (element as { text: { text: string } }).text.text
: "",
);
expect(labels).toEqual(["Allow Once", "Deny"]);
expect(JSON.stringify(payload.blocks)).not.toContain("Allow Always");
});
it("renders resolved updates without interactive blocks", async () => {
const result = await slackApprovalNativeRuntime.presentation.buildResolvedResult({
cfg: {} as never,
accountId: "default",
context: {
app: {} as never,
config: {} as never,
},
request: {
id: "req-1",
request: {
command: "echo hi",
},
createdAtMs: 0,
expiresAtMs: 60_000,
},
resolved: {
id: "req-1",
decision: "allow-once",
resolvedBy: "U123APPROVER",
ts: 0,
} as never,
view: {
approvalKind: "exec",
approvalId: "req-1",
decision: "allow-once",
commandText: "echo hi",
resolvedBy: "U123APPROVER",
} as never,
entry: {
channelId: "D123APPROVER",
messageTs: "1712345678.999999",
},
});
expect(result.kind).toBe("update");
if (result.kind !== "update") {
throw new Error("expected Slack resolved update payload");
}
const payload = result.payload as SlackPayload;
expect(payload.text).toContain("*Exec approval: Allowed once*");
expect(payload.text).toContain("Resolved by <@U123APPROVER>.");
expect(
(payload.blocks as Array<{ type?: string }>).some((block) => block.type === "actions"),
).toBe(false);
});
it("caps resolved update fallback text to Slack chat.update limits while preserving blocks", async () => {
const blocks = [
{
type: "section",
text: {
type: "mrkdwn",
text: "*Command*\n```short preview```",
},
},
];
const chatUpdate = vi.fn(async (_payload: { text: string; blocks: typeof blocks }) => ({}));
const context = {
app: {
client: {
chat: {
update: chatUpdate,
},
},
},
config: {},
} as never;
await slackApprovalNativeRuntime.transport.updateEntry?.({
cfg: {} as never,
accountId: "default",
context,
entry: {
channelId: "C123",
messageTs: "1712345678.999999",
},
payload: {
text: "a".repeat(SLACK_CHAT_UPDATE_TEXT_LIMIT),
blocks,
},
phase: "resolved",
});
await slackApprovalNativeRuntime.transport.updateEntry?.({
cfg: {} as never,
accountId: "default",
context,
entry: {
channelId: "C123",
messageTs: "1712345678.999999",
},
payload: {
text: "a".repeat(5000),
blocks,
},
phase: "resolved",
});
const firstUpdate = readChatUpdatePayload(chatUpdate, 0);
const secondUpdate = readChatUpdatePayload(chatUpdate, 1);
expect(firstUpdate.channel).toBe("C123");
expect(firstUpdate.ts).toBe("1712345678.999999");
expect(firstUpdate.text).toBe("a".repeat(SLACK_CHAT_UPDATE_TEXT_LIMIT));
expect(firstUpdate.blocks).toBe(blocks);
expect(secondUpdate.channel).toBe("C123");
expect(secondUpdate.ts).toBe("1712345678.999999");
expect(secondUpdate.text).toMatch(/…$/);
expect(secondUpdate.blocks).toBe(blocks);
expect(secondUpdate.text).toHaveLength(SLACK_CHAT_UPDATE_TEXT_LIMIT);
});
it("keeps pending metadata context within Slack Block Kit limits", async () => {
const payload = (await slackApprovalNativeRuntime.presentation.buildPendingPayload({
cfg: {} as never,
accountId: "default",
context: {
app: {} as never,
config: {} as never,
},
request: {
id: "req-1",
request: {
command: "echo hi",
},
createdAtMs: 0,
expiresAtMs: 60_000,
},
approvalKind: "exec",
nowMs: 0,
view: {
approvalKind: "exec",
approvalId: "req-1",
commandText: "echo hi",
metadata: Array.from({ length: 12 }, (_entry, index) => ({
label: `Metadata ${index + 1}`,
value: index === 0 ? "x".repeat(3100) : `value-${index + 1}`,
})),
actions: [
{
decision: "allow-once",
label: "Allow Once",
command: "/approve req-1 allow-once",
style: "success",
},
],
} as never,
})) as SlackPayload;
const contextBlock = (payload.blocks as Array<{ type?: string; elements?: unknown[] }>).find(
(block) => block.type === "context",
);
const elements = contextBlock?.elements as Array<{ text?: string }> | undefined;
expect(elements).toHaveLength(10);
expect(elements?.[0]?.text).toHaveLength(3000);
expect(elements?.[0]?.text?.endsWith("…")).toBe(true);
expect(elements?.at(-1)?.text).toBe("…+3 more");
});
});