@@ -418,4 +418,104 @@ describe("RunsRepository cursor pagination", () => {
418418 expect ( new Set ( seen ) . size ) . toBe ( ids . length ) ;
419419 }
420420 ) ;
421+
422+ replicationContainerTest (
423+ "a partial backward page still exposes a forward cursor (no stranding)" ,
424+ async ( { clickhouseContainer, redisOptions, postgresContainer, prisma } ) => {
425+ const { clickhouse } = await setupClickhouseReplication ( {
426+ prisma,
427+ databaseUrl : postgresContainer . getConnectionUri ( ) ,
428+ clickhouseUrl : clickhouseContainer . getConnectionUrl ( ) ,
429+ redisOptions,
430+ } ) ;
431+
432+ const organization = await prisma . organization . create ( {
433+ data : { title : "test" , slug : "test" } ,
434+ } ) ;
435+ const project = await prisma . project . create ( {
436+ data : {
437+ name : "test" ,
438+ slug : "test" ,
439+ organizationId : organization . id ,
440+ externalRef : "test" ,
441+ } ,
442+ } ) ;
443+ const runtimeEnvironment = await prisma . runtimeEnvironment . create ( {
444+ data : {
445+ slug : "test" ,
446+ type : "DEVELOPMENT" ,
447+ projectId : project . id ,
448+ organizationId : organization . id ,
449+ apiKey : "test" ,
450+ pkApiKey : "test" ,
451+ shortcode : "test" ,
452+ } ,
453+ } ) ;
454+
455+ // Three runs; created_at descending order is [a, b, c] (a newest).
456+ const ids = [
457+ "aaaaaaaaaaaaaaaaaaaaaaaa" ,
458+ "bbbbbbbbbbbbbbbbbbbbbbbb" ,
459+ "cccccccccccccccccccccccc" ,
460+ ] ;
461+ const base = new Date ( "2026-06-04T16:55:07.000Z" ) . getTime ( ) ;
462+ for ( let i = 0 ; i < ids . length ; i ++ ) {
463+ await prisma . taskRun . create ( {
464+ data : {
465+ id : ids [ i ] ,
466+ createdAt : new Date ( base + ( ids . length - 1 - i ) * 1000 ) ,
467+ friendlyId : `run_${ ids [ i ] } ` ,
468+ taskIdentifier : "my-task" ,
469+ payload : JSON . stringify ( { foo : "bar" } ) ,
470+ traceId : `trace_${ i } ` ,
471+ spanId : `span_${ i } ` ,
472+ queue : "test" ,
473+ runtimeEnvironmentId : runtimeEnvironment . id ,
474+ projectId : project . id ,
475+ organizationId : organization . id ,
476+ environmentType : "DEVELOPMENT" ,
477+ engine : "V2" ,
478+ } ,
479+ } ) ;
480+ }
481+
482+ await setTimeout ( 1000 ) ;
483+
484+ const runsRepository = new RunsRepository ( { prisma, clickhouse } ) ;
485+ const baseOptions = {
486+ projectId : project . id ,
487+ environmentId : runtimeEnvironment . id ,
488+ organizationId : organization . id ,
489+ } ;
490+
491+ // First page (size 2) = {a, b}; its nextCursor sits at b's boundary.
492+ const first = await runsRepository . listRuns ( { ...baseOptions , page : { size : 2 } } ) ;
493+ expect ( first . runs . map ( ( r ) => r . id ) . sort ( ) ) . toEqual ( [
494+ "aaaaaaaaaaaaaaaaaaaaaaaa" ,
495+ "bbbbbbbbbbbbbbbbbbbbbbbb" ,
496+ ] ) ;
497+
498+ // Paging backward from that cursor lands on a *partial* page — just the
499+ // newest run {a}, with no rows before it (hasMore === false).
500+ const back = await runsRepository . listRuns ( {
501+ ...baseOptions ,
502+ page : { size : 2 , cursor : first . pagination . nextCursor ! , direction : "backward" } ,
503+ } ) ;
504+ expect ( back . runs . map ( ( r ) => r . id ) ) . toEqual ( [ "aaaaaaaaaaaaaaaaaaaaaaaa" ] ) ;
505+
506+ // A partial backward page must still expose a forward cursor, or the user
507+ // is stranded with no way to page back down.
508+ expect ( back . pagination . nextCursor ) . toBeTruthy ( ) ;
509+
510+ // And paging forward from it reaches the remaining runs.
511+ const forward = await runsRepository . listRuns ( {
512+ ...baseOptions ,
513+ page : { size : 2 , cursor : back . pagination . nextCursor ! , direction : "forward" } ,
514+ } ) ;
515+ expect ( forward . runs . map ( ( r ) => r . id ) . sort ( ) ) . toEqual ( [
516+ "bbbbbbbbbbbbbbbbbbbbbbbb" ,
517+ "cccccccccccccccccccccccc" ,
518+ ] ) ;
519+ }
520+ ) ;
421521} ) ;
0 commit comments