1915 lines · 64.5 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,  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,  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(input: {  id: string;  previewUrl: string;  name?: string;  mimeType?: string;  sizeBytes?: number;  lastModified?: number;}): ComposerFileAttachment {  const name = input.name ?? "report.pdf";  const mimeType = input.mimeType ?? "application/pdf";  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: "file",    id: input.id,    name,    mimeType,    sizeBytes: file.size,    previewUrl: input.previewUrl,    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 syncPersistedAttachments", () => {  const threadId = ThreadId.make("thread-sync-persisted");  const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);   beforeEach(() => {    removeLocalStorageItem(COMPOSER_DRAFT_STORAGE_KEY);    useComposerDraftStore.setState({      draftsByThreadKey: {},      draftThreadsByThreadKey: {},      logicalProjectDraftThreadKeyByLogicalProjectKey: {},      stickyModelSelectionByProvider: {},      stickyActiveProvider: null,    });  });   afterEach(() => {    removeLocalStorageItem(COMPOSER_DRAFT_STORAGE_KEY);  });   it("treats malformed persisted draft storage as empty", async () => {    const image = makeImage({      id: "img-persisted",      previewUrl: "blob:persisted",    });    useComposerDraftStore.getState().addImage(threadRef, image);    setLocalStorageItem(      COMPOSER_DRAFT_STORAGE_KEY,      {        version: 2,        state: {          draftsByThreadId: {            [threadId]: {              attachments: "not-an-array",            },          },        },      },      Schema.Unknown,    );     useComposerDraftStore.getState().syncPersistedAttachments(threadRef, [      {        id: image.id,        name: image.name,        mimeType: image.mimeType,        sizeBytes: image.sizeBytes,        dataUrl: image.previewUrl,      },    ]);    await Promise.resolve();     expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.persistedAttachments).toEqual([]);    expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.nonPersistedImageIds).toEqual([image.id]);  });}); describe("composerDraftStore file attachments", () => {  const threadId = ThreadId.make("thread-file-attachments");  const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);   beforeEach(() => {    removeLocalStorageItem(COMPOSER_DRAFT_STORAGE_KEY);    resetComposerDraftStore();  });   afterEach(() => {    removeLocalStorageItem(COMPOSER_DRAFT_STORAGE_KEY);  });   function mergePersistedDraft(attachment: Record<string, unknown>) {    const persistApi = useComposerDraftStore.persist as unknown as {      getOptions: () => {        merge: (          persistedState: unknown,          currentState: ReturnType<typeof useComposerDraftStore.getState>,        ) => ReturnType<typeof useComposerDraftStore.getState>;      };    };    return persistApi.getOptions().merge(      {        draftsByThreadId: {          [threadId]: {            prompt: "",            attachments: [attachment],          },        },        draftThreadsByThreadId: {},        projectDraftThreadIdByProjectKey: {},      },      useComposerDraftStore.getInitialState(),    );  }   it("hydrates persisted attachments without a type as images (legacy drafts)", () => {    const mergedState = mergePersistedDraft({      id: "img-legacy",      name: "legacy.png",      mimeType: "image/png",      sizeBytes: 5,      dataUrl: "data:image/png;base64,aGVsbG8=",    });     expect(mergedState.draftsByThreadKey[threadKeyFor(threadId)]?.images).toMatchObject([      {        type: "image",        id: "img-legacy",        name: "legacy.png",        mimeType: "image/png",        sizeBytes: 5,        previewUrl: "data:image/png;base64,aGVsbG8=",      },    ]);  });   it("hydrates persisted attachments with type file as file attachments", () => {    const mergedState = mergePersistedDraft({      type: "file",      id: "file-persisted",      name: "report.pdf",      mimeType: "application/pdf",      sizeBytes: 5,      dataUrl: "data:application/pdf;base64,aGVsbG8=",    });     expect(mergedState.draftsByThreadKey[threadKeyFor(threadId)]?.images).toMatchObject([      {        type: "file",        id: "file-persisted",        name: "report.pdf",        mimeType: "application/pdf",        sizeBytes: 5,        previewUrl: "data:application/pdf;base64,aGVsbG8=",      },    ]);  });   it("persists the file type for staged file attachments (round trip)", () => {    const attachment = makeFile({      id: "file-round-trip",      previewUrl: "data:application/pdf;base64,aGVsbG8=",    });    useComposerDraftStore.getState().addImage(threadRef, attachment);    useComposerDraftStore.getState().syncPersistedAttachments(threadRef, [      {        type: "file",        id: attachment.id,        name: attachment.name,        mimeType: attachment.mimeType,        sizeBytes: attachment.sizeBytes,        dataUrl: attachment.previewUrl,      },    ]);     const persistApi = useComposerDraftStore.persist as unknown as {      getOptions: () => {        partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown;      };    };    const persisted = persistApi.getOptions().partialize(useComposerDraftStore.getState()) as {      draftsByThreadKey?: Record<string, { attachments?: Array<Record<string, unknown>> }>;    };     expect(      persisted.draftsByThreadKey?.[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.attachments?.[0],    ).toMatchObject({      type: "file",      id: "file-round-trip",      name: "report.pdf",      mimeType: "application/pdf",    });  });}); describe("composerDraftStore terminal contexts", () => {  const threadId = ThreadId.make("thread-dedupe");  const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);   beforeEach(() => {    useComposerDraftStore.setState({      draftsByThreadKey: {},      draftThreadsByThreadKey: {},

Lines 1–500 of 1915.