271 lines · 8.0 KB · TypeScript
RawDownload
import { afterEach, describe, expect, it } from "@effect/vitest";import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts"; import { appAtomRegistry } from "./atom-registry";import {  clearComposerDraftContentState,  composerDraftsAtom,  decodePersistedComposerDrafts,  type ComposerDraft,  getComposerDraftSnapshot,  mergeComposerDraftContentState,  removeComposerDraftsForEnvironment,  restoreComposerDraftSnapshotState,} from "./use-composer-drafts"; const DRAFT: ComposerDraft = {  text: "hello",  attachments: [],}; afterEach(() => {  appAtomRegistry.set(composerDraftsAtom, {});}); describe("mobile composer drafts", () => {  it("hydrates selector state even when the message content is empty", () => {    expect(      decodePersistedComposerDrafts({        schemaVersion: 1,        drafts: {          "new-task:environment-1:project-1": {            text: "",            attachments: [],            modelSelection: {              instanceId: "codex",              model: "gpt-5.4",              options: [{ id: "reasoningEffort", value: "xhigh" }],            },            runtimeMode: "approval-required",            interactionMode: "plan",            workspaceSelection: {              mode: "worktree",              branch: "main",              worktreePath: null,            },          },        },      }),    ).toEqual({      "new-task:environment-1:project-1": {        text: "",        attachments: [],        modelSelection: {          instanceId: "codex",          model: "gpt-5.4",          options: [{ id: "reasoningEffort", value: "xhigh" }],        },        runtimeMode: "approval-required",        interactionMode: "plan",        workspaceSelection: {          mode: "worktree",          branch: "main",          worktreePath: null,        },      },    });  });   it("keeps legacy content-only drafts and rejects invalid selector state", () => {    expect(      decodePersistedComposerDrafts({        schemaVersion: 1,        drafts: {          "environment-1:thread-1": DRAFT,        },      }),    ).toEqual({      "environment-1:thread-1": DRAFT,    });     expect(() =>      decodePersistedComposerDrafts({        schemaVersion: 1,        drafts: {          "environment-1:thread-1": {            ...DRAFT,            runtimeMode: "sometimes-safe",          },        },      }),    ).toThrow();  });   it("clears sent content without clearing the selected model or workspace", () => {    const draftKey = "environment-1:thread-1";    const draft: ComposerDraft = {      text: "send this",      attachments: [],      importedShareIds: ["share-1"],      modelSelection: {        instanceId: ProviderInstanceId.make("codex"),        model: "gpt-5.4",        options: [{ id: "reasoningEffort", value: "xhigh" }],      },      workspaceSelection: {        mode: "worktree",        branch: "main",        worktreePath: null,      },    };     expect(clearComposerDraftContentState({ [draftKey]: draft }, draftKey)).toEqual({      [draftKey]: {        modelSelection: draft.modelSelection,        workspaceSelection: draft.workspaceSelection,        text: "",        attachments: [],      },    });  });   it("drops the workspace selection when clearing a sent new-task draft", () => {    const draftKey = "new-task:environment-1:project-1";    const draft: ComposerDraft = {      text: "send this",      attachments: [],      modelSelection: {        instanceId: ProviderInstanceId.make("codex"),        model: "gpt-5.4",      },      workspaceSelection: {        mode: "worktree",        branch: "main",        worktreePath: null,        startFromOrigin: false,      },    };     expect(      clearComposerDraftContentState({ [draftKey]: draft }, draftKey, {        clearWorkspaceSelection: true,      }),    ).toEqual({      [draftKey]: {        modelSelection: draft.modelSelection,        text: "",        attachments: [],      },    });  });   it("reads the latest selector state synchronously for send", () => {    const draftKey = "environment-1:thread-1";    const selectedDraft: ComposerDraft = {      text: "send this",      attachments: [],      modelSelection: {        instanceId: ProviderInstanceId.make("codex"),        model: "gpt-5.4",        options: [{ id: "reasoningEffort", value: "xhigh" }],      },    };    appAtomRegistry.set(composerDraftsAtom, { [draftKey]: selectedDraft });     expect(getComposerDraftSnapshot(draftKey)).toEqual(selectedDraft);  });   it("merges shared content into a project draft without duplicating retries", () => {    const draftKey = "new-task:environment-1:project-1";    const sharedAttachment = {      id: "share-1:image:0",      type: "image" as const,      name: "Screenshot.png",      mimeType: "image/png",      sizeBytes: 3,      dataUrl: "data:image/png;base64,YWJj",      previewUri: "data:image/png;base64,YWJj",    };    const existing: Record<string, ComposerDraft> = {      [draftKey]: { text: "Existing context", attachments: [] },    };    const content = {      text: "Shared note",      attachments: [sharedAttachment],      sourceShareId: "share-1",    };     const merged = mergeComposerDraftContentState(existing, draftKey, content);    expect(merged[draftKey]).toMatchObject({      text: "Existing context\n\nShared note",      attachments: [sharedAttachment],      importedShareIds: ["share-1"],    });    expect(mergeComposerDraftContentState(merged, draftKey, content)).toBe(merged);     const edited = {      ...merged,      [draftKey]: { ...merged[draftKey]!, text: "User edited the imported context" },    };    expect(mergeComposerDraftContentState(edited, draftKey, content)).toBe(edited);  });   it("preserves existing images when shared content exceeds the draft attachment limit", () => {    const draftKey = "new-task:environment-1:project-1";    const image = (id: string) => ({      id,      type: "image" as const,      name: `${id}.png`,      mimeType: "image/png",      sizeBytes: 3,      dataUrl: "data:image/png;base64,YWJj",      previewUri: "data:image/png;base64,YWJj",    });    const existingImage = image("existing");    const sharedImages = Array.from({ length: 8 }, (_, index) => image(`shared-${index}`));     const merged = mergeComposerDraftContentState(      { [draftKey]: { text: "", attachments: [existingImage] } },      draftKey,      { text: "", attachments: sharedImages },    );     expect(merged[draftKey]?.attachments).toHaveLength(8);    expect(merged[draftKey]?.attachments[0]).toEqual(existingImage);    expect(merged[draftKey]?.attachments.at(-1)?.id).toBe("shared-6");  });   it("restores the exact draft captured before an interrupted share import", () => {    const draftKey = "new-task:environment-1:project-1";    const beforeImport: ComposerDraft = {      text: "Existing context",      attachments: [],      runtimeMode: "approval-required",    };    const imported: ComposerDraft = {      ...beforeImport,      text: "Existing context\n\nShared note",      importedShareIds: ["share-1"],    };     expect(      restoreComposerDraftSnapshotState({ [draftKey]: imported }, draftKey, beforeImport),    ).toEqual({ [draftKey]: beforeImport });    expect(      restoreComposerDraftSnapshotState({ [draftKey]: imported }, draftKey, {        text: "",        attachments: [],      }),    ).toEqual({});  });   it("removes only drafts owned by the selected environment", () => {    const environmentId = EnvironmentId.make("environment-cloud");    const retainedEnvironmentId = EnvironmentId.make("environment-local");     expect(      removeComposerDraftsForEnvironment(        {          [`${environmentId}:thread-cloud`]: DRAFT,          [`new-task:${environmentId}:project-cloud`]: DRAFT,          [`${retainedEnvironmentId}:thread-local`]: DRAFT,          [`new-task:${retainedEnvironmentId}:project-local`]: DRAFT,        },        environmentId,      ),    ).toEqual({      [`${retainedEnvironmentId}:thread-local`]: DRAFT,      [`new-task:${retainedEnvironmentId}:project-local`]: DRAFT,    });  });});