@@ -3,6 +3,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
33import { clearAgentHarnesses } from "../../agents/harness/registry.js" ;
44import type { PluginHookReplyDispatchResult } from "../../plugins/hooks.js" ;
55import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js" ;
6+ import { setReplyPayloadMetadata , type ReplyPayload } from "../types.js" ;
67import {
78 acpManagerRuntimeMocks ,
89 acpMocks ,
@@ -350,6 +351,109 @@ describe("dispatchReplyFromConfig reply_dispatch hook", () => {
350351 }
351352 } ) ;
352353
354+ it ( "dedupes byte-identical non-streaming final payload entries for one turn" , async ( ) => {
355+ hookMocks . runner . hasHooks . mockReturnValue ( false ) ;
356+ const dispatcher = createDispatcher ( ) ;
357+ const replyPayload = {
358+ text : "repeat once" ,
359+ mediaUrls : [ "file:///tmp/repeat.png" ] ,
360+ channelData : { telegram : { parseMode : "MarkdownV2" } } ,
361+ } satisfies ReplyPayload ;
362+
363+ const result = await dispatchReplyFromConfig ( {
364+ ctx : createHookCtx ( ) ,
365+ cfg : emptyConfig ,
366+ dispatcher,
367+ replyResolver : async ( ) => [ replyPayload , { ...replyPayload } ] ,
368+ } ) ;
369+
370+ expect ( result . queuedFinal ) . toBe ( true ) ;
371+ expect ( dispatcher . sendFinalReply ) . toHaveBeenCalledOnce ( ) ;
372+ expect ( dispatcher . sendFinalReply ) . toHaveBeenCalledWith ( replyPayload ) ;
373+ } ) ;
374+
375+ it ( "preserves same-content final payloads with distinct route metadata" , async ( ) => {
376+ hookMocks . runner . hasHooks . mockReturnValue ( false ) ;
377+ const dispatcher = createDispatcher ( ) ;
378+ const firstReply = setReplyPayloadMetadata (
379+ { text : "same visible reply" } satisfies ReplyPayload ,
380+ {
381+ replyDelivery : { chatType : "channel" , replyToMode : "off" } ,
382+ replyDeliverySource : { channel : "slack" , accountId : "primary" } ,
383+ } ,
384+ ) ;
385+ const secondReply = setReplyPayloadMetadata (
386+ { text : "same visible reply" } satisfies ReplyPayload ,
387+ {
388+ replyDelivery : { chatType : "channel" , replyToMode : "off" } ,
389+ replyDeliverySource : { channel : "slack" , accountId : "secondary" } ,
390+ } ,
391+ ) ;
392+
393+ const result = await dispatchReplyFromConfig ( {
394+ ctx : createHookCtx ( ) ,
395+ cfg : emptyConfig ,
396+ dispatcher,
397+ replyResolver : async ( ) => [ firstReply , secondReply ] ,
398+ } ) ;
399+
400+ expect ( result . queuedFinal ) . toBe ( true ) ;
401+ expect ( dispatcher . sendFinalReply ) . toHaveBeenCalledTimes ( 2 ) ;
402+ expect ( dispatcher . sendFinalReply ) . toHaveBeenNthCalledWith ( 1 , firstReply ) ;
403+ expect ( dispatcher . sendFinalReply ) . toHaveBeenNthCalledWith ( 2 , secondReply ) ;
404+ } ) ;
405+
406+ it ( "preserves same-content final payloads with distinct reply-threading identity" , async ( ) => {
407+ hookMocks . runner . hasHooks . mockReturnValue ( false ) ;
408+ const dispatcher = createDispatcher ( ) ;
409+ const implicitReply = {
410+ text : "same threaded reply" ,
411+ replyToId : "message-1" ,
412+ } satisfies ReplyPayload ;
413+ const explicitReply = setReplyPayloadMetadata (
414+ {
415+ text : "same threaded reply" ,
416+ replyToId : "message-1" ,
417+ } satisfies ReplyPayload ,
418+ { replyToIdExplicit : true } ,
419+ ) ;
420+
421+ await dispatchReplyFromConfig ( {
422+ ctx : createHookCtx ( ) ,
423+ cfg : emptyConfig ,
424+ dispatcher,
425+ replyResolver : async ( ) => [ implicitReply , explicitReply ] ,
426+ } ) ;
427+
428+ expect ( dispatcher . sendFinalReply ) . toHaveBeenCalledTimes ( 2 ) ;
429+ expect ( dispatcher . sendFinalReply ) . toHaveBeenNthCalledWith ( 1 , implicitReply ) ;
430+ expect ( dispatcher . sendFinalReply ) . toHaveBeenNthCalledWith ( 2 , explicitReply ) ;
431+ } ) ;
432+
433+ it ( "preserves same-content final payloads from distinct assistant messages" , async ( ) => {
434+ hookMocks . runner . hasHooks . mockReturnValue ( false ) ;
435+ const dispatcher = createDispatcher ( ) ;
436+ const firstReply = setReplyPayloadMetadata (
437+ { text : "intentional repeat" } satisfies ReplyPayload ,
438+ { assistantMessageIndex : 1 } ,
439+ ) ;
440+ const secondReply = setReplyPayloadMetadata (
441+ { text : "intentional repeat" } satisfies ReplyPayload ,
442+ { assistantMessageIndex : 2 } ,
443+ ) ;
444+
445+ await dispatchReplyFromConfig ( {
446+ ctx : createHookCtx ( ) ,
447+ cfg : emptyConfig ,
448+ dispatcher,
449+ replyResolver : async ( ) => [ firstReply , secondReply ] ,
450+ } ) ;
451+
452+ expect ( dispatcher . sendFinalReply ) . toHaveBeenCalledTimes ( 2 ) ;
453+ expect ( dispatcher . sendFinalReply ) . toHaveBeenNthCalledWith ( 1 , firstReply ) ;
454+ expect ( dispatcher . sendFinalReply ) . toHaveBeenNthCalledWith ( 2 , secondReply ) ;
455+ } ) ;
456+
353457 it ( "clears the reply lane but defers follow-up admission until final delivery settles" , async ( ) => {
354458 const deliveryOrder : string [ ] = [ ] ;
355459 let startDelivery : ( ) => void = ( ) => { } ;
0 commit comments