1088 lines · 32.1 KB · TypeScript
RawDownload
import { type ProviderKind, type ServerProvider } from "@t3tools/contracts";import { EnvironmentId } from "@t3tools/contracts";import { createModelCapabilities } from "@t3tools/shared/model";import { page, userEvent } from "vitest/browser";import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import { render } from "vitest-browser-react"; import { ProviderModelPicker } from "./ProviderModelPicker";import { getCustomModelOptionsByProvider } from "../../modelSelection";import { DEFAULT_CLIENT_SETTINGS, DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";import { __resetLocalApiForTests } from "../../localApi"; // Mock the environments/runtime module to provide a mock primary environment connectionvi.mock("../../environments/runtime", () => {  const primaryConnection = {    kind: "primary" as const,    knownEnvironment: {      id: "environment-local",      label: "Local environment",      source: "manual" as const,      environmentId: EnvironmentId.make("environment-local"),      target: {        httpBaseUrl: "http://localhost:3000",        wsBaseUrl: "ws://localhost:3000",      },    },    environmentId: EnvironmentId.make("environment-local"),    client: {      server: {        getConfig: vi.fn(),        updateSettings: vi.fn(),      },    },    ensureBootstrapped: async () => undefined,    reconnect: async () => undefined,    dispose: async () => undefined,  };   return {    getEnvironmentHttpBaseUrl: () => "http://localhost:3000",    getSavedEnvironmentRecord: () => null,    getSavedEnvironmentRuntimeState: () => null,    hasSavedEnvironmentRegistryHydrated: () => true,    listSavedEnvironmentRecords: () => [],    resetSavedEnvironmentRegistryStoreForTests: vi.fn(),    resetSavedEnvironmentRuntimeStoreForTests: vi.fn(),    resolveEnvironmentHttpUrl: (_environmentId: unknown, path: string) =>      new URL(path, "http://localhost:3000").toString(),    waitForSavedEnvironmentRegistryHydration: async () => undefined,    addSavedEnvironment: vi.fn(),    disconnectSavedEnvironment: vi.fn(),    ensureEnvironmentConnectionBootstrapped: async () => undefined,    getPrimaryEnvironmentConnection: () => primaryConnection,    readEnvironmentConnection: () => primaryConnection,    reconnectSavedEnvironment: vi.fn(),    removeSavedEnvironment: vi.fn(),    requireEnvironmentConnection: () => primaryConnection,    resetEnvironmentServiceForTests: vi.fn(),    startEnvironmentConnectionService: vi.fn(),    subscribeEnvironmentConnections: () => () => {},    useSavedEnvironmentRegistryStore: (      selector: (state: { byId: Record<string, never> }) => unknown,    ) => selector({ byId: {} }),    useSavedEnvironmentRuntimeStore: (      selector: (state: { byId: Record<string, never> }) => unknown,    ) => selector({ byId: {} }),  };}); function selectDescriptor(  id: string,  label: string,  options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>,) {  return {    id,    label,    type: "select" as const,    options: [...options],    ...(options.find((option) => option.isDefault)?.id      ? { currentValue: options.find((option) => option.isDefault)?.id }      : {}),  };} function booleanDescriptor(id: string, label: string) {  return {    id,    label,    type: "boolean" as const,  };} const TEST_PROVIDERS: ReadonlyArray<ServerProvider> = [  {    provider: "codex",    displayName: "Codex",    enabled: true,    installed: true,    version: "0.116.0",    status: "ready",    auth: { status: "authenticated" },    checkedAt: new Date().toISOString(),    slashCommands: [],    skills: [],    models: [      {        slug: "gpt-5-codex",        name: "GPT-5 Codex",        isCustom: false,        capabilities: createModelCapabilities({          optionDescriptors: [            selectDescriptor("reasoningEffort", "Reasoning", [              { id: "low", label: "low" },              { id: "medium", label: "medium", isDefault: true },              { id: "high", label: "high" },            ]),            booleanDescriptor("fastMode", "Fast Mode"),          ],        }),      },      {        slug: "gpt-5.3-codex",        name: "GPT-5.3 Codex",        isCustom: false,        capabilities: createModelCapabilities({          optionDescriptors: [            selectDescriptor("reasoningEffort", "Reasoning", [              { id: "low", label: "low" },              { id: "medium", label: "medium", isDefault: true },              { id: "high", label: "high" },            ]),            booleanDescriptor("fastMode", "Fast Mode"),          ],        }),      },    ],  },  {    provider: "claudeAgent",    displayName: "Claude",    enabled: true,    installed: true,    version: "1.0.0",    status: "ready",    auth: { status: "authenticated" },    checkedAt: new Date().toISOString(),    slashCommands: [],    skills: [],    models: [      {        slug: "claude-opus-4-6",        name: "Claude Opus 4.6",        isCustom: false,        capabilities: createModelCapabilities({          optionDescriptors: [            selectDescriptor("effort", "Reasoning", [              { id: "low", label: "low" },              { id: "medium", label: "medium", isDefault: true },              { id: "high", label: "high" },              { id: "max", label: "max" },            ]),            booleanDescriptor("thinking", "Thinking"),          ],        }),      },      {        slug: "claude-sonnet-4-6",        name: "Claude Sonnet 4.6",        isCustom: false,        capabilities: createModelCapabilities({          optionDescriptors: [            selectDescriptor("effort", "Reasoning", [              { id: "low", label: "low" },              { id: "medium", label: "medium", isDefault: true },              { id: "high", label: "high" },              { id: "max", label: "max" },            ]),            booleanDescriptor("thinking", "Thinking"),          ],        }),      },      {        slug: "claude-haiku-4-5",        name: "Claude Haiku 4.5",        isCustom: false,        capabilities: createModelCapabilities({          optionDescriptors: [            selectDescriptor("effort", "Reasoning", [              { id: "low", label: "low" },              { id: "medium", label: "medium", isDefault: true },              { id: "high", label: "high" },            ]),            booleanDescriptor("thinking", "Thinking"),          ],        }),      },    ],  },]; function buildCodexProvider(models: ServerProvider["models"]): ServerProvider {  return {    provider: "codex",    displayName: "Codex",    enabled: true,    installed: true,    version: "0.116.0",    status: "ready",    auth: { status: "authenticated" },    checkedAt: new Date().toISOString(),    models,    slashCommands: [],    skills: [],  };} function buildOpenCodeProvider(models: ServerProvider["models"]): ServerProvider {  return {    provider: "opencode",    enabled: true,    installed: true,    version: "1.0.0",    status: "ready",    auth: { status: "authenticated" },    checkedAt: new Date().toISOString(),    models,    slashCommands: [],    skills: [],  };} async function mountPicker(props: {  provider: ProviderKind;  model: string;  lockedProvider: ProviderKind | null;  providers?: ReadonlyArray<ServerProvider>;  triggerVariant?: "ghost" | "outline";}) {  const host = document.createElement("div");  document.body.append(host);  const onProviderModelChange = vi.fn();  const providers = props.providers ?? TEST_PROVIDERS;  const modelOptionsByProvider = getCustomModelOptionsByProvider(    DEFAULT_UNIFIED_SETTINGS,    providers,    props.provider,    props.model,  );  const screen = await render(    <ProviderModelPicker      provider={props.provider}      model={props.model}      lockedProvider={props.lockedProvider}      providers={providers}      modelOptionsByProvider={modelOptionsByProvider}      triggerVariant={props.triggerVariant}      onProviderModelChange={onProviderModelChange}    />,    { container: host },  );   return {    onProviderModelChange,    cleanup: async () => {      await screen.unmount();      host.remove();    },  };} function getModelPickerListElement() {  const modelPickerList = document.querySelector<HTMLElement>(".model-picker-list");  expect(modelPickerList).not.toBeNull();  return modelPickerList!;} function getModelPickerListText() {  return getModelPickerListElement().textContent ?? "";} function getVisibleModelNames() {  return Array.from(getModelPickerListElement().querySelectorAll<HTMLDivElement>("div.font-medium"))    .map((element) => element.textContent?.replace(/New$/u, "").trim() ?? "")    .filter((text) => text.length > 0);} function getSidebarProviderOrder() {  return Array.from(document.querySelectorAll<HTMLElement>("[data-model-picker-provider]")).map(    (element) => element.dataset.modelPickerProvider ?? "",  );} describe("ProviderModelPicker", () => {  beforeEach(async () => {    // Reset test environment before each test    await __resetLocalApiForTests();  });   afterEach(async () => {    document.body.innerHTML = "";    await __resetLocalApiForTests();  });   it("shows provider sidebar in unlocked mode", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: null,    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        const text = document.body.textContent ?? "";        expect(text).not.toContain("Codex");        expect(text).toContain("Claude");        expect(text).toContain("Claude Opus 4.6");      });    } finally {      await mounted.cleanup();    }  });   it("shows favorites first in the provider sidebar", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: null,    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        expect(getSidebarProviderOrder().slice(0, 3)).toEqual([          "favorites",          "codex",          "claudeAgent",        ]);      });    } finally {      await mounted.cleanup();    }  });   it("filters models by selected provider in sidebar", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: null,    });     try {      await page.getByRole("button").click();       // Start with Claude models visible      await vi.waitFor(() => {        const text = document.body.textContent ?? "";        expect(text).not.toContain("GPT-5 Codex");        expect(text).toContain("Claude Opus 4.6");      });       // Click on Codex provider in sidebar      await vi.waitFor(() => {        expect(document.querySelector('[data-model-picker-provider="codex"]')).not.toBeNull();      });      await page.getByRole("button", { name: "Codex", exact: true }).click();       // Now should only show Codex models      await vi.waitFor(() => {        const listText = getModelPickerListText();        expect(listText).toContain("GPT-5 Codex");        expect(listText).not.toContain("Claude Opus 4.6");      });    } finally {      await mounted.cleanup();    }  });   it("focuses the search input after selecting a sidebar provider", async () => {    const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: null,    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        expect(document.querySelector('[data-model-picker-provider="codex"]')).not.toBeNull();      });      await page.getByRole("button", { name: "Codex", exact: true }).click();       await vi.waitFor(() => {        const searchInput = document.querySelector<HTMLInputElement>(          'input[placeholder="Search models..."]',        );        expect(searchInput).not.toBeNull();        expect(document.activeElement).toBe(searchInput);      });    } finally {      await mounted.cleanup();    }  });   it("shows locked provider header and only its models in locked mode", async () => {    localStorage.setItem(      "t3code:client-settings:v1",      JSON.stringify({        ...DEFAULT_CLIENT_SETTINGS,        favorites: [          { provider: "codex", model: "gpt-5-codex" },          { provider: "claudeAgent", model: "claude-sonnet-4-6" },        ],      }),    );     const mounted = await mountPicker({      provider: "claudeAgent",      model: "claude-opus-4-6",      lockedProvider: "claudeAgent",    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        const text = document.body.textContent ?? "";        // Should show locked provider label        expect(text).toContain("Claude");        expect(getVisibleModelNames()).toEqual([          "Claude Sonnet 4.6",          "Claude Opus 4.6",          "Claude Haiku 4.5",        ]);      });    } finally {      localStorage.removeItem("t3code:client-settings:v1");      await mounted.cleanup();    }  });   it("falls back to the active provider's first model when props.model belongs to another provider (#1982)", async () => {    const host = document.createElement("div");    document.body.append(host);    const onProviderModelChange = vi.fn();    const modelOptionsByProvider = {      claudeAgent: [        { slug: "claude-opus-4-6", name: "Claude Opus 4.6" },        { slug: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },      ],      codex: [{ slug: "gpt-5-codex", name: "GPT-5 Codex" }],      cursor: [],      opencode: [],    } as const;    const screen = await render(      <ProviderModelPicker        provider="claudeAgent"        model="gpt-5-codex"        lockedProvider={null}        providers={TEST_PROVIDERS}        modelOptionsByProvider={modelOptionsByProvider}        onProviderModelChange={onProviderModelChange}      />,      { container: host },    );     try {      const trigger = document.querySelector<HTMLElement>(        '[data-chat-provider-model-picker="true"]',      );      expect(trigger).not.toBeNull();      const label = trigger?.textContent ?? "";      expect(label).not.toContain("gpt-5-codex");      expect(label).toContain("Claude Opus 4.6");    } finally {      await screen.unmount();      host.remove();    }  });   it("uses the trigger label for locked opencode rows", async () => {    const providers: ReadonlyArray<ServerProvider> = [      buildOpenCodeProvider([        {          slug: "github-copilot/claude-opus-4.5",          name: "Claude Opus 4.5",          subProvider: "GitHub Copilot",          shortName: "Opus 4.5",          isCustom: false,          capabilities: createModelCapabilities({            optionDescriptors: [              selectDescriptor("reasoningEffort", "Reasoning", [                { id: "low", label: "low" },                { id: "medium", label: "medium", isDefault: true },                { id: "high", label: "high" },              ]),

Lines 1–500 of 1088.