mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-22 06:44:02 +00:00
235 lines
6.8 KiB
TypeScript
235 lines
6.8 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { slackApprovalNativeRuntime } from "./approval-handler.runtime.js";
|
|
|
|
type SlackPayload = {
|
|
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");
|
|
}
|
|
|
|
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 = chatUpdate.mock.calls[0]?.[0] as
|
|
| { channel?: string; ts?: string; text?: string; blocks?: unknown }
|
|
| undefined;
|
|
const secondUpdate = chatUpdate.mock.calls[1]?.[0] as
|
|
| { channel?: string; ts?: string; text?: string; blocks?: unknown }
|
|
| undefined;
|
|
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(chatUpdate.mock.calls[1]?.[0].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");
|
|
});
|
|
});
|