@@ -27,6 +27,25 @@ import {
2727 startWebAutoReplyMonitor ,
2828} from "./auto-reply.test-harness.js" ;
2929
30+ type DrainSelectionEntry = {
31+ channel : string ;
32+ accountId ?: string | null ;
33+ lastError ?: string ;
34+ } ;
35+ type DrainPendingDeliveriesCall = {
36+ drainKey : string ;
37+ logLabel : string ;
38+ selectEntry : ( entry : DrainSelectionEntry ) => { match : boolean ; bypassBackoff : boolean } ;
39+ } ;
40+
41+ const deliveryQueueMocks = vi . hoisted ( ( ) => ( {
42+ drainPendingDeliveries : vi . fn ( async ( _opts : unknown ) => undefined ) ,
43+ } ) ) ;
44+
45+ vi . mock ( "openclaw/plugin-sdk/delivery-queue-runtime" , ( ) => ( {
46+ drainPendingDeliveries : deliveryQueueMocks . drainPendingDeliveries ,
47+ } ) ) ;
48+
3049installWebAutoReplyTestHomeHooks ( ) ;
3150
3251function requireOnMessage (
@@ -247,6 +266,78 @@ describe("web auto-reply connection", () => {
247266 expect ( sleep ) . toHaveBeenCalled ( ) ;
248267 } ) ;
249268
269+ it ( "drains pending deliveries while connected and stops after close" , async ( ) => {
270+ vi . useFakeTimers ( ) ;
271+ try {
272+ const sleep = vi . fn ( async ( ) => { } ) ;
273+ const scripted = createScriptedWebListenerFactory ( ) ;
274+ const { controller, run } = startWebAutoReplyMonitor ( {
275+ monitorWebChannelFn : monitorWebChannel as never ,
276+ listenerFactory : scripted . listenerFactory ,
277+ sleep,
278+ accountId : "work" ,
279+ } ) ;
280+
281+ await vi . waitFor (
282+ ( ) => {
283+ expect ( scripted . getListenerCount ( ) ) . toBe ( 1 ) ;
284+ } ,
285+ { timeout : 250 , interval : 2 } ,
286+ ) ;
287+ expect ( deliveryQueueMocks . drainPendingDeliveries ) . toHaveBeenCalledWith (
288+ expect . objectContaining ( {
289+ drainKey : "whatsapp:work" ,
290+ logLabel : "WhatsApp reconnect drain" ,
291+ } ) ,
292+ ) ;
293+
294+ deliveryQueueMocks . drainPendingDeliveries . mockClear ( ) ;
295+ await vi . advanceTimersByTimeAsync ( 30_000 ) ;
296+ await vi . waitFor ( ( ) => {
297+ expect ( deliveryQueueMocks . drainPendingDeliveries ) . toHaveBeenCalledTimes ( 1 ) ;
298+ } ) ;
299+
300+ const periodicCall = deliveryQueueMocks . drainPendingDeliveries . mock . calls . at ( - 1 ) ?. [ 0 ] as
301+ | DrainPendingDeliveriesCall
302+ | undefined ;
303+ expect ( periodicCall ) . toBeDefined ( ) ;
304+ if ( ! periodicCall ) {
305+ throw new Error ( "Expected WhatsApp periodic drain call" ) ;
306+ }
307+ expect ( periodicCall . drainKey ) . toBe ( "whatsapp:work" ) ;
308+ expect ( periodicCall . logLabel ) . toBe ( "WhatsApp periodic drain" ) ;
309+ expect (
310+ periodicCall . selectEntry ( {
311+ channel : "whatsapp" ,
312+ accountId : "work" ,
313+ } ) ,
314+ ) . toEqual ( { match : true , bypassBackoff : false } ) ;
315+ expect (
316+ periodicCall . selectEntry ( {
317+ channel : "whatsapp" ,
318+ accountId : "default" ,
319+ } ) ,
320+ ) . toEqual ( { match : false , bypassBackoff : false } ) ;
321+ expect (
322+ periodicCall . selectEntry ( {
323+ channel : "telegram" ,
324+ accountId : "work" ,
325+ } ) ,
326+ ) . toEqual ( { match : false , bypassBackoff : false } ) ;
327+
328+ controller . abort ( ) ;
329+ scripted . resolveClose ( 0 , { status : 499 , isLoggedOut : false , error : "aborted" } ) ;
330+ await Promise . resolve ( ) ;
331+ await run ;
332+
333+ deliveryQueueMocks . drainPendingDeliveries . mockClear ( ) ;
334+ await vi . advanceTimersByTimeAsync ( 30_000 ) ;
335+ expect ( deliveryQueueMocks . drainPendingDeliveries ) . not . toHaveBeenCalled ( ) ;
336+ } finally {
337+ vi . useRealTimers ( ) ;
338+ }
339+ } ) ;
340+
250341 it ( "treats status 440 as non-retryable and stops without retrying" , async ( ) => {
251342 const sleep = vi . fn ( async ( ) => { } ) ;
252343 const scripted = createScriptedWebListenerFactory ( ) ;
0 commit comments