1770 lines · 60.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,  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 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 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 terminal contexts", () => {  const threadId = ThreadId.make("thread-dedupe");  const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);   beforeEach(() => {    useComposerDraftStore.setState({      draftsByThreadKey: {},      draftThreadsByThreadKey: {},      logicalProjectDraftThreadKeyByLogicalProjectKey: {},      stickyModelSelectionByProvider: {},      stickyActiveProvider: null,    });  });   it("deduplicates identical terminal contexts by selection signature", () => {    const first = makeTerminalContext({ id: "ctx-1" });    const duplicate = makeTerminalContext({ id: "ctx-2" });     useComposerDraftStore.getState().addTerminalContexts(threadRef, [first, duplicate]);     const draft = draftFor(threadId, TEST_ENVIRONMENT_ID);    expect(draft?.terminalContexts.map((context) => context.id)).toEqual(["ctx-1"]);  });   it("clears terminal contexts when clearing composer content", () => {    useComposerDraftStore      .getState()      .addTerminalContext(threadRef, makeTerminalContext({ id: "ctx-1" }));     useComposerDraftStore.getState().clearComposerContent(threadRef);     expect(draftFor(threadId, TEST_ENVIRONMENT_ID)).toBeUndefined();  });   it("inserts terminal contexts at the requested inline prompt position", () => {    const firstInsertion = insertInlineTerminalContextPlaceholder("alpha beta", 6);    const secondInsertion = insertInlineTerminalContextPlaceholder(firstInsertion.prompt, 0);     expect(      useComposerDraftStore        .getState()        .insertTerminalContext(          threadRef,          firstInsertion.prompt,          makeTerminalContext({ id: "ctx-1" }),          firstInsertion.contextIndex,        ),    ).toBe(true);    expect(      useComposerDraftStore.getState().insertTerminalContext(        threadRef,        secondInsertion.prompt,        makeTerminalContext({          id: "ctx-2",          terminalLabel: "Terminal 2",          lineStart: 9,          lineEnd: 10,        }),        secondInsertion.contextIndex,      ),    ).toBe(true);     const draft = draftFor(threadId, TEST_ENVIRONMENT_ID);    expect(draft?.prompt).toBe(      `${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} alpha ${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} beta`,    );    expect(draft?.terminalContexts.map((context) => context.id)).toEqual(["ctx-2", "ctx-1"]);  });   it("omits terminal context text from persisted drafts", () => {    useComposerDraftStore      .getState()      .addTerminalContext(threadRef, makeTerminalContext({ id: "ctx-persist" }));     const persistApi = useComposerDraftStore.persist as unknown as {      getOptions: () => {        partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown;      };    };    const persistedState = persistApi.getOptions().partialize(useComposerDraftStore.getState()) as {      draftsByThreadKey?: Record<string, { terminalContexts?: Array<Record<string, unknown>> }>;    };     expect(      persistedState.draftsByThreadKey?.[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]        ?.terminalContexts?.[0],      "Expected terminal context metadata to be persisted.",    ).toMatchObject({      id: "ctx-persist",      terminalId: "default",      terminalLabel: "Terminal 1",      lineStart: 4,      lineEnd: 5,    });    expect(      persistedState.draftsByThreadKey?.[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]        ?.terminalContexts?.[0]?.text,    ).toBeUndefined();  });   it("hydrates persisted terminal contexts without in-memory snapshot text", () => {    const persistApi = useComposerDraftStore.persist as unknown as {      getOptions: () => {        merge: (          persistedState: unknown,          currentState: ReturnType<typeof useComposerDraftStore.getState>,        ) => ReturnType<typeof useComposerDraftStore.getState>;      };    };    const mergedState = persistApi.getOptions().merge(      {        draftsByThreadId: {          [threadId]: {            prompt: INLINE_TERMINAL_CONTEXT_PLACEHOLDER,            attachments: [],            terminalContexts: [              {                id: "ctx-rehydrated",                threadId,                createdAt: "2026-03-13T12:00:00.000Z",                terminalId: "default",                terminalLabel: "Terminal 1",                lineStart: 4,                lineEnd: 5,              },            ],          },        },        draftThreadsByThreadId: {},        projectDraftThreadIdByProjectKey: {},      },      useComposerDraftStore.getInitialState(),    );     expect(mergedState.draftsByThreadKey[threadKeyFor(threadId)]?.terminalContexts).toMatchObject([      {        id: "ctx-rehydrated",        terminalId: "default",        terminalLabel: "Terminal 1",        lineStart: 4,        lineEnd: 5,        text: "",      },    ]);  });   it("sanitizes malformed persisted drafts during merge", () => {    const persistApi = useComposerDraftStore.persist as unknown as {      getOptions: () => {        merge: (          persistedState: unknown,          currentState: ReturnType<typeof useComposerDraftStore.getState>,        ) => ReturnType<typeof useComposerDraftStore.getState>;

Lines 1–500 of 1770.