1- import { afterAll , beforeAll , describe , expect , it , vi } from "vitest" ;
1+ import { afterAll , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
22import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js" ;
33import type { OpenClawConfig } from "../../config/config.js" ;
44import { defaultRuntime } from "../../runtime.js" ;
@@ -8,7 +8,11 @@ import { finalizeInboundContext } from "./inbound-context.js";
88import { normalizeInboundTextNewlines } from "./inbound-text.js" ;
99import { parseLineDirectives , hasLineDirectives } from "./line-directives.js" ;
1010import type { FollowupRun , QueueSettings } from "./queue.js" ;
11- import { enqueueFollowupRun , scheduleFollowupDrain } from "./queue.js" ;
11+ import {
12+ enqueueFollowupRun ,
13+ resetRecentQueuedMessageIdDedupe ,
14+ scheduleFollowupDrain ,
15+ } from "./queue.js" ;
1216import { createReplyDispatcher } from "./reply-dispatcher.js" ;
1317import { createReplyToModeFilter , resolveReplyToMode } from "./reply-threading.js" ;
1418
@@ -627,6 +631,10 @@ function createRun(params: {
627631}
628632
629633describe ( "followup queue deduplication" , ( ) => {
634+ beforeEach ( ( ) => {
635+ resetRecentQueuedMessageIdDedupe ( ) ;
636+ } ) ;
637+
630638 it ( "deduplicates messages with same Discord message_id" , async ( ) => {
631639 const key = `test-dedup-message-id-${ Date . now ( ) } ` ;
632640 const calls : FollowupRun [ ] = [ ] ;
@@ -690,6 +698,51 @@ describe("followup queue deduplication", () => {
690698 expect ( calls [ 0 ] ?. prompt ) . toContain ( "[Queued messages while agent was busy]" ) ;
691699 } ) ;
692700
701+ it ( "deduplicates same message_id after queue drain restarts" , async ( ) => {
702+ const key = `test-dedup-after-drain-${ Date . now ( ) } ` ;
703+ const calls : FollowupRun [ ] = [ ] ;
704+ const done = createDeferred < void > ( ) ;
705+ const runFollowup = async ( run : FollowupRun ) => {
706+ calls . push ( run ) ;
707+ done . resolve ( ) ;
708+ } ;
709+ const settings : QueueSettings = {
710+ mode : "collect" ,
711+ debounceMs : 0 ,
712+ cap : 50 ,
713+ dropPolicy : "summarize" ,
714+ } ;
715+
716+ const first = enqueueFollowupRun (
717+ key ,
718+ createRun ( {
719+ prompt : "first" ,
720+ messageId : "same-id" ,
721+ originatingChannel : "signal" ,
722+ originatingTo : "+10000000000" ,
723+ } ) ,
724+ settings ,
725+ ) ;
726+ expect ( first ) . toBe ( true ) ;
727+
728+ scheduleFollowupDrain ( key , runFollowup ) ;
729+ await done . promise ;
730+
731+ const redelivery = enqueueFollowupRun (
732+ key ,
733+ createRun ( {
734+ prompt : "first-redelivery" ,
735+ messageId : "same-id" ,
736+ originatingChannel : "signal" ,
737+ originatingTo : "+10000000000" ,
738+ } ) ,
739+ settings ,
740+ ) ;
741+
742+ expect ( redelivery ) . toBe ( false ) ;
743+ expect ( calls ) . toHaveLength ( 1 ) ;
744+ } ) ;
745+
693746 it ( "deduplicates exact prompt when routing matches and no message id" , async ( ) => {
694747 const key = `test-dedup-whatsapp-${ Date . now ( ) } ` ;
695748 const settings : QueueSettings = {
0 commit comments