1226 lines · 37.6 KB · TypeScript
RawDownload
import { ProviderDriverKind, ProviderInstanceId, 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 { getCustomModelOptionsByInstance } from "../../modelSelection";import {  deriveProviderInstanceEntries,  sortProviderInstanceEntries,} from "../../providerInstances";import type { ModelEsque } from "./providerIconUtils";import {  DEFAULT_CLIENT_SETTINGS,  DEFAULT_UNIFIED_SETTINGS,  type UnifiedSettings,} 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> = [  {    driver: ProviderDriverKind.make("codex"),    instanceId: ProviderInstanceId.make("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"),          ],        }),      },    ],  },  {    driver: ProviderDriverKind.make("claudeAgent"),    instanceId: ProviderInstanceId.make("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"),          ],        }),      },    ],  },]; const CODEX_INSTANCE_ID = ProviderInstanceId.make("codex");const CLAUDE_INSTANCE_ID = ProviderInstanceId.make("claudeAgent");const OPENCODE_INSTANCE_ID = ProviderInstanceId.make("opencode"); function buildCodexProvider(models: ServerProvider["models"]): ServerProvider {  return {    driver: ProviderDriverKind.make("codex"),    instanceId: ProviderInstanceId.make("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 {    driver: ProviderDriverKind.make("opencode"),    instanceId: ProviderInstanceId.make("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: {  activeInstanceId?: ProviderInstanceId;  model: string;  lockedProvider: ProviderDriverKind | null;  lockedContinuationGroupKey?: string | null;  providers?: ReadonlyArray<ServerProvider>;  settings?: UnifiedSettings;  triggerVariant?: "ghost" | "outline";}) {  const host = document.createElement("div");  document.body.append(host);  const onInstanceModelChange = vi.fn();  const providers = props.providers ?? TEST_PROVIDERS;  const instanceEntries = sortProviderInstanceEntries(deriveProviderInstanceEntries(providers));  const activeInstanceId = props.activeInstanceId ?? CODEX_INSTANCE_ID;  const modelOptionsByInstance = getCustomModelOptionsByInstance(    props.settings ?? DEFAULT_UNIFIED_SETTINGS,    providers,    activeInstanceId,    props.model,  );  const screen = await render(    <ProviderModelPicker      activeInstanceId={activeInstanceId}      model={props.model}      lockedProvider={props.lockedProvider}      lockedContinuationGroupKey={props.lockedContinuationGroupKey ?? null}      instanceEntries={instanceEntries}      modelOptionsByInstance={modelOptionsByInstance}      triggerVariant={props.triggerVariant}      onInstanceModelChange={onInstanceModelChange}    />,    { container: host },  );   return {    onInstanceModelChange,    // Back-compat alias used by callers that still assert on the old callback    // name. Delegates to the instance-aware mock so existing expectations work.    get onProviderModelChange() {      return onInstanceModelChange;    },    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({      activeInstanceId: CLAUDE_INSTANCE_ID,      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({      activeInstanceId: CLAUDE_INSTANCE_ID,      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({      activeInstanceId: CLAUDE_INSTANCE_ID,      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("uses client model visibility and ordering preferences", async () => {    const mounted = await mountPicker({      activeInstanceId: CLAUDE_INSTANCE_ID,      model: "claude-opus-4-6",      lockedProvider: null,      settings: {        ...DEFAULT_UNIFIED_SETTINGS,        providerModelPreferences: {          [CLAUDE_INSTANCE_ID]: {            hiddenModels: ["claude-opus-4-6"],            modelOrder: ["claude-haiku-4-5", "claude-sonnet-4-6"],          },        },      },    });     try {      await page.getByRole("button").click();       await vi.waitFor(() => {        expect(getVisibleModelNames()).toEqual(["Claude Haiku 4.5", "Claude Sonnet 4.6"]);        expect(getModelPickerListText()).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({      activeInstanceId: CLAUDE_INSTANCE_ID,      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({      activeInstanceId: CLAUDE_INSTANCE_ID,      model: "claude-opus-4-6",      lockedProvider: ProviderDriverKind.make("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();    }  }); 

Lines 1–500 of 1226.