2693 lines · 94.3 KB · TypeScript
RawDownload
import {  scopedProjectKey,  scopedThreadKey,  scopeProjectRef,  scopeThreadRef,} from "@t3tools/client-runtime/environment";import * as Schema from "effect/Schema";import {  defaultInstanceIdForDriver,  EnvironmentId,  ProjectId,  ProviderDriverKind,  ProviderInstanceId,  PROVIDER_SEND_TURN_MAX_ATTACHMENTS,  ThreadId,  type ModelSelection,  type ProviderOptionSelection,} from "@t3tools/contracts";import { createModelSelection } from "@t3tools/shared/model"; // The composer draft's `modelSelectionByProvider` and// `stickyModelSelectionByProvider` maps are keyed by `ProviderInstanceId`// in production; these aliases keep the legacy-key migration tests concise.const CODEX_INSTANCE = ProviderInstanceId.make("codex");const CODEX_SECONDARY_INSTANCE = ProviderInstanceId.make("codex_secondary");const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent");const CURSOR_INSTANCE = ProviderInstanceId.make("cursor");const CODEX_DRIVER = ProviderDriverKind.make("codex");const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent");const CURSOR_DRIVER = ProviderDriverKind.make("cursor"); type ProviderOptionSelectionBag = ReadonlyArray<ProviderOptionSelection>;type ProviderOptionSelectionsByProvider = Partial<Record<string, ProviderOptionSelectionBag>>; function toSelections(  options: Record<string, string | boolean | undefined> | undefined,): ReadonlyArray<ProviderOptionSelection> {  const result: Array<ProviderOptionSelection> = [];  if (!options) return result;  for (const [id, value] of Object.entries(options)) {    if (typeof value === "string" || typeof value === "boolean") {      result.push({ id, value });    }  }  return result;} function selectionsByProvider(  options: Partial<Record<ProviderDriverKind, Record<string, string | boolean | undefined>>>,): ProviderOptionSelectionsByProvider {  const result: ProviderOptionSelectionsByProvider = {};  for (const [provider, bag] of Object.entries(options) as Array<    [ProviderDriverKind, Record<string, string | boolean | undefined>]  >) {    result[provider] = toSelections(bag);  }  return result;}import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import {  COMPOSER_DRAFT_STORAGE_KEY,  clearComposerDraftsEnvironment,  finalizePromotedDraftThreadByRef,  markPromotedDraftThread,  markPromotedDraftThreadByRef,  markPromotedDraftThreads,  markPromotedDraftThreadsByRef,  type ComposerFileAttachment,  type ComposerImageAttachment,  composerFileNeedsReattach,  useComposerDraftStore,  DraftId,} from "./composerDraftStore";import { removeLocalStorageItem, setLocalStorageItem } from "./hooks/useLocalStorage";import {  INLINE_TERMINAL_CONTEXT_PLACEHOLDER,  insertInlineTerminalContextPlaceholder,  type TerminalContextDraft,} from "./lib/terminalContext";import { createDebouncedStorage } from "./lib/storage"; function makeImage(input: {  id: string;  previewUrl: string;  name?: string;  mimeType?: string;  sizeBytes?: number;  lastModified?: number;}): ComposerImageAttachment {  const name = input.name ?? "image.png";  const mimeType = input.mimeType ?? "image/png";  const sizeBytes = input.sizeBytes ?? 4;  const lastModified = input.lastModified ?? 1_700_000_000_000;  const file = new File([new Uint8Array(sizeBytes).fill(1)], name, {    type: mimeType,    lastModified,  });  return {    type: "image",    id: input.id,    name,    mimeType,    sizeBytes: file.size,    previewUrl: input.previewUrl,    file,  };} function makeFile(id: string): ComposerFileAttachment {  const file = new File(["report"], "report.pdf", { type: "application/pdf" });  return {    type: "file",    id,    name: file.name,    mimeType: file.type,    sizeBytes: file.size,    file,  };} function makeTerminalContext(input: {  id: string;  text?: string;  terminalId?: string;  terminalLabel?: string;  lineStart?: number;  lineEnd?: number;}): TerminalContextDraft {  return {    id: input.id,    threadId: ThreadId.make("thread-dedupe"),    terminalId: input.terminalId ?? "default",    terminalLabel: input.terminalLabel ?? "Terminal 1",    lineStart: input.lineStart ?? 4,    lineEnd: input.lineEnd ?? 5,    text: input.text ?? "git status\nOn branch main",    createdAt: "2026-03-13T12:00:00.000Z",  };} function resetComposerDraftStore() {  useComposerDraftStore.setState({    draftsByThreadKey: {},    draftThreadsByThreadKey: {},    logicalProjectDraftThreadKeyByLogicalProjectKey: {},    stickyModelSelectionByProvider: {},    stickyActiveProvider: null,  });} function modelSelection(  provider: ProviderDriverKind,  model: string,  options?: Record<string, string | boolean | undefined>,): ModelSelection {  return createModelSelection(defaultInstanceIdForDriver(provider), model, toSelections(options));} function providerModelOptions(  options: Partial<Record<string, Record<string, string | boolean | undefined>>>,): ProviderOptionSelectionsByProvider {  return selectionsByProvider(options);} const TEST_ENVIRONMENT_ID = EnvironmentId.make("environment-local");const OTHER_TEST_ENVIRONMENT_ID = EnvironmentId.make("environment-remote");const LEGACY_TEST_ENVIRONMENT_ID = EnvironmentId.make("__legacy__"); function threadKeyFor(  threadId: ThreadId,  environmentId: EnvironmentId = LEGACY_TEST_ENVIRONMENT_ID,): string {  if (environmentId === LEGACY_TEST_ENVIRONMENT_ID) {    return threadId;  }  return scopedThreadKey(scopeThreadRef(environmentId, threadId));} function draftFor(threadId: ThreadId, environmentId: EnvironmentId = LEGACY_TEST_ENVIRONMENT_ID) {  const store = useComposerDraftStore.getState().draftsByThreadKey;  return store[threadKeyFor(threadId, environmentId)] ?? store[threadId] ?? undefined;} function draftByKey(key: string) {  return useComposerDraftStore.getState().draftsByThreadKey[key] ?? undefined;} describe("composerDraftStore addImages", () => {  const threadId = ThreadId.make("thread-dedupe");  const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);  let originalRevokeObjectUrl: typeof URL.revokeObjectURL;  let revokeSpy: ReturnType<typeof vi.fn<(url: string) => void>>;   beforeEach(() => {    resetComposerDraftStore();    originalRevokeObjectUrl = URL.revokeObjectURL;    revokeSpy = vi.fn();    URL.revokeObjectURL = revokeSpy;  });   afterEach(() => {    URL.revokeObjectURL = originalRevokeObjectUrl;  });   it("deduplicates identical images in one batch by file signature", () => {    const first = makeImage({      id: "img-1",      previewUrl: "blob:first",      name: "same.png",      mimeType: "image/png",      sizeBytes: 12,      lastModified: 12345,    });    const duplicate = makeImage({      id: "img-2",      previewUrl: "blob:duplicate",      name: "same.png",      mimeType: "image/png",      sizeBytes: 12,      lastModified: 12345,    });     useComposerDraftStore.getState().addImages(threadRef, [first, duplicate]);     const draft = draftFor(threadId, TEST_ENVIRONMENT_ID);    expect(draft?.images.map((image) => image.id)).toEqual(["img-1"]);    expect(revokeSpy).toHaveBeenCalledWith("blob:duplicate");  });   it("deduplicates against existing images across calls by file signature", () => {    const first = makeImage({      id: "img-a",      previewUrl: "blob:a",      name: "same.png",      mimeType: "image/png",      sizeBytes: 9,      lastModified: 777,    });    const duplicateLater = makeImage({      id: "img-b",      previewUrl: "blob:b",      name: "same.png",      mimeType: "image/png",      sizeBytes: 9,      lastModified: 999,    });     useComposerDraftStore.getState().addImage(threadRef, first);    useComposerDraftStore.getState().addImage(threadRef, duplicateLater);     const draft = draftFor(threadId, TEST_ENVIRONMENT_ID);    expect(draft?.images.map((image) => image.id)).toEqual(["img-a"]);    expect(revokeSpy).toHaveBeenCalledWith("blob:b");  });   it("does not revoke blob URLs that are still used by an accepted duplicate image", () => {    const first = makeImage({      id: "img-shared",      previewUrl: "blob:shared",    });    const duplicateSameUrl = makeImage({      id: "img-shared",      previewUrl: "blob:shared",    });     useComposerDraftStore.getState().addImages(threadRef, [first, duplicateSameUrl]);     const draft = draftFor(threadId, TEST_ENVIRONMENT_ID);    expect(draft?.images.map((image) => image.id)).toEqual(["img-shared"]);    expect(revokeSpy).not.toHaveBeenCalledWith("blob:shared");  });}); describe("composerDraftStore clearComposerContent", () => {  const threadId = ThreadId.make("thread-clear");  const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);  let originalRevokeObjectUrl: typeof URL.revokeObjectURL;  let revokeSpy: ReturnType<typeof vi.fn<(url: string) => void>>;   beforeEach(() => {    resetComposerDraftStore();    originalRevokeObjectUrl = URL.revokeObjectURL;    revokeSpy = vi.fn();    URL.revokeObjectURL = revokeSpy;  });   afterEach(() => {    URL.revokeObjectURL = originalRevokeObjectUrl;  });   it("does not revoke blob preview URLs when clearing composer content", () => {    const first = makeImage({      id: "img-optimistic",      previewUrl: "blob:optimistic",    });    useComposerDraftStore.getState().addImage(threadRef, first);     useComposerDraftStore.getState().clearComposerContent(threadRef);     const draft = draftFor(threadId, TEST_ENVIRONMENT_ID);    expect(draft).toBeUndefined();    expect(revokeSpy).not.toHaveBeenCalledWith("blob:optimistic");  });}); describe("composerDraftStore file attachments", () => {  const threadId = ThreadId.make("thread-files");  const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);   beforeEach(() => {    resetComposerDraftStore();  });   it("persists uploaded file references without including file contents", () => {    const store = useComposerDraftStore.getState();    store.addFiles(threadRef, [makeFile("file-1")]);    store.setFileUpload(threadRef, "file-1", TEST_ENVIRONMENT_ID, "pending-report-pdf");     const persistApi = useComposerDraftStore.persist as unknown as {      getOptions: () => {        partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown;        merge: (          persistedState: unknown,          currentState: ReturnType<typeof useComposerDraftStore.getState>,        ) => ReturnType<typeof useComposerDraftStore.getState>;      };    };    const options = persistApi.getOptions();    const persisted = options.partialize(useComposerDraftStore.getState()) as {      draftsByThreadKey: Record<string, { files?: Array<Record<string, unknown>> }>;    };     expect(persisted.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files).toEqual(      [        {          id: "file-1",          name: "report.pdf",          mimeType: "application/pdf",          sizeBytes: 6,          attachmentId: "pending-report-pdf",          environmentId: TEST_ENVIRONMENT_ID,        },      ],    );     const hydrated = options.merge(persisted, useComposerDraftStore.getState());    expect(hydrated.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files).toEqual([      {        type: "file",        id: "file-1",        name: "report.pdf",        mimeType: "application/pdf",        sizeBytes: 6,        file: null,        uploadedAttachmentId: "pending-report-pdf",        uploadEnvironmentId: TEST_ENVIRONMENT_ID,      },    ]);  });   it("persists a pending file as a needs-reattach marker instead of dropping it", () => {    const store = useComposerDraftStore.getState();    // No setFileUpload: the upload never finished, so there is no attachment    // id and the File handle cannot serialize.    store.addFiles(threadRef, [makeFile("file-pending")]);     const persistApi = useComposerDraftStore.persist as unknown as {      getOptions: () => {        partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown;        merge: (          persistedState: unknown,          currentState: ReturnType<typeof useComposerDraftStore.getState>,        ) => ReturnType<typeof useComposerDraftStore.getState>;      };    };    const options = persistApi.getOptions();    const persisted = options.partialize(useComposerDraftStore.getState()) as {      draftsByThreadKey: Record<string, { files?: Array<Record<string, unknown>> }>;    };     expect(persisted.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files).toEqual(      [        {          id: "file-pending",          name: "report.pdf",          mimeType: "application/pdf",          sizeBytes: 6,        },      ],    );     const hydrated = options.merge(persisted, useComposerDraftStore.getState());    const hydratedFiles =      hydrated.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files;    expect(hydratedFiles).toEqual([      {        type: "file",        id: "file-pending",        name: "report.pdf",        mimeType: "application/pdf",        sizeBytes: 6,        file: null,      },    ]);    expect(hydratedFiles?.every(composerFileNeedsReattach)).toBe(true);  });   it("marks only the matching byte-less upload as missing", () => {    const store = useComposerDraftStore.getState();    const hydrated: ComposerFileAttachment = {      ...makeFile("file-hydrated"),      file: null,      uploadedAttachmentId: "pending-old",      uploadEnvironmentId: TEST_ENVIRONMENT_ID,    };    const local: ComposerFileAttachment = {      ...makeFile("file-local"),      name: "local.txt",      mimeType: "text/plain",    };    store.addFiles(threadRef, [hydrated, local]);    store.setFileUpload(threadRef, hydrated.id, TEST_ENVIRONMENT_ID, "pending-new");    store.setFileUpload(threadRef, local.id, TEST_ENVIRONMENT_ID, "pending-local");     expect(      store.markFileUploadMissing(threadRef, hydrated.id, OTHER_TEST_ENVIRONMENT_ID, "pending-new"),    ).toBe(false);    expect(      store.markFileUploadMissing(threadRef, hydrated.id, TEST_ENVIRONMENT_ID, "pending-old"),    ).toBe(false);    expect(      store.markFileUploadMissing(threadRef, local.id, TEST_ENVIRONMENT_ID, "pending-local"),    ).toBe(false);     expect(store.getComposerDraft(threadRef)?.files).toMatchObject([      {        id: hydrated.id,        uploadedAttachmentId: "pending-new",        uploadEnvironmentId: TEST_ENVIRONMENT_ID,      },      {        id: local.id,        file: local.file,        uploadedAttachmentId: "pending-local",        uploadEnvironmentId: TEST_ENVIRONMENT_ID,      },    ]);     expect(      store.markFileUploadMissing(threadRef, hydrated.id, TEST_ENVIRONMENT_ID, "pending-new"),    ).toBe(true);    const marker = store.getComposerDraft(threadRef)?.files[0];    expect(marker && composerFileNeedsReattach(marker)).toBe(true);    expect(marker?.uploadedAttachmentId).toBeUndefined();    expect(marker?.uploadEnvironmentId).toBeUndefined();  });   it("removes generic files when the composer is cleared", () => {    const store = useComposerDraftStore.getState();    store.addFiles(threadRef, [makeFile("file-clear")]);     store.clearComposerContent(threadRef);     expect(store.getComposerDraft(threadRef)).toBeNull();  });   it("removes generic files when a prompt is moved into the stash", () => {    const store = useComposerDraftStore.getState();    store.setPrompt(threadRef, "Review the report");    store.addFiles(threadRef, [makeFile("file-stash")]);     store.clearComposerPromptAndImages(threadRef);     expect(store.getComposerDraft(threadRef)).toBeNull();  });   it("enforces the combined file and image limit across separate updates", () => {    const store = useComposerDraftStore.getState();    const images = Array.from({ length: PROVIDER_SEND_TURN_MAX_ATTACHMENTS - 1 }, (_, index) =>      makeImage({        id: `image-${index}`,        name: `image-${index}.png`,        previewUrl: `blob:image-${index}`,      }),    );    store.addImages(threadRef, images);    store.addFiles(threadRef, [      makeFile("file-accepted"),      { ...makeFile("file-overflow"), name: "other.pdf" },    ]);    store.addImages(threadRef, [      makeImage({ id: "image-overflow", name: "overflow.png", previewUrl: "blob:overflow" }),    ]);     const draft = store.getComposerDraft(threadRef);    expect(draft?.images).toHaveLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS - 1);    expect(draft?.files.map((file) => file.id)).toEqual(["file-accepted"]);  }); 

Lines 1–500 of 2693.