@@ -2503,6 +2503,146 @@ func TestStreamOffset(t *testing.T) {
25032503 require .ErrorContains (t , rdb .Do (ctx , "XREVRANGE" , "mystream" ).Err (), "wrong number of arguments" )
25042504 require .ErrorContains (t , rdb .Do (ctx , "XREVRANGE" , "mystream" , "+" ).Err (), "wrong number of arguments" )
25052505 })
2506+
2507+ t .Run ("XPENDING with specific end ID should filter correctly" , func (t * testing.T ) {
2508+ streamKey := "xpending-endid-test"
2509+ group := "grp"
2510+ consumer := "con"
2511+ require .NoError (t , rdb .Del (ctx , streamKey ).Err ())
2512+
2513+ id1 , err := rdb .XAdd (ctx , & redis.XAddArgs {Stream : streamKey , Values : []string {"f" , "1" }}).Result ()
2514+ require .NoError (t , err )
2515+ id2 , err := rdb .XAdd (ctx , & redis.XAddArgs {Stream : streamKey , Values : []string {"f" , "2" }}).Result ()
2516+ require .NoError (t , err )
2517+ id3 , err := rdb .XAdd (ctx , & redis.XAddArgs {Stream : streamKey , Values : []string {"f" , "3" }}).Result ()
2518+ require .NoError (t , err )
2519+
2520+ require .NoError (t , rdb .XGroupCreateMkStream (ctx , streamKey , group , "0" ).Err ())
2521+ // Read all entries so they become pending
2522+ _ , err = rdb .XReadGroup (ctx , & redis.XReadGroupArgs {Group : group , Consumer : consumer , Streams : []string {streamKey , ">" }, Count : 10 }).Result ()
2523+ require .NoError (t , err )
2524+
2525+ // XPENDING extended form: each row is [id, consumer, idle_ms, delivery_count].
2526+ assertXPendingExtRow := func (t * testing.T , row interface {}, wantID string ) {
2527+ t .Helper ()
2528+ fields , ok := row .([]interface {})
2529+ require .True (t , ok )
2530+ require .Len (t , fields , 4 )
2531+ gotID , ok := fields [0 ].(string )
2532+ require .True (t , ok )
2533+ require .Equal (t , wantID , gotID )
2534+ gotConsumer , ok := fields [1 ].(string )
2535+ require .True (t , ok )
2536+ require .Equal (t , consumer , gotConsumer )
2537+ require .GreaterOrEqual (t , fields [2 ], int64 (0 ))
2538+ require .EqualValues (t , 1 , fields [3 ])
2539+ }
2540+
2541+ // XPENDING extended form: same ID range rules as XRANGE (see Redis docs). Use XPENDING with end_id = id1.
2542+ result , err := rdb .Do (ctx , "XPENDING" , streamKey , group , id1 , id1 , "10" ).Result ()
2543+ require .NoError (t , err )
2544+ entries , ok := result .([]interface {})
2545+ require .True (t , ok )
2546+ require .Len (t , entries , 1 , "XPENDING with end_id=id1 should return only 1 entry" )
2547+ assertXPendingExtRow (t , entries [0 ], id1 )
2548+
2549+ // Use XPENDING with range [id1, id2] (should return 2 entries).
2550+ result , err = rdb .Do (ctx , "XPENDING" , streamKey , group , id1 , id2 , "10" ).Result ()
2551+ require .NoError (t , err )
2552+ entries , ok = result .([]interface {})
2553+ require .True (t , ok )
2554+ require .Len (t , entries , 2 , "XPENDING with range [id1,id2] should return 2 entries" )
2555+ assertXPendingExtRow (t , entries [0 ], id1 )
2556+ assertXPendingExtRow (t , entries [1 ], id2 )
2557+
2558+ // Use XPENDING with range [id1, id3] (should return all 3 entries).
2559+ result , err = rdb .Do (ctx , "XPENDING" , streamKey , group , id1 , id3 , "10" ).Result ()
2560+ require .NoError (t , err )
2561+ entries , ok = result .([]interface {})
2562+ require .True (t , ok )
2563+ require .Len (t , entries , 3 , "XPENDING with range [id1,id3] should return 3 entries" )
2564+ assertXPendingExtRow (t , entries [0 ], id1 )
2565+ assertXPendingExtRow (t , entries [1 ], id2 )
2566+ assertXPendingExtRow (t , entries [2 ], id3 )
2567+
2568+ require .NoError (t , rdb .Del (ctx , streamKey ).Err ())
2569+ })
2570+
2571+ t .Run ("XPENDING with incomplete end ID should include the whole millisecond" , func (t * testing.T ) {
2572+ streamKey := "xpending-incomplete-end-test"
2573+ group := "grp"
2574+ consumer := "con"
2575+ ids := []string {"1-0" , "1-1" , "1-2" }
2576+
2577+ require .NoError (t , rdb .Del (ctx , streamKey ).Err ())
2578+ for i , id := range ids {
2579+ require .NoError (t , rdb .XAdd (ctx , & redis.XAddArgs {Stream : streamKey , ID : id , Values : []string {"f" , strconv .Itoa (i )}}).Err ())
2580+ }
2581+
2582+ require .NoError (t , rdb .XGroupCreateMkStream (ctx , streamKey , group , "0" ).Err ())
2583+ _ , err := rdb .XReadGroup (ctx , & redis.XReadGroupArgs {Group : group , Consumer : consumer , Streams : []string {streamKey , ">" }, Count : 10 }).Result ()
2584+ require .NoError (t , err )
2585+
2586+ result , err := rdb .Do (ctx , "XPENDING" , streamKey , group , "1" , "1" , "10" ).Result ()
2587+ require .NoError (t , err )
2588+ entries , ok := result .([]interface {})
2589+ require .True (t , ok )
2590+ require .Len (t , entries , 3 , "XPENDING 1 1 should include every pending entry in millisecond 1, matching Redis" )
2591+
2592+ for i , entry := range entries {
2593+ fields , ok := entry .([]interface {})
2594+ require .True (t , ok )
2595+ require .Len (t , fields , 4 )
2596+ gotID , ok := fields [0 ].(string )
2597+ require .True (t , ok )
2598+ require .Equal (t , ids [i ], gotID )
2599+ gotConsumer , ok := fields [1 ].(string )
2600+ require .True (t , ok )
2601+ require .Equal (t , consumer , gotConsumer )
2602+ require .GreaterOrEqual (t , fields [2 ], int64 (0 ))
2603+ require .EqualValues (t , 1 , fields [3 ])
2604+ }
2605+
2606+ require .NoError (t , rdb .Del (ctx , streamKey ).Err ())
2607+ })
2608+
2609+ t .Run ("XPENDING with exclusive start should match Redis" , func (t * testing.T ) {
2610+ streamKey := "xpending-exclusive-start-test"
2611+ group := "grp"
2612+ consumer := "con"
2613+ ids := []string {"1-0" , "1-1" , "1-2" }
2614+
2615+ require .NoError (t , rdb .Del (ctx , streamKey ).Err ())
2616+ for i , id := range ids {
2617+ require .NoError (t , rdb .XAdd (ctx , & redis.XAddArgs {Stream : streamKey , ID : id , Values : []string {"f" , strconv .Itoa (i )}}).Err ())
2618+ }
2619+
2620+ require .NoError (t , rdb .XGroupCreateMkStream (ctx , streamKey , group , "0" ).Err ())
2621+ _ , err := rdb .XReadGroup (ctx , & redis.XReadGroupArgs {Group : group , Consumer : consumer , Streams : []string {streamKey , ">" }, Count : 10 }).Result ()
2622+ require .NoError (t , err )
2623+
2624+ result , err := rdb .Do (ctx , "XPENDING" , streamKey , group , "(1-0" , "+" , "10" ).Result ()
2625+ require .NoError (t , err )
2626+ entries , ok := result .([]interface {})
2627+ require .True (t , ok )
2628+ require .Len (t , entries , 2 , "XPENDING (1-0 + 10 should exclude the first pending entry, matching Redis" )
2629+
2630+ for i , entry := range entries {
2631+ fields , ok := entry .([]interface {})
2632+ require .True (t , ok )
2633+ require .Len (t , fields , 4 )
2634+ gotID , ok := fields [0 ].(string )
2635+ require .True (t , ok )
2636+ require .Equal (t , ids [i + 1 ], gotID )
2637+ gotConsumer , ok := fields [1 ].(string )
2638+ require .True (t , ok )
2639+ require .Equal (t , consumer , gotConsumer )
2640+ require .GreaterOrEqual (t , fields [2 ], int64 (0 ))
2641+ require .EqualValues (t , 1 , fields [3 ])
2642+ }
2643+
2644+ require .NoError (t , rdb .Del (ctx , streamKey ).Err ())
2645+ })
25062646}
25072647
25082648func parseStreamEntryID (id string ) (ts int64 , seqNum int64 ) {
0 commit comments