Files
paseo/packages/app/src/provider-selection/provider-selection.test.ts
T
Mohamed BoudraandGitHub f2054cc701 Preserve Claude model and thinking preferences (#2912)
* fix(agents): preserve Claude thinking selections

Model switches now resolve thinking against the target model's saved preference instead of resetting to the catalog default. Claude owns its configured-model defaults and legacy Fable alias handling, while persisted permission modes remain unchanged.

* chore(claude): tag Fable compatibility shim

* fix(app): resolve persisted model aliases

* fix(models): preserve aliases for older clients

* fix(models): hide compatibility aliases from listings

* fix(settings): hide compatibility model aliases

* test(claude): cover legacy catalog entry

* test(app): expect mock thinking controls in schedules

* fix(app): preserve queued thinking preferences

* fix(app): reconcile schedule provider snapshots

* fix(app): preserve schedule model selections

* fix(claude): preserve exact configured model ids

* fix(app): preserve committed thinking preferences

* fix(app): preserve aliased schedule thinking
2026-08-06 11:33:18 +02:00

429 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AgentModelDefinition, ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import { i18n } from "@/i18n/i18next";
import {
buildProviderSelectorProviders,
buildSelectableProviderSelectorProviders,
buildSelectedTriggerLabel,
filterAndRankModelRows,
matchesModelSearch,
resolveSelectedModelLabel,
resolveSubmissionReadiness,
} from "./provider-selection";
describe("combined model selector data", () => {
const codexModel: AgentModelDefinition = {
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
};
function snapshotEntry(
overrides: Partial<ProviderSnapshotEntry> & Pick<ProviderSnapshotEntry, "provider">,
): ProviderSnapshotEntry {
return {
...overrides,
provider: overrides.provider,
status: overrides.status ?? "ready",
enabled: overrides.enabled ?? true,
label: overrides.label ?? overrides.provider,
description: overrides.description ?? `${overrides.provider} provider`,
defaultModeId: overrides.defaultModeId ?? "default",
modes: overrides.modes ?? [],
models: overrides.models ?? [codexModel],
};
}
it("builds selector providers from ready enabled snapshot entries", () => {
expect(
buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "codex",
label: "Codex",
models: [codexModel],
}),
]),
).toEqual([
{
id: "codex",
label: "Codex",
modelSelection: {
kind: "models",
rows: [
{
favoriteKey: "codex:gpt-5.4",
provider: "codex",
providerLabel: "Codex",
modelId: "gpt-5.4",
modelLabel: "GPT-5.4",
description: "gpt-5.4",
isDefault: undefined,
},
],
},
},
]);
});
it("hides compatibility-only model entries from new clients", () => {
const compatibilityModel: AgentModelDefinition = {
...codexModel,
id: "gpt-5.4-legacy",
label: "GPT-5.4 legacy",
isSelectable: false,
};
const [provider] = buildSelectableProviderSelectorProviders([
snapshotEntry({ provider: "codex", models: [codexModel, compatibilityModel] }),
]);
expect(provider?.modelSelection).toMatchObject({
kind: "models",
rows: [{ modelId: "gpt-5.4" }],
});
});
it("synthesizes a default model row for ready enabled providers without explicit models", () => {
expect(
buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "codewhale",
label: "CodeWhale",
models: [],
}),
]),
).toEqual([
{
id: "codewhale",
label: "CodeWhale",
modelSelection: {
kind: "models",
rows: [
{
favoriteKey: "codewhale:",
provider: "codewhale",
providerLabel: "CodeWhale",
modelId: "",
modelLabel: "Default",
description: undefined,
isDefault: true,
},
],
},
},
]);
});
it("excludes disabled providers from selector data", () => {
expect(
buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "codewhale",
label: "CodeWhale",
enabled: false,
models: [],
}),
]),
).toEqual([]);
});
it("surfaces non-ready providers with their state-specific selection", () => {
expect(
buildSelectableProviderSelectorProviders([
snapshotEntry({ provider: "loading-provider", status: "loading", models: [] }),
snapshotEntry({
provider: "error-provider",
status: "error",
error: "boom",
models: [],
}),
snapshotEntry({
provider: "unavailable-provider",
status: "unavailable",
models: [],
}),
]),
).toEqual([
{
id: "loading-provider",
label: "loading-provider",
modelSelection: { kind: "loading" },
},
{
id: "error-provider",
label: "error-provider",
modelSelection: { kind: "error", message: "boom" },
},
{
id: "unavailable-provider",
label: "unavailable-provider",
modelSelection: { kind: "error", message: "Unavailable" },
},
]);
});
it("builds selector providers from an already-curated provider list", () => {
const providerDefinitions: AgentProviderDefinition[] = [
{
id: "codex",
label: "Codex",
description: "Codex provider",
defaultModeId: "auto",
modes: [],
},
];
expect(
buildProviderSelectorProviders({
providerDefinitions,
modelsByProvider: new Map([["codex", [codexModel]]]),
}),
).toEqual([
{
id: "codex",
label: "Codex",
modelSelection: {
kind: "models",
rows: [
expect.objectContaining({
provider: "codex",
providerLabel: "Codex",
modelId: "gpt-5.4",
modelLabel: "GPT-5.4",
}),
],
},
},
]);
});
it("matches across label, provider, and description with multi-token fuzzy search", () => {
const row = {
favoriteKey: "opencode:opencode-zen/kimi-k2.5",
provider: "opencode",
providerLabel: "OpenCode",
modelId: "opencode-zen/kimi-k2.5",
modelLabel: "Kimi K2.5",
description: "OpenCode Zen - kimi",
};
expect(matchesModelSearch(row, "kimi zen")).toBe(true);
expect(matchesModelSearch(row, "zen kimi")).toBe(true);
expect(matchesModelSearch(row, "k2.5 zen")).toBe(true);
expect(matchesModelSearch(row, "kimi gemini")).toBe(false);
});
it("ranks model search results by fuzzy match quality", () => {
const rows = [
{
favoriteKey: "openai:gpt-4.1",
provider: "openai",
providerLabel: "OpenAI",
modelId: "gpt-4.1",
modelLabel: "GPT-4.1",
},
{
favoriteKey: "openai:gpt-5.4",
provider: "openai",
providerLabel: "OpenAI",
modelId: "gpt-5.4",
modelLabel: "GPT-5.4",
},
{
favoriteKey: "google:gemini",
provider: "google",
providerLabel: "Google",
modelId: "gemini",
modelLabel: "Gemini",
},
];
expect(filterAndRankModelRows(rows, "gpt54").map((row) => row.modelId)).toEqual(["gpt-5.4"]);
});
it("keeps the selected trigger label model-only", () => {
expect(buildSelectedTriggerLabel("GPT-5.4")).toBe("GPT-5.4");
});
it("resolves selected labels from explicit provider model-selection state", () => {
const providers = buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "codex",
label: "Codex",
models: [codexModel],
}),
snapshotEntry({
provider: "codewhale",
label: "CodeWhale",
models: [],
}),
]);
expect(
resolveSelectedModelLabel({
providers,
selectedProvider: "codex",
selectedModel: "gpt-5.4",
isLoading: false,
}),
).toBe("GPT-5.4");
expect(
resolveSelectedModelLabel({
providers,
selectedProvider: "codewhale",
selectedModel: "",
isLoading: false,
}),
).toBe("Default");
});
it("distinguishes a loading selection from a resolved empty selection", () => {
expect(
resolveSelectedModelLabel({
providers: [],
selectedProvider: "",
selectedModel: "",
isLoading: true,
}),
).toBe("Loading...");
expect(
resolveSelectedModelLabel({
providers: [],
selectedProvider: "",
selectedModel: "",
isLoading: false,
}),
).toBe("Select model");
});
it("keeps a stored selected model visible when current snapshot rows no longer offer it", () => {
const providers = buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "codex",
label: "Codex",
models: [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4", isDefault: true }],
}),
]);
expect(
resolveSelectedModelLabel({
providers,
selectedProvider: "codex",
selectedModel: "gpt-5.3",
isLoading: false,
}),
).toBe("gpt-5.3");
});
it("keeps provider snapshot errors visible in the selected trigger label", () => {
const providers = buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "opencode",
label: "OpenCode",
status: "error",
error: "OpenCode app.agents timed out after 10s",
models: [],
}),
]);
expect(
resolveSelectedModelLabel({
providers,
selectedProvider: "opencode",
selectedModel: "",
isLoading: false,
}),
).toBe("Error");
});
it("returns observable submission readiness reasons", () => {
expect(
resolveSubmissionReadiness({
text: "hello",
allowsEmptyAutoSubmit: false,
providerCount: 1,
selection: {
provider: "codex",
modelId: "",
availableModels: [codexModel],
isModelLoading: false,
},
autoSubmitConfig: null,
workspaceDirectory: "/repo",
hasClient: true,
}),
).toEqual({
ok: false,
reason: "No model is available for the selected provider",
});
expect(
resolveSubmissionReadiness({
text: "hello",
allowsEmptyAutoSubmit: false,
providerCount: 1,
selection: {
provider: "codewhale",
modelId: "",
availableModels: [],
isModelLoading: false,
},
autoSubmitConfig: null,
workspaceDirectory: "/repo",
hasClient: true,
}),
).toEqual({ ok: true });
});
it("uses the active app language for utility labels", async () => {
await i18n.changeLanguage("zh-CN");
try {
const providers = buildSelectableProviderSelectorProviders([
snapshotEntry({
provider: "deepseek-tui",
label: "DeepSeek TUI",
models: [],
}),
snapshotEntry({
provider: "unavailable-provider",
status: "unavailable",
models: [],
}),
]);
expect(getAllModelLabels(providers)).toContain("默认");
expect(providers[1]?.modelSelection).toEqual({
kind: "error",
message: "不可用",
});
expect(
resolveSubmissionReadiness({
text: "",
allowsEmptyAutoSubmit: false,
providerCount: 1,
selection: {
provider: "codex",
modelId: "gpt-5.4",
availableModels: [codexModel],
isModelLoading: false,
},
autoSubmitConfig: null,
workspaceDirectory: "/repo",
hasClient: true,
}),
).toEqual({ ok: false, reason: "初始 prompt 必填" });
} finally {
await i18n.changeLanguage("en");
}
});
});
function getAllModelLabels(providers: ReturnType<typeof buildSelectableProviderSelectorProviders>) {
return providers.flatMap((provider) =>
provider.modelSelection.kind === "models"
? provider.modelSelection.rows.map((row) => row.modelLabel)
: [],
);
}