11import path from "node:path" ;
22import { beforeEach , describe , expect , it , vi } from "vitest" ;
3+ import type { ChannelMessagingAdapter } from "../channels/plugins/types.js" ;
34import type { OpenClawConfig } from "../config/config.js" ;
5+ import { createTestRegistry } from "../test-utils/channel-plugins.js" ;
46
57const callGatewayMock = vi . fn ( ) ;
68vi . mock ( "../gateway/call.js" , ( ) => ( {
@@ -28,6 +30,7 @@ vi.mock("../config/config.js", async () => {
2830} ) ;
2931
3032import "./test-helpers/fast-openclaw-tools-sessions.js" ;
33+ import { setActivePluginRegistry } from "../plugins/runtime.js" ;
3134import { __testing as agentStepTesting } from "./tools/agent-step.js" ;
3235import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js" ;
3336import { createSessionsListTool } from "./tools/sessions-list-tool.js" ;
@@ -47,6 +50,71 @@ const TEST_CONFIG = {
4750 } ,
4851} as OpenClawConfig ;
4952
53+ const resolveSessionConversationStub : NonNullable <
54+ ChannelMessagingAdapter [ "resolveSessionConversation" ]
55+ > = ( { rawId } ) => ( {
56+ id : rawId ,
57+ } ) ;
58+ const resolveSessionTargetStub : NonNullable < ChannelMessagingAdapter [ "resolveSessionTarget" ] > = ( {
59+ kind,
60+ id,
61+ threadId,
62+ } ) => ( threadId ? `${ kind } :${ id } :thread:${ threadId } ` : `${ kind } :${ id } ` ) ;
63+
64+ function installMessagingTestRegistry ( ) {
65+ setActivePluginRegistry (
66+ createTestRegistry ( [
67+ {
68+ pluginId : "discord" ,
69+ source : "test" ,
70+ plugin : {
71+ id : "discord" ,
72+ meta : {
73+ id : "discord" ,
74+ label : "Discord" ,
75+ selectionLabel : "Discord" ,
76+ docsPath : "/channels/discord" ,
77+ blurb : "Discord test stub." ,
78+ } ,
79+ capabilities : { chatTypes : [ "direct" , "channel" , "thread" ] } ,
80+ messaging : {
81+ resolveSessionConversation : resolveSessionConversationStub ,
82+ resolveSessionTarget : resolveSessionTargetStub ,
83+ } ,
84+ config : {
85+ listAccountIds : ( ) => [ "default" ] ,
86+ resolveAccount : ( ) => ( { } ) ,
87+ } ,
88+ } ,
89+ } ,
90+ {
91+ pluginId : "whatsapp" ,
92+ source : "test" ,
93+ plugin : {
94+ id : "whatsapp" ,
95+ meta : {
96+ id : "whatsapp" ,
97+ label : "WhatsApp" ,
98+ selectionLabel : "WhatsApp" ,
99+ docsPath : "/channels/whatsapp" ,
100+ blurb : "WhatsApp test stub." ,
101+ preferSessionLookupForAnnounceTarget : true ,
102+ } ,
103+ capabilities : { chatTypes : [ "direct" , "group" ] } ,
104+ messaging : {
105+ resolveSessionConversation : resolveSessionConversationStub ,
106+ resolveSessionTarget : resolveSessionTargetStub ,
107+ } ,
108+ config : {
109+ listAccountIds : ( ) => [ "default" ] ,
110+ resolveAccount : ( ) => ( { } ) ,
111+ } ,
112+ } ,
113+ } ,
114+ ] ) ,
115+ ) ;
116+ }
117+
50118function createOpenClawTools ( options ?: {
51119 agentSessionKey ?: string ;
52120 agentChannel ?: string ;
@@ -90,6 +158,7 @@ const waitForCalls = async (getCount: () => number, count: number, timeoutMs = 2
90158describe ( "sessions tools" , ( ) => {
91159 beforeEach ( ( ) => {
92160 callGatewayMock . mockClear ( ) ;
161+ installMessagingTestRegistry ( ) ;
93162 agentStepTesting . setDepsForTest ( {
94163 callGateway : ( opts : unknown ) => callGatewayMock ( opts ) ,
95164 } ) ;
@@ -894,4 +963,133 @@ describe("sessions tools", () => {
894963 message : "announce now" ,
895964 } ) ;
896965 } ) ;
966+
967+ it ( "sessions_send preserves threadId when announce target is hydrated via sessions.list" , async ( ) => {
968+ const calls : Array < { method ?: string ; params ?: unknown } > = [ ] ;
969+ let agentCallCount = 0 ;
970+ let lastWaitedRunId : string | undefined ;
971+ const replyByRunId = new Map < string , string > ( ) ;
972+ const requesterKey = "discord:group:req" ;
973+ const targetKey = "agent:main:worker" ;
974+ let sendParams : {
975+ to ?: string ;
976+ channel ?: string ;
977+ accountId ?: string ;
978+ message ?: string ;
979+ threadId ?: string ;
980+ } = { } ;
981+
982+ callGatewayMock . mockImplementation ( async ( opts : unknown ) => {
983+ const request = opts as { method ?: string ; params ?: unknown } ;
984+ calls . push ( request ) ;
985+ if ( request . method === "agent" ) {
986+ agentCallCount += 1 ;
987+ const runId = `run-${ agentCallCount } ` ;
988+ const params = request . params as
989+ | {
990+ sessionKey ?: string ;
991+ extraSystemPrompt ?: string ;
992+ }
993+ | undefined ;
994+ let reply = "initial" ;
995+ if ( params ?. extraSystemPrompt ?. includes ( "Agent-to-agent reply step" ) ) {
996+ reply = params . sessionKey === requesterKey ? "pong-1" : "pong-2" ;
997+ }
998+ if ( params ?. extraSystemPrompt ?. includes ( "Agent-to-agent announce step" ) ) {
999+ reply = "announce now" ;
1000+ }
1001+ replyByRunId . set ( runId , reply ) ;
1002+ return {
1003+ runId,
1004+ status : "accepted" ,
1005+ acceptedAt : 3000 + agentCallCount ,
1006+ } ;
1007+ }
1008+ if ( request . method === "agent.wait" ) {
1009+ const params = request . params as { runId ?: string } | undefined ;
1010+ lastWaitedRunId = params ?. runId ;
1011+ return { runId : params ?. runId ?? "run-1" , status : "ok" } ;
1012+ }
1013+ if ( request . method === "chat.history" ) {
1014+ const text = ( lastWaitedRunId && replyByRunId . get ( lastWaitedRunId ) ) ?? "" ;
1015+ return {
1016+ messages : [
1017+ {
1018+ role : "assistant" ,
1019+ content : [ { type : "text" , text } ] ,
1020+ timestamp : 20 ,
1021+ } ,
1022+ ] ,
1023+ } ;
1024+ }
1025+ if ( request . method === "sessions.list" ) {
1026+ return {
1027+ sessions : [
1028+ {
1029+ key : targetKey ,
1030+ deliveryContext : {
1031+ channel : "whatsapp" ,
1032+ to : "123@g.us" ,
1033+ accountId : "work" ,
1034+ threadId : 99 ,
1035+ } ,
1036+ } ,
1037+ ] ,
1038+ } ;
1039+ }
1040+ if ( request . method === "send" ) {
1041+ const params = request . params as
1042+ | {
1043+ to ?: string ;
1044+ channel ?: string ;
1045+ accountId ?: string ;
1046+ message ?: string ;
1047+ threadId ?: string ;
1048+ }
1049+ | undefined ;
1050+ sendParams = {
1051+ to : params ?. to ,
1052+ channel : params ?. channel ,
1053+ accountId : params ?. accountId ,
1054+ message : params ?. message ,
1055+ threadId : params ?. threadId ,
1056+ } ;
1057+ return { messageId : "m-threaded-announce" } ;
1058+ }
1059+ return { } ;
1060+ } ) ;
1061+
1062+ const tool = createOpenClawTools ( {
1063+ agentSessionKey : requesterKey ,
1064+ agentChannel : "discord" ,
1065+ } ) . find ( ( candidate ) => candidate . name === "sessions_send" ) ;
1066+ expect ( tool ) . toBeDefined ( ) ;
1067+ if ( ! tool ) {
1068+ throw new Error ( "missing sessions_send tool" ) ;
1069+ }
1070+
1071+ const waited = await tool . execute ( "call-thread" , {
1072+ sessionKey : targetKey ,
1073+ message : "ping" ,
1074+ timeoutSeconds : 1 ,
1075+ } ) ;
1076+ expect ( waited . details ) . toMatchObject ( {
1077+ status : "ok" ,
1078+ reply : "initial" ,
1079+ } ) ;
1080+ await vi . waitFor (
1081+ ( ) => {
1082+ expect ( calls . filter ( ( call ) => call . method === "send" ) ) . toHaveLength ( 1 ) ;
1083+ } ,
1084+ { timeout : 2_000 , interval : 5 } ,
1085+ ) ;
1086+
1087+ expect ( sendParams ) . toMatchObject ( {
1088+ to : "123@g.us" ,
1089+ channel : "whatsapp" ,
1090+ accountId : "work" ,
1091+ message : "announce now" ,
1092+ threadId : "99" ,
1093+ } ) ;
1094+ } ) ;
8971095} ) ;
0 commit comments