diff --git a/apps/web/src/pages/profile-select.test.tsx b/apps/web/src/pages/profile-select.test.tsx new file mode 100644 index 00000000..228f7750 --- /dev/null +++ b/apps/web/src/pages/profile-select.test.tsx @@ -0,0 +1,84 @@ +import { describe, expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ModelCatalog, Tiers } from "@corbits-builder/schemas/models"; +import { keys } from "../queries/keys.ts"; +import { NewProjectProfile } from "./profile-select.tsx"; + +function tiers(provider: string): Tiers { + const binding = { provider, model: "m", effort: "medium" as const }; + return { low: binding, medium: binding, high: binding, max: binding }; +} + +function catalog(): ModelCatalog { + return { + providers: [], + roles: [], + tiers: [], + efforts: [], + customApis: [], + profiles: [ + { + id: "xai_default", + label: "Grok", + hint: "Not connected", + tiers: tiers("xai"), + available: false, + }, + { + id: "anthropic_default", + label: "Claude", + hint: "Anthropic", + tiers: tiers("anthropic"), + available: true, + }, + { + id: "openai_default", + label: "GPT", + hint: "OpenAI", + tiers: tiers("openai"), + available: true, + }, + ], + }; +} + +function render(value: string | undefined, models = catalog()) { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: Infinity } }, + }); + client.setQueryData(keys.modelCatalog, models); + client.setQueryData(keys.modelChoice, null); + return renderToStaticMarkup( + + {}} /> + , + ); +} + +describe("NewProjectProfile", () => { + test("selects the first runnable profile when none is automatic", () => { + const initial = render(undefined); + expect(initial).toContain("Model profile, Claude"); + expect(initial).not.toContain("Grok"); + + const picked = render("openai_default"); + expect(picked).toContain("Model profile, GPT"); + expect(picked).not.toContain("Model profile, Claude"); + }); + + test("keeps the automatic profile when one is set", () => { + const models = catalog(); + models.automaticProfile = "openai_default"; + expect(render(undefined, models)).toContain("Model profile, GPT"); + }); + + test("renders nothing when no profile can run", () => { + const models = catalog(); + models.profiles = models.profiles.map((profile) => ({ + ...profile, + available: false, + })); + expect(render(undefined, models)).toBe(""); + }); +}); diff --git a/apps/web/src/pages/profile-select.tsx b/apps/web/src/pages/profile-select.tsx index a95ecbaa..cb34aed0 100644 --- a/apps/web/src/pages/profile-select.tsx +++ b/apps/web/src/pages/profile-select.tsx @@ -1,7 +1,8 @@ /** - * The model profile in the new-project prompt box. A new project starts on the one picked here - * (the person's active profile, else the only connected provider's, until they pick another); its - * bindings never change once it starts. With neither, nothing is picked and the hub says why. + * The model profile in the new-project prompt box. A new project starts on the one picked here: + * the person's active profile, else the only connected provider's, else the first profile that can + * run, until they pick another. Its bindings never change once it starts. With no runnable + * profile, nothing is picked and the hub says why. */ import { PromptInputModelSelect, @@ -74,9 +75,8 @@ export function NewProjectProfile({ const models = catalog && choice !== undefined ? profileOptions(catalog, choice) : []; const active = choice?.profile ?? catalog?.automaticProfile; - const shown = models.some((model) => model.id === active) - ? active - : undefined; + const shown = + models.find((model) => model.id === active)?.id ?? models[0]?.id; useEffect(() => { if (value === undefined && shown !== undefined) onValueChange(shown); }, [value, shown, onValueChange]);