import { describe, expect, it } from "vitest"; import { DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, DEFAULT_SIDEBAR_THREAD_SORT_ORDER, DEFAULT_TIMESTAMP_FORMAT,} from "./appSettings";import { getAppModelOptions, getCustomModelOptionsByProvider, getCustomModelsByProvider, getCustomModelsForProvider, getDefaultCustomModelsForProvider, MODEL_PROVIDER_SETTINGS, normalizeCustomModelSlugs, patchCustomModels, resolveAppModelSelectionState, resolveAppModelSelection,} from "./modelSelection"; describe("normalizeCustomModelSlugs", () => { it("normalizes aliases, removes built-ins, and deduplicates values", () => { expect( normalizeCustomModelSlugs([ " custom/internal-model ", "gpt-5.3-codex", "5.3", "custom/internal-model", "", null, ]), ).toEqual(["custom/internal-model"]); }); it("normalizes provider-specific aliases for claude", () => { expect(normalizeCustomModelSlugs(["sonnet"], "claudeAgent")).toEqual([]); expect(normalizeCustomModelSlugs(["claude/custom-sonnet"], "claudeAgent")).toEqual([ "claude/custom-sonnet", ]); });}); describe("getAppModelOptions", () => { it("appends saved custom models after the built-in options", () => { const options = getAppModelOptions("codex", ["custom/internal-model"]); expect(options.map((option) => option.slug)).toEqual([ "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.2-codex", "gpt-5.2", "custom/internal-model", ]); }); it("keeps the currently selected custom model available even if it is no longer saved", () => { const options = getAppModelOptions("codex", [], "custom/selected-model"); expect(options.at(-1)).toEqual({ slug: "custom/selected-model", name: "custom/selected-model", isCustom: true, }); }); it("keeps a saved custom provider model available as an exact slug option", () => { const options = getAppModelOptions("claudeAgent", ["claude/custom-opus"], "claude/custom-opus"); expect(options.some((option) => option.slug === "claude/custom-opus" && option.isCustom)).toBe( true, ); });}); describe("resolveAppModelSelection", () => { it("preserves saved custom model slugs instead of falling back to the default", () => { expect( resolveAppModelSelection( "codex", { codex: ["galapagos-alpha"], claudeAgent: [] }, "galapagos-alpha", ), ).toBe("galapagos-alpha"); }); it("falls back to the provider default when no model is selected", () => { expect(resolveAppModelSelection("codex", { codex: [], claudeAgent: [] }, "")).toBe("gpt-5.4"); }); it("resolves display names through the shared resolver", () => { expect(resolveAppModelSelection("codex", { codex: [], claudeAgent: [] }, "GPT-5.3 Codex")).toBe( "gpt-5.3-codex", ); }); it("resolves aliases through the shared resolver", () => { expect(resolveAppModelSelection("claudeAgent", { codex: [], claudeAgent: [] }, "sonnet")).toBe( "claude-sonnet-4-6", ); }); it("resolves transient selected custom models included in app model options", () => { expect( resolveAppModelSelection("codex", { codex: [], claudeAgent: [] }, "custom/selected-model"), ).toBe("custom/selected-model"); });}); describe("timestamp format defaults", () => { it("defaults timestamp format to locale", () => { expect(DEFAULT_TIMESTAMP_FORMAT).toBe("locale"); });}); describe("sidebar sort defaults", () => { it("defaults project sorting to updated_at", () => { expect(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER).toBe("updated_at"); }); it("defaults thread sorting to updated_at", () => { expect(DEFAULT_SIDEBAR_THREAD_SORT_ORDER).toBe("updated_at"); });}); describe("provider-specific custom models", () => { it("includes provider-specific custom slugs in non-codex model lists", () => { const claudeOptions = getAppModelOptions("claudeAgent", ["claude/custom-opus"]); expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true); });}); describe("provider-indexed custom model settings", () => { const settings = { customCodexModels: ["custom/codex-model"], customClaudeModels: ["claude/custom-opus"], } as const; it("exports one provider config per provider", () => { expect(MODEL_PROVIDER_SETTINGS.map((config) => config.provider)).toEqual([ "codex", "claudeAgent", ]); }); it("reads custom models for each provider", () => { expect(getCustomModelsForProvider(settings, "codex")).toEqual(["custom/codex-model"]); expect(getCustomModelsForProvider(settings, "claudeAgent")).toEqual(["claude/custom-opus"]); }); it("reads default custom models for each provider", () => { const defaults = { customCodexModels: ["default/codex-model"], customClaudeModels: ["claude/default-opus"], } as const; expect(getDefaultCustomModelsForProvider(defaults, "codex")).toEqual(["default/codex-model"]); expect(getDefaultCustomModelsForProvider(defaults, "claudeAgent")).toEqual([ "claude/default-opus", ]); }); it("patches custom models for codex", () => { expect(patchCustomModels("codex", ["custom/codex-model"])).toEqual({ customCodexModels: ["custom/codex-model"], }); }); it("patches custom models for claude", () => { expect(patchCustomModels("claudeAgent", ["claude/custom-opus"])).toEqual({ customClaudeModels: ["claude/custom-opus"], }); }); it("builds a complete provider-indexed custom model record", () => { expect(getCustomModelsByProvider(settings)).toEqual({ codex: ["custom/codex-model"], claudeAgent: ["claude/custom-opus"], }); }); it("builds provider-indexed model options including custom models", () => { const modelOptionsByProvider = getCustomModelOptionsByProvider(settings); expect( modelOptionsByProvider.codex.some((option) => option.slug === "custom/codex-model"), ).toBe(true); expect( modelOptionsByProvider.claudeAgent.some((option) => option.slug === "claude/custom-opus"), ).toBe(true); }); it("normalizes and deduplicates custom model options per provider", () => { const modelOptionsByProvider = getCustomModelOptionsByProvider({ customCodexModels: [" custom/codex-model ", "gpt-5.4", "custom/codex-model"], customClaudeModels: [" sonnet ", "claude/custom-opus", "claude/custom-opus"], }); expect( modelOptionsByProvider.codex.filter((option) => option.slug === "custom/codex-model"), ).toHaveLength(1); expect(modelOptionsByProvider.codex.some((option) => option.slug === "gpt-5.4")).toBe(true); expect( modelOptionsByProvider.claudeAgent.filter((option) => option.slug === "claude/custom-opus"), ).toHaveLength(1); expect( modelOptionsByProvider.claudeAgent.some((option) => option.slug === "claude-sonnet-4-6"), ).toBe(true); });}); describe("resolveAppModelSelectionState", () => { it("falls back to the default git-writing codex selection", () => { expect( resolveAppModelSelectionState({ customCodexModels: [], customClaudeModels: [], textGenerationModelSelection: undefined, }), ).toEqual({ provider: "codex", model: "gpt-5.4-mini", }); }); it("preserves the selected provider and resolves saved custom models", () => { expect( resolveAppModelSelectionState({ customCodexModels: [], customClaudeModels: ["claude/custom-haiku"], textGenerationModelSelection: { provider: "claudeAgent", model: "claude/custom-haiku", }, }), ).toEqual({ provider: "claudeAgent", model: "claude/custom-haiku", }); }); it("normalizes provider options against the resolved model capabilities", () => { expect( resolveAppModelSelectionState({ customCodexModels: [], customClaudeModels: [], textGenerationModelSelection: { provider: "claudeAgent", model: "claude-haiku-4-5", options: { effort: "max", thinking: false, fastMode: true, }, }, }), ).toEqual({ provider: "claudeAgent", model: "claude-haiku-4-5", options: { thinking: false, }, }); });});
import { describe, expect, it } from "vitest";
import {
DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
DEFAULT_TIMESTAMP_FORMAT,
} from "./appSettings";
import {
getAppModelOptions,
getCustomModelOptionsByProvider,
getCustomModelsByProvider,
getCustomModelsForProvider,
getDefaultCustomModelsForProvider,
MODEL_PROVIDER_SETTINGS,
normalizeCustomModelSlugs,
patchCustomModels,
resolveAppModelSelectionState,
resolveAppModelSelection,
} from "./modelSelection";
describe("normalizeCustomModelSlugs", () => {
it("normalizes aliases, removes built-ins, and deduplicates values", () => {
expect(
normalizeCustomModelSlugs([
" custom/internal-model ",
"gpt-5.3-codex",
"5.3",
"custom/internal-model",
"",
null,
]),
).toEqual(["custom/internal-model"]);
});
it("normalizes provider-specific aliases for claude", () => {
expect(normalizeCustomModelSlugs(["sonnet"], "claudeAgent")).toEqual([]);
expect(normalizeCustomModelSlugs(["claude/custom-sonnet"], "claudeAgent")).toEqual([
"claude/custom-sonnet",
]);
});
});
describe("getAppModelOptions", () => {
it("appends saved custom models after the built-in options", () => {
const options = getAppModelOptions("codex", ["custom/internal-model"]);
expect(options.map((option) => option.slug)).toEqual([
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.2-codex",
"gpt-5.2",
"custom/internal-model",
]);
});
it("keeps the currently selected custom model available even if it is no longer saved", () => {
const options = getAppModelOptions("codex", [], "custom/selected-model");
expect(options.at(-1)).toEqual({
slug: "custom/selected-model",
name: "custom/selected-model",
isCustom: true,
});
});
it("keeps a saved custom provider model available as an exact slug option", () => {
const options = getAppModelOptions("claudeAgent", ["claude/custom-opus"], "claude/custom-opus");
expect(options.some((option) => option.slug === "claude/custom-opus" && option.isCustom)).toBe(
true,
);
});
});
describe("resolveAppModelSelection", () => {
it("preserves saved custom model slugs instead of falling back to the default", () => {
expect(
resolveAppModelSelection(
"codex",
{ codex: ["galapagos-alpha"], claudeAgent: [] },
"galapagos-alpha",
),
).toBe("galapagos-alpha");
});
it("falls back to the provider default when no model is selected", () => {
expect(resolveAppModelSelection("codex", { codex: [], claudeAgent: [] }, "")).toBe("gpt-5.4");
});
it("resolves display names through the shared resolver", () => {
expect(resolveAppModelSelection("codex", { codex: [], claudeAgent: [] }, "GPT-5.3 Codex")).toBe(
"gpt-5.3-codex",
);
});
it("resolves aliases through the shared resolver", () => {
expect(resolveAppModelSelection("claudeAgent", { codex: [], claudeAgent: [] }, "sonnet")).toBe(
"claude-sonnet-4-6",
);
});
it("resolves transient selected custom models included in app model options", () => {
expect(
resolveAppModelSelection("codex", { codex: [], claudeAgent: [] }, "custom/selected-model"),
).toBe("custom/selected-model");
});
});
describe("timestamp format defaults", () => {
it("defaults timestamp format to locale", () => {
expect(DEFAULT_TIMESTAMP_FORMAT).toBe("locale");
});
});
describe("sidebar sort defaults", () => {
it("defaults project sorting to updated_at", () => {
expect(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER).toBe("updated_at");
});
it("defaults thread sorting to updated_at", () => {
expect(DEFAULT_SIDEBAR_THREAD_SORT_ORDER).toBe("updated_at");
});
});
describe("provider-specific custom models", () => {
it("includes provider-specific custom slugs in non-codex model lists", () => {
const claudeOptions = getAppModelOptions("claudeAgent", ["claude/custom-opus"]);
expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true);
});
});
describe("provider-indexed custom model settings", () => {
const settings = {
customCodexModels: ["custom/codex-model"],
customClaudeModels: ["claude/custom-opus"],
} as const;
it("exports one provider config per provider", () => {
expect(MODEL_PROVIDER_SETTINGS.map((config) => config.provider)).toEqual([
"codex",
"claudeAgent",
]);
});
it("reads custom models for each provider", () => {
expect(getCustomModelsForProvider(settings, "codex")).toEqual(["custom/codex-model"]);
expect(getCustomModelsForProvider(settings, "claudeAgent")).toEqual(["claude/custom-opus"]);
});
it("reads default custom models for each provider", () => {
const defaults = {
customCodexModels: ["default/codex-model"],
customClaudeModels: ["claude/default-opus"],
} as const;
expect(getDefaultCustomModelsForProvider(defaults, "codex")).toEqual(["default/codex-model"]);
expect(getDefaultCustomModelsForProvider(defaults, "claudeAgent")).toEqual([
"claude/default-opus",
]);
});
it("patches custom models for codex", () => {
expect(patchCustomModels("codex", ["custom/codex-model"])).toEqual({
customCodexModels: ["custom/codex-model"],
});
});
it("patches custom models for claude", () => {
expect(patchCustomModels("claudeAgent", ["claude/custom-opus"])).toEqual({
customClaudeModels: ["claude/custom-opus"],
});
});
it("builds a complete provider-indexed custom model record", () => {
expect(getCustomModelsByProvider(settings)).toEqual({
codex: ["custom/codex-model"],
claudeAgent: ["claude/custom-opus"],
});
});
it("builds provider-indexed model options including custom models", () => {
const modelOptionsByProvider = getCustomModelOptionsByProvider(settings);
expect(
modelOptionsByProvider.codex.some((option) => option.slug === "custom/codex-model"),
).toBe(true);
expect(
modelOptionsByProvider.claudeAgent.some((option) => option.slug === "claude/custom-opus"),
).toBe(true);
});
it("normalizes and deduplicates custom model options per provider", () => {
const modelOptionsByProvider = getCustomModelOptionsByProvider({
customCodexModels: [" custom/codex-model ", "gpt-5.4", "custom/codex-model"],
customClaudeModels: [" sonnet ", "claude/custom-opus", "claude/custom-opus"],
});
expect(
modelOptionsByProvider.codex.filter((option) => option.slug === "custom/codex-model"),
).toHaveLength(1);
expect(modelOptionsByProvider.codex.some((option) => option.slug === "gpt-5.4")).toBe(true);
expect(
modelOptionsByProvider.claudeAgent.filter((option) => option.slug === "claude/custom-opus"),
).toHaveLength(1);
expect(
modelOptionsByProvider.claudeAgent.some((option) => option.slug === "claude-sonnet-4-6"),
).toBe(true);
});
});
describe("resolveAppModelSelectionState", () => {
it("falls back to the default git-writing codex selection", () => {
expect(
resolveAppModelSelectionState({
customCodexModels: [],
customClaudeModels: [],
textGenerationModelSelection: undefined,
}),
).toEqual({
provider: "codex",
model: "gpt-5.4-mini",
});
});
it("preserves the selected provider and resolves saved custom models", () => {
expect(
resolveAppModelSelectionState({
customCodexModels: [],
customClaudeModels: ["claude/custom-haiku"],
textGenerationModelSelection: {
provider: "claudeAgent",
model: "claude/custom-haiku",
},
}),
).toEqual({
provider: "claudeAgent",
model: "claude/custom-haiku",
});
});
it("normalizes provider options against the resolved model capabilities", () => {
expect(
resolveAppModelSelectionState({
customCodexModels: [],
customClaudeModels: [],
textGenerationModelSelection: {
provider: "claudeAgent",
model: "claude-haiku-4-5",
options: {
effort: "max",
thinking: false,
fastMode: true,
},
},
}),
).toEqual({
provider: "claudeAgent",
model: "claude-haiku-4-5",
options: {
thinking: false,
},
});
});
});