2026-02-15 10:53:45 -05:00
import path from "node:path" ;
2026-01-30 11:57:25 +05:30
import { beforeEach , describe , expect , it , vi } from "vitest" ;
2026-03-14 02:50:17 -07:00
import type { OpenClawConfig } from "../../../src/config/config.js" ;
import { STATE_DIR } from "../../../src/config/paths.js" ;
import { TELEGRAM_COMMAND_NAME_PATTERN } from "../../../src/config/telegram-custom-commands.js" ;
import type { TelegramAccountConfig } from "../../../src/config/types.js" ;
import type { RuntimeEnv } from "../../../src/runtime.js" ;
2026-01-30 11:57:25 +05:30
import { registerTelegramNativeCommands } from "./bot-native-commands.js" ;
const { listSkillCommandsForAgents } = vi . hoisted ( ( ) = > ( {
listSkillCommandsForAgents : vi.fn ( ( ) = > [ ] ) ,
} ) ) ;
2026-02-15 10:53:45 -05:00
const pluginCommandMocks = vi . hoisted ( ( ) = > ( {
getPluginCommandSpecs : vi.fn ( ( ) = > [ ] ) ,
matchPluginCommand : vi.fn ( ( ) = > null ) ,
executePluginCommand : vi.fn ( async ( ) = > ( { text : "ok" } ) ) ,
} ) ) ;
const deliveryMocks = vi . hoisted ( ( ) = > ( {
deliverReplies : vi.fn ( async ( ) = > ( { delivered : true } ) ) ,
} ) ) ;
2026-01-30 11:57:25 +05:30
2026-03-14 02:50:17 -07:00
vi . mock ( "../../../src/auto-reply/skill-commands.js" , async ( importOriginal ) = > {
const actual = await importOriginal < typeof import ( "../../../src/auto-reply/skill-commands.js" ) > ( ) ;
2026-02-16 14:52:15 +00:00
return {
. . . actual ,
listSkillCommandsForAgents ,
} ;
} ) ;
2026-03-14 02:50:17 -07:00
vi . mock ( "../../../src/plugins/commands.js" , ( ) = > ( {
2026-02-15 10:53:45 -05:00
getPluginCommandSpecs : pluginCommandMocks.getPluginCommandSpecs ,
matchPluginCommand : pluginCommandMocks.matchPluginCommand ,
executePluginCommand : pluginCommandMocks.executePluginCommand ,
} ) ) ;
vi . mock ( "./bot/delivery.js" , ( ) = > ( {
deliverReplies : deliveryMocks.deliverReplies ,
} ) ) ;
2026-01-30 11:57:25 +05:30
describe ( "registerTelegramNativeCommands" , ( ) = > {
2026-02-22 07:37:54 +00:00
type RegisteredCommand = {
command : string ;
description : string ;
} ;
async function waitForRegisteredCommands (
setMyCommands : ReturnType < typeof vi.fn > ,
) : Promise < RegisteredCommand [ ] > {
await vi . waitFor ( ( ) = > {
expect ( setMyCommands ) . toHaveBeenCalled ( ) ;
} ) ;
return setMyCommands . mock . calls [ 0 ] ? . [ 0 ] as RegisteredCommand [ ] ;
}
2026-01-30 11:57:25 +05:30
beforeEach ( ( ) = > {
2026-02-22 00:02:08 +00:00
listSkillCommandsForAgents . mockClear ( ) ;
listSkillCommandsForAgents . mockReturnValue ( [ ] ) ;
pluginCommandMocks . getPluginCommandSpecs . mockClear ( ) ;
2026-02-15 10:53:45 -05:00
pluginCommandMocks . getPluginCommandSpecs . mockReturnValue ( [ ] ) ;
2026-02-22 00:02:08 +00:00
pluginCommandMocks . matchPluginCommand . mockClear ( ) ;
2026-02-15 10:53:45 -05:00
pluginCommandMocks . matchPluginCommand . mockReturnValue ( null ) ;
2026-02-22 00:02:08 +00:00
pluginCommandMocks . executePluginCommand . mockClear ( ) ;
2026-02-15 10:53:45 -05:00
pluginCommandMocks . executePluginCommand . mockResolvedValue ( { text : "ok" } ) ;
2026-02-22 00:02:08 +00:00
deliveryMocks . deliverReplies . mockClear ( ) ;
2026-02-15 10:53:45 -05:00
deliveryMocks . deliverReplies . mockResolvedValue ( { delivered : true } ) ;
2026-01-30 11:57:25 +05:30
} ) ;
2026-02-22 07:48:35 +00:00
const buildParams = ( cfg : OpenClawConfig , accountId = "default" ) = >
2026-03-13 20:59:47 +00:00
( {
2026-02-22 07:48:35 +00:00
bot : {
api : {
setMyCommands : vi.fn ( ) . mockResolvedValue ( undefined ) ,
sendMessage : vi.fn ( ) . mockResolvedValue ( undefined ) ,
} ,
command : vi.fn ( ) ,
} as unknown as Parameters < typeof registerTelegramNativeCommands > [ 0 ] [ "bot" ] ,
cfg ,
runtime : { } as RuntimeEnv ,
accountId ,
telegramCfg : { } as TelegramAccountConfig ,
2026-03-13 20:59:47 +00:00
allowFrom : [ ] ,
groupAllowFrom : [ ] ,
replyToMode : "off" ,
textLimit : 4000 ,
useAccessGroups : false ,
nativeEnabled : true ,
nativeSkillsEnabled : true ,
nativeDisabledExplicit : false ,
resolveGroupPolicy : ( ) = >
( {
allowlistEnabled : false ,
allowed : true ,
} ) as ReturnType <
Parameters < typeof registerTelegramNativeCommands > [ 0 ] [ "resolveGroupPolicy" ]
> ,
resolveTelegramGroupConfig : ( ) = > ( {
groupConfig : undefined ,
topicConfig : undefined ,
} ) ,
shouldSkipUpdate : ( ) = > false ,
opts : { token : "token" } ,
} ) satisfies Parameters < typeof registerTelegramNativeCommands > [ 0 ] ;
2026-01-30 11:57:25 +05:30
it ( "scopes skill commands when account binding exists" , ( ) = > {
const cfg : OpenClawConfig = {
agents : {
list : [ { id : "main" , default : true } , { id : "butler" } ] ,
} ,
bindings : [
{
agentId : "butler" ,
match : { channel : "telegram" , accountId : "bot-a" } ,
} ,
] ,
} ;
registerTelegramNativeCommands ( buildParams ( cfg , "bot-a" ) ) ;
expect ( listSkillCommandsForAgents ) . toHaveBeenCalledWith ( {
cfg ,
agentIds : [ "butler" ] ,
} ) ;
} ) ;
2026-02-14 01:37:09 +08:00
it ( "scopes skill commands to default agent without a matching binding (#15599)" , ( ) = > {
2026-01-30 11:57:25 +05:30
const cfg : OpenClawConfig = {
agents : {
list : [ { id : "main" , default : true } , { id : "butler" } ] ,
} ,
} ;
registerTelegramNativeCommands ( buildParams ( cfg , "bot-a" ) ) ;
2026-02-14 01:37:09 +08:00
expect ( listSkillCommandsForAgents ) . toHaveBeenCalledWith ( {
cfg ,
agentIds : [ "main" ] ,
} ) ;
2026-01-30 11:57:25 +05:30
} ) ;
2026-02-09 22:25:13 +05:30
2026-03-03 20:29:46 -06:00
it ( "truncates Telegram command registration to 100 commands" , async ( ) = > {
2026-02-09 22:25:13 +05:30
const cfg : OpenClawConfig = {
commands : { native : false } ,
} ;
const customCommands = Array . from ( { length : 120 } , ( _ , index ) = > ( {
command : ` cmd_ ${ index } ` ,
description : ` Command ${ index } ` ,
} ) ) ;
const setMyCommands = vi . fn ( ) . mockResolvedValue ( undefined ) ;
const runtimeLog = vi . fn ( ) ;
registerTelegramNativeCommands ( {
. . . buildParams ( cfg ) ,
bot : {
api : {
setMyCommands ,
sendMessage : vi.fn ( ) . mockResolvedValue ( undefined ) ,
} ,
command : vi.fn ( ) ,
} as unknown as Parameters < typeof registerTelegramNativeCommands > [ 0 ] [ "bot" ] ,
2026-02-17 14:30:36 +09:00
runtime : { log : runtimeLog } as unknown as RuntimeEnv ,
2026-02-09 22:25:13 +05:30
telegramCfg : { customCommands } as TelegramAccountConfig ,
nativeEnabled : false ,
nativeSkillsEnabled : false ,
} ) ;
2026-03-03 20:29:46 -06:00
const registeredCommands = await waitForRegisteredCommands ( setMyCommands ) ;
2026-02-09 22:25:13 +05:30
expect ( registeredCommands ) . toHaveLength ( 100 ) ;
expect ( registeredCommands ) . toEqual ( customCommands . slice ( 0 , 100 ) ) ;
expect ( runtimeLog ) . toHaveBeenCalledWith (
2026-02-14 02:02:44 +01:00
"Telegram limits bots to 100 commands. 120 configured; registering first 100. Use channels.telegram.commands.native: false to disable, or reduce plugin/skill/custom commands." ,
2026-02-09 22:25:13 +05:30
) ;
} ) ;
2026-02-15 10:53:45 -05:00
2026-02-17 23:20:36 +05:30
it ( "normalizes hyphenated native command names for Telegram registration" , async ( ) = > {
const setMyCommands = vi . fn ( ) . mockResolvedValue ( undefined ) ;
const command = vi . fn ( ) ;
registerTelegramNativeCommands ( {
. . . buildParams ( { } ) ,
bot : {
api : {
setMyCommands ,
sendMessage : vi.fn ( ) . mockResolvedValue ( undefined ) ,
} ,
command ,
} as unknown as Parameters < typeof registerTelegramNativeCommands > [ 0 ] [ "bot" ] ,
} ) ;
2026-02-22 07:37:54 +00:00
const registeredCommands = await waitForRegisteredCommands ( setMyCommands ) ;
2026-02-17 23:20:36 +05:30
expect ( registeredCommands . some ( ( entry ) = > entry . command === "export_session" ) ) . toBe ( true ) ;
expect ( registeredCommands . some ( ( entry ) = > entry . command === "export-session" ) ) . toBe ( false ) ;
const registeredHandlers = command . mock . calls . map ( ( [ name ] ) = > name ) ;
expect ( registeredHandlers ) . toContain ( "export_session" ) ;
expect ( registeredHandlers ) . not . toContain ( "export-session" ) ;
} ) ;
2026-02-18 09:16:31 +05:30
it ( "registers only Telegram-safe command names across native, custom, and plugin sources" , async ( ) = > {
const setMyCommands = vi . fn ( ) . mockResolvedValue ( undefined ) ;
pluginCommandMocks . getPluginCommandSpecs . mockReturnValue ( [
{ name : "plugin-status" , description : "Plugin status" } ,
{ name : "plugin@bad" , description : "Bad plugin command" } ,
] as never ) ;
registerTelegramNativeCommands ( {
. . . buildParams ( { } ) ,
bot : {
api : {
setMyCommands ,
sendMessage : vi.fn ( ) . mockResolvedValue ( undefined ) ,
} ,
command : vi.fn ( ) ,
} as unknown as Parameters < typeof registerTelegramNativeCommands > [ 0 ] [ "bot" ] ,
telegramCfg : {
customCommands : [
{ command : "custom-backup" , description : "Custom backup" } ,
{ command : "custom!bad" , description : "Bad custom command" } ,
] ,
} as TelegramAccountConfig ,
} ) ;
2026-02-22 07:37:54 +00:00
const registeredCommands = await waitForRegisteredCommands ( setMyCommands ) ;
2026-02-18 09:16:31 +05:30
expect ( registeredCommands . length ) . toBeGreaterThan ( 0 ) ;
for ( const entry of registeredCommands ) {
expect ( entry . command . includes ( "-" ) ) . toBe ( false ) ;
expect ( TELEGRAM_COMMAND_NAME_PATTERN . test ( entry . command ) ) . toBe ( true ) ;
}
expect ( registeredCommands . some ( ( entry ) = > entry . command === "export_session" ) ) . toBe ( true ) ;
expect ( registeredCommands . some ( ( entry ) = > entry . command === "custom_backup" ) ) . toBe ( true ) ;
expect ( registeredCommands . some ( ( entry ) = > entry . command === "plugin_status" ) ) . toBe ( true ) ;
expect ( registeredCommands . some ( ( entry ) = > entry . command === "plugin-status" ) ) . toBe ( false ) ;
expect ( registeredCommands . some ( ( entry ) = > entry . command === "custom-bad" ) ) . toBe ( false ) ;
} ) ;
2026-02-15 10:53:45 -05:00
it ( "passes agent-scoped media roots for plugin command replies with media" , async ( ) = > {
const commandHandlers = new Map < string , ( ctx : unknown ) = > Promise < void > > ( ) ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( undefined ) ;
const cfg : OpenClawConfig = {
agents : {
list : [ { id : "main" , default : true } , { id : "work" } ] ,
} ,
bindings : [ { agentId : "work" , match : { channel : "telegram" , accountId : "default" } } ] ,
} ;
pluginCommandMocks . getPluginCommandSpecs . mockReturnValue ( [
{
name : "plug" ,
description : "Plugin command" ,
} ,
2026-02-17 14:30:36 +09:00
] as never ) ;
2026-02-15 10:53:45 -05:00
pluginCommandMocks . matchPluginCommand . mockReturnValue ( {
command : { key : "plug" , requireAuth : false } ,
args : undefined ,
2026-02-17 14:30:36 +09:00
} as never ) ;
2026-02-15 10:53:45 -05:00
pluginCommandMocks . executePluginCommand . mockResolvedValue ( {
text : "with media" ,
mediaUrl : "/tmp/workspace-work/render.png" ,
2026-02-17 14:30:36 +09:00
} as never ) ;
2026-02-15 10:53:45 -05:00
registerTelegramNativeCommands ( {
. . . buildParams ( cfg ) ,
bot : {
api : {
setMyCommands : vi.fn ( ) . mockResolvedValue ( undefined ) ,
sendMessage ,
} ,
command : vi.fn ( ( name : string , cb : ( ctx : unknown ) = > Promise < void > ) = > {
commandHandlers . set ( name , cb ) ;
} ) ,
} as unknown as Parameters < typeof registerTelegramNativeCommands > [ 0 ] [ "bot" ] ,
} ) ;
const handler = commandHandlers . get ( "plug" ) ;
expect ( handler ) . toBeTruthy ( ) ;
await handler ? . ( {
match : "" ,
message : {
message_id : 1 ,
date : Math.floor ( Date . now ( ) / 1000 ) ,
chat : { id : 123 , type : "private" } ,
from : { id : 456 , username : "alice" } ,
} ,
} ) ;
expect ( deliveryMocks . deliverReplies ) . toHaveBeenCalledWith (
expect . objectContaining ( {
mediaLocalRoots : expect.arrayContaining ( [ path . join ( STATE_DIR , "workspace-work" ) ] ) ,
} ) ,
) ;
expect ( sendMessage ) . not . toHaveBeenCalledWith ( 123 , "Command not found." ) ;
} ) ;
2026-01-30 11:57:25 +05:30
} ) ;