feat: orchestrate infrared key learning
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.InfraredGateway
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
|
||||
class LearnInfraredKey(
|
||||
private val repository: InfraredKeyRepository,
|
||||
private val gateway: InfraredGateway,
|
||||
private val factory: InfraredKeyFactory,
|
||||
private val clock: EpochMillisClock,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
displayName: String,
|
||||
): DomainResult<InfraredKeyId> {
|
||||
val key = when (val result = factory.create(profileId, deviceName, displayName)) {
|
||||
is DomainResult.Success -> result.value
|
||||
is DomainResult.Failure -> return result
|
||||
}
|
||||
|
||||
when (val result = repository.insert(key)) {
|
||||
is DomainResult.Success -> Unit
|
||||
is DomainResult.Failure -> return result
|
||||
}
|
||||
|
||||
return when (
|
||||
val result = gateway.learnKey(
|
||||
profileId = key.profileId,
|
||||
deviceName = key.deviceName,
|
||||
key = key.sdkKey,
|
||||
)
|
||||
) {
|
||||
is DomainResult.Success -> when (
|
||||
val transition = repository.transitionStatus(
|
||||
ref = key.ref(),
|
||||
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
|
||||
targetStatus = InfraredKeyStatus.READY,
|
||||
lastErrorCode = null,
|
||||
updatedAtEpochMillis = clock.now(),
|
||||
)
|
||||
) {
|
||||
is DomainResult.Success -> DomainResult.Success(key.id)
|
||||
is DomainResult.Failure -> transition
|
||||
}
|
||||
|
||||
is DomainResult.Failure -> {
|
||||
repository.transitionStatus(
|
||||
ref = key.ref(),
|
||||
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
|
||||
targetStatus = InfraredKeyStatus.FAILED,
|
||||
lastErrorCode = result.error.externalCode,
|
||||
updatedAtEpochMillis = clock.now(),
|
||||
)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RetryInfraredKeyLearning(
|
||||
private val repository: InfraredKeyRepository,
|
||||
private val gateway: InfraredGateway,
|
||||
private val clock: EpochMillisClock,
|
||||
) {
|
||||
suspend operator fun invoke(ref: InfraredKeyRef): DomainResult<Unit> {
|
||||
val key = when (val result = repository.getKey(ref)) {
|
||||
is DomainResult.Success -> result.value
|
||||
?: return DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Infrared key not found"),
|
||||
)
|
||||
|
||||
is DomainResult.Failure -> return result
|
||||
}
|
||||
if (key.status == InfraredKeyStatus.LEARNING) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Infrared key is already learning"),
|
||||
)
|
||||
}
|
||||
|
||||
when (
|
||||
val result = repository.transitionStatus(
|
||||
ref = ref,
|
||||
expectedStatuses = setOf(
|
||||
InfraredKeyStatus.FAILED,
|
||||
InfraredKeyStatus.READY,
|
||||
),
|
||||
targetStatus = InfraredKeyStatus.LEARNING,
|
||||
lastErrorCode = null,
|
||||
updatedAtEpochMillis = clock.now(),
|
||||
)
|
||||
) {
|
||||
is DomainResult.Success -> Unit
|
||||
is DomainResult.Failure -> return result
|
||||
}
|
||||
|
||||
return when (
|
||||
val result = gateway.learnKey(
|
||||
profileId = key.profileId,
|
||||
deviceName = key.deviceName,
|
||||
key = key.sdkKey,
|
||||
)
|
||||
) {
|
||||
is DomainResult.Success -> repository.transitionStatus(
|
||||
ref = ref,
|
||||
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
|
||||
targetStatus = InfraredKeyStatus.READY,
|
||||
lastErrorCode = null,
|
||||
updatedAtEpochMillis = clock.now(),
|
||||
)
|
||||
|
||||
is DomainResult.Failure -> {
|
||||
repository.transitionStatus(
|
||||
ref = ref,
|
||||
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
|
||||
targetStatus = InfraredKeyStatus.FAILED,
|
||||
lastErrorCode = result.error.externalCode,
|
||||
updatedAtEpochMillis = clock.now(),
|
||||
)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SavedInfraredKey.ref() = InfraredKeyRef(
|
||||
profileId = profileId,
|
||||
keyId = id,
|
||||
)
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.InfraredCode
|
||||
import com.flagship.abox.manager.domain.InfraredCodeDownloadResult
|
||||
import com.flagship.abox.manager.domain.InfraredGateway
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
data class InfraredLearningCall(
|
||||
val profileId: ProviderProfileId,
|
||||
val deviceName: DeviceName,
|
||||
val key: String,
|
||||
)
|
||||
|
||||
data class InfraredStatusTransitionCall(
|
||||
val ref: InfraredKeyRef,
|
||||
val expectedStatuses: Set<InfraredKeyStatus>,
|
||||
val targetStatus: InfraredKeyStatus,
|
||||
val lastErrorCode: String?,
|
||||
val updatedAtEpochMillis: Long,
|
||||
)
|
||||
|
||||
class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
||||
private val keys = linkedMapOf<InfraredKeyRef, SavedInfraredKey>()
|
||||
private val state = MutableStateFlow<List<SavedInfraredKey>>(emptyList())
|
||||
|
||||
var getError: DomainError? = null
|
||||
var insertError: DomainError? = null
|
||||
val transitionErrors = mutableMapOf<InfraredKeyStatus, DomainError>()
|
||||
|
||||
var onInsert: ((SavedInfraredKey) -> Unit)? = null
|
||||
var onTransition: ((InfraredStatusTransitionCall) -> Unit)? = null
|
||||
|
||||
val insertedKeys = mutableListOf<SavedInfraredKey>()
|
||||
val transitionCalls = mutableListOf<InfraredStatusTransitionCall>()
|
||||
|
||||
override fun observeKeys(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
): Flow<DomainResult<List<SavedInfraredKey>>> = state.map { values ->
|
||||
DomainResult.Success(
|
||||
values.filter { it.profileId == profileId && it.deviceName == deviceName },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getKey(ref: InfraredKeyRef): DomainResult<SavedInfraredKey?> {
|
||||
getError?.let { return DomainResult.Failure(it) }
|
||||
return DomainResult.Success(keys[ref])
|
||||
}
|
||||
|
||||
override suspend fun insert(key: SavedInfraredKey): DomainResult<Unit> {
|
||||
insertedKeys += key
|
||||
onInsert?.invoke(key)
|
||||
insertError?.let { return DomainResult.Failure(it) }
|
||||
keys[key.ref()] = key
|
||||
publish()
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun transitionStatus(
|
||||
ref: InfraredKeyRef,
|
||||
expectedStatuses: Set<InfraredKeyStatus>,
|
||||
targetStatus: InfraredKeyStatus,
|
||||
lastErrorCode: String?,
|
||||
updatedAtEpochMillis: Long,
|
||||
): DomainResult<Unit> {
|
||||
val call = InfraredStatusTransitionCall(
|
||||
ref = ref,
|
||||
expectedStatuses = expectedStatuses,
|
||||
targetStatus = targetStatus,
|
||||
lastErrorCode = lastErrorCode,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
transitionCalls += call
|
||||
onTransition?.invoke(call)
|
||||
transitionErrors[targetStatus]?.let { return DomainResult.Failure(it) }
|
||||
|
||||
val existing = keys[ref]
|
||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key not found"))
|
||||
if (existing.status !in expectedStatuses) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Infrared key status changed"),
|
||||
)
|
||||
}
|
||||
keys[ref] = existing.copy(
|
||||
status = targetStatus,
|
||||
lastErrorCode = lastErrorCode,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
publish()
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun rename(
|
||||
ref: InfraredKeyRef,
|
||||
displayName: String,
|
||||
updatedAtEpochMillis: Long,
|
||||
): DomainResult<Unit> {
|
||||
val existing = keys[ref]
|
||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key not found"))
|
||||
keys[ref] = existing.copy(
|
||||
displayName = displayName,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
publish()
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun deleteIfNotLearning(ref: InfraredKeyRef): DomainResult<Unit> {
|
||||
val existing = keys[ref]
|
||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key not found"))
|
||||
if (existing.status == InfraredKeyStatus.LEARNING) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Cannot delete a learning infrared key"),
|
||||
)
|
||||
}
|
||||
keys.remove(ref)
|
||||
publish()
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun failInterruptedLearning(
|
||||
profileId: ProviderProfileId,
|
||||
updatedAtEpochMillis: Long,
|
||||
): DomainResult<Int> {
|
||||
var changed = 0
|
||||
keys.replaceAll { _, key ->
|
||||
if (key.profileId == profileId && key.status == InfraredKeyStatus.LEARNING) {
|
||||
changed += 1
|
||||
key.copy(
|
||||
status = InfraredKeyStatus.FAILED,
|
||||
lastErrorCode = null,
|
||||
updatedAtEpochMillis = updatedAtEpochMillis,
|
||||
)
|
||||
} else {
|
||||
key
|
||||
}
|
||||
}
|
||||
publish()
|
||||
return DomainResult.Success(changed)
|
||||
}
|
||||
|
||||
fun seed(key: SavedInfraredKey) {
|
||||
keys[key.ref()] = key
|
||||
publish()
|
||||
}
|
||||
|
||||
fun onlyKey(): SavedInfraredKey = keys.values.single()
|
||||
|
||||
private fun publish() {
|
||||
state.value = keys.values.toList()
|
||||
}
|
||||
}
|
||||
|
||||
class RecordingInfraredGateway : InfraredGateway {
|
||||
var learnResult: DomainResult<Unit> = DomainResult.Success(Unit)
|
||||
var onLearn: ((ProviderProfileId, DeviceName, String) -> DomainResult<Unit>)? = null
|
||||
val learnedKeys = mutableListOf<InfraredLearningCall>()
|
||||
|
||||
override suspend fun learnKey(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
key: String,
|
||||
): DomainResult<Unit> {
|
||||
learnedKeys += InfraredLearningCall(profileId, deviceName, key)
|
||||
return onLearn?.invoke(profileId, deviceName, key) ?: learnResult
|
||||
}
|
||||
|
||||
override suspend fun sendKey(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
key: String,
|
||||
): DomainResult<Unit> = DomainResult.Success(Unit)
|
||||
|
||||
override suspend fun downloadCodes(
|
||||
profileId: ProviderProfileId,
|
||||
codes: List<InfraredCode>,
|
||||
): DomainResult<List<InfraredCodeDownloadResult>> = DomainResult.Success(emptyList())
|
||||
}
|
||||
|
||||
fun SavedInfraredKey.ref() = InfraredKeyRef(
|
||||
profileId = profileId,
|
||||
keyId = id,
|
||||
)
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
package com.flagship.abox.manager.domain.infrared
|
||||
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class LearnInfraredKeyTest {
|
||||
private val profile = ProviderProfileId("abox")
|
||||
private val device = DeviceName("ir-1")
|
||||
private val keyId = InfraredKeyId("key1")
|
||||
private val clock = SteppingClock(100L)
|
||||
private val factory = InfraredKeyFactory(
|
||||
idGenerator = InfraredKeyIdGenerator { keyId },
|
||||
clock = clock,
|
||||
)
|
||||
private val repository = InMemoryInfraredKeyRepository()
|
||||
private val gateway = RecordingInfraredGateway()
|
||||
private val learn = LearnInfraredKey(repository, gateway, factory, clock)
|
||||
private val retry = RetryInfraredKeyLearning(repository, gateway, clock)
|
||||
|
||||
@Test
|
||||
fun persistsLearningBeforeCallingHardwareThenMarksReady() = runTest {
|
||||
val events = mutableListOf<String>()
|
||||
repository.onInsert = { events += "insert:${it.status}" }
|
||||
gateway.onLearn = { _, _, _ ->
|
||||
events += "hardware"
|
||||
DomainResult.Success(Unit)
|
||||
}
|
||||
repository.onTransition = { events += "transition:${it.targetStatus}" }
|
||||
|
||||
val result = learn(profile, device, "Power")
|
||||
|
||||
assertEquals(DomainResult.Success(keyId), result)
|
||||
assertEquals(listOf("insert:LEARNING", "hardware", "transition:READY"), events)
|
||||
assertEquals(InfraredKeyStatus.READY, repository.onlyKey().status)
|
||||
assertEquals(null, repository.onlyKey().lastErrorCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hardwareFailureMarksFailedAndReturnsOriginalError() = runTest {
|
||||
val gatewayError = DomainError.DeviceOffline("20206")
|
||||
gateway.learnResult = DomainResult.Failure(gatewayError)
|
||||
|
||||
val result = learn(profile, device, "Power")
|
||||
|
||||
assertEquals(gatewayError, (result as DomainResult.Failure).error)
|
||||
assertEquals(InfraredKeyStatus.FAILED, repository.onlyKey().status)
|
||||
assertEquals("20206", repository.onlyKey().lastErrorCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertFailureNeverCallsHardware() = runTest {
|
||||
val insertError = DomainError.Unknown(detail = "database unavailable")
|
||||
repository.insertError = insertError
|
||||
|
||||
val result = learn(profile, device, "Power")
|
||||
|
||||
assertEquals(insertError, (result as DomainResult.Failure).error)
|
||||
assertTrue(gateway.learnedKeys.isEmpty())
|
||||
assertTrue(repository.transitionCalls.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun factoryFailureNeverTouchesRepositoryOrHardware() = runTest {
|
||||
val result = learn(profile, device, " ")
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||
assertTrue(repository.insertedKeys.isEmpty())
|
||||
assertTrue(repository.transitionCalls.isEmpty())
|
||||
assertTrue(gateway.learnedKeys.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readyTransitionFailureIsReturnedAfterHardwareSuccess() = runTest {
|
||||
val transitionError = DomainError.Unknown(detail = "write conflict")
|
||||
repository.transitionErrors[InfraredKeyStatus.READY] = transitionError
|
||||
|
||||
val result = learn(profile, device, "Power")
|
||||
|
||||
assertEquals(transitionError, (result as DomainResult.Failure).error)
|
||||
assertEquals(InfraredKeyStatus.LEARNING, repository.onlyKey().status)
|
||||
assertEquals(1, gateway.learnedKeys.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun failedTransitionErrorDoesNotReplaceGatewayFailure() = runTest {
|
||||
val gatewayError = DomainError.DeviceOffline("20206")
|
||||
gateway.learnResult = DomainResult.Failure(gatewayError)
|
||||
repository.transitionErrors[InfraredKeyStatus.FAILED] =
|
||||
DomainError.Unknown(detail = "write conflict")
|
||||
|
||||
val result = learn(profile, device, "Power")
|
||||
|
||||
assertEquals(gatewayError, (result as DomainResult.Failure).error)
|
||||
assertEquals(InfraredKeyStatus.LEARNING, repository.onlyKey().status)
|
||||
assertEquals("20206", repository.transitionCalls.single().lastErrorCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun newLearningUsesExpectedLearningTransitionsAndFreshTimestamps() = runTest {
|
||||
learn(profile, device, "Power")
|
||||
|
||||
val inserted = repository.insertedKeys.single()
|
||||
val transition = repository.transitionCalls.single()
|
||||
assertEquals(setOf(InfraredKeyStatus.LEARNING), transition.expectedStatuses)
|
||||
assertEquals(inserted.ref(), transition.ref)
|
||||
assertEquals(100L, inserted.createdAtEpochMillis)
|
||||
assertEquals(100L, inserted.updatedAtEpochMillis)
|
||||
assertEquals(101L, transition.updatedAtEpochMillis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryReusesStableIdAndSdkKey() = runTest {
|
||||
val original = failedKey(id = InfraredKeyId("key1"), sdkKey = "app_ir_key1")
|
||||
repository.seed(original)
|
||||
|
||||
val result = retry(original.ref())
|
||||
|
||||
assertEquals(DomainResult.Success(Unit), result)
|
||||
assertEquals("app_ir_key1", gateway.learnedKeys.single().key)
|
||||
assertEquals(InfraredKeyId("key1"), repository.onlyKey().id)
|
||||
assertEquals(InfraredKeyStatus.READY, repository.onlyKey().status)
|
||||
assertEquals(0, repository.insertedKeys.size)
|
||||
assertEquals(2, repository.transitionCalls.size)
|
||||
assertEquals(
|
||||
setOf(InfraredKeyStatus.FAILED, InfraredKeyStatus.READY),
|
||||
repository.transitionCalls.first().expectedStatuses,
|
||||
)
|
||||
assertEquals(
|
||||
setOf(InfraredKeyStatus.LEARNING),
|
||||
repository.transitionCalls.last().expectedStatuses,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryRejectsAlreadyLearningKey() = runTest {
|
||||
repository.seed(learningKey())
|
||||
|
||||
val result = retry(repository.onlyKey().ref())
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||
assertTrue(gateway.learnedKeys.isEmpty())
|
||||
assertTrue(repository.transitionCalls.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryRejectsMissingKey() = runTest {
|
||||
val result = retry(InfraredKeyRef(profile, InfraredKeyId("missing")))
|
||||
|
||||
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||
assertTrue(gateway.learnedKeys.isEmpty())
|
||||
assertTrue(repository.transitionCalls.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryReturnsGetFailureWithoutCallingHardware() = runTest {
|
||||
val getError = DomainError.Unknown(detail = "database unavailable")
|
||||
repository.getError = getError
|
||||
|
||||
val result = retry(InfraredKeyRef(profile, keyId))
|
||||
|
||||
assertEquals(getError, (result as DomainResult.Failure).error)
|
||||
assertTrue(gateway.learnedKeys.isEmpty())
|
||||
assertTrue(repository.transitionCalls.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryDoesNotCallHardwareWhenLearningTransitionFails() = runTest {
|
||||
val original = failedKey()
|
||||
repository.seed(original)
|
||||
val transitionError = DomainError.Unknown(detail = "write conflict")
|
||||
repository.transitionErrors[InfraredKeyStatus.LEARNING] = transitionError
|
||||
|
||||
val result = retry(original.ref())
|
||||
|
||||
assertEquals(transitionError, (result as DomainResult.Failure).error)
|
||||
assertTrue(gateway.learnedKeys.isEmpty())
|
||||
assertEquals(InfraredKeyStatus.FAILED, repository.onlyKey().status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryGatewayFailureMarksFailedWithExternalCodeAndReturnsOriginalError() = runTest {
|
||||
val original = failedKey()
|
||||
repository.seed(original)
|
||||
val gatewayError = DomainError.DeviceOffline("20206")
|
||||
gateway.learnResult = DomainResult.Failure(gatewayError)
|
||||
|
||||
val result = retry(original.ref())
|
||||
|
||||
assertEquals(gatewayError, (result as DomainResult.Failure).error)
|
||||
assertEquals(InfraredKeyStatus.FAILED, repository.onlyKey().status)
|
||||
assertEquals("20206", repository.onlyKey().lastErrorCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryFailedTransitionErrorDoesNotReplaceGatewayFailure() = runTest {
|
||||
val original = failedKey()
|
||||
repository.seed(original)
|
||||
val gatewayError = DomainError.DeviceOffline("20206")
|
||||
gateway.learnResult = DomainResult.Failure(gatewayError)
|
||||
repository.transitionErrors[InfraredKeyStatus.FAILED] =
|
||||
DomainError.Unknown(detail = "write conflict")
|
||||
|
||||
val result = retry(original.ref())
|
||||
|
||||
assertEquals(gatewayError, (result as DomainResult.Failure).error)
|
||||
assertEquals(InfraredKeyStatus.LEARNING, repository.onlyKey().status)
|
||||
assertEquals("20206", repository.transitionCalls.last().lastErrorCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retryReadyTransitionFailureIsReturnedAfterHardwareSuccess() = runTest {
|
||||
val original = failedKey()
|
||||
repository.seed(original)
|
||||
val transitionError = DomainError.Unknown(detail = "write conflict")
|
||||
repository.transitionErrors[InfraredKeyStatus.READY] = transitionError
|
||||
|
||||
val result = retry(original.ref())
|
||||
|
||||
assertEquals(transitionError, (result as DomainResult.Failure).error)
|
||||
assertEquals(InfraredKeyStatus.LEARNING, repository.onlyKey().status)
|
||||
assertEquals(1, gateway.learnedKeys.size)
|
||||
}
|
||||
|
||||
private fun failedKey(
|
||||
id: InfraredKeyId = keyId,
|
||||
sdkKey: String = "app_ir_key1",
|
||||
) = SavedInfraredKey(
|
||||
id = id,
|
||||
profileId = profile,
|
||||
deviceName = device,
|
||||
displayName = "Power",
|
||||
sdkKey = sdkKey,
|
||||
status = InfraredKeyStatus.FAILED,
|
||||
lastErrorCode = "old-error",
|
||||
createdAtEpochMillis = 1L,
|
||||
updatedAtEpochMillis = 2L,
|
||||
)
|
||||
|
||||
private fun learningKey() = failedKey().copy(
|
||||
status = InfraredKeyStatus.LEARNING,
|
||||
lastErrorCode = null,
|
||||
)
|
||||
}
|
||||
|
||||
private class SteppingClock(start: Long) : EpochMillisClock {
|
||||
private var next = start
|
||||
|
||||
override fun now(): Long = next++
|
||||
}
|
||||
Reference in New Issue
Block a user