fix(slack): record canonical sent thread (#95250)

This commit is contained in:
Bek
2026-06-20 02:45:58 -04:00
committed by GitHub
parent fb022a2b07
commit 3d05e973f0
2 changed files with 111 additions and 4 deletions

View File

@@ -100,16 +100,44 @@ describe("sendMessageSlack thread participation", () => {
clearSlackThreadParticipationCache();
const client = createSlackSendTestClient();
await sendMessageSlack("channel:C123", "hello thread", {
const result = await sendMessageSlack("channel:C123", "hello thread", {
token: "xoxb-test",
cfg: SLACK_TEST_CFG,
client,
threadTs: "1712345678.123456",
});
expect(result.threadTs).toBe("1712345678.123456");
expect(result.receipt.threadId).toBe("1712345678.123456");
expect(hasSlackThreadParticipation("default", "C123", "1712345678.123456")).toBe(true);
});
it("records canonical Slack response thread participation instead of requested child thread", async () => {
clearSlackThreadParticipationCache();
const client = createSlackSendTestClient();
client.chat.postMessage.mockResolvedValueOnce({
ts: "1781932190.115869",
channel: "C123",
message: {
ts: "1781932190.115869",
thread_ts: "1781803536.235489",
},
});
const result = await sendMessageSlack("channel:C123", "hello thread", {
token: "xoxb-test",
cfg: SLACK_TEST_CFG,
client,
threadTs: "1781932168.648159",
});
expect(postedMessage(client).thread_ts).toBe("1781932168.648159");
expect(result.threadTs).toBe("1781803536.235489");
expect(result.receipt.threadId).toBe("1781803536.235489");
expect(hasSlackThreadParticipation("default", "C123", "1781803536.235489")).toBe(true);
expect(hasSlackThreadParticipation("default", "C123", "1781932168.648159")).toBe(false);
});
it("does not record participation for unthreaded sends", async () => {
clearSlackThreadParticipationCache();
const client = createSlackSendTestClient();
@@ -174,6 +202,40 @@ describe("sendMessageSlack chunking", () => {
).toStrictEqual([]);
expect(postedTexts.join("")).toBe(message);
});
it("preserves the first canonical response thread across chunked sends", async () => {
clearSlackThreadParticipationCache();
const client = createSlackSendTestClient();
client.chat.postMessage
.mockResolvedValueOnce({
ts: "1781932190.115869",
channel: "C123",
message: {
ts: "1781932190.115869",
thread_ts: "1781803536.235489",
},
})
.mockResolvedValueOnce({
ts: "1781932191.000000",
channel: "C123",
});
const message = "a".repeat(8500);
const result = await sendMessageSlack("channel:C123", message, {
token: "xoxb-test",
cfg: SLACK_TEST_CFG,
client,
threadTs: "1781932168.648159",
});
expect(client.chat.postMessage).toHaveBeenCalledTimes(2);
expect(postedMessage(client).thread_ts).toBe("1781932168.648159");
expect(postedMessage(client, 1).thread_ts).toBe("1781932168.648159");
expect(result.threadTs).toBe("1781803536.235489");
expect(result.receipt.threadId).toBe("1781803536.235489");
expect(hasSlackThreadParticipation("default", "C123", "1781803536.235489")).toBe(true);
expect(hasSlackThreadParticipation("default", "C123", "1781932168.648159")).toBe(false);
});
});
describe("sendMessageSlack blocks", () => {
@@ -202,6 +264,34 @@ describe("sendMessageSlack blocks", () => {
expect((receiptPart?.raw as Record<string, unknown> | undefined)?.channelId).toBe("C123");
});
it("uses canonical Slack response thread for block receipts and participation", async () => {
clearSlackThreadParticipationCache();
const client = createSlackSendTestClient();
client.chat.postMessage.mockResolvedValueOnce({
ts: "1781932190.115869",
channel: "C123",
message: {
ts: "1781932190.115869",
thread_ts: "1781803536.235489",
},
});
const result = await sendMessageSlack("channel:C123", "", {
token: "xoxb-test",
cfg: SLACK_TEST_CFG,
client,
threadTs: "1781932168.648159",
blocks: [{ type: "divider" }],
});
expect(postedMessage(client).thread_ts).toBe("1781932168.648159");
expect(result.threadTs).toBe("1781803536.235489");
expect(result.receipt.threadId).toBe("1781803536.235489");
expect(result.receipt.parts[0]?.kind).toBe("card");
expect(hasSlackThreadParticipation("default", "C123", "1781803536.235489")).toBe(true);
expect(hasSlackThreadParticipation("default", "C123", "1781932168.648159")).toBe(false);
});
it("posts user-target block messages directly without conversations.open", async () => {
const client = createSlackSendTestClient();
client.conversations.open.mockRejectedValueOnce(new Error("missing_scope"));

View File

@@ -360,10 +360,18 @@ async function postSlackMessageBestEffort(params: {
}
}
function resolvePostedMessageThreadTs(response: {
message?: { thread_ts?: unknown };
}): string | undefined {
const threadTs = response.message?.thread_ts;
return typeof threadTs === "string" ? normalizeSlackThreadTsCandidate(threadTs) : undefined;
}
export type SlackSendResult = {
messageId: string;
channelId: string;
receipt: MessageReceipt;
threadTs?: string;
};
function createSlackSendReceipt(params: {
@@ -664,7 +672,7 @@ export async function sendMessageSlack(
blocks,
}),
);
const threadTs = normalizeSlackThreadTsCandidate(opts.threadTs);
const threadTs = result.threadTs ?? normalizeSlackThreadTsCandidate(opts.threadTs);
if (threadTs && result.channelId && account.accountId) {
recordSlackThreadParticipation(account.accountId, result.channelId, threadTs);
}
@@ -737,14 +745,17 @@ async function sendMessageSlackQueuedInner(params: {
});
const messageId = response.ts ?? "unknown";
const deliveredChannelId = resolvePostedMessageChannelId(response, channelId);
const deliveredThreadTs =
resolvePostedMessageThreadTs(response) ?? normalizeSlackThreadTsCandidate(opts.threadTs);
return {
messageId,
channelId: deliveredChannelId,
threadTs: deliveredThreadTs,
receipt: createSlackSendReceipt({
platformMessageIds: [messageId],
channelId: deliveredChannelId,
kind: "card",
threadTs: opts.threadTs,
threadTs: deliveredThreadTs,
}),
};
}
@@ -774,6 +785,7 @@ async function sendMessageSlackQueuedInner(params: {
const sentMessageIds: string[] = [];
let lastMessageId = "";
let deliveredChannelId = channelId;
let canonicalDeliveredThreadTs: string | undefined;
if (opts.mediaUrl) {
const [firstChunk, ...rest] = resolvedChunks;
lastMessageId = await uploadSlackFile({
@@ -803,6 +815,7 @@ async function sendMessageSlackQueuedInner(params: {
});
lastMessageId = response.ts ?? lastMessageId;
deliveredChannelId = resolvePostedMessageChannelId(response, deliveredChannelId);
canonicalDeliveredThreadTs ??= resolvePostedMessageThreadTs(response);
if (response.ts) {
sentMessageIds.push(response.ts);
}
@@ -821,6 +834,7 @@ async function sendMessageSlackQueuedInner(params: {
});
lastMessageId = response.ts ?? lastMessageId;
deliveredChannelId = resolvePostedMessageChannelId(response, deliveredChannelId);
canonicalDeliveredThreadTs ??= resolvePostedMessageThreadTs(response);
if (response.ts) {
sentMessageIds.push(response.ts);
}
@@ -828,14 +842,17 @@ async function sendMessageSlackQueuedInner(params: {
}
const messageId = lastMessageId || "unknown";
const deliveredThreadTs =
canonicalDeliveredThreadTs ?? normalizeSlackThreadTsCandidate(opts.threadTs);
return {
messageId,
channelId: deliveredChannelId,
threadTs: deliveredThreadTs,
receipt: createSlackSendReceipt({
platformMessageIds: sentMessageIds.length ? sentMessageIds : [messageId],
channelId: deliveredChannelId,
kind: opts.mediaUrl ? "media" : "text",
threadTs: opts.threadTs,
threadTs: deliveredThreadTs,
}),
};
}