From 51709c63fe18e0e7b5effdcad5393613da57664c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 21:03:31 +0000 Subject: [PATCH] perf(test): consolidate model selection suites --- ...model-selection.override-respected.test.ts | 132 -------------- ...parent.test.ts => model-selection.test.ts} | 163 +++++++++++++++--- 2 files changed, 137 insertions(+), 158 deletions(-) delete mode 100644 src/auto-reply/reply/model-selection.override-respected.test.ts rename src/auto-reply/reply/{model-selection.inherit-parent.test.ts => model-selection.test.ts} (56%) diff --git a/src/auto-reply/reply/model-selection.override-respected.test.ts b/src/auto-reply/reply/model-selection.override-respected.test.ts deleted file mode 100644 index b3457fc5596..00000000000 --- a/src/auto-reply/reply/model-selection.override-respected.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig } from "../../config/config.js"; -import { createModelSelectionState } from "./model-selection.js"; - -vi.mock("../../agents/model-catalog.js", () => ({ - loadModelCatalog: vi.fn(async () => [ - { provider: "inferencer", id: "deepseek-v3-4bit-mlx", name: "DeepSeek V3" }, - { provider: "kimi-coding", id: "k2p5", name: "Kimi K2.5" }, - { provider: "anthropic", id: "claude-opus-4-5", name: "Claude Opus 4.5" }, - ]), -})); - -const defaultProvider = "inferencer"; -const defaultModel = "deepseek-v3-4bit-mlx"; - -const makeEntry = (overrides: Record = {}) => ({ - sessionId: "session-id", - updatedAt: Date.now(), - ...overrides, -}); - -describe("createModelSelectionState respects session model override", () => { - it("applies session modelOverride when set", async () => { - const cfg = {} as OpenClawConfig; - const sessionKey = "agent:main:main"; - const sessionEntry = makeEntry({ - providerOverride: "kimi-coding", - modelOverride: "k2p5", - }); - const sessionStore = { [sessionKey]: sessionEntry }; - - const state = await createModelSelectionState({ - cfg, - agentCfg: undefined, - sessionEntry, - sessionStore, - sessionKey, - defaultProvider, - defaultModel, - provider: defaultProvider, - model: defaultModel, - hasModelDirective: false, - }); - - expect(state.provider).toBe("kimi-coding"); - expect(state.model).toBe("k2p5"); - }); - - it("falls back to default when no modelOverride is set", async () => { - const cfg = {} as OpenClawConfig; - const sessionKey = "agent:main:main"; - const sessionEntry = makeEntry(); - const sessionStore = { [sessionKey]: sessionEntry }; - - const state = await createModelSelectionState({ - cfg, - agentCfg: undefined, - sessionEntry, - sessionStore, - sessionKey, - defaultProvider, - defaultModel, - provider: defaultProvider, - model: defaultModel, - hasModelDirective: false, - }); - - expect(state.provider).toBe(defaultProvider); - expect(state.model).toBe(defaultModel); - }); - - it("respects modelOverride even when session model field differs", async () => { - // This tests the scenario from issue #14783: user switches model via /model, - // the override is stored, but session.model still reflects the last-used - // fallback model. The override should take precedence. - const cfg = {} as OpenClawConfig; - const sessionKey = "agent:main:main"; - const sessionEntry = makeEntry({ - // Last-used model (from fallback) - should NOT be used for selection - model: "k2p5", - modelProvider: "kimi-coding", - contextTokens: 262_000, - // User's explicit override - SHOULD be used - providerOverride: "anthropic", - modelOverride: "claude-opus-4-5", - }); - const sessionStore = { [sessionKey]: sessionEntry }; - - const state = await createModelSelectionState({ - cfg, - agentCfg: undefined, - sessionEntry, - sessionStore, - sessionKey, - defaultProvider, - defaultModel, - provider: defaultProvider, - model: defaultModel, - hasModelDirective: false, - }); - - // Should use the override, not the last-used model - expect(state.provider).toBe("anthropic"); - expect(state.model).toBe("claude-opus-4-5"); - }); - - it("uses default provider when providerOverride is not set but modelOverride is", async () => { - const cfg = {} as OpenClawConfig; - const sessionKey = "agent:main:main"; - const sessionEntry = makeEntry({ - modelOverride: "deepseek-v3-4bit-mlx", - // no providerOverride - }); - const sessionStore = { [sessionKey]: sessionEntry }; - - const state = await createModelSelectionState({ - cfg, - agentCfg: undefined, - sessionEntry, - sessionStore, - sessionKey, - defaultProvider, - defaultModel, - provider: defaultProvider, - model: defaultModel, - hasModelDirective: false, - }); - - expect(state.provider).toBe(defaultProvider); - expect(state.model).toBe("deepseek-v3-4bit-mlx"); - }); -}); diff --git a/src/auto-reply/reply/model-selection.inherit-parent.test.ts b/src/auto-reply/reply/model-selection.test.ts similarity index 56% rename from src/auto-reply/reply/model-selection.inherit-parent.test.ts rename to src/auto-reply/reply/model-selection.test.ts index e80088b42a0..3da30c3c6da 100644 --- a/src/auto-reply/reply/model-selection.inherit-parent.test.ts +++ b/src/auto-reply/reply/model-selection.test.ts @@ -4,44 +4,46 @@ import { createModelSelectionState } from "./model-selection.js"; vi.mock("../../agents/model-catalog.js", () => ({ loadModelCatalog: vi.fn(async () => [ + { provider: "anthropic", id: "claude-opus-4-5", name: "Claude Opus 4.5" }, + { provider: "inferencer", id: "deepseek-v3-4bit-mlx", name: "DeepSeek V3" }, + { provider: "kimi-coding", id: "k2p5", name: "Kimi K2.5" }, { provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" }, { provider: "openai", id: "gpt-4o", name: "GPT-4o" }, - { provider: "anthropic", id: "claude-opus-4-5", name: "Claude Opus 4.5" }, ]), })); -const defaultProvider = "openai"; -const defaultModel = "gpt-4o-mini"; - const makeEntry = (overrides: Record = {}) => ({ sessionId: "session-id", updatedAt: Date.now(), ...overrides, }); -async function resolveState(params: { - cfg: OpenClawConfig; - sessionEntry: ReturnType; - sessionStore: Record>; - sessionKey: string; - parentSessionKey?: string; -}) { - return createModelSelectionState({ - cfg: params.cfg, - agentCfg: params.cfg.agents?.defaults, - sessionEntry: params.sessionEntry, - sessionStore: params.sessionStore, - sessionKey: params.sessionKey, - parentSessionKey: params.parentSessionKey, - defaultProvider, - defaultModel, - provider: defaultProvider, - model: defaultModel, - hasModelDirective: false, - }); -} - describe("createModelSelectionState parent inheritance", () => { + const defaultProvider = "openai"; + const defaultModel = "gpt-4o-mini"; + + async function resolveState(params: { + cfg: OpenClawConfig; + sessionEntry: ReturnType; + sessionStore: Record>; + sessionKey: string; + parentSessionKey?: string; + }) { + return createModelSelectionState({ + cfg: params.cfg, + agentCfg: params.cfg.agents?.defaults, + sessionEntry: params.sessionEntry, + sessionStore: params.sessionStore, + sessionKey: params.sessionKey, + parentSessionKey: params.parentSessionKey, + defaultProvider, + defaultModel, + provider: defaultProvider, + model: defaultModel, + hasModelDirective: false, + }); + } + it("inherits parent override from explicit parentSessionKey", async () => { const cfg = {} as OpenClawConfig; const parentKey = "agent:main:discord:channel:c1"; @@ -212,3 +214,112 @@ describe("createModelSelectionState parent inheritance", () => { expect(state.model).toBe("claude-opus-4-5"); }); }); + +describe("createModelSelectionState respects session model override", () => { + const defaultProvider = "inferencer"; + const defaultModel = "deepseek-v3-4bit-mlx"; + + it("applies session modelOverride when set", async () => { + const cfg = {} as OpenClawConfig; + const sessionKey = "agent:main:main"; + const sessionEntry = makeEntry({ + providerOverride: "kimi-coding", + modelOverride: "k2p5", + }); + const sessionStore = { [sessionKey]: sessionEntry }; + + const state = await createModelSelectionState({ + cfg, + agentCfg: undefined, + sessionEntry, + sessionStore, + sessionKey, + defaultProvider, + defaultModel, + provider: defaultProvider, + model: defaultModel, + hasModelDirective: false, + }); + + expect(state.provider).toBe("kimi-coding"); + expect(state.model).toBe("k2p5"); + }); + + it("falls back to default when no modelOverride is set", async () => { + const cfg = {} as OpenClawConfig; + const sessionKey = "agent:main:main"; + const sessionEntry = makeEntry(); + const sessionStore = { [sessionKey]: sessionEntry }; + + const state = await createModelSelectionState({ + cfg, + agentCfg: undefined, + sessionEntry, + sessionStore, + sessionKey, + defaultProvider, + defaultModel, + provider: defaultProvider, + model: defaultModel, + hasModelDirective: false, + }); + + expect(state.provider).toBe(defaultProvider); + expect(state.model).toBe(defaultModel); + }); + + it("respects modelOverride even when session model field differs", async () => { + // From issue #14783: stored override should beat last-used fallback model. + const cfg = {} as OpenClawConfig; + const sessionKey = "agent:main:main"; + const sessionEntry = makeEntry({ + model: "k2p5", + modelProvider: "kimi-coding", + contextTokens: 262_000, + providerOverride: "anthropic", + modelOverride: "claude-opus-4-5", + }); + const sessionStore = { [sessionKey]: sessionEntry }; + + const state = await createModelSelectionState({ + cfg, + agentCfg: undefined, + sessionEntry, + sessionStore, + sessionKey, + defaultProvider, + defaultModel, + provider: defaultProvider, + model: defaultModel, + hasModelDirective: false, + }); + + expect(state.provider).toBe("anthropic"); + expect(state.model).toBe("claude-opus-4-5"); + }); + + it("uses default provider when providerOverride is not set but modelOverride is", async () => { + const cfg = {} as OpenClawConfig; + const sessionKey = "agent:main:main"; + const sessionEntry = makeEntry({ + modelOverride: "deepseek-v3-4bit-mlx", + }); + const sessionStore = { [sessionKey]: sessionEntry }; + + const state = await createModelSelectionState({ + cfg, + agentCfg: undefined, + sessionEntry, + sessionStore, + sessionKey, + defaultProvider, + defaultModel, + provider: defaultProvider, + model: defaultModel, + hasModelDirective: false, + }); + + expect(state.provider).toBe(defaultProvider); + expect(state.model).toBe("deepseek-v3-4bit-mlx"); + }); +});