@@ -4,6 +4,7 @@ import { expect } from "vitest";
4
4
import { z } from "zod" ;
5
5
import { SimpleQueue } from "./queue.js" ;
6
6
import { Logger } from "@trigger.dev/core/logger" ;
7
+ import { createRedisClient } from "@internal/redis" ;
7
8
8
9
describe ( "SimpleQueue" , ( ) => {
9
10
redisTest ( "enqueue/dequeue" , { timeout : 20_000 } , async ( { redisContainer } ) => {
@@ -209,6 +210,10 @@ describe("SimpleQueue", () => {
209
210
timestamp : expect . any ( Date ) ,
210
211
} )
211
212
) ;
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 ) ;
212
217
} finally {
213
218
await queue . close ( ) ;
214
219
}
@@ -328,6 +333,7 @@ describe("SimpleQueue", () => {
328
333
329
334
// Redrive item from DLQ
330
335
await queue . redriveFromDeadLetterQueue ( "1" ) ;
336
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) ) ;
331
337
expect ( await queue . size ( ) ) . toBe ( 1 ) ;
332
338
expect ( await queue . sizeOfDeadLetterQueue ( ) ) . toBe ( 0 ) ;
333
339
@@ -357,4 +363,64 @@ describe("SimpleQueue", () => {
357
363
await queue . close ( ) ;
358
364
}
359
365
} ) ;
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
+ } ) ;
360
426
} ) ;
0 commit comments