@@ -325,13 +325,32 @@ class FakeSessionManager {
325325 }
326326}
327327
328+ type FakeFinishedGame = {
329+ id : string ;
330+ players : Array < { playerId : string ; profileId : string | null } > ;
331+ gameResult : { winningPlayerId : string | null } | null ;
332+ sessionId ?: string ;
333+ } ;
334+
328335class FakeGameHistoryRepository {
329- async getFinishedGame ( ) : Promise < null > {
336+ finishedGames = new Map < string , FakeFinishedGame > ( ) ;
337+
338+ async getFinishedGame ( ) : Promise < FakeFinishedGame | null > {
330339 return null ;
331340 }
332341
333- async getFinishedGameBySessionId ( ) : Promise < null > {
334- return null ;
342+ async getFinishedGameBySessionId ( sessionId : string ) : Promise < FakeFinishedGame | null > {
343+ return [ ...this . finishedGames . values ( ) ] . find ( ( game ) => game . sessionId === sessionId ) ?? null ;
344+ }
345+ }
346+
347+ class FinishedGameByIdRepository extends FakeGameHistoryRepository {
348+ constructor ( private readonly game : FakeFinishedGame ) {
349+ super ( ) ;
350+ }
351+
352+ override async getFinishedGame ( ) : Promise < FakeFinishedGame | null > {
353+ return this . game ;
335354 }
336355}
337356
@@ -416,6 +435,7 @@ function createServiceWithOverrides(options: {
416435 sessionManager ?: FakeSessionManager ;
417436 eventSink ?: FakeTournamentEventSink ;
418437 authRepository ?: FakeAuthRepository ;
438+ gameHistoryRepository ?: FakeGameHistoryRepository ;
419439} ) {
420440 const repository = options . repository ;
421441 const sessionManager = options . sessionManager ?? new FakeSessionManager ( ) ;
@@ -425,7 +445,7 @@ function createServiceWithOverrides(options: {
425445 repository as never ,
426446 authRepository as never ,
427447 sessionManager as never ,
428- new FakeGameHistoryRepository ( ) as never ,
448+ ( options . gameHistoryRepository ?? new FakeGameHistoryRepository ( ) ) as never ,
429449 ) ;
430450 service . setEventHandlers ( eventSink ) ;
431451
@@ -1334,6 +1354,75 @@ test(`getTournamentDetail refreshes double-elimination participant statuses when
13341354 assert . equal ( runnerUp ?. status , `eliminated` ) ;
13351355} ) ;
13361356
1357+ test ( `getTournamentDetail repairs stale best-of session recovery without repeated updatedAt changes` , async ( ) => {
1358+ const tournament = createTournament ( {
1359+ status : `live` ,
1360+ format : `double-elimination` ,
1361+ updatedAt : 123 ,
1362+ seriesSettings : {
1363+ earlyRoundsBestOf : 3 ,
1364+ finalsBestOf : 3 ,
1365+ grandFinalBestOf : 5 ,
1366+ grandFinalResetEnabled : true ,
1367+ } ,
1368+ participants : [
1369+ createParticipant ( { profileId : `player-1` , displayName : `Player 1` , registeredAt : 1 , checkedInAt : 11 , status : `checked-in` , checkInState : `checked-in` , seed : 1 } ) ,
1370+ createParticipant ( { profileId : `player-2` , displayName : `Player 2` , registeredAt : 2 , checkedInAt : 12 , status : `checked-in` , checkInState : `checked-in` , seed : 2 } ) ,
1371+ ] ,
1372+ matches : [
1373+ createMatch ( {
1374+ id : `match-winners-1-1` ,
1375+ bracket : `winners` ,
1376+ round : 1 ,
1377+ order : 1 ,
1378+ state : `in-progress` ,
1379+ bestOf : 3 ,
1380+ sessionId : `missing-game-two-session` ,
1381+ startedAt : 456 ,
1382+ gameIds : [ `finished-game-one` ] ,
1383+ currentGameNumber : 2 ,
1384+ leftWins : 1 ,
1385+ rightWins : 0 ,
1386+ slots : [
1387+ createSlot ( { profileId : `player-1` , displayName : `Player 1` , seed : 1 } ) ,
1388+ createSlot ( { profileId : `player-2` , displayName : `Player 2` , seed : 2 } ) ,
1389+ ] ,
1390+ } ) ,
1391+ ] ,
1392+ } ) ;
1393+ const repository = new FakeTournamentRepository ( tournament ) ;
1394+ const gameHistoryRepository = new FinishedGameByIdRepository ( {
1395+ id : `finished-game-one` ,
1396+ players : [
1397+ { playerId : `left-player` , profileId : `player-1` } ,
1398+ { playerId : `right-player` , profileId : `player-2` } ,
1399+ ] ,
1400+ gameResult : { winningPlayerId : `left-player` } ,
1401+ } ) ;
1402+ const { service } = createServiceWithOverrides ( {
1403+ repository,
1404+ gameHistoryRepository,
1405+ } ) ;
1406+
1407+ await service . getTournamentDetail ( tournament . id , null ) ;
1408+
1409+ const repaired = repository . getSync ( tournament . id ) . matches [ 0 ] ! ;
1410+ assert . equal ( repaired . state , `ready` ) ;
1411+ assert . equal ( repaired . sessionId , null ) ;
1412+ assert . deepEqual ( repaired . gameIds , [ `finished-game-one` ] ) ;
1413+ assert . equal ( repaired . leftWins , 1 ) ;
1414+ assert . equal ( repaired . currentGameNumber , 2 ) ;
1415+
1416+ await service . getTournamentDetail ( tournament . id , null ) ;
1417+ const recreated = repository . getSync ( tournament . id ) ;
1418+ const recreatedMatch = recreated . matches [ 0 ] ! ;
1419+ assert . equal ( recreatedMatch . state , `in-progress` ) ;
1420+ assert . notEqual ( recreatedMatch . sessionId , null ) ;
1421+
1422+ await service . getTournamentDetail ( tournament . id , null ) ;
1423+ assert . equal ( repository . getSync ( tournament . id ) . updatedAt , recreated . updatedAt ) ;
1424+ } ) ;
1425+
13371426test ( `roundDelayMinutes keeps the grand final pending until its cooldown expires` , async ( ) => {
13381427 const now = Date . now ( ) ;
13391428 const tournament = createTournament ( {
0 commit comments