fix(infrared): add relearn use case for ready keys and clean up interrupted learning in onCleared and init

This commit is contained in:
2026-08-25 12:44:07 +08:00
parent 8cd44c93e1
commit b6cae9b2c9
8 changed files with 245 additions and 12 deletions
@@ -106,7 +106,88 @@ class RetryInfraredKeyLearning(
ref = ref,
expectedStatuses = setOf(
InfraredKeyStatus.FAILED,
),
targetStatus = InfraredKeyStatus.LEARNING,
lastErrorCode = null,
lastErrorDetail = null,
updatedAtEpochMillis = clock.now(),
)
) {
is DomainResult.Success -> Unit
is DomainResult.Failure -> return result
}
val result = try {
gateway.learnKey(
profileId = key.profileId,
deviceName = key.deviceName,
key = key.sdkKey,
)
} catch (e: CancellationException) {
withContext(NonCancellable) {
repository.transitionStatus(
ref = ref,
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
targetStatus = InfraredKeyStatus.FAILED,
lastErrorCode = null,
lastErrorDetail = "Cancelled",
updatedAtEpochMillis = clock.now(),
)
}
throw e
}
return when (result) {
is DomainResult.Success -> repository.transitionStatus(
ref = ref,
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
targetStatus = InfraredKeyStatus.READY,
lastErrorCode = null,
lastErrorDetail = null,
updatedAtEpochMillis = clock.now(),
)
is DomainResult.Failure -> {
repository.transitionStatus(
ref = ref,
expectedStatuses = setOf(InfraredKeyStatus.LEARNING),
targetStatus = InfraredKeyStatus.FAILED,
lastErrorCode = result.error.externalCode,
lastErrorDetail = null,
updatedAtEpochMillis = clock.now(),
)
result
}
}
}
}
class RelearnInfraredKey(
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.READY,
InfraredKeyStatus.FAILED,
),
targetStatus = InfraredKeyStatus.LEARNING,
lastErrorCode = null,
@@ -22,6 +22,7 @@ class LearnInfraredKeyTest {
private val gateway = RecordingInfraredGateway()
private val learn = LearnInfraredKey(repository, gateway, factory, clock)
private val retry = RetryInfraredKeyLearning(repository, gateway, clock)
private val relearn = RelearnInfraredKey(repository, gateway, clock)
@Test
fun persistsLearningBeforeCallingHardwareThenMarksReady() = runTest {
@@ -131,7 +132,34 @@ class LearnInfraredKeyTest {
assertEquals(0, repository.insertedKeys.size)
assertEquals(2, repository.transitionCalls.size)
assertEquals(
setOf(InfraredKeyStatus.FAILED, InfraredKeyStatus.READY),
setOf(InfraredKeyStatus.FAILED),
repository.transitionCalls.first().expectedStatuses,
)
assertEquals(
setOf(InfraredKeyStatus.LEARNING),
repository.transitionCalls.last().expectedStatuses,
)
}
@Test
fun relearnReusesStableIdAndSdkKeyForReadyKey() = runTest {
val original = failedKey(id = InfraredKeyId("key1"), sdkKey = "app_ir_key1").copy(
status = InfraredKeyStatus.READY,
lastErrorCode = null,
lastErrorDetail = null,
)
repository.seed(original)
val result = relearn(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.READY, InfraredKeyStatus.FAILED),
repository.transitionCalls.first().expectedStatuses,
)
assertEquals(