Make player history failures non-fatal

This commit is contained in:
Jay
2026-07-26 19:40:58 -04:00
parent 1ec3dd1276
commit 843957d154
3 changed files with 262 additions and 53 deletions
@@ -0,0 +1,122 @@
package com.jsjdesigns.poker
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class PlayerHistoryStoreTest {
private class FakePersistence(
var stored: PlayerHistory = PlayerHistory(),
var readFailure: RuntimeException? = null,
var writeResult: Boolean = true,
var writeFailure: RuntimeException? = null,
) : PlayerHistoryPersistence {
var reads = 0
var writes = 0
override fun read(): PlayerHistory {
reads++
readFailure?.let { throw it }
return stored
}
override fun write(history: PlayerHistory): Boolean {
writes++
writeFailure?.let { throw it }
if (writeResult) stored = history
return writeResult
}
}
@Test
fun `schema dispatch has an explicit migration branch`() {
assertEquals(HistorySchemaPlan.FRESH, historySchemaPlan(0))
assertEquals(HistorySchemaPlan.READ_VERSION_1, historySchemaPlan(1))
assertEquals(HistorySchemaPlan.UNSUPPORTED, historySchemaPlan(2))
assertEquals(HistorySchemaPlan.UNSUPPORTED, historySchemaPlan(99))
assertEquals(HistorySchemaPlan.UNSUPPORTED, historySchemaPlan(-1))
}
@Test
fun `newer schema falls back in memory without overwrite or crash`() = runTest {
val persistence = FakePersistence(
readFailure = UnsupportedPlayerHistorySchema(99),
)
val failures = mutableListOf<String>()
val store = ResilientPlayerHistoryStore(persistence) { message, _ ->
failures += message
}
assertEquals(PlayerHistory(), store.load())
val currentSession = store.recordSession("Jay", coachEnabled = true)
assertEquals("Jay", currentSession.playerName)
assertTrue(currentSession.coachEnabled)
assertEquals(1L, currentSession.sessionsStarted)
assertEquals("fallback remains useful in this process", currentSession, store.load())
assertEquals(1, persistence.reads)
assertEquals("an older build must not overwrite newer data", 0, persistence.writes)
assertEquals(1, failures.size)
}
@Test
fun `corrupt history also degrades to an in-memory session`() = runTest {
val persistence = FakePersistence(
readFailure = IllegalArgumentException("corrupt counter"),
)
val store = ResilientPlayerHistoryStore(persistence)
assertEquals(PlayerHistory(), store.load())
assertEquals(1L, store.recordCompletedHand().handsCompleted)
assertEquals(0, persistence.writes)
}
@Test
fun `failed commit disables further writes but keeps current counters`() = runTest {
val persistence = FakePersistence(writeResult = false)
val failures = mutableListOf<String>()
val store = ResilientPlayerHistoryStore(persistence) { message, _ ->
failures += message
}
assertEquals(1L, store.recordCompletedHand().handsCompleted)
assertEquals(2L, store.recordCompletedHand().handsCompleted)
assertEquals("do not repeatedly hammer failed storage", 1, persistence.writes)
assertEquals(1, failures.size)
assertTrue("commit" in failures.single())
}
@Test
fun `write exception is contained like a false commit`() = runTest {
val persistence = FakePersistence(
writeFailure = IllegalStateException("disk unavailable"),
)
val store = ResilientPlayerHistoryStore(persistence)
assertEquals(1L, store.recordCompletedHand().handsCompleted)
assertEquals(2L, store.recordCompletedHand().handsCompleted)
assertEquals(1, persistence.writes)
}
@Test
fun `concurrent hand updates cannot clobber one another`() = runTest {
val persistence = FakePersistence()
val store = ResilientPlayerHistoryStore(persistence)
coroutineScope {
repeat(100) {
launch { store.recordCompletedHand() }
}
}
assertEquals(100L, store.load().handsCompleted)
assertEquals(100, persistence.writes)
assertFalse(persistence.stored.coachEnabled)
}
}