Files
openclaw/src/auto-reply/thinking.test.ts
Peter Lindsey 6add1cc969 feat(messages): config-level default for the persistent /usage footer
Adds `messages.responseUsage` (precedence session -> channel -> config default
-> off) so the persistent /usage footer can default-on, with three distinct
states: explicit on (tokens/full), explicit off (persisted), and unset (inherit
the configured default).

Unifies effective-value resolution behind a single channel-aware resolver
`resolveEffectiveResponseUsage` used by reply rendering, the no-arg /usage
toggle, the ACP control, and the gateway session-row builder; the row builder's
`effectiveResponseUsage` is carried through sessions.changed events, chat
snapshots, and the UI row so live consumers never go stale. `/usage reset`
(aliases inherit/clear/default) clears the override to inherit; only explicit
off persists; a full session reset preserves the preference. ACP "Usage detail"
gains an "inherit" option for unset sessions. Docs/help/completions updated; "on"
documented as a legacy alias; config-doc baseline regenerated.
2026-06-24 07:12:33 -07:00

880 lines
29 KiB
TypeScript

/** Tests thinking, reasoning, verbosity, and usage directive normalization. */
import { beforeEach, describe, expect, it, vi } from "vitest";
const providerRuntimeMocks = vi.hoisted(() => ({
resolveProviderBinaryThinking: vi.fn(),
resolveProviderDefaultThinkingLevel: vi.fn(),
resolveProviderThinkingProfile: vi.fn(),
resolveProviderXHighThinking: vi.fn(),
}));
vi.mock("../plugins/provider-thinking.js", () => ({
resolveProviderBinaryThinking: providerRuntimeMocks.resolveProviderBinaryThinking,
resolveProviderDefaultThinkingLevel: providerRuntimeMocks.resolveProviderDefaultThinkingLevel,
resolveProviderThinkingProfile: providerRuntimeMocks.resolveProviderThinkingProfile,
resolveProviderXHighThinking: providerRuntimeMocks.resolveProviderXHighThinking,
}));
const {
listThinkingLevelLabels,
listThinkingLevelOptions,
listThinkingLevels,
normalizeReasoningLevel,
normalizeThinkLevel,
isThinkingLevelSupported,
formatThinkingLevels,
resolveSupportedThinkingLevel,
resolveThinkingDefaultForModel,
resolveMessagesResponseUsageDefault,
resolveEffectiveResponseUsage,
} = await import("./thinking.js");
beforeEach(() => {
providerRuntimeMocks.resolveProviderBinaryThinking.mockReset();
providerRuntimeMocks.resolveProviderBinaryThinking.mockReturnValue(undefined);
providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReset();
providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined);
providerRuntimeMocks.resolveProviderThinkingProfile.mockReset();
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue(undefined);
providerRuntimeMocks.resolveProviderXHighThinking.mockReset();
providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(undefined);
});
describe("normalizeThinkLevel", () => {
it("accepts mid as medium", () => {
expect(normalizeThinkLevel("mid")).toBe("medium");
});
it("accepts xhigh aliases", () => {
expect(normalizeThinkLevel("xhigh")).toBe("xhigh");
expect(normalizeThinkLevel("x-high")).toBe("xhigh");
expect(normalizeThinkLevel("x_high")).toBe("xhigh");
expect(normalizeThinkLevel("x high")).toBe("xhigh");
});
it("accepts extra-high aliases as xhigh", () => {
expect(normalizeThinkLevel("extra-high")).toBe("xhigh");
expect(normalizeThinkLevel("extra high")).toBe("xhigh");
expect(normalizeThinkLevel("extra_high")).toBe("xhigh");
expect(normalizeThinkLevel(" extra high ")).toBe("xhigh");
});
it("does not over-match nearby xhigh words", () => {
expect(normalizeThinkLevel("extra-highest")).toBeUndefined();
expect(normalizeThinkLevel("xhigher")).toBeUndefined();
});
it("accepts on as low", () => {
expect(normalizeThinkLevel("on")).toBe("low");
});
it("accepts adaptive and auto aliases", () => {
expect(normalizeThinkLevel("adaptive")).toBe("adaptive");
expect(normalizeThinkLevel("auto")).toBe("adaptive");
expect(normalizeThinkLevel("Adaptive")).toBe("adaptive");
});
it("accepts max as its own level", () => {
expect(normalizeThinkLevel("max")).toBe("max");
expect(normalizeThinkLevel("MAX")).toBe("max");
});
});
describe("listThinkingLevels", () => {
it("uses provider runtime hooks for xhigh support", () => {
providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(true);
expect(listThinkingLevels("demo", "demo-model")).toContain("xhigh");
});
it("uses provider runtime hooks for xhigh labels", () => {
providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(true);
expect(listThinkingLevelLabels("demo", "demo-model")).toContain("xhigh");
});
it("includes xhigh for provider-advertised models", () => {
providerRuntimeMocks.resolveProviderXHighThinking.mockImplementation(({ provider, context }) =>
(provider === "openai" &&
["gpt-5.4", "gpt-5.4-pro", "gpt-5.3-codex-spark"].includes(context.modelId)) ||
(provider === "github-copilot" && context.modelId === "gpt-5.4")
? true
: undefined,
);
for (const [provider, model] of [
["openai", "gpt-5.4"],
["openai", "gpt-5.4-pro"],
["openai", "gpt-5.3-codex-spark"],
["github-copilot", "gpt-5.4"],
] as const) {
expect(listThinkingLevels(provider, model)).toContain("xhigh");
}
});
it("excludes xhigh for non-codex models", () => {
expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain("xhigh");
});
it("does not include max without provider support", () => {
expect(listThinkingLevels("openai", "gpt-5.4")).not.toContain("max");
});
it("does not include adaptive without provider support", () => {
expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain("adaptive");
expect(listThinkingLevels("openai", "gpt-5.4")).not.toContain("adaptive");
});
it("uses provider thinking profiles for adaptive and max support", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ provider }) =>
provider === "anthropic"
? { levels: [{ id: "off" }, { id: "adaptive" }, { id: "max" }] }
: undefined,
);
expect(listThinkingLevels("anthropic", "claude-opus-4-6")).toContain("adaptive");
expect(listThinkingLevels("anthropic", "claude-opus-4-7")).toContain("max");
});
it("preserves provider profile ids and labels", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "adaptive", label: "auto" }, { id: "max", label: "maximum" }],
defaultLevel: "adaptive",
});
expect(listThinkingLevelOptions("demo", "demo-model")).toEqual([
{ id: "off", label: "off" },
{ id: "adaptive", label: "auto" },
{ id: "max", label: "maximum" },
]);
});
it("uses provider thinking profiles ahead of legacy hooks", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "low", label: "on" }],
defaultLevel: "off",
});
providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(true);
expect(listThinkingLevels("demo", "demo-model")).toEqual(["off", "low"]);
expect(listThinkingLevelLabels("demo", "demo-model")).toEqual(["off", "on"]);
});
it("treats catalog reasoning=false as an explicit thinking opt-out", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }],
defaultLevel: "medium",
});
const catalog = [
{
provider: "google",
id: "gemma-4-26b-a4b-it",
name: "Gemma 4 26B",
reasoning: false,
},
];
expect(listThinkingLevels("google", "gemma-4-26b-a4b-it", catalog)).toEqual(["off"]);
expect(
isThinkingLevelSupported({
provider: "google",
model: "gemma-4-26b-a4b-it",
level: "medium",
catalog,
}),
).toBe(false);
expect(
resolveThinkingDefaultForModel({
provider: "google",
model: "gemma-4-26b-a4b-it",
catalog,
}),
).toBe("off");
});
it("preserves provider-authoritative thinking profiles over stale catalog reasoning", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "minimal" }, { id: "low" }, { id: "medium" }],
preserveWhenCatalogReasoningFalse: true,
});
const catalog = [
{
provider: "google",
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash Preview",
reasoning: false,
},
];
expect(
isThinkingLevelSupported({
provider: "google",
model: "gemini-3-flash-preview",
level: "low",
catalog,
}),
).toBe(true);
expect(
resolveSupportedThinkingLevel({
provider: "google",
model: "gemini-3-flash-preview",
level: "low",
catalog,
}),
).toBe("low");
});
it("passes catalog reasoning into provider thinking profiles for support checks", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ context }) => ({
levels:
context.reasoning === true
? [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }]
: [{ id: "off" }],
defaultLevel: "off",
}));
const catalog = [{ provider: "ollama", id: "gpt-oss:20b", name: "gpt-oss", reasoning: true }];
expect(
isThinkingLevelSupported({
provider: "ollama",
model: "gpt-oss:20b",
level: "max",
catalog,
}),
).toBe(true);
expect(formatThinkingLevels("ollama", "gpt-oss:20b", ", ", catalog)).toBe(
"off, low, medium, high, max",
);
expect(
resolveSupportedThinkingLevel({
provider: "ollama",
model: "gpt-oss:20b",
level: "max",
catalog,
}),
).toBe("max");
});
it("passes catalog compat into provider thinking profiles", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ context }) =>
context.reasoning === true && context.compat?.thinkingFormat === "qwen-chat-template"
? {
levels: [{ id: "off" }, { id: "low", label: "on" }],
defaultLevel: "off",
}
: undefined,
);
const catalog = [
{
provider: "vllm",
id: "Qwen/Qwen3-8B",
reasoning: true,
compat: { thinkingFormat: "qwen-chat-template" },
},
];
expect(listThinkingLevelLabels("vllm", "Qwen/Qwen3-8B", catalog)).toEqual(["off", "on"]);
expect(
resolveSupportedThinkingLevel({
provider: "vllm",
model: "Qwen/Qwen3-8B",
level: "high",
catalog,
}),
).toBe("low");
});
it("uses canonical Fable params when no provider thinking profile exists", () => {
const catalog = [
{
provider: "microsoft-foundry",
id: "company-fable",
api: "anthropic-messages",
reasoning: false,
params: { canonicalModelId: "claude-fable-5" },
},
];
expect(listThinkingLevels("microsoft-foundry", "company-fable", catalog)).toEqual([
"off",
"minimal",
"low",
"medium",
"adaptive",
"high",
"xhigh",
"max",
]);
expect(
resolveThinkingDefaultForModel({
provider: "microsoft-foundry",
model: "company-fable",
catalog,
}),
).toBe("high");
});
it("exposes Claude Opus xhigh on custom anthropic-messages providers without a plugin profile", () => {
// Regression for openclaw#91975: a renamed provider serving Claude Opus over
// anthropic-messages used to fall back to a base profile (no xhigh) and silently
// clamp `--thinking xhigh` to `off`.
const catalog = [
{
provider: "jdcloud-anthropic",
id: "claude-opus-4.7-hq",
api: "anthropic-messages",
reasoning: true,
},
];
expect(listThinkingLevels("jdcloud-anthropic", "claude-opus-4.7-hq", catalog)).toEqual([
"off",
"minimal",
"low",
"medium",
"adaptive",
"high",
"xhigh",
"max",
]);
expect(
isThinkingLevelSupported({
provider: "jdcloud-anthropic",
model: "claude-opus-4.7-hq",
level: "xhigh",
catalog,
}),
).toBe(true);
expect(
resolveSupportedThinkingLevel({
provider: "jdcloud-anthropic",
model: "claude-opus-4.7-hq",
level: "xhigh",
catalog,
}),
).toBe("xhigh");
});
it("does not invent xhigh for non-Claude models on anthropic-messages routes", () => {
const catalog = [
{
provider: "jdcloud-anthropic",
id: "some-non-claude-model",
api: "anthropic-messages",
reasoning: true,
},
];
expect(listThinkingLevels("jdcloud-anthropic", "some-non-claude-model", catalog)).toEqual([
"off",
"minimal",
"low",
"medium",
"high",
]);
});
it("intentionally suppresses compat-driven xhigh for non-Claude anthropic-messages rows", () => {
// Even when the catalog explicitly advertises xhigh via compat, a non-Claude
// model on the anthropic-messages transport stays on the Claude base set.
// The transport itself doesn't carry a generic xhigh contract — only Claude
// families do — so the catalog signal is intentionally suppressed here.
const catalog = [
{
provider: "jdcloud-anthropic",
id: "some-non-claude-model",
api: "anthropic-messages",
reasoning: true,
compat: { supportedReasoningEfforts: ["xhigh"] },
},
];
expect(listThinkingLevels("jdcloud-anthropic", "some-non-claude-model", catalog)).not.toContain(
"xhigh",
);
});
it("does not infer the Claude profile without an anthropic-messages catalog row", () => {
// Same provider id, but the catalog row says openai-completions — must NOT
// grant Claude levels to a non-Anthropic transport.
const catalog = [
{
provider: "jdcloud-anthropic",
id: "claude-opus-4.7-hq",
api: "openai-completions",
reasoning: true,
},
];
expect(listThinkingLevels("jdcloud-anthropic", "claude-opus-4.7-hq", catalog)).toEqual([
"off",
"minimal",
"low",
"medium",
"high",
]);
});
it("matches native Anthropic max parity for adaptive Claude on custom anthropic-messages providers", () => {
// Adaptive Claude families (e.g. claude-sonnet-4-6) take the adaptive-default
// branch in resolveClaudeThinkingProfile, which only exposes `max` when
// includeNativeMax is set. The fallback must pass the same option the
// bundled anthropic plugin uses, otherwise custom providers silently lose
// `max` parity with the native Anthropic policy.
const catalog = [
{
provider: "jdcloud-anthropic",
id: "claude-sonnet-4-6",
api: "anthropic-messages",
reasoning: true,
},
];
expect(listThinkingLevels("jdcloud-anthropic", "claude-sonnet-4-6", catalog)).toContain("max");
expect(
isThinkingLevelSupported({
provider: "jdcloud-anthropic",
model: "claude-sonnet-4-6",
level: "max",
catalog,
}),
).toBe(true);
});
it("preserves provider-specific profiles for Fable Messages routes", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "low" }],
defaultLevel: "off",
});
expect(
listThinkingLevels("proxy", "company-fable", [
{
provider: "proxy",
id: "company-fable",
api: "anthropic-messages",
reasoning: true,
params: { canonicalModelId: "claude-fable-5" },
},
]),
).toEqual(["off", "low"]);
});
it("does not infer the Fable contract without an Anthropic Messages catalog row", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "low" }],
defaultLevel: "off",
});
expect(listThinkingLevels("openrouter", "anthropic/claude-fable-5")).toEqual(["off", "low"]);
});
it("does not apply the Fable profile to OpenAI-compatible catalog rows", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "low" }, { id: "high" }],
defaultLevel: "off",
});
expect(
listThinkingLevels("openrouter", "anthropic/claude-fable-5", [
{
provider: "openrouter",
id: "anthropic/claude-fable-5",
api: "openai-completions",
reasoning: true,
},
]),
).toEqual(["off", "low", "high"]);
});
it("preserves explicit provider opt-outs for canonical Fable aliases", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }],
defaultLevel: "off",
});
const catalog = [
{
provider: "claude-cli",
id: "company-fable",
api: "anthropic-messages",
reasoning: true,
params: { canonicalModelId: "claude-fable-5" },
},
];
expect(listThinkingLevels("claude-cli", "company-fable", catalog)).toEqual(["off"]);
});
it("uses generic thinking levels when a provider has no custom profile", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue(null);
expect(
listThinkingLevels("vllm", "reasoning-model", [
{
provider: "vllm",
id: "reasoning-model",
reasoning: true,
},
]),
).toEqual(["off", "minimal", "low", "medium", "high"]);
});
it("matches provider-qualified catalog ids for provider thinking profiles", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ context }) =>
context.reasoning === true && context.compat?.thinkingFormat === "qwen-chat-template"
? {
levels: [{ id: "off" }, { id: "low", label: "on" }],
defaultLevel: "off",
}
: undefined,
);
const catalog = [
{
provider: "vllm",
id: "vllm/Qwen/Qwen3-8B",
reasoning: true,
compat: { thinkingFormat: "qwen-chat-template" },
},
];
expect(listThinkingLevelLabels("vllm", "Qwen/Qwen3-8B", catalog)).toEqual(["off", "on"]);
expect(
resolveSupportedThinkingLevel({
provider: "vllm",
model: "Qwen/Qwen3-8B",
level: "high",
catalog,
}),
).toBe("low");
});
it("uses catalog compat reasoning efforts to expose xhigh for configured custom models", () => {
const catalog = [
{
provider: "gmn",
id: "gpt-5.4",
name: "GPT 5.4 via GMN",
reasoning: true,
compat: { supportedReasoningEfforts: ["low", "medium", "high", "xhigh"] },
},
];
expect(listThinkingLevels("gmn", "gpt-5.4", catalog)).toContain("xhigh");
expect(formatThinkingLevels("gmn", "gpt-5.4", ", ", catalog)).toBe(
"off, minimal, low, medium, high, xhigh",
);
expect(
isThinkingLevelSupported({
provider: "gmn",
model: "gpt-5.4",
level: "xhigh",
catalog,
}),
).toBe(true);
});
it("does not let catalog xhigh compat override binary thinking providers", () => {
providerRuntimeMocks.resolveProviderBinaryThinking.mockReturnValue(true);
const catalog = [
{
provider: "zai",
id: "glm-4.7",
name: "GLM 4.7",
compat: { supportedReasoningEfforts: ["xhigh"] },
},
];
expect(listThinkingLevels("zai", "glm-4.7", catalog)).toEqual(["off", "low"]);
expect(listThinkingLevelLabels("zai", "glm-4.7", catalog)).toEqual(["off", "on"]);
});
it("maps stale unsupported levels to the largest profile level", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "high" }],
});
expect(
resolveSupportedThinkingLevel({
provider: "demo",
model: "demo-model",
level: "max",
}),
).toBe("high");
});
it("maps xhigh to high for provider profiles with max but no xhigh", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ provider }) =>
provider === "anthropic"
? {
levels: [
{ id: "off" },
{ id: "minimal" },
{ id: "low" },
{ id: "medium" },
{ id: "high" },
{ id: "adaptive" },
{ id: "max" },
],
}
: undefined,
);
expect(
resolveSupportedThinkingLevel({
provider: "anthropic",
model: "claude-sonnet-4-6",
level: "xhigh",
}),
).toBe("high");
});
it("maps unsupported adaptive to medium and unsupported xhigh to high", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({
levels: [{ id: "off" }, { id: "minimal" }, { id: "low" }, { id: "medium" }, { id: "high" }],
});
expect(
resolveSupportedThinkingLevel({
provider: "openai",
model: "gpt-5.4",
level: "adaptive",
}),
).toBe("medium");
expect(
resolveSupportedThinkingLevel({
provider: "openai",
model: "gpt-4.1-mini",
level: "xhigh",
}),
).toBe("high");
});
});
describe("listThinkingLevelLabels", () => {
it("uses provider runtime hooks for binary thinking providers", () => {
providerRuntimeMocks.resolveProviderBinaryThinking.mockReturnValue(true);
expect(listThinkingLevelLabels("demo", "demo-model")).toEqual(["off", "on"]);
});
it("returns on/off for provider-advertised binary thinking", () => {
providerRuntimeMocks.resolveProviderBinaryThinking.mockImplementation(({ provider }) =>
provider === "zai" ? true : undefined,
);
expect(listThinkingLevelLabels("zai", "glm-4.7")).toEqual(["off", "on"]);
});
it("does not assume binary thinking without provider runtime", () => {
expect(listThinkingLevelLabels("zai", "glm-4.7")).toContain("low");
expect(listThinkingLevelLabels("zai", "glm-4.7")).not.toContain("on");
});
it("returns full levels for non-ZAI", () => {
expect(listThinkingLevelLabels("openai", "gpt-4.1-mini")).toContain("low");
expect(listThinkingLevelLabels("openai", "gpt-4.1-mini")).not.toContain("on");
});
});
describe("resolveThinkingDefaultForModel", () => {
it("uses provider runtime hooks for default thinking levels", () => {
providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReturnValue("adaptive");
expect(resolveThinkingDefaultForModel({ provider: "demo", model: "demo-model" })).toBe(
"adaptive",
);
});
it("uses provider-advertised adaptive defaults", () => {
providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockImplementation(
({ provider, context }) =>
provider === "anthropic" && context.modelId === "claude-opus-4-6" ? "adaptive" : undefined,
);
expect(
resolveThinkingDefaultForModel({ provider: "anthropic", model: "claude-opus-4-6" }),
).toBe("adaptive");
});
it("does not apply provider-advertised adaptive defaults across Bedrock id variants", () => {
providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockImplementation(
({ provider, context }) =>
provider === "amazon-bedrock" && context.modelId === "claude-sonnet-4-6"
? "adaptive"
: undefined,
);
expect(
resolveThinkingDefaultForModel({ provider: "aws-bedrock", model: "claude-sonnet-4-6" }),
).toBe("off");
});
it("does not assume adaptive defaults without provider runtime", () => {
expect(
resolveThinkingDefaultForModel({ provider: "anthropic", model: "claude-opus-4-6" }),
).toBe("off");
expect(
resolveThinkingDefaultForModel({ provider: "aws-bedrock", model: "claude-sonnet-4-6" }),
).toBe("off");
});
it("defaults reasoning-capable catalog models to medium", () => {
expect(
resolveThinkingDefaultForModel({
provider: "openai",
model: "gpt-5.4",
catalog: [{ provider: "openai", id: "gpt-5.4", reasoning: true }],
}),
).toBe("medium");
});
it("remaps implicit reasoning defaults to the strongest supported level at or below medium", () => {
providerRuntimeMocks.resolveProviderBinaryThinking.mockImplementation(
({ provider }) => provider === "demo-binary",
);
expect(
resolveThinkingDefaultForModel({
provider: "demo-binary",
model: "demo-model",
catalog: [{ provider: "demo-binary", id: "demo-model", reasoning: true }],
}),
).toBe("low");
});
it("keeps catalog reasoning context when remapping implicit reasoning defaults", () => {
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(
({ provider, context }) =>
provider === "demo-contextual" && context.reasoning
? { levels: [{ id: "off" }, { id: "low" }, { id: "medium" }] }
: provider === "demo-contextual"
? { levels: [{ id: "off" }] }
: undefined,
);
expect(
resolveThinkingDefaultForModel({
provider: "demo-contextual",
model: "demo-model",
catalog: [{ provider: "demo-contextual", id: "demo-model", reasoning: true }],
}),
).toBe("medium");
});
it("defaults to off when no adaptive or reasoning hint is present", () => {
expect(
resolveThinkingDefaultForModel({
provider: "openai",
model: "gpt-4.1-mini",
catalog: [{ provider: "openai", id: "gpt-4.1-mini", reasoning: false }],
}),
).toBe("off");
});
it("respects provider-declared 'off' default for reasoning-capable models", () => {
// Providers like Ollama declare defaultLevel:"off" even for reasoning=true models
// because thinking must be explicitly opted in, not activated by the global default.
providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ provider }) =>
provider === "ollama"
? {
levels: [{ id: "off" }, { id: "low" }, { id: "medium" }, { id: "high" }, { id: "max" }],
defaultLevel: "off",
}
: undefined,
);
expect(
resolveThinkingDefaultForModel({
provider: "ollama",
model: "gemma4",
catalog: [{ provider: "ollama", id: "gemma4", reasoning: true }],
}),
).toBe("off");
});
});
describe("normalizeReasoningLevel", () => {
it("accepts on/off", () => {
expect(normalizeReasoningLevel("on")).toBe("on");
expect(normalizeReasoningLevel("off")).toBe("off");
});
it("accepts show/hide", () => {
expect(normalizeReasoningLevel("show")).toBe("on");
expect(normalizeReasoningLevel("hide")).toBe("off");
});
it("accepts stream", () => {
expect(normalizeReasoningLevel("stream")).toBe("stream");
expect(normalizeReasoningLevel("streaming")).toBe("stream");
});
});
describe("resolveMessagesResponseUsageDefault", () => {
it("returns undefined when unset (preserves off-by-default behavior)", () => {
expect(resolveMessagesResponseUsageDefault(undefined)).toBeUndefined();
expect(resolveMessagesResponseUsageDefault(undefined, "discord")).toBeUndefined();
});
it("returns a bare string default for any channel", () => {
expect(resolveMessagesResponseUsageDefault("full")).toBe("full");
expect(resolveMessagesResponseUsageDefault("full", "telegram")).toBe("full");
});
it("resolves the channel entry from a map", () => {
const cfg = { default: "off", discord: "full", telegram: "tokens" } as const;
expect(resolveMessagesResponseUsageDefault(cfg, "discord")).toBe("full");
expect(resolveMessagesResponseUsageDefault(cfg, "telegram")).toBe("tokens");
});
it("falls back to default for an unmapped channel", () => {
const cfg = { default: "tokens", discord: "full" } as const;
expect(resolveMessagesResponseUsageDefault(cfg, "whatsapp")).toBe("tokens");
});
it("returns undefined for a map with neither the channel nor a default", () => {
expect(resolveMessagesResponseUsageDefault({ discord: "full" }, "telegram")).toBeUndefined();
});
});
describe("resolveEffectiveResponseUsage", () => {
it("returns off when session is unset and no config is provided", () => {
expect(resolveEffectiveResponseUsage(undefined, undefined)).toBe("off");
expect(resolveEffectiveResponseUsage(null, undefined)).toBe("off");
});
it("applies config default when session is unset", () => {
expect(resolveEffectiveResponseUsage(undefined, "tokens")).toBe("tokens");
expect(resolveEffectiveResponseUsage(undefined, "full")).toBe("full");
});
it("applies per-channel config entry when session is unset", () => {
const cfg = { default: "off", discord: "full", telegram: "tokens" } as const;
expect(resolveEffectiveResponseUsage(undefined, cfg, "discord")).toBe("full");
expect(resolveEffectiveResponseUsage(undefined, cfg, "telegram")).toBe("tokens");
// Unknown channel falls back to config default
expect(resolveEffectiveResponseUsage(undefined, cfg, "whatsapp")).toBe("off");
});
it("session explicit off overrides any config default", () => {
// Explicit "off" is stored and wins — non-off config default cannot re-enable it.
expect(resolveEffectiveResponseUsage("off", "tokens")).toBe("off");
expect(resolveEffectiveResponseUsage("off", "full")).toBe("off");
expect(resolveEffectiveResponseUsage("off", { default: "full", discord: "full" }, "discord")).toBe("off");
});
it("session explicit on value overrides config default", () => {
expect(resolveEffectiveResponseUsage("tokens", "full")).toBe("tokens");
expect(resolveEffectiveResponseUsage("full", "off")).toBe("full");
});
it("unset (undefined/null) falls through to config; explicit off does not", () => {
// These two are distinct states:
// - undefined = unset/inherit → gets config default
// - "off" = explicit off → stays off
const cfg = "tokens" as const;
expect(resolveEffectiveResponseUsage(undefined, cfg)).toBe("tokens"); // inherits
expect(resolveEffectiveResponseUsage("off", cfg)).toBe("off"); // explicit off persists
});
});