Fix Slack canvas action types

This commit is contained in:
Alfonso 2026-01-26 00:26:05 -05:00
parent 6ddef0e869
commit d421bbeb48
6 changed files with 48 additions and 32 deletions

View File

@ -131,10 +131,7 @@ describe("handleSlackAction", () => {
it("requires canvases to be enabled", async () => { it("requires canvases to be enabled", async () => {
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig; const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await expect( await expect(
handleSlackAction( handleSlackAction({ action: "createCanvas", title: "Test", content: "hello" }, cfg),
{ action: "createCanvas", title: "Test", content: "hello" },
cfg,
),
).rejects.toThrow(/Slack canvases are disabled/); ).rejects.toThrow(/Slack canvases are disabled/);
}); });
@ -142,10 +139,7 @@ describe("handleSlackAction", () => {
const cfg = { const cfg = {
channels: { slack: { botToken: "tok", actions: { canvases: true } } }, channels: { slack: { botToken: "tok", actions: { canvases: true } } },
} as ClawdbotConfig; } as ClawdbotConfig;
await handleSlackAction( await handleSlackAction({ action: "createCanvas", title: "Test", content: "hello" }, cfg);
{ action: "createCanvas", title: "Test", content: "hello" },
cfg,
);
expect(createSlackCanvas).toHaveBeenCalledWith( expect(createSlackCanvas).toHaveBeenCalledWith(
{ title: "Test", content: "hello", channelId: undefined }, { title: "Test", content: "hello", channelId: undefined },
undefined, undefined,
@ -156,10 +150,7 @@ describe("handleSlackAction", () => {
const cfg = { const cfg = {
channels: { slack: { botToken: "tok", actions: { canvases: true } } }, channels: { slack: { botToken: "tok", actions: { canvases: true } } },
} as ClawdbotConfig; } as ClawdbotConfig;
await handleSlackAction( await handleSlackAction({ action: "updateCanvas", canvasId: "C123", content: "update" }, cfg);
{ action: "updateCanvas", canvasId: "C123", content: "update" },
cfg,
);
expect(updateSlackCanvas).toHaveBeenCalledWith( expect(updateSlackCanvas).toHaveBeenCalledWith(
{ canvasId: "C123", channelId: undefined, content: "update", updateMode: undefined }, { canvasId: "C123", channelId: undefined, content: "update", updateMode: undefined },
undefined, undefined,

View File

@ -47,6 +47,8 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
"timeout", "timeout",
"kick", "kick",
"ban", "ban",
"canvas-create",
"canvas-update",
] as const; ] as const;
export type ChannelMessageActionName = (typeof CHANNEL_MESSAGE_ACTION_NAMES)[number]; export type ChannelMessageActionName = (typeof CHANNEL_MESSAGE_ACTION_NAMES)[number];

View File

@ -56,11 +56,12 @@ describe("extractCanvasRefsFromEvent", () => {
describe("fetchSlackCanvasContent", () => { describe("fetchSlackCanvasContent", () => {
it("downloads and parses JSON canvas content", async () => { it("downloads and parses JSON canvas content", async () => {
const fetchMock = vi.fn(async () => const fetchMock = vi.fn(
new Response(JSON.stringify({ text: "hello canvas" }), { async () =>
status: 200, new Response(JSON.stringify({ text: "hello canvas" }), {
headers: { "content-type": "application/json" }, status: 200,
}), headers: { "content-type": "application/json" },
}),
); );
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
@ -127,11 +128,12 @@ describe("fetchSlackCanvasContent", () => {
}); });
it("truncates large payloads", async () => { it("truncates large payloads", async () => {
const fetchMock = vi.fn(async () => const fetchMock = vi.fn(
new Response(JSON.stringify({ text: "1234567890" }), { async () =>
status: 200, new Response(JSON.stringify({ text: "1234567890" }), {
headers: { "content-type": "application/json" }, status: 200,
}), headers: { "content-type": "application/json" },
}),
); );
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);

View File

@ -173,7 +173,10 @@ function collectJsonText(
} }
} }
function extractTextFromJson(payload: unknown, maxChars: number): { text: string; truncated: boolean } { function extractTextFromJson(
payload: unknown,
maxChars: number,
): { text: string; truncated: boolean } {
const results: string[] = []; const results: string[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
collectJsonText(payload, { maxChars, results, seen, depth: 0 }); collectJsonText(payload, { maxChars, results, seen, depth: 0 });
@ -197,10 +200,22 @@ export async function fetchSlackCanvasContent(params: {
return { ok: false, error: "no_file_id" }; return { ok: false, error: "no_file_id" };
} }
let fileInfo: { file?: SlackFile & { title?: string; name?: string; url_private?: string; url_private_download?: string } }; let fileInfo: {
file?: SlackFile & {
title?: string;
name?: string;
url_private?: string;
url_private_download?: string;
};
};
try { try {
fileInfo = (await client.files.info({ file: fileId })) as { fileInfo = (await client.files.info({ file: fileId })) as {
file?: SlackFile & { title?: string; name?: string; url_private?: string; url_private_download?: string }; file?: SlackFile & {
title?: string;
name?: string;
url_private?: string;
url_private_download?: string;
};
}; };
} catch (err) { } catch (err) {
const label = buildErrorLabel(err); const label = buildErrorLabel(err);
@ -242,7 +257,11 @@ export async function fetchSlackCanvasContent(params: {
let extracted: { text: string; truncated: boolean }; let extracted: { text: string; truncated: boolean };
let rawFormat: SlackCanvasContent["rawFormat"] = "unknown"; let rawFormat: SlackCanvasContent["rawFormat"] = "unknown";
if (contentType.includes("application/json") || text.trim().startsWith("{") || text.trim().startsWith("[")) { if (
contentType.includes("application/json") ||
text.trim().startsWith("{") ||
text.trim().startsWith("[")
) {
rawFormat = "json"; rawFormat = "json";
try { try {
const payload = JSON.parse(text); const payload = JSON.parse(text);

View File

@ -151,11 +151,12 @@ describe("slack prepareSlackMessage inbound contract", () => {
}); });
it("appends canvas content to the inbound body", async () => { it("appends canvas content to the inbound body", async () => {
const fetchMock = vi.fn(async () => const fetchMock = vi.fn(
new Response(JSON.stringify({ title: "Canvas", text: "hello canvas" }), { async () =>
status: 200, new Response(JSON.stringify({ title: "Canvas", text: "hello canvas" }), {
headers: { "content-type": "application/json" }, status: 200,
}), headers: { "content-type": "application/json" },
}),
); );
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);

View File

@ -371,7 +371,8 @@ export async function prepareSlackMessage(params: {
} }
const canvasContext = canvasBlocks.length > 0 ? canvasBlocks.join("\n\n") : ""; const canvasContext = canvasBlocks.length > 0 ? canvasBlocks.join("\n\n") : "";
const baseBody = (message.text ?? "").trim() || media?.placeholder || (canvasContext ? "[Slack canvas]" : ""); const baseBody =
(message.text ?? "").trim() || media?.placeholder || (canvasContext ? "[Slack canvas]" : "");
if (!baseBody) return null; if (!baseBody) return null;
const rawBody = baseBody; const rawBody = baseBody;
const rawBodyWithCanvas = canvasContext ? `${rawBody}\n\n${canvasContext}` : rawBody; const rawBodyWithCanvas = canvasContext ? `${rawBody}\n\n${canvasContext}` : rawBody;