@@ -5,8 +5,9 @@ const assert = require('node:assert').strict;
55const crypto = require ( 'crypto' ) ;
66const msgpack = require ( 'msgpack5' ) ( ) ;
77
8- // Test pure functions and logic without loading the full export module
9- // This avoids the Bugsnag/logger initialization issues
8+ // Test pure functions and logic without loading the full export module.
9+ // This avoids the Bugsnag/logger initialization issues.
10+ // Score calculation must match lib/export.js Export.queueMessage() exactly.
1011
1112const EXPORT_ID_PREFIX = 'exp_' ;
1213
@@ -15,21 +16,19 @@ function generateExportId() {
1516 return EXPORT_ID_PREFIX + crypto . randomBytes ( 12 ) . toString ( 'hex' ) ;
1617}
1718
18- // Replicate the score calculation logic
19- // Uses messageId hash for tiebreaker instead of UID to avoid collisions with large UIDs
20- // Using factor of 1000 to stay within JavaScript safe integer range (< 2^53)
21- function calculateScore ( timestamp , messageId ) {
19+ // Replicate the score calculation logic from lib/export.js Export.queueMessage()
20+ // Uses SHA-256 hash of composite key (folder:messageId:uid) for tiebreaker
21+ // Using factor of 1000000 with baseSeconds to stay within JavaScript safe integer range (< 2^53)
22+ function calculateScore ( timestamp , messageId , folder , uid ) {
2223 const baseTimestamp = timestamp instanceof Date ? timestamp . getTime ( ) : Number ( timestamp ) || Date . now ( ) ;
24+ const baseSeconds = Math . floor ( baseTimestamp / 1000 ) ;
2325
24- // Generate tiebreaker from messageId hash (0-999 range)
25- let tiebreaker = 0 ;
26- const id = messageId || '' ;
27- for ( let i = 0 ; i < id . length ; i ++ ) {
28- tiebreaker = ( ( tiebreaker << 5 ) - tiebreaker + id . charCodeAt ( i ) ) | 0 ;
29- }
30- tiebreaker = Math . abs ( tiebreaker ) % 1000 ;
26+ // Generate tiebreaker from SHA-256 hash of composite key (0-999999 range)
27+ const uniqueKey = `${ folder || '' } :${ messageId || '' } :${ uid || '' } ` ;
28+ const hash = crypto . createHash ( 'sha256' ) . update ( uniqueKey ) . digest ( ) ;
29+ const tiebreaker = ( ( hash [ 0 ] << 16 ) | ( hash [ 1 ] << 8 ) | hash [ 2 ] ) % 1000000 ;
3130
32- return baseTimestamp * 1000 + tiebreaker ;
31+ return baseSeconds * 1000000 + tiebreaker ;
3332}
3433
3534// Replicate the formatStatus function logic
@@ -96,13 +95,14 @@ test('Export functionality tests', async t => {
9695 assert . ok ( / ^ [ 0 - 9 a - f ] + $ / . test ( hexPart ) , 'Hex part should only contain hex characters' ) ;
9796 } ) ;
9897
99- // Score calculation tests - using messageId hash for tiebreaker
98+ // Score calculation tests - using SHA-256 hash of composite key for tiebreaker
99+ // Production algorithm: lib/export.js Export.queueMessage()
100100 await t . test ( 'Score calculation: different messageIds with same timestamp produce different scores' , async ( ) => {
101101 const baseTimestamp = 1700000000000 ;
102102
103- const score1 = calculateScore ( baseTimestamp , 'msg_001' ) ;
104- const score2 = calculateScore ( baseTimestamp , 'msg_002' ) ;
105- const score3 = calculateScore ( baseTimestamp , 'msg_003' ) ;
103+ const score1 = calculateScore ( baseTimestamp , 'msg_001' , 'INBOX' , 1 ) ;
104+ const score2 = calculateScore ( baseTimestamp , 'msg_002' , 'INBOX' , 2 ) ;
105+ const score3 = calculateScore ( baseTimestamp , 'msg_003' , 'INBOX' , 3 ) ;
106106
107107 assert . notStrictEqual ( score1 , score2 ) ;
108108 assert . notStrictEqual ( score2 , score3 ) ;
@@ -114,41 +114,41 @@ test('Export functionality tests', async t => {
114114 const laterTimestamp = 1700000001000 ;
115115
116116 // Even with different messageIds, earlier timestamp should have lower score
117- const scoreEarlier = calculateScore ( earlierTimestamp , 'msg_zzz' ) ;
118- const scoreLater = calculateScore ( laterTimestamp , 'msg_aaa' ) ;
117+ const scoreEarlier = calculateScore ( earlierTimestamp , 'msg_zzz' , 'INBOX' , 1 ) ;
118+ const scoreLater = calculateScore ( laterTimestamp , 'msg_aaa' , 'INBOX' , 2 ) ;
119119
120120 assert . ok ( scoreEarlier < scoreLater , 'Earlier timestamp should produce lower score' ) ;
121121 } ) ;
122122
123- await t . test ( 'Score calculation: same messageId produces same tiebreaker ' , async ( ) => {
123+ await t . test ( 'Score calculation: same inputs produce same score ' , async ( ) => {
124124 const timestamp = 1700000000000 ;
125125
126- const score1 = calculateScore ( timestamp , 'consistent_id' ) ;
127- const score2 = calculateScore ( timestamp , 'consistent_id' ) ;
126+ const score1 = calculateScore ( timestamp , 'consistent_id' , 'INBOX' , 100 ) ;
127+ const score2 = calculateScore ( timestamp , 'consistent_id' , 'INBOX' , 100 ) ;
128128
129- assert . strictEqual ( score1 , score2 , 'Same messageId should produce same score' ) ;
129+ assert . strictEqual ( score1 , score2 , 'Same inputs should produce same score' ) ;
130130 } ) ;
131131
132132 await t . test ( 'Score calculation: handles Date objects' , async ( ) => {
133133 const date = new Date ( 1700000000000 ) ;
134134 const timestamp = 1700000000000 ;
135135 const messageId = 'msg_test' ;
136136
137- const scoreFromDate = calculateScore ( date , messageId ) ;
138- const scoreFromTimestamp = calculateScore ( timestamp , messageId ) ;
137+ const scoreFromDate = calculateScore ( date , messageId , 'INBOX' , 1 ) ;
138+ const scoreFromTimestamp = calculateScore ( timestamp , messageId , 'INBOX' , 1 ) ;
139139
140140 assert . strictEqual ( scoreFromDate , scoreFromTimestamp , 'Date object and timestamp should produce same score' ) ;
141141 } ) ;
142142
143143 await t . test ( 'Score calculation: handles null/undefined/empty messageId' , async ( ) => {
144144 const timestamp = 1700000000000 ;
145145
146- const scoreNull = calculateScore ( timestamp , null ) ;
147- const scoreUndefined = calculateScore ( timestamp , undefined ) ;
148- const scoreEmpty = calculateScore ( timestamp , '' ) ;
146+ const scoreNull = calculateScore ( timestamp , null , null , null ) ;
147+ const scoreUndefined = calculateScore ( timestamp , undefined , undefined , undefined ) ;
148+ const scoreEmpty = calculateScore ( timestamp , '' , '' , '' ) ;
149149
150- assert . strictEqual ( scoreNull , scoreEmpty , 'null messageId should be treated as empty' ) ;
151- assert . strictEqual ( scoreUndefined , scoreEmpty , 'undefined messageId should be treated as empty' ) ;
150+ assert . strictEqual ( scoreNull , scoreEmpty , 'null inputs should be treated as empty' ) ;
151+ assert . strictEqual ( scoreUndefined , scoreEmpty , 'undefined inputs should be treated as empty' ) ;
152152 } ) ;
153153
154154 await t . test ( 'Score calculation: long messageIds work correctly' , async ( ) => {
@@ -158,8 +158,8 @@ test('Export functionality tests', async t => {
158158 const outlookId = 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAADUuTJK1K9sTpCdqXop_4NaBwCd9nJ-tVysQYj2Cekan9XRAAAAAAEMAAC' ;
159159 const gmailId = '18abc123def456789' ;
160160
161- const outlookScore = calculateScore ( timestamp , outlookId ) ;
162- const gmailScore = calculateScore ( timestamp , gmailId ) ;
161+ const outlookScore = calculateScore ( timestamp , outlookId , 'INBOX' , 1 ) ;
162+ const gmailScore = calculateScore ( timestamp , gmailId , 'INBOX' , 2 ) ;
163163
164164 assert . strictEqual ( typeof outlookScore , 'number' ) ;
165165 assert . strictEqual ( typeof gmailScore , 'number' ) ;
@@ -171,10 +171,10 @@ test('Export functionality tests', async t => {
171171 const timestamp = 1700000000000 ;
172172 const messageIds = [ 'msg_001' , 'msg_002' , 'msg_003' , 'msg_004' , 'msg_005' , 'msg_100' , 'msg_200' , 'msg_300' , 'msg_400' , 'msg_500' ] ;
173173
174- const scores = messageIds . map ( id => calculateScore ( timestamp , id ) ) ;
174+ const scores = messageIds . map ( ( id , i ) => calculateScore ( timestamp , id , 'INBOX' , i + 1 ) ) ;
175175 const uniqueScores = new Set ( scores ) ;
176176
177- assert . strictEqual ( uniqueScores . size , messageIds . length , 'All messageIds should produce unique scores' ) ;
177+ assert . strictEqual ( uniqueScores . size , messageIds . length , 'All messages should produce unique scores' ) ;
178178 } ) ;
179179
180180 // formatStatus tests
0 commit comments