@@ -737,6 +737,123 @@ describe("Recap — static helpers", () => {
737737 } )
738738} )
739739
740+ // ---------------------------------------------------------------------------
741+ // listTracesPaginated — pagination boundary math
742+ // ---------------------------------------------------------------------------
743+
744+ describe ( "Recap.listTracesPaginated" , ( ) => {
745+ // Seed a known set of traces in a fresh directory for each test.
746+ // A fresh tracer is created per iteration so spans don't accumulate
747+ // across startTrace/endTrace cycles — matches the pattern used
748+ // elsewhere in this file (see the maxFiles test above).
749+ async function seedTraces ( count : number , dir : string ) : Promise < void > {
750+ for ( let i = 0 ; i < count ; i ++ ) {
751+ const tracer = Recap . withExporters ( [ new FileExporter ( dir ) ] )
752+ tracer . startTrace ( `session-${ String ( i ) . padStart ( 4 , "0" ) } ` , {
753+ prompt : `prompt-${ i } ` ,
754+ } )
755+ await tracer . endTrace ( )
756+ }
757+ }
758+
759+ test ( "returns empty page when directory has no traces" , async ( ) => {
760+ const result = await Recap . listTracesPaginated ( tmpDir , { offset : 0 , limit : 10 } )
761+ expect ( result . traces ) . toEqual ( [ ] )
762+ expect ( result . total ) . toBe ( 0 )
763+ expect ( result . offset ) . toBe ( 0 )
764+ expect ( result . limit ) . toBe ( 10 )
765+ } )
766+
767+ test ( "returns a bounded page of the requested size" , async ( ) => {
768+ await seedTraces ( 15 , tmpDir )
769+ const result = await Recap . listTracesPaginated ( tmpDir , { offset : 0 , limit : 5 } )
770+ expect ( result . traces ) . toHaveLength ( 5 )
771+ expect ( result . total ) . toBe ( 15 )
772+ expect ( result . offset ) . toBe ( 0 )
773+ expect ( result . limit ) . toBe ( 5 )
774+ } )
775+
776+ test ( "applies offset to return later pages" , async ( ) => {
777+ await seedTraces ( 10 , tmpDir )
778+ const page1 = await Recap . listTracesPaginated ( tmpDir , { offset : 0 , limit : 4 } )
779+ const page2 = await Recap . listTracesPaginated ( tmpDir , { offset : 4 , limit : 4 } )
780+ const page3 = await Recap . listTracesPaginated ( tmpDir , { offset : 8 , limit : 4 } )
781+ expect ( page1 . traces ) . toHaveLength ( 4 )
782+ expect ( page2 . traces ) . toHaveLength ( 4 )
783+ expect ( page3 . traces ) . toHaveLength ( 2 ) // only 2 left on the tail
784+ // Pages must not overlap
785+ const ids = new Set < string > ( )
786+ for ( const p of [ page1 , page2 , page3 ] ) {
787+ for ( const t of p . traces ) {
788+ expect ( ids . has ( t . sessionId ) ) . toBe ( false )
789+ ids . add ( t . sessionId )
790+ }
791+ }
792+ expect ( ids . size ) . toBe ( 10 )
793+ } )
794+
795+ test ( "returns empty traces array when offset equals total" , async ( ) => {
796+ await seedTraces ( 5 , tmpDir )
797+ const result = await Recap . listTracesPaginated ( tmpDir , { offset : 5 , limit : 10 } )
798+ expect ( result . traces ) . toEqual ( [ ] )
799+ expect ( result . total ) . toBe ( 5 )
800+ expect ( result . offset ) . toBe ( 5 )
801+ } )
802+
803+ test ( "returns empty traces array when offset exceeds total" , async ( ) => {
804+ await seedTraces ( 3 , tmpDir )
805+ const result = await Recap . listTracesPaginated ( tmpDir , { offset : 99 , limit : 10 } )
806+ expect ( result . traces ) . toEqual ( [ ] )
807+ expect ( result . total ) . toBe ( 3 )
808+ expect ( result . offset ) . toBe ( 99 )
809+ } )
810+
811+ test ( "clamps negative offset to 0" , async ( ) => {
812+ await seedTraces ( 5 , tmpDir )
813+ const result = await Recap . listTracesPaginated ( tmpDir , { offset : - 10 , limit : 3 } )
814+ expect ( result . offset ) . toBe ( 0 )
815+ expect ( result . traces ) . toHaveLength ( 3 )
816+ } )
817+
818+ test ( "clamps non-positive limit to 1" , async ( ) => {
819+ await seedTraces ( 5 , tmpDir )
820+ const zero = await Recap . listTracesPaginated ( tmpDir , { offset : 0 , limit : 0 } )
821+ expect ( zero . limit ) . toBe ( 1 )
822+ expect ( zero . traces ) . toHaveLength ( 1 )
823+ const neg = await Recap . listTracesPaginated ( tmpDir , { offset : 0 , limit : - 5 } )
824+ expect ( neg . limit ) . toBe ( 1 )
825+ expect ( neg . traces ) . toHaveLength ( 1 )
826+ } )
827+
828+ test ( "truncates fractional offset and limit to integers" , async ( ) => {
829+ await seedTraces ( 10 , tmpDir )
830+ const result = await Recap . listTracesPaginated ( tmpDir , { offset : 2.9 , limit : 3.7 } )
831+ expect ( result . offset ) . toBe ( 2 )
832+ expect ( result . limit ) . toBe ( 3 )
833+ expect ( result . traces ) . toHaveLength ( 3 )
834+ } )
835+
836+ test ( "clamps NaN offset and limit to defaults" , async ( ) => {
837+ await seedTraces ( 5 , tmpDir )
838+ const result = await Recap . listTracesPaginated ( tmpDir , {
839+ offset : NaN ,
840+ limit : NaN ,
841+ } )
842+ expect ( result . offset ) . toBe ( 0 )
843+ expect ( result . limit ) . toBe ( 20 ) // default
844+ expect ( result . traces ) . toHaveLength ( 5 ) // all 5 fit in default page
845+ } )
846+
847+ test ( "uses defaults when no options provided" , async ( ) => {
848+ await seedTraces ( 3 , tmpDir )
849+ const result = await Recap . listTracesPaginated ( tmpDir )
850+ expect ( result . offset ) . toBe ( 0 )
851+ expect ( result . limit ) . toBe ( 20 )
852+ expect ( result . total ) . toBe ( 3 )
853+ expect ( result . traces ) . toHaveLength ( 3 )
854+ } )
855+ } )
856+
740857// ---------------------------------------------------------------------------
741858// Edge cases — schema integrity
742859// ---------------------------------------------------------------------------
0 commit comments