@@ -445,25 +445,87 @@ function getReactionEmojis(): string[] {
445445 ) . map ( ( call ) => call [ 2 ] ) ;
446446}
447447
448- function expectAckReactionRuntimeOptions ( params ?: {
449- accountId ?: string ;
450- ackReaction ?: string ;
451- removeAckAfterReply ?: boolean ;
452- } ) {
448+ function requireRecord ( value : unknown , label : string ) : Record < string , unknown > {
449+ expect ( typeof value ) . toBe ( "object" ) ;
450+ expect ( value ) . not . toBeNull ( ) ;
451+ if ( typeof value !== "object" || value === null ) {
452+ throw new Error ( `${ label } was not an object` ) ;
453+ }
454+ return value as Record < string , unknown > ;
455+ }
456+
457+ function expectRecordFields ( record : Record < string , unknown > , fields : Record < string , unknown > ) {
458+ for ( const [ key , value ] of Object . entries ( fields ) ) {
459+ expect ( record [ key ] ) . toEqual ( value ) ;
460+ }
461+ }
462+
463+ function expectAckReactionRuntimeOptions (
464+ options : unknown ,
465+ params ?: {
466+ accountId ?: string ;
467+ ackReaction ?: string ;
468+ removeAckAfterReply ?: boolean ;
469+ } ,
470+ ) {
471+ const optionRecord = requireRecord ( options , "reaction runtime options" ) ;
472+ requireRecord ( optionRecord . rest , "reaction REST client" ) ;
473+ if ( params ?. accountId ) {
474+ expect ( optionRecord . accountId ) . toBe ( params . accountId ) ;
475+ }
453476 const messages : Record < string , unknown > = { } ;
454477 if ( params ?. ackReaction ) {
455478 messages . ackReaction = params . ackReaction ;
456479 }
457480 if ( params ?. removeAckAfterReply !== undefined ) {
458481 messages . removeAckAfterReply = params . removeAckAfterReply ;
459482 }
460- return expect . objectContaining ( {
461- rest : expect . anything ( ) ,
462- ...( Object . keys ( messages ) . length > 0
463- ? { cfg : expect . objectContaining ( { messages : expect . objectContaining ( messages ) } ) }
464- : { } ) ,
465- ...( params ?. accountId ? { accountId : params . accountId } : { } ) ,
466- } ) ;
483+ if ( Object . keys ( messages ) . length > 0 ) {
484+ const cfg = requireRecord ( optionRecord . cfg , "reaction config" ) ;
485+ expectRecordFields ( requireRecord ( cfg . messages , "reaction message config" ) , messages ) ;
486+ }
487+ }
488+
489+ function requireReactionCall (
490+ mock : typeof sendMocks . reactMessageDiscord | typeof sendMocks . removeReactionDiscord ,
491+ index : number ,
492+ ) {
493+ const call = mock . mock . calls [ index ] as unknown [ ] | undefined ;
494+ expect ( call ) . toBeDefined ( ) ;
495+ if ( ! call ) {
496+ throw new Error ( `missing reaction call ${ index + 1 } ` ) ;
497+ }
498+ return call ;
499+ }
500+
501+ function expectReactionCallAt (
502+ mock : typeof sendMocks . reactMessageDiscord | typeof sendMocks . removeReactionDiscord ,
503+ index : number ,
504+ emoji : string ,
505+ params ?: {
506+ accountId ?: string ;
507+ ackReaction ?: string ;
508+ removeAckAfterReply ?: boolean ;
509+ channelId ?: string ;
510+ messageId ?: string ;
511+ } ,
512+ ) {
513+ const call = requireReactionCall ( mock , index ) ;
514+ expect ( call [ 0 ] ) . toBe ( params ?. channelId ?? "c1" ) ;
515+ expect ( call [ 1 ] ) . toBe ( params ?. messageId ?? "m1" ) ;
516+ expect ( call [ 2 ] ) . toBe ( emoji ) ;
517+ expectAckReactionRuntimeOptions ( call [ 3 ] , params ) ;
518+ }
519+
520+ function expectReactionCallsContain ( channelId : string , messageId : string , emoji : string ) {
521+ const calls = sendMocks . reactMessageDiscord . mock . calls as unknown as Array <
522+ [ string , string , string ]
523+ > ;
524+ const hasCall = calls . some (
525+ ( [ actualChannelId , actualMessageId , actualEmoji ] ) =>
526+ actualChannelId === channelId && actualMessageId === messageId && actualEmoji === emoji ,
527+ ) ;
528+ expect ( hasCall ) . toBe ( true ) ;
467529}
468530
469531function expectReactAckCallAt (
@@ -477,13 +539,7 @@ function expectReactAckCallAt(
477539 removeAckAfterReply ?: boolean ;
478540 } ,
479541) {
480- expect ( sendMocks . reactMessageDiscord ) . toHaveBeenNthCalledWith (
481- index + 1 ,
482- params ?. channelId ?? "c1" ,
483- params ?. messageId ?? "m1" ,
484- emoji ,
485- expectAckReactionRuntimeOptions ( params ) ,
486- ) ;
542+ expectReactionCallAt ( sendMocks . reactMessageDiscord , index , emoji , params ) ;
487543}
488544
489545function expectRemoveAckCallAt (
@@ -497,13 +553,7 @@ function expectRemoveAckCallAt(
497553 removeAckAfterReply ?: boolean ;
498554 } ,
499555) {
500- expect ( sendMocks . removeReactionDiscord ) . toHaveBeenNthCalledWith (
501- index + 1 ,
502- params ?. channelId ?? "c1" ,
503- params ?. messageId ?? "m1" ,
504- emoji ,
505- expectAckReactionRuntimeOptions ( params ) ,
506- ) ;
556+ expectReactionCallAt ( sendMocks . removeReactionDiscord , index , emoji , params ) ;
507557}
508558
509559function createMockDraftStreamForTest ( ) {
@@ -512,13 +562,20 @@ function createMockDraftStreamForTest() {
512562 return draftStream ;
513563}
514564
565+ function expectPreviewEditContent ( content : string ) {
566+ const call = editMessageDiscord . mock . calls [ 0 ] as unknown [ ] | undefined ;
567+ expect ( call ) . toBeDefined ( ) ;
568+ if ( ! call ) {
569+ throw new Error ( "missing preview edit call" ) ;
570+ }
571+ expect ( call [ 0 ] ) . toBe ( "c1" ) ;
572+ expect ( call [ 1 ] ) . toBe ( "preview-1" ) ;
573+ expect ( call [ 2 ] ) . toEqual ( { content } ) ;
574+ requireRecord ( requireRecord ( call [ 3 ] , "preview edit options" ) . rest , "preview edit REST client" ) ;
575+ }
576+
515577function expectSinglePreviewEdit ( ) {
516- expect ( editMessageDiscord ) . toHaveBeenCalledWith (
517- "c1" ,
518- "preview-1" ,
519- { content : "Hello\nWorld" } ,
520- expect . objectContaining ( { rest : expect . anything ( ) } ) ,
521- ) ;
578+ expectPreviewEditContent ( "Hello\nWorld" ) ;
522579 expect ( deliverDiscordReply ) . not . toHaveBeenCalled ( ) ;
523580}
524581
@@ -601,12 +658,13 @@ describe("processDiscordMessage ack reactions", () => {
601658 await runProcessDiscordMessage ( ctx ) ;
602659
603660 expect ( sendMocks . reactMessageDiscord ) . toHaveBeenCalled ( ) ;
604- expect ( sendMocks . reactMessageDiscord . mock . calls [ 0 ] ?. [ 3 ] ) . toEqual (
605- expect . objectContaining ( { rest : feedbackRest } ) ,
606- ) ;
607- expect ( deliverDiscordReply ) . toHaveBeenCalledWith (
608- expect . objectContaining ( { rest : deliveryRest } ) ,
661+ const feedbackOptions = requireRecord (
662+ sendMocks . reactMessageDiscord . mock . calls [ 0 ] ?. [ 3 ] ,
663+ "feedback reaction options" ,
609664 ) ;
665+ expect ( feedbackOptions . rest ) . toBe ( feedbackRest ) ;
666+ const deliveryParams = requireRecord ( deliverDiscordReply . mock . calls [ 0 ] ?. [ 0 ] , "delivery params" ) ;
667+ expect ( deliveryParams . rest ) . toBe ( deliveryRest ) ;
610668 expect ( feedbackRest ) . not . toBe ( deliveryRest ) ;
611669 } ) ;
612670
@@ -669,12 +727,9 @@ describe("processDiscordMessage ack reactions", () => {
669727 await runProcessDiscordMessage ( ctx ) ;
670728 await vi . runAllTimersAsync ( ) ;
671729
672- const calls = sendMocks . reactMessageDiscord . mock . calls as unknown as Array <
673- [ string , string , string ]
674- > ;
675- expect ( calls ) . toContainEqual ( expect . arrayContaining ( [ "c1" , "m1" , "📈" ] ) ) ;
676- expect ( calls ) . toContainEqual ( expect . arrayContaining ( [ "c1" , "m1" , "✉️" ] ) ) ;
677- expect ( calls ) . toContainEqual ( expect . arrayContaining ( [ "c1" , "m1" , DEFAULT_EMOJIS . done ] ) ) ;
730+ expectReactionCallsContain ( "c1" , "m1" , "📈" ) ;
731+ expectReactionCallsContain ( "c1" , "m1" , "✉️" ) ;
732+ expectReactionCallsContain ( "c1" , "m1" , DEFAULT_EMOJIS . done ) ;
678733 } ) ;
679734
680735 it ( "resolves tracked reaction to targets like the Discord reaction action" , async ( ) => {
@@ -702,16 +757,20 @@ describe("processDiscordMessage ack reactions", () => {
702757 await runProcessDiscordMessage ( ctx ) ;
703758 await vi . runAllTimersAsync ( ) ;
704759
705- expect ( discordTargetMocks . resolveDiscordTargetChannelId ) . toHaveBeenCalledWith (
706- "user:u1" ,
707- expect . objectContaining ( { accountId : "default" } ) ,
760+ const resolveCall = discordTargetMocks . resolveDiscordTargetChannelId . mock . calls [ 0 ] as
761+ | unknown [ ]
762+ | undefined ;
763+ expect ( resolveCall ) . toBeDefined ( ) ;
764+ if ( ! resolveCall ) {
765+ throw new Error ( "missing Discord target resolve call" ) ;
766+ }
767+ expect ( resolveCall [ 0 ] ) . toBe ( "user:u1" ) ;
768+ expect ( requireRecord ( resolveCall [ 1 ] , "Discord target resolve options" ) . accountId ) . toBe (
769+ "default" ,
708770 ) ;
709- const calls = sendMocks . reactMessageDiscord . mock . calls as unknown as Array <
710- [ string , string , string ]
711- > ;
712- expect ( calls ) . toContainEqual ( expect . arrayContaining ( [ "dm-u1" , "m1" , "📈" ] ) ) ;
713- expect ( calls ) . toContainEqual ( expect . arrayContaining ( [ "dm-u1" , "m1" , "✉️" ] ) ) ;
714- expect ( calls ) . toContainEqual ( expect . arrayContaining ( [ "dm-u1" , "m1" , DEFAULT_EMOJIS . done ] ) ) ;
771+ expectReactionCallsContain ( "dm-u1" , "m1" , "📈" ) ;
772+ expectReactionCallsContain ( "dm-u1" , "m1" , "✉️" ) ;
773+ expectReactionCallsContain ( "dm-u1" , "m1" , DEFAULT_EMOJIS . done ) ;
715774 } ) ;
716775
717776 it ( "shows stall emojis for long no-progress runs" , async ( ) => {
@@ -911,7 +970,7 @@ describe("processDiscordMessage session routing", () => {
911970
912971 await runProcessDiscordMessage ( ctx ) ;
913972
914- expect ( getLastDispatchCtx ( ) ) . toMatchObject ( {
973+ expectRecordFields ( requireRecord ( getLastDispatchCtx ( ) , "dispatch context" ) , {
915974 BodyForAgent : "hello from discord voice" ,
916975 CommandBody : "hello from discord voice" ,
917976 Transcript : "hello from discord voice" ,
@@ -939,7 +998,7 @@ describe("processDiscordMessage session routing", () => {
939998 to : "user:U1" ,
940999 accountId : "default" ,
9411000 } ) ;
942- expect ( getLastDispatchCtx ( ) ) . toMatchObject ( {
1001+ expectRecordFields ( requireRecord ( getLastDispatchCtx ( ) , "dispatch context" ) , {
9431002 ChatType : "direct" ,
9441003 From : "discord:U1" ,
9451004 To : "user:U1" ,
@@ -978,16 +1037,22 @@ describe("processDiscordMessage session routing", () => {
9781037
9791038 await runProcessDiscordMessage ( ctx ) ;
9801039
981- expect ( getLastRouteUpdate ( ) ) . toMatchObject ( {
1040+ expectRecordFields ( requireRecord ( getLastRouteUpdate ( ) , "last route update" ) , {
9821041 sessionKey : "agent:main:main" ,
9831042 channel : "discord" ,
9841043 to : "user:222" ,
9851044 accountId : "default" ,
986- mainDmOwnerPin : {
1045+ } ) ;
1046+ expectRecordFields (
1047+ requireRecord (
1048+ requireRecord ( getLastRouteUpdate ( ) , "last route update" ) . mainDmOwnerPin ,
1049+ "main DM owner pin" ,
1050+ ) ,
1051+ {
9871052 ownerRecipient : "111" ,
9881053 senderRecipient : "222" ,
9891054 } ,
990- } ) ;
1055+ ) ;
9911056 } ) ;
9921057
9931058 it ( "stores group lastRoute with channel target" , async ( ) => {
@@ -1016,7 +1081,7 @@ describe("processDiscordMessage session routing", () => {
10161081
10171082 await runProcessDiscordMessage ( ctx ) ;
10181083
1019- expect ( getLastDispatchReplyOptions ( ) ) . toMatchObject ( {
1084+ expectRecordFields ( requireRecord ( getLastDispatchReplyOptions ( ) , "dispatch reply options" ) , {
10201085 sourceReplyDeliveryMode : "message_tool_only" ,
10211086 disableBlockStreaming : true ,
10221087 } ) ;
@@ -1098,7 +1163,7 @@ describe("processDiscordMessage session routing", () => {
10981163
10991164 await runProcessDiscordMessage ( ctx ) ;
11001165
1101- expect ( getLastDispatchCtx ( ) ) . toMatchObject ( {
1166+ expectRecordFields ( requireRecord ( getLastDispatchCtx ( ) , "dispatch context" ) , {
11021167 MessageSid : "orig-123" ,
11031168 MessageSidFull : "proxy-456" ,
11041169 } ) ;
@@ -1169,7 +1234,7 @@ describe("processDiscordMessage session routing", () => {
11691234
11701235 await runProcessDiscordMessage ( ctx ) ;
11711236
1172- expect ( getLastDispatchCtx ( ) ) . toMatchObject ( {
1237+ expectRecordFields ( requireRecord ( getLastDispatchCtx ( ) , "dispatch context" ) , {
11731238 SessionKey : "agent:main:subagent:child" ,
11741239 MessageThreadId : "thread-1" ,
11751240 } ) ;
@@ -1202,7 +1267,7 @@ describe("processDiscordMessage session routing", () => {
12021267
12031268 await runProcessDiscordMessage ( ctx ) ;
12041269
1205- expect ( getLastDispatchCtx ( ) ) . toMatchObject ( {
1270+ expectRecordFields ( requireRecord ( getLastDispatchCtx ( ) , "dispatch context" ) , {
12061271 SessionKey : "agent:main:discord:channel:thread-1" ,
12071272 MessageThreadId : "thread-1" ,
12081273 ModelParentSessionKey : "agent:main:discord:channel:parent-1" ,
@@ -1246,7 +1311,7 @@ describe("processDiscordMessage session routing", () => {
12461311 await runProcessDiscordMessage ( ctx ) ;
12471312
12481313 expect ( rest . get ) . toHaveBeenCalled ( ) ;
1249- expect ( getLastDispatchCtx ( ) ) . toMatchObject ( {
1314+ expectRecordFields ( requireRecord ( getLastDispatchCtx ( ) , "dispatch context" ) , {
12501315 SessionKey : threadSessionKey ,
12511316 MessageThreadId : "thread-1" ,
12521317 } ) ;
@@ -1320,12 +1385,7 @@ describe("processDiscordMessage draft streaming", () => {
13201385
13211386 expect ( draftStream . update ) . toHaveBeenCalledWith ( expect . stringContaining ( "Exec" ) ) ;
13221387 expect ( draftStream . update ) . toHaveBeenCalledWith ( expect . stringContaining ( "exec done" ) ) ;
1323- expect ( editMessageDiscord ) . toHaveBeenCalledWith (
1324- "c1" ,
1325- "preview-1" ,
1326- { content : "done" } ,
1327- expect . objectContaining ( { rest : expect . anything ( ) } ) ,
1328- ) ;
1388+ expectPreviewEditContent ( "done" ) ;
13291389 expect ( deliverDiscordReply ) . not . toHaveBeenCalled ( ) ;
13301390 } ) ;
13311391
@@ -1365,12 +1425,7 @@ describe("processDiscordMessage draft streaming", () => {
13651425
13661426 await runProcessDiscordMessage ( ctx ) ;
13671427
1368- expect ( editMessageDiscord ) . toHaveBeenCalledWith (
1369- "c1" ,
1370- "preview-1" ,
1371- { content : longReply } ,
1372- expect . objectContaining ( { rest : expect . anything ( ) } ) ,
1373- ) ;
1428+ expectPreviewEditContent ( longReply ) ;
13741429 expect ( deliverDiscordReply ) . not . toHaveBeenCalled ( ) ;
13751430 } ) ;
13761431
@@ -1535,9 +1590,12 @@ describe("processDiscordMessage draft streaming", () => {
15351590 expect ( draftStream . update ) . toHaveBeenCalledTimes ( 1 ) ;
15361591 expect ( draftStream . update ) . toHaveBeenCalledWith ( "Shelling" ) ;
15371592 expect ( draftStream . flush ) . toHaveBeenCalledTimes ( 1 ) ;
1538- expect ( dispatchInboundMessage . mock . calls [ 0 ] ?. [ 0 ] ?. replyOptions ) . toMatchObject ( {
1539- suppressDefaultToolProgressMessages : true ,
1540- } ) ;
1593+ expect (
1594+ requireRecord (
1595+ dispatchInboundMessage . mock . calls [ 0 ] ?. [ 0 ] ?. replyOptions ,
1596+ "dispatch reply options" ,
1597+ ) . suppressDefaultToolProgressMessages ,
1598+ ) . toBe ( true ) ;
15411599 } ) ;
15421600
15431601 it ( "does not start Discord progress drafts for text-only accepted turns" , async ( ) => {
@@ -1588,12 +1646,7 @@ describe("processDiscordMessage draft streaming", () => {
15881646
15891647 expect ( draftStream . update ) . toHaveBeenCalledWith ( "Shelling\n🛠️ Exec\n• exec done" ) ;
15901648 expect ( deliverDiscordReply ) . not . toHaveBeenCalled ( ) ;
1591- expect ( editMessageDiscord ) . toHaveBeenCalledWith (
1592- "c1" ,
1593- "preview-1" ,
1594- { content : "done" } ,
1595- expect . objectContaining ( { rest : expect . anything ( ) } ) ,
1596- ) ;
1649+ expectPreviewEditContent ( "done" ) ;
15971650 } ) ;
15981651
15991652 it ( "uses raw tool-progress detail in Discord progress drafts" , async ( ) => {
0 commit comments