fix(slack): await draft stream flush before messageId check to prevent duplicate messages

This commit is contained in:
Alberto Leal 2026-02-21 05:01:39 -05:00
parent 8a05c05596
commit 3bbbf789cb
2 changed files with 40 additions and 0 deletions

View File

@ -137,4 +137,39 @@ describe("createSlackDraftStream", () => {
expect(stream.messageId()).toBeUndefined();
expect(stream.channelId()).toBeUndefined();
});
it("flush resolves in-flight send so messageId is available (race condition fix)", async () => {
let resolveDeferred!: (value: { channelId: string; messageId: string }) => void;
const send = vi.fn<DraftSendFn>(
() =>
new Promise((resolve) => {
resolveDeferred = resolve;
}),
);
const { stream } = createDraftStreamHarness({ send });
stream.update("hello");
// Start flush — send is in-flight but hasn't resolved
const flushPromise = stream.flush();
// At this point, sendMessageSlack is in-flight but hasn't resolved
expect(stream.messageId()).toBeUndefined();
// Simulate send completing
resolveDeferred({ channelId: "C123", messageId: "999.888" });
await flushPromise;
// Now messageId should be populated
expect(stream.messageId()).toBe("999.888");
});
it("flush is safe to call when no draft has been started", async () => {
const { stream, send } = createDraftStreamHarness();
// Flush without any update — should be a no-op
await stream.flush();
expect(send).not.toHaveBeenCalled();
expect(stream.messageId()).toBeUndefined();
});
});

View File

@ -310,6 +310,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
const reply = resolveSendableOutboundReplyParts(payload);
const slackBlocks = readSlackReplyBlocks(payload);
// Flush any in-flight draft send so messageId() is populated.
// Without this, a race between the fire-and-forget sendMessageSlack()
// and the deliver callback can cause canFinalizeViaPreviewEdit to
// evaluate false, resulting in a duplicate Slack message.
await draftStream?.flush();
const draftMessageId = draftStream?.messageId();
const draftChannelId = draftStream?.channelId();
const finalText = reply.text;