@@ -494,3 +494,141 @@ describe('filterRecentBlocks', () => {
494494 expect ( result ) . toHaveLength ( 0 ) ;
495495 } ) ;
496496} ) ;
497+
498+ describe ( 'identifySessionBlocks with configurable duration' , ( ) => {
499+ test ( 'creates single block for entries within custom 3-hour duration' , ( ) => {
500+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
501+ const entries : LoadedUsageEntry [ ] = [
502+ createMockEntry ( baseTime ) ,
503+ createMockEntry ( new Date ( baseTime . getTime ( ) + 60 * 60 * 1000 ) ) , // 1 hour later
504+ createMockEntry ( new Date ( baseTime . getTime ( ) + 2 * 60 * 60 * 1000 ) ) , // 2 hours later
505+ ] ;
506+
507+ const blocks = identifySessionBlocks ( entries , 3 ) ;
508+ expect ( blocks ) . toHaveLength ( 1 ) ;
509+ expect ( blocks [ 0 ] ?. startTime ) . toEqual ( baseTime ) ;
510+ expect ( blocks [ 0 ] ?. entries ) . toHaveLength ( 3 ) ;
511+ expect ( blocks [ 0 ] ?. endTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 3 * 60 * 60 * 1000 ) ) ;
512+ } ) ;
513+
514+ test ( 'creates multiple blocks with custom 2-hour duration' , ( ) => {
515+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
516+ const entries : LoadedUsageEntry [ ] = [
517+ createMockEntry ( baseTime ) ,
518+ createMockEntry ( new Date ( baseTime . getTime ( ) + 3 * 60 * 60 * 1000 ) ) , // 3 hours later (beyond 2h limit)
519+ ] ;
520+
521+ const blocks = identifySessionBlocks ( entries , 2 ) ;
522+ expect ( blocks ) . toHaveLength ( 3 ) ; // first block, gap block, second block
523+ expect ( blocks [ 0 ] ?. entries ) . toHaveLength ( 1 ) ;
524+ expect ( blocks [ 0 ] ?. endTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 2 * 60 * 60 * 1000 ) ) ;
525+ expect ( blocks [ 1 ] ?. isGap ) . toBe ( true ) ; // gap block
526+ expect ( blocks [ 2 ] ?. entries ) . toHaveLength ( 1 ) ;
527+ } ) ;
528+
529+ test ( 'creates gap block with custom 1-hour duration' , ( ) => {
530+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
531+ const entries : LoadedUsageEntry [ ] = [
532+ createMockEntry ( baseTime ) ,
533+ createMockEntry ( new Date ( baseTime . getTime ( ) + 30 * 60 * 1000 ) ) , // 30 minutes later (within 1h)
534+ createMockEntry ( new Date ( baseTime . getTime ( ) + 2 * 60 * 60 * 1000 ) ) , // 2 hours later (beyond 1h)
535+ ] ;
536+
537+ const blocks = identifySessionBlocks ( entries , 1 ) ;
538+ expect ( blocks ) . toHaveLength ( 3 ) ; // first block, gap block, second block
539+ expect ( blocks [ 0 ] ?. entries ) . toHaveLength ( 2 ) ;
540+ expect ( blocks [ 1 ] ?. isGap ) . toBe ( true ) ;
541+ expect ( blocks [ 2 ] ?. entries ) . toHaveLength ( 1 ) ;
542+ } ) ;
543+
544+ test ( 'works with fractional hours (2.5 hours)' , ( ) => {
545+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
546+ const entries : LoadedUsageEntry [ ] = [
547+ createMockEntry ( baseTime ) ,
548+ createMockEntry ( new Date ( baseTime . getTime ( ) + 2 * 60 * 60 * 1000 ) ) , // 2 hours later (within 2.5h)
549+ createMockEntry ( new Date ( baseTime . getTime ( ) + 6 * 60 * 60 * 1000 ) ) , // 6 hours later (4 hours from last entry, beyond 2.5h)
550+ ] ;
551+
552+ const blocks = identifySessionBlocks ( entries , 2.5 ) ;
553+ expect ( blocks ) . toHaveLength ( 3 ) ; // first block, gap block, second block
554+ expect ( blocks [ 0 ] ?. entries ) . toHaveLength ( 2 ) ;
555+ expect ( blocks [ 0 ] ?. endTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 2.5 * 60 * 60 * 1000 ) ) ;
556+ expect ( blocks [ 1 ] ?. isGap ) . toBe ( true ) ;
557+ expect ( blocks [ 2 ] ?. entries ) . toHaveLength ( 1 ) ;
558+ } ) ;
559+
560+ test ( 'works with very short duration (0.5 hours)' , ( ) => {
561+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
562+ const entries : LoadedUsageEntry [ ] = [
563+ createMockEntry ( baseTime ) ,
564+ createMockEntry ( new Date ( baseTime . getTime ( ) + 20 * 60 * 1000 ) ) , // 20 minutes later (within 0.5h)
565+ createMockEntry ( new Date ( baseTime . getTime ( ) + 80 * 60 * 1000 ) ) , // 80 minutes later (60 minutes from last entry, beyond 0.5h)
566+ ] ;
567+
568+ const blocks = identifySessionBlocks ( entries , 0.5 ) ;
569+ expect ( blocks ) . toHaveLength ( 3 ) ; // first block, gap block, second block
570+ expect ( blocks [ 0 ] ?. entries ) . toHaveLength ( 2 ) ;
571+ expect ( blocks [ 0 ] ?. endTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 0.5 * 60 * 60 * 1000 ) ) ;
572+ expect ( blocks [ 1 ] ?. isGap ) . toBe ( true ) ;
573+ expect ( blocks [ 2 ] ?. entries ) . toHaveLength ( 1 ) ;
574+ } ) ;
575+
576+ test ( 'works with very long duration (24 hours)' , ( ) => {
577+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
578+ const entries : LoadedUsageEntry [ ] = [
579+ createMockEntry ( baseTime ) ,
580+ createMockEntry ( new Date ( baseTime . getTime ( ) + 12 * 60 * 60 * 1000 ) ) , // 12 hours later (within 24h)
581+ createMockEntry ( new Date ( baseTime . getTime ( ) + 20 * 60 * 60 * 1000 ) ) , // 20 hours later (within 24h)
582+ ] ;
583+
584+ const blocks = identifySessionBlocks ( entries , 24 ) ;
585+ expect ( blocks ) . toHaveLength ( 1 ) ; // single block
586+ expect ( blocks [ 0 ] ?. entries ) . toHaveLength ( 3 ) ;
587+ expect ( blocks [ 0 ] ?. endTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 24 * 60 * 60 * 1000 ) ) ;
588+ } ) ;
589+
590+ test ( 'gap detection respects custom duration' , ( ) => {
591+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
592+ const entries : LoadedUsageEntry [ ] = [
593+ createMockEntry ( baseTime ) ,
594+ createMockEntry ( new Date ( baseTime . getTime ( ) + 1 * 60 * 60 * 1000 ) ) , // 1 hour later
595+ createMockEntry ( new Date ( baseTime . getTime ( ) + 5 * 60 * 60 * 1000 ) ) , // 5 hours later (4h from last entry, beyond 3h)
596+ ] ;
597+
598+ const blocks = identifySessionBlocks ( entries , 3 ) ;
599+ expect ( blocks ) . toHaveLength ( 3 ) ; // first block, gap block, second block
600+
601+ // Gap block should start 3 hours after last activity in first block
602+ const gapBlock = blocks [ 1 ] ;
603+ expect ( gapBlock ?. isGap ) . toBe ( true ) ;
604+ expect ( gapBlock ?. startTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 1 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000 ) ) ; // 1h + 3h
605+ expect ( gapBlock ?. endTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 5 * 60 * 60 * 1000 ) ) ; // 5h
606+ } ) ;
607+
608+ test ( 'no gap created when gap is exactly equal to session duration' , ( ) => {
609+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
610+ const entries : LoadedUsageEntry [ ] = [
611+ createMockEntry ( baseTime ) ,
612+ createMockEntry ( new Date ( baseTime . getTime ( ) + 2 * 60 * 60 * 1000 ) ) , // exactly 2 hours later (equal to session duration)
613+ ] ;
614+
615+ const blocks = identifySessionBlocks ( entries , 2 ) ;
616+ expect ( blocks ) . toHaveLength ( 1 ) ; // single block (entries are exactly at session boundary)
617+ expect ( blocks [ 0 ] ?. entries ) . toHaveLength ( 2 ) ;
618+ } ) ;
619+
620+ test ( 'defaults to 5 hours when no duration specified' , ( ) => {
621+ const baseTime = new Date ( '2024-01-01T10:00:00Z' ) ;
622+ const entries : LoadedUsageEntry [ ] = [
623+ createMockEntry ( baseTime ) ,
624+ ] ;
625+
626+ const blocksDefault = identifySessionBlocks ( entries ) ;
627+ const blocksExplicit = identifySessionBlocks ( entries , 5 ) ;
628+
629+ expect ( blocksDefault ) . toHaveLength ( 1 ) ;
630+ expect ( blocksExplicit ) . toHaveLength ( 1 ) ;
631+ expect ( blocksDefault [ 0 ] ! . endTime ) . toEqual ( blocksExplicit [ 0 ] ! . endTime ) ;
632+ expect ( blocksDefault [ 0 ] ! . endTime ) . toEqual ( new Date ( baseTime . getTime ( ) + 5 * 60 * 60 * 1000 ) ) ;
633+ } ) ;
634+ } ) ;
0 commit comments