import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { CommandId, EnvironmentId, MessageId, ThreadId } from "@t3tools/contracts"; import type { QueuedThreadMessage } from "./thread-outbox-model"; const state = vi.hoisted(() => ({ dispatching: null as string | null, held: {} as Record, draft: { text: "Existing draft", attachments: [] as { id: string }[] }, confirm: vi.fn(async () => true), remove: vi.fn(async () => true), flush: vi.fn(async () => {}), })); vi.mock("./atom-registry", () => ({ appAtomRegistry: { get: (atom: string) => (atom === "dispatching" ? state.dispatching : state.held), }, })); vi.mock("./use-thread-outbox", () => ({ dispatchingQueuedMessageIdAtom: "dispatching", editingQueuedMessageIdsAtom: "editing", holdEditingQueuedMessage: (id: string) => { state.held[id] = true; }, releaseEditingQueuedMessage: (id: string) => { delete state.held[id]; }, })); vi.mock("./thread-outbox", () => ({ confirmThreadOutboxMessageQueued: state.confirm, threadOutboxRevision: () => 1, })); vi.mock("./thread-outbox-removal", () => ({ removeThreadOutboxMessage: state.remove })); vi.mock("./use-composer-drafts", () => ({ waitForComposerDraftsLoaded: async () => {}, getComposerDraftSnapshot: () => state.draft, mergeComposerDraftContent: async (_key: string, message: QueuedThreadMessage) => { state.draft = { text: `${state.draft.text}\n\n${message.text}`, attachments: [...state.draft.attachments, ...message.attachments], }; }, updateComposerDraftSettings: () => {}, flushComposerDrafts: state.flush, undoComposerDraftMerge: async (_key: string, snapshot: typeof state.draft) => { state.draft = snapshot; }, })); import { editPendingThreadMessage } from "./edit-pending-thread-message"; const message: QueuedThreadMessage = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread"), messageId: MessageId.make("message"), commandId: CommandId.make("command"), text: "Queued task", createdAt: "2026-09-06T10:00:00.000Z", attachments: [ { id: "file", type: "file", name: "notes.txt", mimeType: "text/plain", sizeBytes: 10, fileUri: "file:///notes.txt", }, ], }; beforeEach(() => { vi.clearAllMocks(); state.dispatching = null; state.held = {}; state.draft = { text: "Existing draft", attachments: [] }; state.confirm.mockResolvedValue(true); state.remove.mockResolvedValue(true); state.flush.mockResolvedValue(undefined); }); describe("editing a pending message", () => { it("locks delivery and persists text and attachments before removing the queued copy", async () => { state.confirm.mockImplementationOnce(async () => { expect(state.held[message.messageId]).toBe(true); return true; }); state.remove.mockImplementationOnce(async () => { expect(state.flush).toHaveBeenCalled(); expect(state.draft.text).toBe("Existing draft\n\nQueued task"); expect(state.draft.attachments).toEqual(message.attachments); return true; }); expect(await editPendingThreadMessage(message)).toBe(true); expect(state.held).toEqual({}); }); it("does not reclaim a message already being dispatched", async () => { state.dispatching = message.messageId; expect(await editPendingThreadMessage(message)).toBe(false); expect(state.confirm).not.toHaveBeenCalled(); expect(state.draft.text).toBe("Existing draft"); }); it("rolls back the draft if removing the queued message fails", async () => { state.remove.mockRejectedValueOnce(new Error("disk error")); await expect(editPendingThreadMessage(message)).rejects.toThrow("disk error"); expect(state.draft).toEqual({ text: "Existing draft", attachments: [] }); expect(state.held).toEqual({}); }); it("rolls back when a newer queue revision wins", async () => { state.remove.mockResolvedValueOnce(false); expect(await editPendingThreadMessage(message)).toBe(false); expect(state.draft.text).toBe("Existing draft"); }); });