fix(slack): support exact message reads

This commit is contained in:
Peter Steinberger
2026-05-02 03:24:01 +01:00
parent 9d4a98e599
commit a22f065043
12 changed files with 173 additions and 16 deletions

View File

@@ -689,6 +689,29 @@ describe("handleSlackAction", () => {
);
});
it("passes messageId through to readSlackMessages", async () => {
readSlackMessages.mockResolvedValueOnce({ messages: [], hasMore: false });
await handleSlackAction(
{
action: "readMessages",
channelId: "C1",
threadId: "1712345678.123456",
messageId: "1712345678.654321",
},
slackConfig(),
);
expect(readSlackMessages).toHaveBeenCalledWith(
"C1",
expect.objectContaining({
cfg: expect.any(Object),
threadId: "1712345678.123456",
messageId: "1712345678.654321",
}),
);
});
it("adds normalized timestamps to pin payloads", async () => {
listSlackPins.mockResolvedValueOnce([{ message: { ts: "1712345678.123456", text: "pin" } }]);

View File

@@ -366,12 +366,14 @@ export async function handleSlackAction(
const before = readStringParam(params, "before");
const after = readStringParam(params, "after");
const threadId = readStringParam(params, "threadId");
const messageId = readStringParam(params, "messageId");
const result = await slackActionRuntime.readSlackMessages(channelId, {
...readOpts,
limit,
before: before ?? undefined,
after: after ?? undefined,
threadId: threadId ?? undefined,
messageId: messageId ?? undefined,
});
const messages = result.messages.map((message) =>
withNormalizedTimestamp(

View File

@@ -41,6 +41,35 @@ describe("readSlackMessages", () => {
expect(result.messages.map((message) => message.ts)).toEqual(["171234.890", "171235.000"]);
});
it("filters a specific thread reply by messageId", async () => {
const client = createClient();
client.conversations.replies.mockResolvedValueOnce({
messages: [{ ts: "171234.567" }, { ts: "171234.890", text: "reply" }],
has_more: true,
});
const result = await readSlackMessages("C1", {
client,
threadId: "171234.567",
messageId: "171234.890",
limit: 20,
token: "xoxb-test",
});
expect(client.conversations.replies).toHaveBeenCalledWith({
channel: "C1",
ts: "171234.567",
limit: 1,
inclusive: true,
latest: "171234.890",
oldest: undefined,
});
expect(result).toEqual({
messages: [{ ts: "171234.890", text: "reply" }],
hasMore: false,
});
});
it("uses conversations.history when threadId is missing", async () => {
const client = createClient();
client.conversations.history.mockResolvedValueOnce({
@@ -63,4 +92,30 @@ describe("readSlackMessages", () => {
expect(client.conversations.replies).not.toHaveBeenCalled();
expect(result.messages.map((message) => message.ts)).toEqual(["1"]);
});
it("filters a specific channel message by messageId", async () => {
const client = createClient();
client.conversations.history.mockResolvedValueOnce({
messages: [{ ts: "171234.890", text: "exact" }, { ts: "171234.891" }],
has_more: true,
});
const result = await readSlackMessages("C1", {
client,
messageId: "171234.890",
token: "xoxb-test",
});
expect(client.conversations.history).toHaveBeenCalledWith({
channel: "C1",
limit: 1,
inclusive: true,
latest: "171234.890",
oldest: undefined,
});
expect(result).toEqual({
messages: [{ ts: "171234.890", text: "exact" }],
hasMore: false,
});
});
});

View File

@@ -257,37 +257,55 @@ export async function readSlackMessages(
before?: string;
after?: string;
threadId?: string;
messageId?: string;
} = {},
): Promise<{ messages: SlackMessageSummary[]; hasMore: boolean }> {
const client = await getClient(opts);
const exactMessageId = opts.messageId?.trim();
const readLimit = exactMessageId ? 1 : opts.limit;
const exactBounds = exactMessageId
? {
inclusive: true,
latest: exactMessageId,
oldest: undefined,
}
: {
latest: opts.before,
oldest: opts.after,
};
// Use conversations.replies for thread messages, conversations.history for channel messages.
if (opts.threadId) {
const result = await client.conversations.replies({
channel: channelId,
ts: opts.threadId,
limit: opts.limit,
latest: opts.before,
oldest: opts.after,
limit: readLimit,
...exactBounds,
});
const messages = ((result.messages ?? []) as SlackMessageSummary[]).filter((message) => {
if (exactMessageId) {
return message.ts === exactMessageId;
}
// conversations.replies includes the parent message; drop it for replies-only reads.
return message.ts !== opts.threadId;
});
return {
// conversations.replies includes the parent message; drop it for replies-only reads.
messages: (result.messages ?? []).filter(
(message) => (message as SlackMessageSummary)?.ts !== opts.threadId,
) as SlackMessageSummary[],
hasMore: Boolean(result.has_more),
messages,
hasMore: exactMessageId ? false : Boolean(result.has_more),
};
}
const result = await client.conversations.history({
channel: channelId,
limit: opts.limit,
latest: opts.before,
oldest: opts.after,
limit: readLimit,
...exactBounds,
});
const messages = ((result.messages ?? []) as SlackMessageSummary[]).filter(
(message) => !exactMessageId || message.ts === exactMessageId,
);
return {
messages: (result.messages ?? []) as SlackMessageSummary[],
hasMore: Boolean(result.has_more),
messages,
hasMore: exactMessageId ? false : Boolean(result.has_more),
};
}

View File

@@ -268,6 +268,7 @@ describe("slackPlugin actions", () => {
params: {
channelId: "C123",
threadId: "1712345678.123456",
messageId: "1712345678.654321",
},
});
@@ -276,6 +277,7 @@ describe("slackPlugin actions", () => {
action: "readMessages",
channelId: "C123",
threadId: "1712345678.123456",
messageId: "1712345678.654321",
}),
{},
undefined,

View File

@@ -194,6 +194,32 @@ describe("handleSlackMessageAction", () => {
);
});
it("forwards messageId for read actions", async () => {
const invoke = createInvokeSpy();
await handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "read",
cfg: {},
params: {
channelId: "C1",
messageId: "1712345678.654321",
},
} as never,
invoke: invoke as never,
});
expect(invoke).toHaveBeenCalledWith(
expect.objectContaining({
action: "readMessages",
channelId: "C1",
messageId: "1712345678.654321",
}),
{},
);
});
it("requires filePath, path, or media for upload-file", async () => {
await expect(
handleSlackMessageAction({

View File

@@ -122,6 +122,7 @@ export async function handleSlackMessageAction(params: {
limit,
before: readStringParam(actionParams, "before"),
after: readStringParam(actionParams, "after"),
messageId: readStringParam(actionParams, "messageId"),
accountId,
};
if (includeReadThreadId) {