305 lines · 11.3 KB · TypeScript
RawDownload
import { CommandId, EnvironmentId, MessageId, ProjectId, ThreadId } from "@t3tools/contracts";import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const harness = vi.hoisted(() => ({  cleanup: vi.fn(),  clearDraft: vi.fn<typeof import("./use-composer-drafts").clearComposerDraft>(),  flushDrafts: vi.fn<typeof import("./use-composer-drafts").flushComposerDrafts>(async () => {}),  waitForDrafts: vi.fn<typeof import("./use-composer-drafts").waitForComposerDraftsLoaded>(    async () => {},  ),  manager: null as unknown as ReturnType<    typeof import("./thread-outbox-manager").createThreadOutboxManager  >,})); vi.mock("./thread-outbox", async () => {  const { createThreadOutboxManager } = await import("./thread-outbox-manager");  const { appAtomRegistry } = await import("./atom-registry");  harness.manager = createThreadOutboxManager({    registry: appAtomRegistry,    storage: {      load: async () => ({ messages: [], errors: [] }),      write: async () => undefined,      remove: async () => undefined,    },  });  return { threadOutboxManager: harness.manager };}); vi.mock("./use-composer-drafts", async (importOriginal) => {  const original = await importOriginal<typeof import("./use-composer-drafts")>();  return {    ...original,    clearComposerDraft: harness.clearDraft,    flushComposerDrafts: harness.flushDrafts,    scheduleUnusedComposerAttachmentCleanup: harness.cleanup,    waitForComposerDraftsLoaded: harness.waitForDrafts,  };}); import { appAtomRegistry } from "./atom-registry";import { clearThreadOutboxEnvironment, removeThreadOutboxMessage } from "./thread-outbox-removal";import type { QueuedThreadMessage } from "./thread-outbox-model";import { composerDraftsAtom } from "./use-composer-drafts"; function queuedMessage(input: {  readonly environmentId: string;  readonly messageId: string;  readonly fileUri: string;  readonly creation?: true;}): QueuedThreadMessage {  return {    environmentId: EnvironmentId.make(input.environmentId),    threadId: ThreadId.make(`thread-${input.messageId}`),    messageId: MessageId.make(input.messageId),    commandId: CommandId.make(`command-${input.messageId}`),    text: "Review the report",    attachments: [      {        id: `file-${input.messageId}`,        type: "file",        name: "report.pdf",        mimeType: "application/pdf",        sizeBytes: 42,        fileUri: input.fileUri,      },    ],    ...(input.creation      ? {          creation: {            projectId: ProjectId.make(`project-${input.messageId}`),            workspaceMode: "local" as const,            branch: null,            worktreePath: null,          },        }      : {}),    createdAt: "2026-08-24T12:00:00.000Z",  };} afterEach(() => {  appAtomRegistry.set(harness.manager.queuedMessagesByThreadKeyAtom, {});  appAtomRegistry.set(composerDraftsAtom, {});  harness.cleanup.mockClear();  harness.clearDraft.mockClear();  harness.flushDrafts.mockReset();  harness.flushDrafts.mockResolvedValue(undefined);  harness.waitForDrafts.mockReset();  harness.waitForDrafts.mockResolvedValue(undefined);}); describe("thread outbox removal", () => {  it("releases a removed message's attachment files with the removal itself", async () => {    const message = queuedMessage({      environmentId: "environment-1",      messageId: "message-1",      fileUri: "file:///documents/t3-composer-attachments/report.pdf",    });    await harness.manager.enqueue(message);     await removeThreadOutboxMessage(message);     expect(appAtomRegistry.get(harness.manager.queuedMessagesByThreadKeyAtom)).toEqual({});    expect(harness.cleanup).toHaveBeenCalledExactlyOnceWith(message.attachments);  });   it("keeps an edited message and its files when a revision-checked removal loses", async () => {    const message = queuedMessage({      environmentId: "environment-1",      messageId: "message-edited",      fileUri: "file:///documents/t3-composer-attachments/report.pdf",      creation: true,    });    await harness.manager.enqueue(message);    const revision = harness.manager.revisionOf(message.messageId);    const edited = { ...message, text: "edited while restoring" };    await harness.manager.update(edited);     await expect(removeThreadOutboxMessage(message, revision)).resolves.toBe(false);     expect(harness.cleanup).not.toHaveBeenCalled();    expect(harness.waitForDrafts).not.toHaveBeenCalled();    expect(harness.clearDraft).not.toHaveBeenCalled();    expect(harness.flushDrafts).not.toHaveBeenCalled();    const remaining = Object.values(      appAtomRegistry.get(harness.manager.queuedMessagesByThreadKeyAtom),    ).flat();    expect(remaining).toEqual([edited]);  });   it("clears a removed pending task draft and includes its editor-only files", async () => {    const message = queuedMessage({      environmentId: "environment-1",      messageId: "message-pending",      fileUri: "file:///documents/t3-composer-attachments/queued.pdf",      creation: true,    });    const editorOnlyFile = {      id: "file-editor-only",      type: "file" as const,      name: "editor-only.pdf",      mimeType: "application/pdf",      sizeBytes: 84,      fileUri: "file:///documents/t3-composer-attachments/editor-only.pdf",    };    const draftKey = `pending-task:${message.messageId}`;    appAtomRegistry.set(composerDraftsAtom, {      [draftKey]: { text: "edited", attachments: [editorOnlyFile] },    });    await harness.manager.enqueue(message);     await expect(removeThreadOutboxMessage(message)).resolves.toBe(true);     expect(harness.waitForDrafts).toHaveBeenCalledOnce();    expect(harness.clearDraft).toHaveBeenCalledExactlyOnceWith(draftKey, {      deferAttachmentCleanup: true,    });    expect(harness.flushDrafts).toHaveBeenCalledOnce();    expect(harness.cleanup).toHaveBeenCalledExactlyOnceWith([      ...message.attachments,      editorOnlyFile,    ]);    expect(harness.flushDrafts.mock.invocationCallOrder[0]).toBeLessThan(      harness.cleanup.mock.invocationCallOrder[0]!,    );  });   it("does not flush composer drafts when a removed creation has no editor draft", async () => {    const message = queuedMessage({      environmentId: "environment-1",      messageId: "message-without-editor-draft",      fileUri: "file:///documents/t3-composer-attachments/queued.pdf",      creation: true,    });    await harness.manager.enqueue(message);     await expect(removeThreadOutboxMessage(message)).resolves.toBe(true);     expect(harness.waitForDrafts).toHaveBeenCalledOnce();    expect(harness.clearDraft).not.toHaveBeenCalled();    expect(harness.flushDrafts).not.toHaveBeenCalled();    expect(harness.cleanup).toHaveBeenCalledExactlyOnceWith(message.attachments);  });   it("releases only the cleared environment's queued attachment files", async () => {    const cleared = queuedMessage({      environmentId: "environment-1",      messageId: "message-cleared",      fileUri: "file:///documents/t3-composer-attachments/cleared.pdf",    });    const kept = queuedMessage({      environmentId: "environment-2",      messageId: "message-kept",      fileUri: "file:///documents/t3-composer-attachments/kept.pdf",    });    await harness.manager.enqueue(cleared);    await harness.manager.enqueue(kept);     await clearThreadOutboxEnvironment(cleared.environmentId);     expect(harness.cleanup).toHaveBeenCalledExactlyOnceWith(cleared.attachments);    const remaining = Object.values(      appAtomRegistry.get(harness.manager.queuedMessagesByThreadKeyAtom),    ).flat();    expect(remaining.map((message) => message.messageId)).toEqual([kept.messageId]);  });   it("clears only removed pending drafts and keeps drafts for live messages", async () => {    const cleared = queuedMessage({      environmentId: "environment-1",      messageId: "message-cleared-pending",      fileUri: "file:///documents/t3-composer-attachments/cleared-pending.pdf",      creation: true,    });    const replaced = queuedMessage({      environmentId: "environment-1",      messageId: "message-replaced-pending",      fileUri: "file:///documents/t3-composer-attachments/replaced-pending.pdf",      creation: true,    });    const kept = queuedMessage({      environmentId: "environment-2",      messageId: "message-kept-pending",      fileUri: "file:///documents/t3-composer-attachments/kept-pending.pdf",      creation: true,    });    const replacement = { ...replaced, text: "replacement queued while drafts hydrate" };    const editorOnlyFile = {      id: "file-cleared-editor",      type: "file" as const,      name: "cleared-editor.pdf",      mimeType: "application/pdf",      sizeBytes: 84,      fileUri: "file:///documents/t3-composer-attachments/cleared-editor.pdf",    };    const hydrationStarted = Promise.withResolvers<void>();    const hydrationBarrier = Promise.withResolvers<void>();    harness.waitForDrafts.mockImplementationOnce(async () => {      hydrationStarted.resolve();      await hydrationBarrier.promise;    });    const clearedDraftKey = `pending-task:${cleared.messageId}`;    appAtomRegistry.set(composerDraftsAtom, {      [clearedDraftKey]: { text: "edited", attachments: [editorOnlyFile] },      [`pending-task:${replaced.messageId}`]: { text: "replacement", attachments: [] },      [`pending-task:${kept.messageId}`]: { text: "other environment", attachments: [] },    });    await Promise.all([      harness.manager.enqueue(cleared),      harness.manager.enqueue(replaced),      harness.manager.enqueue(kept),    ]);     const clearing = clearThreadOutboxEnvironment(cleared.environmentId);    await hydrationStarted.promise;    const replacing = harness.manager.enqueue(replacement);    hydrationBarrier.resolve();    await clearing;    await replacing;     expect(harness.clearDraft).toHaveBeenCalledExactlyOnceWith(clearedDraftKey, {      deferAttachmentCleanup: true,    });    expect(harness.cleanup).toHaveBeenCalledExactlyOnceWith([      ...cleared.attachments,      ...replaced.attachments,      editorOnlyFile,    ]);    const remaining = Object.values(      appAtomRegistry.get(harness.manager.queuedMessagesByThreadKeyAtom),    ).flat();    expect(remaining).toEqual(expect.arrayContaining([replacement, kept]));  });   it("keeps removal successful when pending draft persistence fails", async () => {    const message = queuedMessage({      environmentId: "environment-1",      messageId: "message-draft-flush-fails",      fileUri: "file:///documents/t3-composer-attachments/queued.pdf",      creation: true,    });    const flushError = new Error("composer storage unavailable");    const warning = vi.spyOn(console, "warn").mockImplementation(() => {});    harness.flushDrafts.mockRejectedValueOnce(flushError);    appAtomRegistry.set(composerDraftsAtom, {      [`pending-task:${message.messageId}`]: { text: "edited", attachments: [] },    });    await harness.manager.enqueue(message);     try {      await expect(removeThreadOutboxMessage(message)).resolves.toBe(true);       expect(harness.clearDraft).toHaveBeenCalledOnce();      expect(harness.cleanup).not.toHaveBeenCalled();      expect(warning).toHaveBeenCalledWith(        "[thread-outbox] failed to clean up removed pending task drafts",        flushError,      );      expect(appAtomRegistry.get(harness.manager.queuedMessagesByThreadKeyAtom)).toEqual({});    } finally {      warning.mockRestore();    }  });});