@@ -796,6 +796,53 @@ describe('calculateStreak — timezone awareness', () => {
796796 expect ( result . currentStreak ) . toBe ( 3 ) ;
797797 } ) ;
798798
799+ it ( 'handles contributions at 23:59 and 00:01 UTC across timezone boundaries' , ( ) => {
800+ // Simulate two commits that occur around the UTC midnight boundary:
801+ // - One commit at 2024-07-10T23:59:00Z (falls on 2024-07-10 UTC)
802+ // - Another commit at 2024-07-11T00:01:00Z (falls on 2024-07-11 UTC)
803+ // The flattened calendar only stores dates; these two commits appear on
804+ // consecutive dates (2024-07-10 and 2024-07-11). Depending on the
805+ // caller's timezone, the local "today" may be either 2024-07-11 or
806+ // still 2024-07-10 which can expose off-by-one errors.
807+ const calendar = {
808+ totalContributions : 2 ,
809+ weeks : [
810+ {
811+ contributionDays : [
812+ { contributionCount : 0 , date : '2024-07-09' } ,
813+ { contributionCount : 1 , date : '2024-07-10' } , // 23:59 UTC commit
814+ { contributionCount : 1 , date : '2024-07-11' } , // 00:01 UTC commit
815+ ] ,
816+ } ,
817+ ] ,
818+ } ;
819+
820+ // Use a UTC moment shortly after the second commit.
821+ const nowUTC = new Date ( '2024-07-11T00:01:00.000Z' ) ;
822+
823+ // In UTC the local date is 2024-07-11 — both days are in scope → streak=2
824+ const utcResult = calculateStreak ( calendar , 'UTC' , nowUTC ) ;
825+ expect ( utcResult . todayDate ) . toBe ( '2024-07-11' ) ;
826+ expect ( utcResult . currentStreak ) . toBe ( 2 ) ;
827+ expect ( utcResult . longestStreak ) . toBe ( 2 ) ;
828+
829+ // In a timezone ahead of UTC by 1 hour (Etc/GMT-1), local date is also 2024-07-11
830+ // and the streak remains 2 (no split occurs).
831+ const aheadResult = calculateStreak ( calendar , 'Etc/GMT-1' , nowUTC ) ;
832+ expect ( aheadResult . todayDate ) . toBe ( '2024-07-11' ) ;
833+ expect ( aheadResult . currentStreak ) . toBe ( 2 ) ;
834+ expect ( aheadResult . longestStreak ) . toBe ( 2 ) ;
835+
836+ // In a timezone behind UTC by 1 hour (Etc/GMT+1), local date is still 2024-07-10
837+ // at the same instant — only the earlier day's commit is considered "today",
838+ // so currentStreak should be 1 while longestStreak across the whole calendar
839+ // remains 2.
840+ const behindResult = calculateStreak ( calendar , 'Etc/GMT+1' , nowUTC ) ;
841+ expect ( behindResult . todayDate ) . toBe ( '2024-07-10' ) ;
842+ expect ( behindResult . currentStreak ) . toBe ( 1 ) ;
843+ expect ( behindResult . longestStreak ) . toBe ( 2 ) ;
844+ } ) ;
845+
799846 it ( 'falls back to the last available day when the local date is ahead of the calendar data' , ( ) => {
800847 const futureNow = new Date ( '2024-06-16T12:00:00.000Z' ) ;
801848 const result = calculateStreak ( tzCalendar , 'Etc/GMT-14' , futureNow ) ;
0 commit comments