fix(scene): address bot review on fake executor delays, time input validation, and localization

This commit is contained in:
2026-08-25 17:35:32 +08:00
parent 56451be0d8
commit b48079564f
7 changed files with 329 additions and 100 deletions
@@ -25,6 +25,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
@@ -201,13 +202,17 @@ class FakeSceneRepository : SceneRepository {
}
/**
* Fake [SceneExecutor] that drives the existing [SocketGateway] and [InfraredGateway] fakes.
* Fake [SceneExecutor] that drives the existing [SocketGateway] and [InfraredGateway] fakes,
* respecting delays and recording virtual remote / notification actions.
*/
class FakeSceneExecutor(
private val socketGateway: SocketGateway,
private val infraredGateway: InfraredGateway,
private val delayDurationProvider: suspend (Long) -> Unit = { delay(it) },
) : SceneExecutor {
private val running = ConcurrentHashMap<SceneId, Boolean>()
val executedRemoteButtons = mutableListOf<SceneAction.VirtualRemoteButton>()
val sentNotifications = mutableListOf<SceneAction.Notification>()
override suspend fun execute(scene: Scene): DomainResult<SceneExecutionResult> {
val alreadyRunning = running.putIfAbsent(scene.id, true) != null
@@ -277,35 +282,51 @@ class FakeSceneExecutor(
action: SceneAction,
): SceneActionExecutionResult {
return try {
val result = when (action) {
is SceneAction.SocketPower -> socketGateway.setPower(
profileId = profileId,
deviceName = action.deviceName,
powerState = action.powerState,
)
when (action) {
is SceneAction.SocketPower -> {
val result = socketGateway.setPower(
profileId = profileId,
deviceName = action.deviceName,
powerState = action.powerState,
)
toExecutionResult(action, result)
}
is SceneAction.InfraredKey -> infraredGateway.sendKey(
profileId = profileId,
deviceName = action.deviceName,
key = action.key,
)
is SceneAction.InfraredKey -> {
val result = infraredGateway.sendKey(
profileId = profileId,
deviceName = action.deviceName,
key = action.key,
)
toExecutionResult(action, result)
}
is SceneAction.VirtualRemoteButton,
is SceneAction.Delay,
is SceneAction.Notification -> DomainResult.Success(Unit)
}
when (result) {
is DomainResult.Success -> SceneActionExecutionResult(
action = action,
outcome = SceneActionOutcome.SUCCESS,
error = null,
)
is SceneAction.VirtualRemoteButton -> {
executedRemoteButtons.add(action)
SceneActionExecutionResult(
action = action,
outcome = SceneActionOutcome.SUCCESS,
error = null,
)
}
is DomainResult.Failure -> SceneActionExecutionResult(
action = action,
outcome = SceneActionOutcome.FAILURE,
error = result.error,
)
is SceneAction.Delay -> {
delayDurationProvider(action.durationSeconds * 1000L)
SceneActionExecutionResult(
action = action,
outcome = SceneActionOutcome.SUCCESS,
error = null,
)
}
is SceneAction.Notification -> {
sentNotifications.add(action)
SceneActionExecutionResult(
action = action,
outcome = SceneActionOutcome.SUCCESS,
error = null,
)
}
}
} catch (e: CancellationException) {
throw e
@@ -317,4 +338,20 @@ class FakeSceneExecutor(
)
}
}
private fun toExecutionResult(action: SceneAction, result: DomainResult<*>): SceneActionExecutionResult {
return when (result) {
is DomainResult.Success -> SceneActionExecutionResult(
action = action,
outcome = SceneActionOutcome.SUCCESS,
error = null,
)
is DomainResult.Failure -> SceneActionExecutionResult(
action = action,
outcome = SceneActionOutcome.FAILURE,
error = result.error,
)
}
}
}
@@ -116,6 +116,46 @@ class FakeSceneExecutorTest {
assertFalse(executionResult.allSucceeded)
}
@Test
fun executesDelayVirtualRemoteAndNotification() = runTest {
val delays = mutableListOf<Long>()
val executor = FakeSceneExecutor(
socketGateway = FakeSocketGateway(),
infraredGateway = FakeInfraredGateway(),
delayDurationProvider = { delays.add(it) },
)
val remoteAction = SceneAction.VirtualRemoteButton(
id = SceneActionId("abox:btn-1"),
profileId = profileId,
remoteId = "remote-1",
remoteDisplayName = "TV Remote",
buttonId = "power",
buttonDisplayName = "Power",
)
val delayAction = SceneAction.Delay(
id = SceneActionId("abox:delay-1"),
profileId = profileId,
durationSeconds = 5,
)
val notificationAction = SceneAction.Notification(
id = SceneActionId("abox:notif-1"),
profileId = profileId,
title = "Test Done",
message = "Success",
)
val scene = scene(listOf(remoteAction, delayAction, notificationAction))
val result = executor.execute(scene)
assertTrue(result is DomainResult.Success)
val execResult = (result as DomainResult.Success).value
assertTrue(execResult.allSucceeded)
assertEquals(listOf(5000L), delays)
assertEquals(listOf(remoteAction), executor.executedRemoteButtons)
assertEquals(listOf(notificationAction), executor.sentNotifications)
}
private fun scene(actions: List<SceneAction>): Scene = Scene(
id = SceneId("${profileId.value}:scene-test"),
profileId = profileId,