fix(slack): retire replay-filtered debounce keys (#128331)

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-23 12:10:26 -07:00
committed by GitHub
parent a2e4c277c0
commit 3a3f4e3fd6
2 changed files with 64 additions and 16 deletions
@@ -563,6 +563,47 @@ describe("createSlackMessageHandler", () => {
expect(flushKeyMock).toHaveBeenCalledWith("slack:default:C111:1709000000.000100:U111");
});
it("retires a buffered key when replay filtering drops every entry", async () => {
const handler = createSlackMessageHandler({
ctx: createContext(),
account: { accountId: "default" } as Parameters<
typeof createSlackMessageHandler
>[0]["account"],
});
const bufferedMessage = {
type: "message" as const,
channel: "C111",
user: "U111",
ts: "1709000000.000300",
text: "duplicate buffered text",
};
await handler(bufferedMessage as never, { source: "message" });
const first = enqueueMock.mock.calls[0]?.[0] as Record<string, unknown>;
await runOnFlush([first]);
await handler(bufferedMessage as never, { source: "message" });
const duplicate = enqueueMock.mock.calls[1]?.[0] as Record<string, unknown>;
await runOnFlush([duplicate]);
expect(dispatchPreparedSlackMessageMock).toHaveBeenCalledTimes(1);
flushKeyMock.mockClear();
await handler(
{
type: "message",
subtype: "file_share",
channel: "C111",
user: "U111",
ts: "1709000000.000400",
text: "file follows",
files: [{ id: "F1" }],
} as never,
{ source: "message" },
);
expect(flushKeyMock).not.toHaveBeenCalled();
});
it("waits for debounced dispatch completion when requested by relay delivery", async () => {
const { handler } = createHandlerWithTracker();
const handled = handler(
+23 -16
View File
@@ -208,6 +208,29 @@ export function createSlackMessageHandler(params: {
.filter((completion) => completion !== undefined);
try {
await (async () => {
const flushedEntry = entries.at(-1);
if (flushedEntry) {
const teamId = flushedEntry.opts.eventScope?.teamId;
const flushedKey = buildSlackDebounceKey(
flushedEntry.message,
ctx.accountId,
teamId,
);
const topLevelConversationKey = buildTopLevelSlackConversationKey(
flushedEntry.message,
ctx.accountId,
teamId,
);
if (flushedKey && topLevelConversationKey) {
const pendingKeys = pendingTopLevelDebounceKeys.get(topLevelConversationKey);
if (pendingKeys) {
pendingKeys.delete(flushedKey);
if (pendingKeys.size === 0) {
pendingTopLevelDebounceKeys.delete(topLevelConversationKey);
}
}
}
}
// Logical-identity claims: Slack sends message + app_mention twins with
// distinct event_ids for one post, so the durable queue cannot dedupe
// them. Same-flush twins share one claim and one logical message while
@@ -271,22 +294,6 @@ export function createSlackMessageHandler(params: {
releaseClaims();
return;
}
const teamId = last.opts.eventScope?.teamId;
const flushedKey = buildSlackDebounceKey(last.message, ctx.accountId, teamId);
const topLevelConversationKey = buildTopLevelSlackConversationKey(
last.message,
ctx.accountId,
teamId,
);
if (flushedKey && topLevelConversationKey) {
const pendingKeys = pendingTopLevelDebounceKeys.get(topLevelConversationKey);
if (pendingKeys) {
pendingKeys.delete(flushedKey);
if (pendingKeys.size === 0) {
pendingTopLevelDebounceKeys.delete(topLevelConversationKey);
}
}
}
const combinedText =
surviving.length === 1
? (last.message.text ?? "")