mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
cb52ded58d
* refactor(telegram): split native commands by executor * refactor(telegram): deduplicate DM-thread target session * chore(lint): ratchet max-lines baseline after telegram commands split * test(telegram): fix native command split checks
458 lines
15 KiB
TypeScript
458 lines
15 KiB
TypeScript
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
executorTestMocks,
|
|
expectRecordFields,
|
|
expectSendMessageCall,
|
|
registerAndResolveCommandHandler,
|
|
resetSessionMetaMocks,
|
|
} from "./bot-native-command-executors.test-support.js";
|
|
import { createTelegramPrivateCommandContext } from "./bot-native-commands.fixture-test-support.js";
|
|
|
|
const { agentRuntimeMocks, commandAuthMocks, replyMocks, sessionMocks } = executorTestMocks;
|
|
|
|
describe("Telegram native command built-ins", () => {
|
|
beforeEach(resetSessionMetaMocks);
|
|
|
|
it("uses the target session model when building native argument menus", async () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
thinkingDefault: "low",
|
|
models: {
|
|
"anthropic/claude-opus-4-7": {
|
|
params: { thinking: "xhigh" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({
|
|
"agent:main:main": {
|
|
providerOverride: "anthropic",
|
|
modelOverride: "claude-opus-4-7",
|
|
modelOverrideSource: "user",
|
|
thinkingLevel: "high",
|
|
updatedAt: 0,
|
|
},
|
|
});
|
|
|
|
const { handler, sendMessage } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
|
([params]) => params.command.key === "think" && params.provider === "anthropic",
|
|
)?.[0];
|
|
expectRecordFields(
|
|
menuCall,
|
|
{ provider: "anthropic", model: "claude-opus-4-7" },
|
|
"thinking menu call",
|
|
);
|
|
expect(sessionMocks.getSessionEntry).toHaveBeenCalledWith({
|
|
storePath: "/tmp/openclaw-sessions.json",
|
|
sessionKey: "agent:main:main",
|
|
});
|
|
expectSendMessageCall({
|
|
sendMessage,
|
|
chatId: 100,
|
|
textIncludes: "Current thinking level: high.\nChoose level for /think.",
|
|
requireReplyMarkup: true,
|
|
label: "thinking menu",
|
|
});
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{ sessionRuntime: undefined, expectedRuntime: "codex" },
|
|
{ sessionRuntime: "openclaw", expectedRuntime: "openclaw" },
|
|
])(
|
|
"uses the effective $expectedRuntime runtime for native /think menus",
|
|
async ({ sessionRuntime, expectedRuntime }) => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"openai/gpt-5.6-luna": { agentRuntime: { id: "codex" } },
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({
|
|
"agent:main:main": {
|
|
providerOverride: "openai",
|
|
modelOverride: "gpt-5.6-luna",
|
|
modelOverrideSource: "user",
|
|
...(sessionRuntime ? { agentRuntimeOverride: sessionRuntime } : {}),
|
|
updatedAt: 0,
|
|
},
|
|
});
|
|
|
|
const { handler } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
|
([params]) => params.command.key === "think" && params.model === "gpt-5.6-luna",
|
|
)?.[0];
|
|
expectRecordFields(
|
|
menuCall,
|
|
{
|
|
provider: "openai",
|
|
model: "gpt-5.6-luna",
|
|
agentRuntime: expectedRuntime,
|
|
},
|
|
"runtime-aware thinking menu call",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("resolves /think menu choices against the runtime catalog for live-discovered models", async () => {
|
|
const cfg = {
|
|
agents: { defaults: { models: { "ollama/*": {} } } },
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({
|
|
"agent:main:main": {
|
|
providerOverride: "ollama",
|
|
modelOverride: "glm-5.2:cloud",
|
|
modelOverrideSource: "user",
|
|
updatedAt: 0,
|
|
},
|
|
});
|
|
const runtimeCatalog = [
|
|
{ provider: "ollama", id: "glm-5.2:cloud", name: "glm-5.2:cloud", reasoning: true },
|
|
];
|
|
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue(runtimeCatalog);
|
|
|
|
const { handler } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
|
([params]) => params.command.key === "think" && params.provider === "ollama",
|
|
)?.[0];
|
|
const menuRecord = expectRecordFields(
|
|
menuCall,
|
|
{ provider: "ollama", model: "glm-5.2:cloud" },
|
|
"ollama thinking menu call",
|
|
);
|
|
expect(agentRuntimeMocks.loadModelCatalog).toHaveBeenCalled();
|
|
expect(menuRecord.catalog).toEqual(runtimeCatalog);
|
|
});
|
|
|
|
it("loads the runtime catalog for /think when no session model override is set", async () => {
|
|
const cfg = {
|
|
agents: { defaults: { model: "ollama/glm-5.2:cloud", models: { "ollama/*": {} } } },
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({});
|
|
const runtimeCatalog = [
|
|
{ provider: "ollama", id: "glm-5.2:cloud", name: "glm-5.2:cloud", reasoning: true },
|
|
];
|
|
agentRuntimeMocks.loadModelCatalog.mockClear().mockResolvedValue(runtimeCatalog);
|
|
|
|
const { handler } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
expect(agentRuntimeMocks.loadModelCatalog).toHaveBeenCalled();
|
|
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
|
([params]) => params.command.key === "think",
|
|
)?.[0];
|
|
const menuRecord = expectRecordFields(menuCall, {}, "default-model thinking menu call");
|
|
expect(menuRecord.provider).toBeUndefined();
|
|
expect(menuRecord.catalog).toEqual(runtimeCatalog);
|
|
});
|
|
|
|
it("inherits the parent session model when building DM thread native argument menus", async () => {
|
|
const cfg: OpenClawConfig = {};
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({
|
|
"agent:main:main": {
|
|
providerOverride: "anthropic",
|
|
modelOverride: "claude-opus-4-7",
|
|
modelOverrideSource: "user",
|
|
updatedAt: 0,
|
|
},
|
|
});
|
|
|
|
const { handler, sendMessage } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext({ threadId: 77 }));
|
|
|
|
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
|
([params]) => params.command.key === "think" && params.provider === "anthropic",
|
|
)?.[0];
|
|
expectRecordFields(
|
|
menuCall,
|
|
{ provider: "anthropic", model: "claude-opus-4-7" },
|
|
"thread thinking menu call",
|
|
);
|
|
expectSendMessageCall({
|
|
sendMessage,
|
|
chatId: 100,
|
|
textIncludes: "Choose level for /think.",
|
|
requireReplyMarkup: true,
|
|
label: "thread thinking menu",
|
|
});
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses the configured default model instead of temporary auto fallback overrides", async () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "openai/gpt-5.5" },
|
|
thinkingDefault: "medium",
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({
|
|
"agent:main:main": {
|
|
providerOverride: "anthropic",
|
|
modelOverride: "claude-opus-4-7",
|
|
modelOverrideSource: "auto",
|
|
modelProvider: "anthropic",
|
|
model: "claude-opus-4-7",
|
|
updatedAt: 0,
|
|
},
|
|
});
|
|
|
|
const { handler, sendMessage } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
|
([params]) => params.command.key === "think" && params.provider === "openai",
|
|
)?.[0];
|
|
expectRecordFields(
|
|
menuCall,
|
|
{ provider: "openai", model: "gpt-5.5" },
|
|
"default model thinking menu call",
|
|
);
|
|
expectSendMessageCall({
|
|
sendMessage,
|
|
chatId: 100,
|
|
textIncludes: "Current thinking level: medium.\nChoose level for /think.",
|
|
requireReplyMarkup: true,
|
|
label: "default model thinking menu",
|
|
});
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses configured model defaults instead of runtime auth metadata for the fast menu", async () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "openai/gpt-5.5" },
|
|
models: {
|
|
"openai/gpt-5.5": {
|
|
params: { fastMode: "auto", fastAutoOnSeconds: 30 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({
|
|
"agent:main:main": {
|
|
modelProvider: "openai-codex",
|
|
model: "gpt-5.5",
|
|
updatedAt: 0,
|
|
},
|
|
});
|
|
|
|
const { handler, sendMessage } = registerAndResolveCommandHandler({
|
|
commandName: "fast",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
const menuCall = commandAuthMocks.resolveCommandArgMenu.mock.calls.find(
|
|
([params]) => params.command.key === "fast",
|
|
)?.[0];
|
|
expectRecordFields(menuCall, { cfg }, "fast menu call");
|
|
expect(
|
|
commandAuthMocks.resolveCommandArgMenu.mock.calls.some(
|
|
([params]) =>
|
|
params.command.key === "fast" &&
|
|
params.provider === "openai" &&
|
|
params.model === "gpt-5.5",
|
|
),
|
|
).toBe(true);
|
|
const options = expectSendMessageCall({
|
|
sendMessage,
|
|
chatId: 100,
|
|
textIncludes:
|
|
"Current fast mode: auto (30 sec) (default: model).\nOptions: on, off, auto (30 sec), default, status.",
|
|
requireReplyMarkup: true,
|
|
label: "fast menu",
|
|
});
|
|
const replyMarkup = options.reply_markup as
|
|
| { inline_keyboard?: Array<Array<{ text?: string }>> }
|
|
| undefined;
|
|
const labels = (replyMarkup?.inline_keyboard ?? []).flatMap((row) =>
|
|
row.map((button) => button.text),
|
|
);
|
|
expect(labels).toContain("auto (30 sec)");
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses the read-only catalog for Claude CLI thinking menus", async () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
model: { primary: "anthropic/claude-opus-4-8" },
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({});
|
|
agentRuntimeMocks.loadModelCatalog.mockImplementation(async (params) => {
|
|
if (!params?.readOnly) {
|
|
throw new Error("native /think must not start full model discovery");
|
|
}
|
|
return [
|
|
{
|
|
provider: "anthropic",
|
|
id: "claude-opus-4-8",
|
|
name: "Claude Opus 4.8",
|
|
reasoning: true,
|
|
},
|
|
];
|
|
});
|
|
|
|
const { handler, sendMessage } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
expect(agentRuntimeMocks.loadModelCatalog).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
config: cfg,
|
|
agentDir: expect.any(String),
|
|
readOnly: true,
|
|
}),
|
|
);
|
|
expect(agentRuntimeMocks.loadModelCatalog.mock.calls[0]?.[0]).not.toHaveProperty(
|
|
"workspaceDir",
|
|
);
|
|
expectSendMessageCall({
|
|
sendMessage,
|
|
chatId: 100,
|
|
textIncludes: "Current thinking level: off.\nChoose level for /think.",
|
|
requireReplyMarkup: true,
|
|
label: "Claude CLI thinking menu",
|
|
});
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses target model thinking defaults before global thinking defaults", async () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
thinkingDefault: "low",
|
|
models: {
|
|
"anthropic/claude-opus-4-7": {
|
|
params: { thinking: "xhigh" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({
|
|
"agent:main:main": {
|
|
providerOverride: "anthropic",
|
|
modelOverride: "claude-opus-4-7",
|
|
modelOverrideSource: "user",
|
|
updatedAt: 0,
|
|
},
|
|
});
|
|
|
|
const { handler, sendMessage } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
expectSendMessageCall({
|
|
sendMessage,
|
|
chatId: 100,
|
|
textIncludes: "Current thinking level: xhigh.\nChoose level for /think.",
|
|
requireReplyMarkup: true,
|
|
label: "target model thinking menu",
|
|
});
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses per-agent thinking defaults before target model and global thinking defaults", async () => {
|
|
const cfg = {
|
|
agents: {
|
|
defaults: {
|
|
thinkingDefault: "low",
|
|
models: {
|
|
"anthropic/claude-opus-4-7": {
|
|
params: { thinking: "xhigh" },
|
|
},
|
|
},
|
|
},
|
|
list: [
|
|
{
|
|
id: "alpha",
|
|
model: { primary: "anthropic/claude-opus-4-7" },
|
|
thinkingDefault: "minimal",
|
|
},
|
|
],
|
|
},
|
|
} as OpenClawConfig;
|
|
sessionMocks.sessionStoreEntries.mockReturnValue({});
|
|
|
|
const { handler, sendMessage } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg,
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext());
|
|
|
|
expectSendMessageCall({
|
|
sendMessage,
|
|
chatId: 100,
|
|
textIncludes: "Current thinking level: minimal.\nChoose level for /think.",
|
|
requireReplyMarkup: true,
|
|
label: "agent thinking menu",
|
|
});
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not load the session store when a native argument menu is skipped", async () => {
|
|
const { handler } = registerAndResolveCommandHandler({
|
|
commandName: "think",
|
|
cfg: {},
|
|
allowFrom: ["*"],
|
|
});
|
|
await handler(createTelegramPrivateCommandContext({ match: "high" }));
|
|
|
|
expect(sessionMocks.sessionStoreEntries).not.toHaveBeenCalled();
|
|
expect(agentRuntimeMocks.loadModelCatalog).not.toHaveBeenCalled();
|
|
expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|