@@ -1074,6 +1074,308 @@ describe("coffeeChatService", () => {
10741074
10751075 expect ( mockPostMessage ) . not . toHaveBeenCalled ( ) ;
10761076 } ) ;
1077+
1078+ it ( "should include a leaderboard block when there are all-time confirmed meetups" , async ( ) => {
1079+ const mockChannelId = "C12345" ;
1080+ const mockChannelName = "coffee-chats" ;
1081+ const mockPairingFrequencyDays = 14 ;
1082+
1083+ await new CoffeeChatConfigModel ( {
1084+ channelId : mockChannelId ,
1085+ channelName : mockChannelName ,
1086+ isActive : true ,
1087+ pairingFrequencyDays : mockPairingFrequencyDays ,
1088+ } ) . save ( ) ;
1089+
1090+ // Pairing within the reporting window (counts toward period stats)
1091+ await new CoffeeChatPairingModel ( {
1092+ channelId : mockChannelId ,
1093+ userIds : [ "U1" , "U2" ] ,
1094+ createdAt : moment ( )
1095+ . tz ( "America/New_York" )
1096+ . subtract ( 10 , "days" )
1097+ . toDate ( ) ,
1098+ dueDate : moment ( ) . tz ( "America/New_York" ) . subtract ( 9 , "days" ) . toDate ( ) ,
1099+ meetupConfirmed : true ,
1100+ } ) . save ( ) ;
1101+
1102+ // Additional all-time confirmed pairings to build up the leaderboard
1103+ await new CoffeeChatPairingModel ( {
1104+ channelId : mockChannelId ,
1105+ userIds : [ "U1" , "U3" ] ,
1106+ createdAt : moment ( )
1107+ . tz ( "America/New_York" )
1108+ . subtract ( 10 , "days" )
1109+ . toDate ( ) ,
1110+ dueDate : moment ( ) . tz ( "America/New_York" ) . subtract ( 9 , "days" ) . toDate ( ) ,
1111+ meetupConfirmed : true ,
1112+ } ) . save ( ) ;
1113+
1114+ await coffeeChatService . reportStats ( ) ;
1115+
1116+ const mockPostMessage =
1117+ jest . requireMock ( "../../src/slackbot" ) . default . client . chat . postMessage ;
1118+
1119+ expect ( mockPostMessage ) . toHaveBeenCalledTimes ( 1 ) ;
1120+
1121+ const blocks : Array < {
1122+ type : string ;
1123+ text ?: { type : string ; text : string } ;
1124+ } > = mockPostMessage . mock . calls [ 0 ] [ 0 ] . blocks ;
1125+
1126+ // Leaderboard block must be present
1127+ const leaderboardBlock = blocks . find (
1128+ ( b ) =>
1129+ b . type === "section" &&
1130+ b . text ?. text . includes ( "All-Time Meetup Leaderboard" ) ,
1131+ ) ;
1132+ expect ( leaderboardBlock ) . toBeDefined ( ) ;
1133+
1134+ // U1 appears in 2 confirmed pairings and must be ranked first
1135+ expect ( leaderboardBlock ?. text ?. text ) . toContain ( "<@U1>" ) ;
1136+ expect ( leaderboardBlock ?. text ?. text ) . toContain ( "🥇" ) ;
1137+ } ) ;
1138+
1139+ it ( "should rank leaderboard entries by meetup count descending" , async ( ) => {
1140+ const mockChannelId = "C12345" ;
1141+ const mockChannelName = "coffee-chats" ;
1142+ const mockPairingFrequencyDays = 14 ;
1143+
1144+ await new CoffeeChatConfigModel ( {
1145+ channelId : mockChannelId ,
1146+ channelName : mockChannelName ,
1147+ isActive : true ,
1148+ pairingFrequencyDays : mockPairingFrequencyDays ,
1149+ } ) . save ( ) ;
1150+
1151+ const recentDueDate = moment ( )
1152+ . tz ( "America/New_York" )
1153+ . subtract ( 1 , "days" )
1154+ . toDate ( ) ;
1155+ const recentCreatedAt = moment ( )
1156+ . tz ( "America/New_York" )
1157+ . subtract ( 10 , "days" )
1158+ . toDate ( ) ;
1159+
1160+ // U_A: 3 meetups, U_B: 2 meetups, U_C: 1 meetup
1161+ for ( let i = 0 ; i < 3 ; i ++ ) {
1162+ await new CoffeeChatPairingModel ( {
1163+ channelId : mockChannelId ,
1164+ userIds : [ "U_A" , `U_OTHER_${ i } ` ] ,
1165+ createdAt : recentCreatedAt ,
1166+ dueDate : recentDueDate ,
1167+ meetupConfirmed : true ,
1168+ } ) . save ( ) ;
1169+ }
1170+ for ( let i = 0 ; i < 2 ; i ++ ) {
1171+ await new CoffeeChatPairingModel ( {
1172+ channelId : mockChannelId ,
1173+ userIds : [ "U_B" , `U_EXTRA_${ i } ` ] ,
1174+ createdAt : recentCreatedAt ,
1175+ dueDate : recentDueDate ,
1176+ meetupConfirmed : true ,
1177+ } ) . save ( ) ;
1178+ }
1179+ await new CoffeeChatPairingModel ( {
1180+ channelId : mockChannelId ,
1181+ userIds : [ "U_C" , "U_LAST" ] ,
1182+ createdAt : recentCreatedAt ,
1183+ dueDate : recentDueDate ,
1184+ meetupConfirmed : true ,
1185+ } ) . save ( ) ;
1186+
1187+ await coffeeChatService . reportStats ( ) ;
1188+
1189+ const mockPostMessage =
1190+ jest . requireMock ( "../../src/slackbot" ) . default . client . chat . postMessage ;
1191+
1192+ const blocks : Array < {
1193+ type : string ;
1194+ text ?: { type : string ; text : string } ;
1195+ } > = mockPostMessage . mock . calls [ 0 ] [ 0 ] . blocks ;
1196+
1197+ const leaderboardBlock = blocks . find (
1198+ ( b ) =>
1199+ b . type === "section" &&
1200+ b . text ?. text . includes ( "All-Time Meetup Leaderboard" ) ,
1201+ ) ;
1202+
1203+ expect ( leaderboardBlock ) . toBeDefined ( ) ;
1204+
1205+ const text = leaderboardBlock ! . text ! . text ;
1206+
1207+ // U_A should appear before U_B, which should appear before U_C
1208+ expect ( text . indexOf ( "<@U_A>" ) ) . toBeLessThan ( text . indexOf ( "<@U_B>" ) ) ;
1209+ expect ( text . indexOf ( "<@U_B>" ) ) . toBeLessThan ( text . indexOf ( "<@U_C>" ) ) ;
1210+
1211+ // Gold medal goes to U_A
1212+ const aLine = text
1213+ . split ( "\n" )
1214+ . find ( ( line : string ) => line . includes ( "<@U_A>" ) ) ;
1215+ expect ( aLine ) . toContain ( "🥇" ) ;
1216+
1217+ // Silver medal goes to U_B
1218+ const bLine = text
1219+ . split ( "\n" )
1220+ . find ( ( line : string ) => line . includes ( "<@U_B>" ) ) ;
1221+ expect ( bLine ) . toContain ( "🥈" ) ;
1222+ } ) ;
1223+
1224+ it ( "should cap the leaderboard at 10 entries" , async ( ) => {
1225+ const mockChannelId = "C12345" ;
1226+ const mockChannelName = "coffee-chats" ;
1227+ const mockPairingFrequencyDays = 14 ;
1228+
1229+ await new CoffeeChatConfigModel ( {
1230+ channelId : mockChannelId ,
1231+ channelName : mockChannelName ,
1232+ isActive : true ,
1233+ pairingFrequencyDays : mockPairingFrequencyDays ,
1234+ } ) . save ( ) ;
1235+
1236+ const recentDueDate = moment ( )
1237+ . tz ( "America/New_York" )
1238+ . subtract ( 1 , "days" )
1239+ . toDate ( ) ;
1240+ const recentCreatedAt = moment ( )
1241+ . tz ( "America/New_York" )
1242+ . subtract ( 10 , "days" )
1243+ . toDate ( ) ;
1244+
1245+ // Create 12 distinct users, each with 1 confirmed meetup
1246+ for ( let i = 1 ; i <= 12 ; i ++ ) {
1247+ await new CoffeeChatPairingModel ( {
1248+ channelId : mockChannelId ,
1249+ userIds : [ `U_TOP${ i } ` , `U_PARTNER${ i } ` ] ,
1250+ createdAt : recentCreatedAt ,
1251+ dueDate : recentDueDate ,
1252+ meetupConfirmed : true ,
1253+ } ) . save ( ) ;
1254+ }
1255+
1256+ await coffeeChatService . reportStats ( ) ;
1257+
1258+ const mockPostMessage =
1259+ jest . requireMock ( "../../src/slackbot" ) . default . client . chat . postMessage ;
1260+
1261+ const blocks : Array < {
1262+ type : string ;
1263+ text ?: { type : string ; text : string } ;
1264+ } > = mockPostMessage . mock . calls [ 0 ] [ 0 ] . blocks ;
1265+
1266+ const leaderboardBlock = blocks . find (
1267+ ( b ) =>
1268+ b . type === "section" &&
1269+ b . text ?. text . includes ( "All-Time Meetup Leaderboard" ) ,
1270+ ) ;
1271+
1272+ expect ( leaderboardBlock ) . toBeDefined ( ) ;
1273+
1274+ // Only 10 lines after the header line
1275+ const lines = leaderboardBlock !
1276+ . text ! . text . split ( "\n" )
1277+ . filter ( ( line : string ) => line . includes ( "<@" ) ) ;
1278+ expect ( lines . length ) . toBe ( 10 ) ;
1279+ } ) ;
1280+
1281+ it ( "should omit the leaderboard block when there are no all-time confirmed meetups" , async ( ) => {
1282+ const mockChannelId = "C12345" ;
1283+ const mockChannelName = "coffee-chats" ;
1284+ const mockPairingFrequencyDays = 14 ;
1285+
1286+ await new CoffeeChatConfigModel ( {
1287+ channelId : mockChannelId ,
1288+ channelName : mockChannelName ,
1289+ isActive : true ,
1290+ pairingFrequencyDays : mockPairingFrequencyDays ,
1291+ } ) . save ( ) ;
1292+
1293+ // One period pairing that is NOT confirmed — still triggers the stats post
1294+ await new CoffeeChatPairingModel ( {
1295+ channelId : mockChannelId ,
1296+ userIds : [ "U1" , "U2" ] ,
1297+ createdAt : moment ( )
1298+ . tz ( "America/New_York" )
1299+ . subtract ( 10 , "days" )
1300+ . toDate ( ) ,
1301+ dueDate : moment ( ) . tz ( "America/New_York" ) . subtract ( 9 , "days" ) . toDate ( ) ,
1302+ meetupConfirmed : false ,
1303+ } ) . save ( ) ;
1304+
1305+ await coffeeChatService . reportStats ( ) ;
1306+
1307+ const mockPostMessage =
1308+ jest . requireMock ( "../../src/slackbot" ) . default . client . chat . postMessage ;
1309+
1310+ expect ( mockPostMessage ) . toHaveBeenCalledTimes ( 1 ) ;
1311+
1312+ const blocks : Array < {
1313+ type : string ;
1314+ text ?: { type : string ; text : string } ;
1315+ } > = mockPostMessage . mock . calls [ 0 ] [ 0 ] . blocks ;
1316+
1317+ const leaderboardBlock = blocks . find (
1318+ ( b ) =>
1319+ b . type === "section" &&
1320+ b . text ?. text . includes ( "All-Time Meetup Leaderboard" ) ,
1321+ ) ;
1322+ expect ( leaderboardBlock ) . toBeUndefined ( ) ;
1323+ } ) ;
1324+
1325+ it ( "should not include meetups from other channels in the leaderboard" , async ( ) => {
1326+ const mockChannelId = "C12345" ;
1327+ const otherChannelId = "C_OTHER" ;
1328+ const mockPairingFrequencyDays = 14 ;
1329+
1330+ await new CoffeeChatConfigModel ( {
1331+ channelId : mockChannelId ,
1332+ channelName : "coffee-chats" ,
1333+ isActive : true ,
1334+ pairingFrequencyDays : mockPairingFrequencyDays ,
1335+ } ) . save ( ) ;
1336+
1337+ // Pairing in the target channel
1338+ await new CoffeeChatPairingModel ( {
1339+ channelId : mockChannelId ,
1340+ userIds : [ "U1" , "U2" ] ,
1341+ createdAt : moment ( )
1342+ . tz ( "America/New_York" )
1343+ . subtract ( 10 , "days" )
1344+ . toDate ( ) ,
1345+ dueDate : moment ( ) . tz ( "America/New_York" ) . subtract ( 9 , "days" ) . toDate ( ) ,
1346+ meetupConfirmed : true ,
1347+ } ) . save ( ) ;
1348+
1349+ // Pairing in a different channel — U_OTHER should NOT appear in leaderboard
1350+ await new CoffeeChatPairingModel ( {
1351+ channelId : otherChannelId ,
1352+ userIds : [ "U_OTHER" , "U_OTHER2" ] ,
1353+ createdAt : moment ( )
1354+ . tz ( "America/New_York" )
1355+ . subtract ( 10 , "days" )
1356+ . toDate ( ) ,
1357+ dueDate : moment ( ) . tz ( "America/New_York" ) . subtract ( 9 , "days" ) . toDate ( ) ,
1358+ meetupConfirmed : true ,
1359+ } ) . save ( ) ;
1360+
1361+ await coffeeChatService . reportStats ( ) ;
1362+
1363+ const mockPostMessage =
1364+ jest . requireMock ( "../../src/slackbot" ) . default . client . chat . postMessage ;
1365+
1366+ const blocks : Array < {
1367+ type : string ;
1368+ text ?: { type : string ; text : string } ;
1369+ } > = mockPostMessage . mock . calls [ 0 ] [ 0 ] . blocks ;
1370+
1371+ const leaderboardBlock = blocks . find (
1372+ ( b ) =>
1373+ b . type === "section" &&
1374+ b . text ?. text . includes ( "All-Time Meetup Leaderboard" ) ,
1375+ ) ;
1376+
1377+ expect ( leaderboardBlock ?. text ?. text ) . not . toContain ( "<@U_OTHER>" ) ;
1378+ } ) ;
10771379 } ) ;
10781380
10791381 it ( "should not report stats if there is a previous pairing in a round long ago" , async ( ) => {
0 commit comments