@@ -4,6 +4,7 @@ import { expect } from "vitest";
44import { z } from "zod" ;
55import { SimpleQueue } from "./queue.js" ;
66import { Logger } from "@trigger.dev/core/logger" ;
7+ import { createRedisClient } from "@internal/redis" ;
78
89describe ( "SimpleQueue" , ( ) => {
910 redisTest ( "enqueue/dequeue" , { timeout : 20_000 } , async ( { redisContainer } ) => {
@@ -209,6 +210,10 @@ describe("SimpleQueue", () => {
209210 timestamp : expect . any ( Date ) ,
210211 } )
211212 ) ;
213+
214+ // Acknowledge the item and verify it's removed
215+ await queue . ack ( second ! . id ) ;
216+ expect ( await queue . size ( { includeFuture : true } ) ) . toBe ( 0 ) ;
212217 } finally {
213218 await queue . close ( ) ;
214219 }
@@ -328,6 +333,7 @@ describe("SimpleQueue", () => {
328333
329334 // Redrive item from DLQ
330335 await queue . redriveFromDeadLetterQueue ( "1" ) ;
336+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) ) ;
331337 expect ( await queue . size ( ) ) . toBe ( 1 ) ;
332338 expect ( await queue . sizeOfDeadLetterQueue ( ) ) . toBe ( 0 ) ;
333339
@@ -357,4 +363,64 @@ describe("SimpleQueue", () => {
357363 await queue . close ( ) ;
358364 }
359365 } ) ;
366+
367+ redisTest ( "cleanup orphaned queue entries" , { timeout : 20_000 } , async ( { redisContainer } ) => {
368+ const queue = new SimpleQueue ( {
369+ name : "test-orphaned" ,
370+ schema : {
371+ test : z . object ( {
372+ value : z . number ( ) ,
373+ } ) ,
374+ } ,
375+ redisOptions : {
376+ host : redisContainer . getHost ( ) ,
377+ port : redisContainer . getPort ( ) ,
378+ password : redisContainer . getPassword ( ) ,
379+ } ,
380+ logger : new Logger ( "test" , "log" ) ,
381+ } ) ;
382+
383+ try {
384+ // First, add a normal item
385+ await queue . enqueue ( { id : "1" , job : "test" , item : { value : 1 } , visibilityTimeoutMs : 2000 } ) ;
386+
387+ const redisClient = createRedisClient ( {
388+ host : redisContainer . getHost ( ) ,
389+ port : redisContainer . getPort ( ) ,
390+ password : redisContainer . getPassword ( ) ,
391+ } ) ;
392+
393+ // Manually add an orphaned item to the queue (without corresponding hash entry)
394+ await redisClient . zadd ( `{queue:test-orphaned:}queue` , Date . now ( ) , "orphaned-id" ) ;
395+
396+ // Verify both items are in the queue
397+ expect ( await queue . size ( ) ) . toBe ( 2 ) ;
398+
399+ // Dequeue should process both items, but only return the valid one
400+ // and clean up the orphaned entry
401+ const dequeued = await queue . dequeue ( 2 ) ;
402+
403+ // Should only get the valid item
404+ expect ( dequeued ) . toHaveLength ( 1 ) ;
405+ expect ( dequeued [ 0 ] ) . toEqual (
406+ expect . objectContaining ( {
407+ id : "1" ,
408+ job : "test" ,
409+ item : { value : 1 } ,
410+ visibilityTimeoutMs : 2000 ,
411+ attempt : 0 ,
412+ timestamp : expect . any ( Date ) ,
413+ } )
414+ ) ;
415+
416+ // The orphaned item should have been removed
417+ expect ( await queue . size ( { includeFuture : true } ) ) . toBe ( 1 ) ;
418+
419+ // Verify the orphaned ID is no longer in the queue
420+ const orphanedScore = await redisClient . zscore ( `{queue:test-orphaned:}queue` , "orphaned-id" ) ;
421+ expect ( orphanedScore ) . toBeNull ( ) ;
422+ } finally {
423+ await queue . close ( ) ;
424+ }
425+ } ) ;
360426} ) ;
0 commit comments