import { describe, expect, it, vi } from "vitest"; import { createSlackDraftStream } from "./draft-stream.js"; type DraftStreamParams = Parameters[0]; type DraftSendFn = NonNullable; type DraftEditFn = NonNullable; type DraftRemoveFn = NonNullable; type DraftWarnFn = NonNullable; function createDraftStreamHarness( params: { maxChars?: number; send?: DraftSendFn; edit?: DraftEditFn; remove?: DraftRemoveFn; warn?: DraftWarnFn; } = {}, ) { const send = params.send ?? vi.fn(async () => ({ channelId: "C123", messageId: "111.222", })); const edit = params.edit ?? vi.fn(async () => {}); const remove = params.remove ?? vi.fn(async () => {}); const warn = params.warn ?? vi.fn(); const stream = createSlackDraftStream({ target: "channel:C123", token: "xoxb-test", throttleMs: 250, maxChars: params.maxChars, send, edit, remove, warn, }); return { stream, send, edit, remove, warn }; } describe("createSlackDraftStream", () => { it("sends the first update and edits subsequent updates", async () => { const { stream, send, edit } = createDraftStreamHarness(); stream.update("hello"); await stream.flush(); stream.update("hello world"); await stream.flush(); expect(send).toHaveBeenCalledTimes(1); expect(edit).toHaveBeenCalledTimes(1); expect(edit).toHaveBeenCalledWith("C123", "111.222", "hello world", { token: "xoxb-test", accountId: undefined, }); }); it("does not send duplicate text", async () => { const { stream, send, edit } = createDraftStreamHarness(); stream.update("same"); await stream.flush(); stream.update("same"); await stream.flush(); expect(send).toHaveBeenCalledTimes(1); expect(edit).toHaveBeenCalledTimes(0); }); it("supports forceNewMessage for subsequent assistant messages", async () => { const send = vi .fn() .mockResolvedValueOnce({ channelId: "C123", messageId: "111.222" }) .mockResolvedValueOnce({ channelId: "C123", messageId: "333.444" }); const { stream, edit } = createDraftStreamHarness({ send }); stream.update("first"); await stream.flush(); stream.forceNewMessage(); stream.update("second"); await stream.flush(); expect(send).toHaveBeenCalledTimes(2); expect(edit).toHaveBeenCalledTimes(0); expect(stream.messageId()).toBe("333.444"); }); it("stops when text exceeds max chars", async () => { const { stream, send, edit, warn } = createDraftStreamHarness({ maxChars: 5 }); stream.update("123456"); await stream.flush(); stream.update("ok"); await stream.flush(); expect(send).not.toHaveBeenCalled(); expect(edit).not.toHaveBeenCalled(); expect(warn).toHaveBeenCalledTimes(1); }); it("clear removes preview message when one exists", async () => { const { stream, remove } = createDraftStreamHarness(); stream.update("hello"); await stream.flush(); await stream.clear(); expect(remove).toHaveBeenCalledTimes(1); expect(remove).toHaveBeenCalledWith("C123", "111.222", { token: "xoxb-test", accountId: undefined, }); expect(stream.messageId()).toBeUndefined(); expect(stream.channelId()).toBeUndefined(); }); it("clear is a no-op when no preview message exists", async () => { const { stream, remove } = createDraftStreamHarness(); await stream.clear(); expect(remove).not.toHaveBeenCalled(); }); it("clear warns when cleanup fails", async () => { const remove = vi.fn(async () => { throw new Error("cleanup failed"); }); const warn = vi.fn(); const { stream } = createDraftStreamHarness({ remove, warn }); stream.update("hello"); await stream.flush(); await stream.clear(); expect(warn).toHaveBeenCalledWith("slack stream preview cleanup failed: cleanup failed"); expect(stream.messageId()).toBeUndefined(); expect(stream.channelId()).toBeUndefined(); }); });