382 lines · 11.0 KB · TypeScript
RawDownload
import "../../index.css"; import {  type ModelSelection,  ClaudeModelOptions,  CodexModelOptions,  DEFAULT_MODEL_BY_PROVIDER,  ProjectId,  ThreadId,} from "@t3tools/contracts";import { page } from "vitest/browser";import { useCallback } from "react";import { afterEach, describe, expect, it, vi } from "vitest";import { render } from "vitest-browser-react"; import { TraitsPicker } from "./TraitsPicker";import {  COMPOSER_DRAFT_STORAGE_KEY,  ComposerThreadDraftState,  useComposerDraftStore,  useComposerThreadDraft,  useEffectiveComposerModelState,} from "../../composerDraftStore"; // ── Claude TraitsPicker tests ───────────────────────────────────────── const CLAUDE_THREAD_ID = ThreadId.makeUnsafe("thread-claude-traits"); function ClaudeTraitsPickerHarness(props: {  model: string;  fallbackModelSelection: ModelSelection | null;  triggerVariant?: "ghost" | "outline";}) {  const prompt = useComposerThreadDraft(CLAUDE_THREAD_ID).prompt;  const setPrompt = useComposerDraftStore((store) => store.setPrompt);  const { modelOptions, selectedModel } = useEffectiveComposerModelState({    threadId: CLAUDE_THREAD_ID,    selectedProvider: "claudeAgent",    threadModelSelection: props.fallbackModelSelection,    projectModelSelection: null,    customModelsByProvider: { codex: [], claudeAgent: [] },  });  const handlePromptChange = useCallback(    (nextPrompt: string) => {      setPrompt(CLAUDE_THREAD_ID, nextPrompt);    },    [setPrompt],  );   return (    <TraitsPicker      provider="claudeAgent"      threadId={CLAUDE_THREAD_ID}      model={selectedModel ?? props.model}      prompt={prompt}      modelOptions={modelOptions?.claudeAgent}      onPromptChange={handlePromptChange}      triggerVariant={props.triggerVariant}    />  );} async function mountClaudePicker(props?: {  model?: string;  prompt?: string;  options?: ClaudeModelOptions;  fallbackModelOptions?: {    effort?: "low" | "medium" | "high" | "max" | "ultrathink";    thinking?: boolean;    fastMode?: boolean;  } | null;  skipDraftModelOptions?: boolean;  triggerVariant?: "ghost" | "outline";}) {  const model = props?.model ?? "claude-opus-4-6";  const claudeOptions = !props?.skipDraftModelOptions ? props?.options : undefined;  const draftsByThreadId: Record<ThreadId, ComposerThreadDraftState> = {    [CLAUDE_THREAD_ID]: {      prompt: props?.prompt ?? "",      images: [],      nonPersistedImageIds: [],      persistedAttachments: [],      terminalContexts: [],      modelSelectionByProvider: props?.skipDraftModelOptions        ? {}        : {            claudeAgent: {              provider: "claudeAgent",              model,              ...(claudeOptions && Object.keys(claudeOptions).length > 0                ? { options: claudeOptions }                : {}),            },          },      activeProvider: "claudeAgent",      runtimeMode: null,      interactionMode: null,    },  };  useComposerDraftStore.setState({    draftsByThreadId,    draftThreadsByThreadId: {},    projectDraftThreadIdByProjectId: {},  });  const host = document.createElement("div");  document.body.append(host);  const fallbackModelSelection =    props?.fallbackModelOptions !== undefined      ? ({          provider: "claudeAgent",          model,          options: props.fallbackModelOptions ?? undefined,        } satisfies ModelSelection)      : null;  const screen = await render(    <ClaudeTraitsPickerHarness      model={model}      fallbackModelSelection={fallbackModelSelection}      {...(props?.triggerVariant ? { triggerVariant: props.triggerVariant } : {})}    />,    { container: host },  );   const cleanup = async () => {    await screen.unmount();    host.remove();  };   return {    [Symbol.asyncDispose]: cleanup,    cleanup,  };} describe("TraitsPicker (Claude)", () => {  afterEach(() => {    document.body.innerHTML = "";    useComposerDraftStore.setState({      draftsByThreadId: {},      draftThreadsByThreadId: {},      projectDraftThreadIdByProjectId: {},      stickyModelSelectionByProvider: {},    });  });   it("shows fast mode controls for Opus", async () => {    await using _ = await mountClaudePicker();     await page.getByRole("button").click();     await vi.waitFor(() => {      const text = document.body.textContent ?? "";      expect(text).toContain("Fast Mode");      expect(text).toContain("off");      expect(text).toContain("on");    });  });   it("hides fast mode controls for non-Opus models", async () => {    await using _ = await mountClaudePicker({ model: "claude-sonnet-4-6" });     await page.getByRole("button").click();     await vi.waitFor(() => {      expect(document.body.textContent ?? "").not.toContain("Fast Mode");    });  });   it("shows only the provided effort options", async () => {    await using _ = await mountClaudePicker({      model: "claude-sonnet-4-6",    });     await page.getByRole("button").click();     await vi.waitFor(() => {      const text = document.body.textContent ?? "";      expect(text).toContain("Low");      expect(text).toContain("Medium");      expect(text).toContain("High");      expect(text).not.toContain("Max");      expect(text).toContain("Ultrathink");    });  });   it("shows a th  inking on/off dropdown for Haiku", async () => {    await using _ = await mountClaudePicker({      model: "claude-haiku-4-5",      options: { thinking: true },    });     await vi.waitFor(() => {      expect(document.body.textContent ?? "").toContain("Thinking On");    });    await page.getByRole("button").click();     await vi.waitFor(() => {      const text = document.body.textContent ?? "";      expect(text).toContain("Thinking");      expect(text).toContain("On (default)");      expect(text).toContain("Off");    });  });   it("shows prompt-controlled Ultrathink state with disabled effort controls", async () => {    await using _ = await mountClaudePicker({      model: "claude-opus-4-6",      options: { effort: "high" },      prompt: "Ultrathink:\nInvestigate this",    });     await vi.waitFor(() => {      expect(document.body.textContent ?? "").toContain("Ultrathink");      expect(document.body.textContent ?? "").not.toContain("Ultrathink · Prompt");    });    await page.getByRole("button").click();     await vi.waitFor(() => {      const text = document.body.textContent ?? "";      expect(text).toContain("Effort");      expect(text).toContain("Remove Ultrathink from the prompt to change effort.");      expect(text).not.toContain("Fallback Effort");    });  });   it("persists sticky claude model options when traits change", async () => {    await using _ = await mountClaudePicker({      model: "claude-opus-4-6",      options: { effort: "medium", fastMode: false },    });     await page.getByRole("button").click();    await page.getByRole("menuitemradio", { name: "Max" }).click();     expect(      useComposerDraftStore.getState().stickyModelSelectionByProvider.claudeAgent,    ).toMatchObject({      provider: "claudeAgent",      options: {        effort: "max",      },    });  });   it("accepts outline trigger styling", async () => {    await using _ = await mountClaudePicker({      triggerVariant: "outline",    });     const button = document.querySelector("button");    if (!(button instanceof HTMLButtonElement)) {      throw new Error("Expected traits trigger button to be rendered.");    }    expect(button.className).toContain("border-input");    expect(button.className).toContain("bg-popover");  });}); // ── Codex TraitsPicker tests ────────────────────────────────────────── async function mountCodexPicker(props: { model?: string; options?: CodexModelOptions }) {  const threadId = ThreadId.makeUnsafe("thread-codex-traits");  const model = props.model ?? DEFAULT_MODEL_BY_PROVIDER.codex;  const draftsByThreadId: Record<ThreadId, ComposerThreadDraftState> = {    [threadId]: {      prompt: "",      images: [],      nonPersistedImageIds: [],      persistedAttachments: [],      terminalContexts: [],      modelSelectionByProvider: {        codex: {          provider: "codex",          model,          ...(props.options ? { options: props.options } : {}),        },      },      activeProvider: "codex",      runtimeMode: null,      interactionMode: null,    },  };   useComposerDraftStore.setState({    draftsByThreadId,    draftThreadsByThreadId: {},    projectDraftThreadIdByProjectId: {      [ProjectId.makeUnsafe("project-codex-traits")]: threadId,    },  });  const host = document.createElement("div");  document.body.append(host);  const screen = await render(    <TraitsPicker      provider="codex"      threadId={threadId}      model={props.model ?? DEFAULT_MODEL_BY_PROVIDER.codex}      prompt=""      modelOptions={props.options}      onPromptChange={() => {}}    />,    { container: host },  );   const cleanup = async () => {    await screen.unmount();    host.remove();  };   return {    [Symbol.asyncDispose]: cleanup,    cleanup,  };} describe("TraitsPicker (Codex)", () => {  afterEach(() => {    document.body.innerHTML = "";    localStorage.removeItem(COMPOSER_DRAFT_STORAGE_KEY);    useComposerDraftStore.setState({      draftsByThreadId: {},      draftThreadsByThreadId: {},      projectDraftThreadIdByProjectId: {},      stickyModelSelectionByProvider: {},    });  });   it("shows fast mode controls", async () => {    await using _ = await mountCodexPicker({      options: { fastMode: false },    });     await page.getByRole("button").click();     await vi.waitFor(() => {      const text = document.body.textContent ?? "";      expect(text).toContain("Fast Mode");      expect(text).toContain("off");      expect(text).toContain("on");    });  });   it("shows Fast in the trigger label when fast mode is active", async () => {    await using _ = await mountCodexPicker({      options: { fastMode: true },    });     await vi.waitFor(() => {      expect(document.body.textContent ?? "").toContain("High · Fast");    });  });   it("shows only the provided effort options", async () => {    await using _ = await mountCodexPicker({      options: { fastMode: false },    });     await page.getByRole("button").click();     await vi.waitFor(() => {      const text = document.body.textContent ?? "";      expect(text).toContain("Low");      expect(text).toContain("Medium");      expect(text).toContain("High");      expect(text).toContain("Extra High");    });  });   it("persists sticky codex model options when traits change", async () => {    await using _ = await mountCodexPicker({      options: { fastMode: false },    });     await page.getByRole("button").click();    await page.getByRole("menuitemradio", { name: "on" }).click();     expect(useComposerDraftStore.getState().stickyModelSelectionByProvider.codex).toMatchObject({      provider: "codex",      options: { fastMode: true },    });  });});