@@ -4,11 +4,33 @@ import type { OpenClawConfig } from "./runtime-api.js";
44
55const hoisted = vi . hoisted ( ( ) => ( {
66 handleWhatsAppAction : vi . fn ( async ( ) => ( { content : [ { type : "text" , text : '{"ok":true}' } ] } ) ) ,
7+ resolveAuthorizedWhatsAppOutboundTarget : vi . fn (
8+ ( {
9+ chatJid,
10+ accountId,
11+ } : {
12+ chatJid : string ;
13+ accountId ?: string ;
14+ } ) : { to : string ; accountId : string } => ( {
15+ to : chatJid ,
16+ accountId : accountId ?? "default" ,
17+ } ) ,
18+ ) ,
19+ resolveWhatsAppAccount : vi . fn ( ( ) => ( { accountId : "default" , mediaMaxMb : 50 } ) ) ,
20+ resolveWhatsAppMediaMaxBytes : vi . fn ( ( ) => 50 * 1024 * 1024 ) ,
21+ sendMessageWhatsApp : vi . fn ( async ( ) => ( {
22+ messageId : "msg-media-1" ,
23+ toJid : "1555@s.whatsapp.net" ,
24+ } ) ) ,
725} ) ) ;
826
927vi . mock ( "./channel-react-action.runtime.js" , async ( ) => {
1028 return {
1129 handleWhatsAppAction : hoisted . handleWhatsAppAction ,
30+ resolveAuthorizedWhatsAppOutboundTarget : hoisted . resolveAuthorizedWhatsAppOutboundTarget ,
31+ resolveWhatsAppAccount : hoisted . resolveWhatsAppAccount ,
32+ resolveWhatsAppMediaMaxBytes : hoisted . resolveWhatsAppMediaMaxBytes ,
33+ sendMessageWhatsApp : hoisted . sendMessageWhatsApp ,
1234 resolveReactionMessageId : ( {
1335 args,
1436 toolContext,
@@ -41,7 +63,7 @@ vi.mock("./channel-react-action.runtime.js", async () => {
4163 readStringParam : (
4264 params : Record < string , unknown > ,
4365 key : string ,
44- options ?: { required ?: boolean ; allowEmpty ?: boolean } ,
66+ options ?: { required ?: boolean ; allowEmpty ?: boolean ; trim ?: boolean } ,
4567 ) => {
4668 const value = params [ key ] ;
4769 if ( value == null ) {
@@ -73,6 +95,145 @@ describe("whatsapp react action messageId resolution", () => {
7395
7496 beforeEach ( ( ) => {
7597 hoisted . handleWhatsAppAction . mockClear ( ) ;
98+ hoisted . resolveAuthorizedWhatsAppOutboundTarget . mockClear ( ) ;
99+ hoisted . resolveWhatsAppAccount . mockClear ( ) ;
100+ hoisted . resolveWhatsAppMediaMaxBytes . mockClear ( ) ;
101+ hoisted . resolveWhatsAppAccount . mockReturnValue ( { accountId : "default" , mediaMaxMb : 50 } ) ;
102+ hoisted . resolveWhatsAppMediaMaxBytes . mockReturnValue ( 50 * 1024 * 1024 ) ;
103+ hoisted . sendMessageWhatsApp . mockClear ( ) ;
104+ } ) ;
105+
106+ it ( "sends upload-file through the WhatsApp media send path" , async ( ) => {
107+ const mediaReadFile = vi . fn ( async ( ) => Buffer . from ( "media" ) ) ;
108+
109+ const result = await handleWhatsAppReactAction ( {
110+ action : "upload-file" ,
111+ params : {
112+ to : "+1555" ,
113+ filePath : "/tmp/pic.png" ,
114+ caption : "picture caption" ,
115+ forceDocument : "true" ,
116+ gifPlayback : true ,
117+ asVoice : "true" ,
118+ } ,
119+ cfg : baseCfg ,
120+ accountId : "default" ,
121+ mediaLocalRoots : [ "/tmp" ] ,
122+ mediaReadFile,
123+ } ) ;
124+
125+ expect ( hoisted . resolveAuthorizedWhatsAppOutboundTarget ) . toHaveBeenCalledWith ( {
126+ cfg : baseCfg ,
127+ chatJid : "+1555" ,
128+ accountId : "default" ,
129+ actionLabel : "upload-file" ,
130+ } ) ;
131+ expect ( hoisted . sendMessageWhatsApp ) . toHaveBeenCalledWith ( "+1555" , "picture caption" , {
132+ verbose : false ,
133+ cfg : baseCfg ,
134+ mediaUrl : "/tmp/pic.png" ,
135+ mediaAccess : undefined ,
136+ mediaLocalRoots : [ "/tmp" ] ,
137+ mediaReadFile,
138+ gifPlayback : true ,
139+ audioAsVoice : true ,
140+ forceDocument : true ,
141+ accountId : "default" ,
142+ } ) ;
143+ expect ( result . details ) . toMatchObject ( {
144+ ok : true ,
145+ channel : "whatsapp" ,
146+ action : "upload-file" ,
147+ messageId : "msg-media-1" ,
148+ toJid : "1555@s.whatsapp.net" ,
149+ } ) ;
150+ } ) ;
151+
152+ it ( "does not send upload-file when target authorization fails" , async ( ) => {
153+ hoisted . resolveAuthorizedWhatsAppOutboundTarget . mockImplementationOnce ( ( ) => {
154+ throw new Error ( "WhatsApp upload-file blocked" ) ;
155+ } ) ;
156+
157+ await expect (
158+ handleWhatsAppReactAction ( {
159+ action : "upload-file" ,
160+ params : {
161+ to : "+1555" ,
162+ filePath : "/tmp/pic.png" ,
163+ } ,
164+ cfg : baseCfg ,
165+ accountId : "default" ,
166+ } ) ,
167+ ) . rejects . toThrow ( "WhatsApp upload-file blocked" ) ;
168+ expect ( hoisted . sendMessageWhatsApp ) . not . toHaveBeenCalled ( ) ;
169+ } ) ;
170+
171+ it ( "sends upload-file from the hydrated buffer payload" , async ( ) => {
172+ await handleWhatsAppReactAction ( {
173+ action : "upload-file" ,
174+ params : {
175+ to : "+1555" ,
176+ buffer : Buffer . from ( "hello" ) . toString ( "base64" ) ,
177+ contentType : "text/plain" ,
178+ filename : "hello.txt" ,
179+ filePath : "/tmp/hello.txt" ,
180+ forceDocument : true ,
181+ message : "file caption" ,
182+ } ,
183+ cfg : baseCfg ,
184+ accountId : "default" ,
185+ } ) ;
186+
187+ expect ( hoisted . sendMessageWhatsApp ) . toHaveBeenCalledWith ( "+1555" , "file caption" , {
188+ verbose : false ,
189+ cfg : baseCfg ,
190+ mediaPayload : {
191+ buffer : Buffer . from ( "hello" ) ,
192+ contentType : "text/plain" ,
193+ fileName : "hello.txt" ,
194+ } ,
195+ mediaAccess : undefined ,
196+ mediaLocalRoots : undefined ,
197+ mediaReadFile : undefined ,
198+ gifPlayback : undefined ,
199+ audioAsVoice : undefined ,
200+ forceDocument : true ,
201+ accountId : "default" ,
202+ } ) ;
203+ } ) ;
204+
205+ it ( "rejects upload-file buffers above the WhatsApp media limit" , async ( ) => {
206+ hoisted . resolveWhatsAppMediaMaxBytes . mockReturnValueOnce ( 4 ) ;
207+
208+ await expect (
209+ handleWhatsAppReactAction ( {
210+ action : "upload-file" ,
211+ params : {
212+ to : "+1555" ,
213+ buffer : Buffer . from ( "hello" ) . toString ( "base64" ) ,
214+ contentType : "text/plain" ,
215+ filename : "hello.txt" ,
216+ } ,
217+ cfg : baseCfg ,
218+ accountId : "default" ,
219+ } ) ,
220+ ) . rejects . toThrow ( "WhatsApp upload-file buffer exceeds configured media limit" ) ;
221+ expect ( hoisted . sendMessageWhatsApp ) . not . toHaveBeenCalled ( ) ;
222+ } ) ;
223+
224+ it ( "requires upload-file media path input" , async ( ) => {
225+ await expect (
226+ handleWhatsAppReactAction ( {
227+ action : "upload-file" ,
228+ params : {
229+ to : "+1555" ,
230+ caption : "missing media" ,
231+ } ,
232+ cfg : baseCfg ,
233+ accountId : "default" ,
234+ } ) ,
235+ ) . rejects . toThrow ( "WhatsApp upload-file requires media" ) ;
236+ expect ( hoisted . sendMessageWhatsApp ) . not . toHaveBeenCalled ( ) ;
76237 } ) ;
77238
78239 it ( "uses explicit messageId when provided" , async ( ) => {
0 commit comments