feat: define activity event contracts

This commit is contained in:
2026-08-26 20:03:22 +08:00
parent 176a70cf7f
commit 25ed255ddf
2 changed files with 146 additions and 0 deletions
@@ -0,0 +1,68 @@
package com.flagship.abox.manager.domain
import kotlinx.coroutines.flow.Flow
@JvmInline
value class ActivityEventId(val value: String)
enum class ActivityEventKind {
SESSION_SIGNED_IN,
SESSION_SIGNED_OUT,
SOCKET_COMMAND,
SOCKET_CONFIRMATION,
SENSOR_REFRESH,
INFRARED_SEND,
INFRARED_LEARN,
SCENE_EXECUTION,
SCENE_ACTION,
}
enum class ActivitySource { USER, AUTOMATION, SYSTEM }
enum class ActivityOutcome {
SUCCEEDED,
FAILED,
CANCELLED,
AWAITING_CONFIRMATION,
}
enum class ActivityTargetType { PROFILE, DEVICE, SCENE, SCENE_ACTION, REMOTE, INFRARED_KEY }
data class ActivityTarget(
val type: ActivityTargetType,
val stableId: String,
val displayName: String,
)
data class ActivityEvent(
val id: ActivityEventId,
val profileId: ProviderProfileId,
val occurredAtEpochMillis: Long,
val kind: ActivityEventKind,
val source: ActivitySource,
val outcome: ActivityOutcome,
val target: ActivityTarget,
val errorCode: String? = null,
)
data class ActivityEventCursor(
val occurredAtEpochMillis: Long,
val id: ActivityEventId,
) : Comparable<ActivityEventCursor> {
override fun compareTo(other: ActivityEventCursor): Int {
val timeComparison = occurredAtEpochMillis.compareTo(other.occurredAtEpochMillis)
return if (timeComparison != 0) timeComparison else id.value.compareTo(other.id.value)
}
}
interface ActivityRepository {
fun observeLatest(profileId: ProviderProfileId, limit: Int): Flow<DomainResult<List<ActivityEvent>>>
suspend fun loadBefore(
profileId: ProviderProfileId,
before: ActivityEventCursor,
limit: Int,
): DomainResult<List<ActivityEvent>>
suspend fun append(event: ActivityEvent): DomainResult<Unit>
}
@@ -0,0 +1,78 @@
package com.flagship.abox.manager.domain.activity
import com.flagship.abox.manager.domain.ActivityEvent
import com.flagship.abox.manager.domain.ActivityEventCursor
import com.flagship.abox.manager.domain.ActivityEventId
import com.flagship.abox.manager.domain.ActivityEventKind
import com.flagship.abox.manager.domain.ActivityOutcome
import com.flagship.abox.manager.domain.ActivityRepository
import com.flagship.abox.manager.domain.ActivitySource
import com.flagship.abox.manager.domain.ActivityTarget
import com.flagship.abox.manager.domain.ActivityTargetType
import com.flagship.abox.manager.domain.DomainResult
import com.flagship.abox.manager.domain.ProviderProfileId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ActivityContractTest {
@Test
fun eventDoesNotContainCredentialsOrPayloadFields() {
val event = ActivityEvent(
id = ActivityEventId("event-1"),
profileId = ProviderProfileId("profile-1"),
occurredAtEpochMillis = 10L,
kind = ActivityEventKind.SOCKET_COMMAND,
source = ActivitySource.USER,
outcome = ActivityOutcome.SUCCEEDED,
target = ActivityTarget(ActivityTargetType.DEVICE, "device-1", "客厅插座"),
errorCode = "20504",
)
val fields = ActivityEvent::class.java.declaredFields.map { it.name }
assertFalse(fields.any { it.contains("password", ignoreCase = true) })
assertFalse(fields.any { it.contains("token", ignoreCase = true) })
assertFalse(fields.any { it.contains("payload", ignoreCase = true) })
assertTrue(event.errorCode == "20504")
}
@Test
fun cursorOrdersEqualTimestampsById() {
val first = ActivityEventCursor(10L, ActivityEventId("a"))
val second = ActivityEventCursor(10L, ActivityEventId("b"))
assertTrue(first < second)
}
@Test
fun recordingFailureDoesNotAlterOriginalDomainResult() = runTest {
val repository = object : ActivityRepository {
override fun observeLatest(profileId: ProviderProfileId, limit: Int): Flow<DomainResult<List<ActivityEvent>>> =
MutableStateFlow(DomainResult.Success(emptyList()))
override suspend fun loadBefore(profileId: ProviderProfileId, before: ActivityEventCursor, limit: Int) =
DomainResult.Success(emptyList<ActivityEvent>())
override suspend fun append(event: ActivityEvent) = DomainResult.Failure(com.flagship.abox.manager.domain.DomainError.Unknown())
}
val original: DomainResult<String> = DomainResult.Success("command-complete")
val recorded = repository.append(testEvent())
assertTrue(recorded is DomainResult.Failure)
assertTrue(original is DomainResult.Success)
assertTrue((original as DomainResult.Success).value == "command-complete")
}
private fun testEvent() = ActivityEvent(
id = ActivityEventId("event-1"),
profileId = ProviderProfileId("profile-1"),
occurredAtEpochMillis = 10L,
kind = ActivityEventKind.SESSION_SIGNED_IN,
source = ActivitySource.SYSTEM,
outcome = ActivityOutcome.SUCCEEDED,
target = ActivityTarget(ActivityTargetType.PROFILE, "profile-1", "账户"),
)
}