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 = { [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, }); }); });