mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 07:50:43 +00:00
two bugs. both squash user model choice silently. bug 1: applyDefaultModel() unconditional primary: model overwrite. wizard calls with setDefaultModel=true, provider returns its default (e.g. openrouter/auto), bam user primary gone. fix: existingPrimary ?? model. bug 2: applyModelFallbacksFromSelection() phantom primary injection. when no primary configured, resolvedKey (hardcoded default) written as primary via nullish coalescing fallback. fix: conditional spread — only include primary key when one actually existed. tests for both. closes #70696
176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { applyDefaultModel, applyProviderAuthConfigPatch } from "./provider-auth-choice-helpers.js";
|
|
|
|
describe("applyProviderAuthConfigPatch", () => {
|
|
const base = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "anthropic/claude-sonnet-4-6", fallbacks: ["openai/gpt-5.2"] },
|
|
models: {
|
|
"anthropic/claude-sonnet-4-6": { alias: "Sonnet" },
|
|
"anthropic/claude-opus-4-6": { alias: "Opus" },
|
|
"openai/gpt-5.2": {},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
it("merges default model maps by default so other providers survive login", () => {
|
|
const patch = { agents: { defaults: { models: { "openai/gpt-5.5": {} } } } };
|
|
const next = applyProviderAuthConfigPatch(base, patch);
|
|
expect(next.agents?.defaults?.models).toEqual({
|
|
...base.agents.defaults.models,
|
|
"openai/gpt-5.5": {},
|
|
});
|
|
expect(next.agents?.defaults?.model).toEqual(base.agents.defaults.model);
|
|
});
|
|
|
|
it("replaces the allowlist only when replaceDefaultModels is set", () => {
|
|
const patch = {
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"claude-cli/claude-sonnet-4-6": { alias: "Sonnet" },
|
|
"openai/gpt-5.2": {},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const next = applyProviderAuthConfigPatch(base, patch, { replaceDefaultModels: true });
|
|
expect(next.agents?.defaults?.models).toEqual(patch.agents.defaults.models);
|
|
expect(next.agents?.defaults?.model).toEqual(base.agents.defaults.model);
|
|
});
|
|
|
|
it("drops prototype-pollution keys from the merge", () => {
|
|
const patch = JSON.parse('{"__proto__":{"polluted":true},"agents":{"defaults":{}}}');
|
|
const next = applyProviderAuthConfigPatch(base, patch);
|
|
expect(next.agents?.defaults?.models).toEqual(base.agents.defaults.models);
|
|
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
|
expect(Object.getPrototypeOf(next).polluted).toBeUndefined();
|
|
});
|
|
|
|
it("drops prototype-pollution keys from opt-in model replacement", () => {
|
|
const patch = JSON.parse(
|
|
'{"agents":{"defaults":{"models":{"__proto__":{"polluted":true},"claude-cli/claude-sonnet-4-6":{"alias":"Sonnet","params":{"constructor":{"polluted":true},"maxTokens":12000}}}}}}',
|
|
);
|
|
const next = applyProviderAuthConfigPatch(base, patch, { replaceDefaultModels: true });
|
|
const models = next.agents?.defaults?.models;
|
|
expect(models).toEqual({
|
|
"claude-cli/claude-sonnet-4-6": {
|
|
alias: "Sonnet",
|
|
params: { maxTokens: 12000 },
|
|
},
|
|
});
|
|
expect(Object.prototype.hasOwnProperty.call(models, "__proto__")).toBe(false);
|
|
expect(Object.getPrototypeOf(Object.assign({}, models)).polluted).toBeUndefined();
|
|
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
|
});
|
|
|
|
it("keeps normal recursive merges for unrelated provider auth patch fields", () => {
|
|
const base = {
|
|
agents: {
|
|
defaults: {
|
|
contextPruning: {
|
|
mode: "cache-ttl",
|
|
ttl: "30m",
|
|
},
|
|
},
|
|
},
|
|
} satisfies OpenClawConfig;
|
|
const patch = {
|
|
agents: {
|
|
defaults: {
|
|
contextPruning: {
|
|
ttl: "1h",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const next = applyProviderAuthConfigPatch(base, patch);
|
|
|
|
expect(next).toEqual({
|
|
agents: {
|
|
defaults: {
|
|
contextPruning: {
|
|
mode: "cache-ttl",
|
|
ttl: "1h",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("applyDefaultModel", () => {
|
|
it("sets the primary when none exists", () => {
|
|
const config = {
|
|
agents: { defaults: {} },
|
|
} as OpenClawConfig;
|
|
const next = applyDefaultModel(config, "openrouter/auto");
|
|
expect(next.agents?.defaults?.model).toEqual({ primary: "openrouter/auto" });
|
|
});
|
|
|
|
it("overwrites an existing primary by default", () => {
|
|
const config = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "anthropic/claude-opus-4-6" },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const next = applyDefaultModel(config, "openrouter/auto");
|
|
expect(next.agents?.defaults?.model).toEqual({
|
|
primary: "openrouter/auto",
|
|
});
|
|
});
|
|
|
|
it("preserves an existing primary when requested", () => {
|
|
const config = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "anthropic/claude-opus-4-6" },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const next = applyDefaultModel(config, "openrouter/auto", {
|
|
preserveExistingPrimary: true,
|
|
});
|
|
expect(next.agents?.defaults?.model).toEqual({
|
|
primary: "anthropic/claude-opus-4-6",
|
|
});
|
|
});
|
|
|
|
it("preserves an existing primary and keeps fallbacks", () => {
|
|
const config = {
|
|
agents: {
|
|
defaults: {
|
|
model: {
|
|
primary: "anthropic/claude-opus-4-6",
|
|
fallbacks: ["openai/gpt-5.4"],
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
const next = applyDefaultModel(config, "openrouter/auto", {
|
|
preserveExistingPrimary: true,
|
|
});
|
|
expect(next.agents?.defaults?.model).toEqual({
|
|
primary: "anthropic/claude-opus-4-6",
|
|
fallbacks: ["openai/gpt-5.4"],
|
|
});
|
|
});
|
|
|
|
it("adds the model to the allowlist", () => {
|
|
const config = {
|
|
agents: { defaults: { models: { "anthropic/claude-sonnet-4-6": {} } } },
|
|
} as OpenClawConfig;
|
|
const next = applyDefaultModel(config, "openrouter/auto");
|
|
expect(next.agents?.defaults?.models).toEqual({
|
|
"anthropic/claude-sonnet-4-6": {},
|
|
"openrouter/auto": {},
|
|
});
|
|
});
|
|
});
|