mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-09 03:22:40 +00:00
fix(slack): record canonical sent thread (#95250)
This commit is contained in:
@@ -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"));
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user