@@ -192,6 +192,67 @@ function createUnboundRouteState(params: {
192192 > ;
193193}
194194
195+ type MockCalls = {
196+ mock : { calls : unknown [ ] [ ] } ;
197+ } ;
198+
199+ function isRecord ( value : unknown ) : value is Record < string , unknown > {
200+ return typeof value === "object" && value !== null ;
201+ }
202+
203+ function requireRecord ( value : unknown , label : string ) : Record < string , unknown > {
204+ expect ( isRecord ( value ) , `${ label } should be an object` ) . toBe ( true ) ;
205+ if ( ! isRecord ( value ) ) {
206+ throw new Error ( `${ label } should be an object` ) ;
207+ }
208+ return value ;
209+ }
210+
211+ function expectFields ( record : Record < string , unknown > , expected : Record < string , unknown > ) {
212+ for ( const [ key , value ] of Object . entries ( expected ) ) {
213+ expect ( record [ key ] , key ) . toEqual ( value ) ;
214+ }
215+ }
216+
217+ function expectSingleCallFirstArg (
218+ mock : MockCalls ,
219+ expected : Record < string , unknown > ,
220+ label = "mock first argument" ,
221+ ) : Record < string , unknown > {
222+ expect ( mock . mock . calls ) . toHaveLength ( 1 ) ;
223+ const [ firstArg ] = mock . mock . calls [ 0 ] ?? [ ] ;
224+ const record = requireRecord ( firstArg , label ) ;
225+ expectFields ( record , expected ) ;
226+ return record ;
227+ }
228+
229+ function expectPluginCommandExecution ( params : {
230+ mock : MockCalls ;
231+ commandName : string ;
232+ expected : Record < string , unknown > ;
233+ } ) {
234+ const payload = expectSingleCallFirstArg ( params . mock , params . expected , "plugin command payload" ) ;
235+ expect ( requireRecord ( payload . command , "plugin command" ) . name ) . toBe ( params . commandName ) ;
236+ return payload ;
237+ }
238+
239+ function expectFollowUpFields (
240+ interaction : MockCommandInteraction ,
241+ expected : Record < string , unknown > ,
242+ ) {
243+ return expectSingleCallFirstArg (
244+ interaction . followUp as unknown as MockCalls ,
245+ expected ,
246+ "followUp" ,
247+ ) ;
248+ }
249+
250+ function expectNoFollowUpContent ( interaction : MockCommandInteraction , content : string ) {
251+ const calls = ( interaction . followUp as unknown as MockCalls ) . mock . calls ;
252+ const matched = calls . some ( ( [ payload ] ) => isRecord ( payload ) && payload . content === content ) ;
253+ expect ( matched ) . toBe ( false ) ;
254+ }
255+
195256async function createPluginCommand ( params : { cfg : OpenClawConfig ; name : string } ) {
196257 return createDiscordNativeCommand ( {
197258 command : {
@@ -269,15 +330,12 @@ async function expectPairCommandReply(params: {
269330 ) ;
270331
271332 expect ( dispatchSpy ) . not . toHaveBeenCalled ( ) ;
272- expect ( executeSpy ) . toHaveBeenCalledWith (
273- expect . objectContaining ( {
274- command : expect . objectContaining ( { name : params . expectedRegisteredName ?? "pair" } ) ,
275- args : "now" ,
276- } ) ,
277- ) ;
278- expect ( params . interaction . followUp ) . toHaveBeenCalledWith (
279- expect . objectContaining ( { content : "paired:now" } ) ,
280- ) ;
333+ expectPluginCommandExecution ( {
334+ mock : executeSpy ,
335+ commandName : params . expectedRegisteredName ?? "pair" ,
336+ expected : { args : "now" } ,
337+ } ) ;
338+ expectFollowUpFields ( params . interaction , { content : "paired:now" } ) ;
281339 expect ( params . interaction . reply ) . not . toHaveBeenCalled ( ) ;
282340}
283341
@@ -421,11 +479,9 @@ describe("Discord native plugin command dispatch", () => {
421479 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
422480
423481 expect ( handler ) . not . toHaveBeenCalled ( ) ;
424- expect ( interaction . followUp ) . toHaveBeenCalledWith (
425- expect . objectContaining ( {
426- content : "⚠️ This command requires gateway scope: operator.pairing." ,
427- } ) ,
428- ) ;
482+ expectFollowUpFields ( interaction , {
483+ content : "⚠️ This command requires gateway scope: operator.pairing." ,
484+ } ) ;
429485 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
430486 } ) ;
431487
@@ -448,9 +504,7 @@ describe("Discord native plugin command dispatch", () => {
448504 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
449505
450506 expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
451- expect ( interaction . followUp ) . toHaveBeenCalledWith (
452- expect . objectContaining ( { content : "paired:now" } ) ,
453- ) ;
507+ expectFollowUpFields ( interaction , { content : "paired:now" } ) ;
454508 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
455509 } ) ;
456510
@@ -464,11 +518,9 @@ describe("Discord native plugin command dispatch", () => {
464518 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
465519
466520 expect ( handler ) . not . toHaveBeenCalled ( ) ;
467- expect ( interaction . followUp ) . toHaveBeenCalledWith (
468- expect . objectContaining ( {
469- content : "⚠️ This command requires gateway scope: operator.pairing." ,
470- } ) ,
471- ) ;
521+ expectFollowUpFields ( interaction , {
522+ content : "⚠️ This command requires gateway scope: operator.pairing." ,
523+ } ) ;
472524 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
473525 } ) ;
474526
@@ -529,12 +581,10 @@ describe("Discord native plugin command dispatch", () => {
529581
530582 expect ( executeSpy ) . not . toHaveBeenCalled ( ) ;
531583 expect ( dispatchSpy ) . not . toHaveBeenCalled ( ) ;
532- expect ( interaction . followUp ) . toHaveBeenCalledWith (
533- expect . objectContaining ( {
534- content : "You are not authorized to use this command." ,
535- ephemeral : true ,
536- } ) ,
537- ) ;
584+ expectFollowUpFields ( interaction , {
585+ content : "You are not authorized to use this command." ,
586+ ephemeral : true ,
587+ } ) ;
538588 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
539589 } ) ;
540590
@@ -589,15 +639,12 @@ describe("Discord native plugin command dispatch", () => {
589639
590640 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
591641
592- expect ( executeSpy ) . toHaveBeenCalledWith (
593- expect . objectContaining ( {
594- command : expect . objectContaining ( { name : "pair" } ) ,
595- args : "now" ,
596- } ) ,
597- ) ;
598- expect ( interaction . followUp ) . toHaveBeenCalledWith (
599- expect . objectContaining ( { content : "open:now" } ) ,
600- ) ;
642+ expectPluginCommandExecution ( {
643+ mock : executeSpy ,
644+ commandName : "pair" ,
645+ expected : { args : "now" } ,
646+ } ) ;
647+ expectFollowUpFields ( interaction , { content : "open:now" } ) ;
601648 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
602649 } ) ;
603650
@@ -652,15 +699,12 @@ describe("Discord native plugin command dispatch", () => {
652699
653700 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
654701
655- expect ( executeSpy ) . toHaveBeenCalledWith (
656- expect . objectContaining ( {
657- command : expect . objectContaining ( { name : "pair" } ) ,
658- args : "now" ,
659- } ) ,
660- ) ;
661- expect ( interaction . followUp ) . toHaveBeenCalledWith (
662- expect . objectContaining ( { content : "open:now" } ) ,
663- ) ;
702+ expectPluginCommandExecution ( {
703+ mock : executeSpy ,
704+ commandName : "pair" ,
705+ expected : { args : "now" } ,
706+ } ) ;
707+ expectFollowUpFields ( interaction , { content : "open:now" } ) ;
664708 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
665709 } ) ;
666710
@@ -692,11 +736,9 @@ describe("Discord native plugin command dispatch", () => {
692736 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
693737
694738 expect ( dispatchSpy ) . not . toHaveBeenCalled ( ) ;
695- expect ( interaction . followUp ) . toHaveBeenCalledWith (
696- expect . objectContaining ( {
697- content : "This group DM is not allowed." ,
698- } ) ,
699- ) ;
739+ expectFollowUpFields ( interaction , {
740+ content : "This group DM is not allowed." ,
741+ } ) ;
700742 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
701743 } ) ;
702744
@@ -732,9 +774,7 @@ describe("Discord native plugin command dispatch", () => {
732774
733775 expect ( executeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
734776 expect ( dispatchSpy ) . not . toHaveBeenCalled ( ) ;
735- expect ( interaction . followUp ) . toHaveBeenCalledWith (
736- expect . objectContaining ( { content : "direct plugin output" } ) ,
737- ) ;
777+ expectFollowUpFields ( interaction , { content : "direct plugin output" } ) ;
738778 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
739779 } ) ;
740780
@@ -754,12 +794,10 @@ describe("Discord native plugin command dispatch", () => {
754794
755795 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
756796
757- expect ( interaction . followUp ) . toHaveBeenCalledWith (
758- expect . objectContaining ( {
759- content : "⚠️ Command produced no visible reply." ,
760- ephemeral : true ,
761- } ) ,
762- ) ;
797+ expectFollowUpFields ( interaction , {
798+ content : "⚠️ Command produced no visible reply." ,
799+ ephemeral : true ,
800+ } ) ;
763801 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
764802 } ) ;
765803
@@ -779,9 +817,7 @@ describe("Discord native plugin command dispatch", () => {
779817
780818 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
781819
782- expect ( interaction . followUp ) . not . toHaveBeenCalledWith (
783- expect . objectContaining ( { content : "⚠️ Command produced no visible reply." } ) ,
784- ) ;
820+ expectNoFollowUpContent ( interaction , "⚠️ Command produced no visible reply." ) ;
785821 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
786822 } ) ;
787823
@@ -814,9 +850,7 @@ describe("Discord native plugin command dispatch", () => {
814850 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
815851
816852 expect ( dispatchSpy ) . not . toHaveBeenCalled ( ) ;
817- expect ( interaction . followUp ) . toHaveBeenCalledWith (
818- expect . objectContaining ( { content : "⚠️ Command produced no visible reply." } ) ,
819- ) ;
853+ expectFollowUpFields ( interaction , { content : "⚠️ Command produced no visible reply." } ) ;
820854 expect ( interaction . reply ) . not . toHaveBeenCalled ( ) ;
821855 } ) ;
822856
@@ -878,16 +912,14 @@ describe("Discord native plugin command dispatch", () => {
878912
879913 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
880914
881- expect ( executeSpy ) . toHaveBeenCalledWith (
882- expect . objectContaining ( {
883- channel : "discord" ,
884- from : "discord:channel:thread-123" ,
885- to : "slash:owner" ,
886- sessionKey : "agent:main:discord:channel:thread-123" ,
887- messageThreadId : "thread-123" ,
888- threadParentId : "parent-456" ,
889- } ) ,
890- ) ;
915+ expectSingleCallFirstArg ( executeSpy , {
916+ channel : "discord" ,
917+ from : "discord:channel:thread-123" ,
918+ to : "slash:owner" ,
919+ sessionKey : "agent:main:discord:channel:thread-123" ,
920+ messageThreadId : "thread-123" ,
921+ threadParentId : "parent-456" ,
922+ } ) ;
891923 } ) ;
892924
893925 it ( "preserves fetched thread parent metadata when interaction parentId getter throws" , async ( ) => {
@@ -963,14 +995,12 @@ describe("Discord native plugin command dispatch", () => {
963995
964996 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
965997
966- expect ( executeSpy ) . toHaveBeenCalledWith (
967- expect . objectContaining ( {
968- channel : "discord" ,
969- from : "discord:channel:partial-thread-123" ,
970- messageThreadId : "partial-thread-123" ,
971- threadParentId : "partial-parent-456" ,
972- } ) ,
973- ) ;
998+ expectSingleCallFirstArg ( executeSpy , {
999+ channel : "discord" ,
1000+ from : "discord:channel:partial-thread-123" ,
1001+ messageThreadId : "partial-thread-123" ,
1002+ threadParentId : "partial-parent-456" ,
1003+ } ) ;
9741004 } ) ;
9751005
9761006 it ( "routes native slash commands through configured ACP Discord channel bindings" , async ( ) => {
@@ -1120,11 +1150,9 @@ describe("Discord native plugin command dispatch", () => {
11201150
11211151 await ( command as { run : ( interaction : unknown ) => Promise < void > } ) . run ( interaction as unknown ) ;
11221152
1123- expect ( resolveRouteState ) . toHaveBeenCalledWith (
1124- expect . objectContaining ( {
1125- enforceConfiguredBindingReadiness : true ,
1126- } ) ,
1127- ) ;
1153+ expectSingleCallFirstArg ( resolveRouteState , {
1154+ enforceConfiguredBindingReadiness : true ,
1155+ } ) ;
11281156 expect ( dispatchSpy ) . toHaveBeenCalledTimes ( 1 ) ;
11291157 } ) ;
11301158
@@ -1161,10 +1189,12 @@ describe("Discord native plugin command dispatch", () => {
11611189 expect ( dispatchCall . ctx ?. CommandTargetSessionKey ) . toMatch (
11621190 / ^ a g e n t : c o d e x : a c p : b i n d i n g : d i s c o r d : d e f a u l t : / ,
11631191 ) ;
1164- expect ( interaction . reply ) . not . toHaveBeenCalledWith (
1165- expect . objectContaining ( {
1166- content : "Configured ACP binding is unavailable right now. Please try again." ,
1167- } ) ,
1192+ const replyCalls = ( interaction . reply as unknown as MockCalls ) . mock . calls ;
1193+ const blockedReply = replyCalls . some (
1194+ ( [ payload ] ) =>
1195+ isRecord ( payload ) &&
1196+ payload . content === "Configured ACP binding is unavailable right now. Please try again." ,
11681197 ) ;
1198+ expect ( blockedReply ) . toBe ( false ) ;
11691199 } ) ;
11701200} ) ;
0 commit comments