test: clear slack action-runtime broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 12:35:53 +01:00
parent 3278f640ce
commit 273a2e1269

View File

@@ -48,17 +48,62 @@ describe("handleSlackAction", () => {
return { cfg, context, hasRepliedRef };
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
expect(typeof value).toBe("object");
expect(value).not.toBeNull();
if (typeof value !== "object" || value === null) {
throw new Error(`${label} was not an object`);
}
return value as Record<string, unknown>;
}
function requireArray(value: unknown, label: string): unknown[] {
expect(Array.isArray(value)).toBe(true);
if (!Array.isArray(value)) {
throw new Error(`${label} was not an array`);
}
return value;
}
function expectRecordFields(record: Record<string, unknown>, fields: Record<string, unknown>) {
for (const [key, value] of Object.entries(fields)) {
expect(record[key]).toEqual(value);
}
}
function requireSlackSendCall(index: number) {
const call = sendSlackMessage.mock.calls[index] as unknown[] | undefined;
expect(call).toBeDefined();
if (!call) {
throw new Error(`missing Slack send call ${index + 1}`);
}
return call;
}
function expectSlackSendCall(
index: number,
target: string,
content: string,
optionFields: Record<string, unknown>,
) {
const [actualTarget, actualContent, options] = requireSlackSendCall(index);
expect(actualTarget).toBe(target);
expect(actualContent).toBe(content);
expectRecordFields(requireRecord(options, "Slack send options"), optionFields);
return requireRecord(options, "Slack send options");
}
function expectLastSlackSend(content: string, cfg: OpenClawConfig, threadTs?: string) {
expect(sendSlackMessage).toHaveBeenLastCalledWith(
"channel:C123",
content,
expect.objectContaining({
cfg,
mediaUrl: undefined,
threadTs,
blocks: undefined,
}),
);
expectSlackSendCall(sendSlackMessage.mock.calls.length - 1, "channel:C123", content, {
cfg,
mediaUrl: undefined,
threadTs,
blocks: undefined,
});
}
function requireDetails(result: Awaited<ReturnType<typeof handleSlackAction>>) {
return requireRecord(result.details, "action result details");
}
async function sendSecondMessageAndExpectNoThread(params: {
@@ -199,16 +244,12 @@ describe("handleSlackAction", () => {
},
cfg,
);
expect(sendSlackMessage).toHaveBeenCalledWith(
"channel:C123",
"Hello thread",
expect.objectContaining({
cfg,
mediaUrl: undefined,
threadTs: "1234567890.123456",
blocks: undefined,
}),
);
expectSlackSendCall(0, "channel:C123", "Hello thread", {
cfg,
mediaUrl: undefined,
threadTs: "1234567890.123456",
blocks: undefined,
});
});
it("returns a friendly error when downloadFile cannot fetch the attachment", async () => {
@@ -220,15 +261,11 @@ describe("handleSlackAction", () => {
},
slackConfig(),
);
expect(downloadSlackFile).toHaveBeenCalledWith(
"F123",
expect.objectContaining({ maxBytes: 20 * 1024 * 1024 }),
);
expect(result).toEqual(
expect.objectContaining({
details: expect.objectContaining({ ok: false }),
}),
expect(downloadSlackFile.mock.calls[0]?.[0]).toBe("F123");
expect(requireRecord(downloadSlackFile.mock.calls[0]?.[1], "download options").maxBytes).toBe(
20 * 1024 * 1024,
);
expect(requireDetails(result).ok).toBe(false);
});
it("passes download scope (channel/thread) to downloadSlackFile", async () => {
@@ -244,18 +281,12 @@ describe("handleSlackAction", () => {
slackConfig(),
);
expect(downloadSlackFile).toHaveBeenCalledWith(
"F123",
expect.objectContaining({
channelId: "C1",
threadId: "123.456",
}),
);
expect(result).toEqual(
expect.objectContaining({
details: expect.objectContaining({ ok: false }),
}),
);
expect(downloadSlackFile.mock.calls[0]?.[0]).toBe("F123");
expectRecordFields(requireRecord(downloadSlackFile.mock.calls[0]?.[1], "download options"), {
channelId: "C1",
threadId: "123.456",
});
expect(requireDetails(result).ok).toBe(false);
});
it("returns non-image downloadFile results as file metadata instead of image content", async () => {
@@ -274,25 +305,21 @@ describe("handleSlackAction", () => {
);
expect(result.content).toHaveLength(1);
expect(result.content[0]).toEqual(
expect.objectContaining({
type: "text",
text: expect.stringContaining("/tmp/openclaw-media/report.pdf"),
}),
);
const firstContent = requireRecord(result.content[0], "first content item");
expect(firstContent.type).toBe("text");
expect(String(firstContent.text)).toContain("/tmp/openclaw-media/report.pdf");
expect(result.content.some((entry) => entry.type === "image")).toBe(false);
expect(result.details).toEqual(
expect.objectContaining({
ok: true,
fileId: "F123",
path: "/tmp/openclaw-media/report.pdf",
contentType: "application/pdf",
media: {
mediaUrl: "/tmp/openclaw-media/report.pdf",
contentType: "application/pdf",
},
}),
);
const details = requireDetails(result);
expectRecordFields(details, {
ok: true,
fileId: "F123",
path: "/tmp/openclaw-media/report.pdf",
contentType: "application/pdf",
});
expect(details.media).toEqual({
mediaUrl: "/tmp/openclaw-media/report.pdf",
contentType: "application/pdf",
});
});
it("forwards resolved botToken to action functions instead of relying on config re-read", async () => {
@@ -343,16 +370,12 @@ describe("handleSlackAction", () => {
},
cfg,
);
expect(sendSlackMessage).toHaveBeenCalledWith(
"channel:C123",
"",
expect.objectContaining({
cfg,
mediaUrl: undefined,
threadTs: undefined,
blocks: expectedBlocks,
}),
);
expectSlackSendCall(0, "channel:C123", "", {
cfg,
mediaUrl: undefined,
threadTs: undefined,
blocks: expectedBlocks,
});
});
it.each([
@@ -404,17 +427,13 @@ describe("handleSlackAction", () => {
cfg,
);
expect(sendSlackMessage).toHaveBeenCalledWith(
"user:U123",
"fresh report",
expect.objectContaining({
cfg,
mediaUrl: "/tmp/report.png",
threadTs: "111.222",
uploadFileName: "report-final.png",
uploadTitle: "Report Final",
}),
);
expectSlackSendCall(0, "user:U123", "fresh report", {
cfg,
mediaUrl: "/tmp/report.png",
threadTs: "111.222",
uploadFileName: "report-final.png",
uploadTitle: "Report Final",
});
});
it("sends media before a separate blocks message", async () => {
@@ -434,27 +453,17 @@ describe("handleSlackAction", () => {
);
expect(sendSlackMessage).toHaveBeenCalledTimes(2);
expect(sendSlackMessage).toHaveBeenNthCalledWith(
1,
"channel:C123",
"",
expect.objectContaining({
cfg,
mediaUrl: "https://example.com/file.png",
threadTs: undefined,
}),
);
expectSlackSendCall(0, "channel:C123", "", {
cfg,
mediaUrl: "https://example.com/file.png",
threadTs: undefined,
});
expect(sendSlackMessage.mock.calls[0]?.[2]).not.toHaveProperty("blocks");
expect(sendSlackMessage).toHaveBeenNthCalledWith(
2,
"channel:C123",
"hello",
expect.objectContaining({
cfg,
blocks: [{ type: "divider" }],
threadTs: undefined,
}),
);
expectSlackSendCall(1, "channel:C123", "hello", {
cfg,
blocks: [{ type: "divider" }],
threadTs: undefined,
});
expect(sendSlackMessage.mock.calls[1]?.[2]).not.toHaveProperty("mediaUrl");
expect(result.details).toEqual({
ok: true,
@@ -485,15 +494,13 @@ describe("handleSlackAction", () => {
},
cfg,
);
expect(editSlackMessage).toHaveBeenCalledWith(
"C123",
"123.456",
"",
expect.objectContaining({
cfg,
blocks: expectedBlocks,
}),
);
expect(editSlackMessage.mock.calls[0]?.[0]).toBe("C123");
expect(editSlackMessage.mock.calls[0]?.[1]).toBe("123.456");
expect(editSlackMessage.mock.calls[0]?.[2]).toBe("");
expectRecordFields(requireRecord(editSlackMessage.mock.calls[0]?.[3], "edit options"), {
cfg,
blocks: expectedBlocks,
});
});
it("requires content or blocks for editMessage", async () => {
@@ -613,16 +620,12 @@ describe("handleSlackAction", () => {
replyToMode: "all",
},
);
expect(sendSlackMessage).toHaveBeenCalledWith(
"channel:C999",
"Other channel",
expect.objectContaining({
cfg,
mediaUrl: undefined,
threadTs: undefined,
blocks: undefined,
}),
);
expectSlackSendCall(0, "channel:C999", "Other channel", {
cfg,
mediaUrl: undefined,
threadTs: undefined,
blocks: undefined,
});
});
it("explicit threadTs overrides context threadTs", async () => {
@@ -651,16 +654,12 @@ describe("handleSlackAction", () => {
currentThreadTs: "1111111111.111111",
replyToMode: "all",
});
expect(sendSlackMessage).toHaveBeenCalledWith(
"C123",
"Bare target",
expect.objectContaining({
cfg,
mediaUrl: undefined,
threadTs: "1111111111.111111",
blocks: undefined,
}),
);
expectSlackSendCall(0, "C123", "Bare target", {
cfg,
mediaUrl: undefined,
threadTs: "1111111111.111111",
blocks: undefined,
});
});
it("adds normalized timestamps to readMessages payloads", async () => {
@@ -674,17 +673,13 @@ describe("handleSlackAction", () => {
slackConfig(),
);
expect(result).toMatchObject({
details: {
ok: true,
hasMore: false,
messages: [
expect.objectContaining({
ts: "1712345678.123456",
timestampMs: 1712345678123,
}),
],
},
const details = requireDetails(result);
expect(details.ok).toBe(true);
expect(details.hasMore).toBe(false);
const messages = requireArray(details.messages, "read messages");
expectRecordFields(requireRecord(messages[0], "first message"), {
ts: "1712345678.123456",
timestampMs: 1712345678123,
});
});
@@ -697,16 +692,14 @@ describe("handleSlackAction", () => {
cfg,
);
expect(readSlackMessages).toHaveBeenCalledWith(
"C1",
expect.objectContaining({
cfg,
threadId: "1712345678.123456",
limit: undefined,
before: undefined,
after: undefined,
}),
);
expect(readSlackMessages.mock.calls[0]?.[0]).toBe("C1");
expectRecordFields(requireRecord(readSlackMessages.mock.calls[0]?.[1], "read options"), {
cfg,
threadId: "1712345678.123456",
limit: undefined,
before: undefined,
after: undefined,
});
});
it("passes messageId through to readSlackMessages", async () => {
@@ -723,14 +716,12 @@ describe("handleSlackAction", () => {
cfg,
);
expect(readSlackMessages).toHaveBeenCalledWith(
"C1",
expect.objectContaining({
cfg,
threadId: "1712345678.123456",
messageId: "1712345678.654321",
}),
);
expect(readSlackMessages.mock.calls[0]?.[0]).toBe("C1");
expectRecordFields(requireRecord(readSlackMessages.mock.calls[0]?.[1], "read options"), {
cfg,
threadId: "1712345678.123456",
messageId: "1712345678.654321",
});
});
it("adds normalized timestamps to pin payloads", async () => {
@@ -738,18 +729,13 @@ describe("handleSlackAction", () => {
const result = await handleSlackAction({ action: "listPins", channelId: "C1" }, slackConfig());
expect(result).toMatchObject({
details: {
ok: true,
pins: [
{
message: expect.objectContaining({
ts: "1712345678.123456",
timestampMs: 1712345678123,
}),
},
],
},
const details = requireDetails(result);
expect(details.ok).toBe(true);
const pins = requireArray(details.pins, "pins");
const firstPin = requireRecord(pins[0], "first pin");
expectRecordFields(requireRecord(firstPin.message, "first pin message"), {
ts: "1712345678.123456",
timestampMs: 1712345678123,
});
});
@@ -819,13 +805,11 @@ describe("handleSlackAction", () => {
const result = await handleSlackAction({ action: "emojiList" }, slackConfig());
expect(result).toMatchObject({
details: {
ok: true,
emojis: {
emoji: { party: "https://example.com/party.png", wave: "https://example.com/wave.png" },
},
},
const details = requireDetails(result);
expect(details.ok).toBe(true);
expect(details.emojis).toEqual({
ok: true,
emoji: { party: "https://example.com/party.png", wave: "https://example.com/wave.png" },
});
});
@@ -841,15 +825,13 @@ describe("handleSlackAction", () => {
const result = await handleSlackAction({ action: "emojiList", limit: 2 }, slackConfig());
expect(result).toMatchObject({
details: {
ok: true,
emojis: {
emoji: {
party: "https://example.com/party.png",
tada: "https://example.com/tada.png",
},
},
const details = requireDetails(result);
expect(details.ok).toBe(true);
expect(details.emojis).toEqual({
ok: true,
emoji: {
party: "https://example.com/party.png",
tada: "https://example.com/tada.png",
},
});
});