重构: 完成网络端口迁移

This commit is contained in:
Kevin
2026-03-12 10:50:46 +08:00
parent cfccaf3a9d
commit c573882f3e
20 changed files with 437 additions and 43 deletions

View File

@@ -9,6 +9,8 @@ import com.huaga.life_echo.data.repository.ConversationRepository
import com.huaga.life_echo.data.repository.MessageRepository
import com.huaga.life_echo.data.repository.PaymentRepository
import com.huaga.life_echo.data.repository.ProfileRepository
import com.huaga.life_echo.feature.conversation.adapters.ConversationApiAdapter
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
import com.huaga.life_echo.feature.memoir.adapters.MemoirApiAdapter
import com.huaga.life_echo.feature.memoir.ports.MemoirApiPort
import com.huaga.life_echo.feature.payment.adapters.PaymentApiAdapter
@@ -61,6 +63,7 @@ class AppContainer(private val context: Context) {
val webSocketClient: WebSocketClient by lazy { WebSocketClient(realtimeTransportProvider) }
// Feature ports
val conversationApi: ConversationApiPort by lazy { ConversationApiAdapter(apiService) }
val memoirApi: MemoirApiPort by lazy { MemoirApiAdapter(apiService) }
val profileApi: ProfileApiPort by lazy { ProfileApiAdapter(apiService) }
val paymentApi: PaymentApiPort by lazy { PaymentApiAdapter(apiService) }
@@ -70,7 +73,7 @@ class AppContainer(private val context: Context) {
ConversationRepository(
conversationDao = database.conversationDao(),
segmentDao = database.conversationSegmentDao(),
apiService = apiService
conversationApi = conversationApi,
)
}
@@ -81,7 +84,7 @@ class AppContainer(private val context: Context) {
val messageRepository by lazy {
MessageRepository(
messageDao = database.messageDao(),
apiService = apiService
conversationApi = conversationApi,
)
}

View File

@@ -1,14 +1,13 @@
package com.huaga.life_echo.data.repository
import com.huaga.life_echo.data.database.*
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
import kotlinx.coroutines.flow.Flow
class ConversationRepository(
private val conversationDao: ConversationDao,
private val segmentDao: ConversationSegmentDao,
private val apiService: ApiService
private val conversationApi: ConversationApiPort,
) {
fun getAllConversations(): Flow<List<Conversation>> {
return conversationDao.getAllConversations()
@@ -27,7 +26,7 @@ class ConversationRepository(
}
suspend fun deleteConversation(id: String): Result<Unit> {
val result = apiService.deleteConversation(id)
val result = conversationApi.deleteConversation(id)
return result.fold(
onSuccess = {
// 从本地数据库删除
@@ -64,7 +63,7 @@ class ConversationRepository(
return Result.success(existingConversation.id)
}
val result = apiService.createConversation()
val result = conversationApi.createConversation()
return result.fold(
onSuccess = { response ->
// 将新对话保存到本地数据库
@@ -101,7 +100,7 @@ class ConversationRepository(
* 从API同步对话列表
*/
suspend fun syncConversations() {
val result = apiService.getConversationList()
val result = conversationApi.getConversationList()
result.fold(
onSuccess = { conversations ->
// 将DTO转换为Entity并保存到数据库
@@ -133,4 +132,3 @@ class ConversationRepository(
)
}
}

View File

@@ -2,21 +2,19 @@ package com.huaga.life_echo.data.repository
import com.huaga.life_echo.data.database.Message
import com.huaga.life_echo.data.database.MessageDao
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.models.MessageDto
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class MessageRepository(
private val messageDao: MessageDao,
private val apiService: ApiService
private val conversationApi: ConversationApiPort,
) {
fun getMessagesByConversationId(conversationId: String): Flow<List<Message>> {
return messageDao.getMessagesByConversationId(conversationId)
}
suspend fun syncMessages(conversationId: String) {
val result = apiService.getMessages(conversationId)
val result = conversationApi.getMessages(conversationId)
result.getOrNull()?.let { messages ->
val dbMessages = messages.map { dto ->
Message(

View File

@@ -3,6 +3,7 @@ package com.huaga.life_echo.feature.conversation.adapters
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.network.models.CreateConversationResponse
import com.huaga.life_echo.network.models.MessageDto
import com.huaga.life_echo.network.models.TasksStatusDto
@@ -15,6 +16,14 @@ class ConversationApiAdapter(
return apiService.createConversation()
}
override suspend fun getConversationList(): Result<List<ConversationListItemDto>> {
return apiService.getConversationList()
}
override suspend fun deleteConversation(conversationId: String): Result<Unit> {
return apiService.deleteConversation(conversationId)
}
override suspend fun getMessages(conversationId: String): Result<List<MessageDto>> {
return apiService.getMessages(conversationId)
}

View File

@@ -18,6 +18,7 @@ class ConversationRealtimeAdapter(
override val state: StateFlow<ConversationRealtimePort.State> = _state.asStateFlow()
override suspend fun prepare() {
webSocketClient.prepare()
_state.value = ConversationRealtimePort.State.Preparing
}

View File

@@ -1,12 +1,15 @@
package com.huaga.life_echo.feature.conversation.ports
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.network.models.CreateConversationResponse
import com.huaga.life_echo.network.models.MessageDto
import com.huaga.life_echo.network.models.TasksStatusDto
interface ConversationApiPort {
suspend fun createConversation(): Result<CreateConversationResponse>
suspend fun getConversationList(): Result<List<ConversationListItemDto>>
suspend fun deleteConversation(conversationId: String): Result<Unit>
suspend fun getMessages(conversationId: String): Result<List<MessageDto>>
suspend fun getTasksStatus(): Result<TasksStatusDto>
suspend fun getChapters(): Result<List<ChapterDto>>

View File

@@ -46,6 +46,10 @@ open class WebSocketClient(
private const val RECONNECT_DELAY_MS = 3000L
private const val MAX_RECONNECT_ATTEMPTS = 5
}
open fun prepare() {
transportProvider.warmUp()
}
open suspend fun connect(
conversationId: String,

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.asStateFlow
class RealtimeTransportProvider(
private val factory: () -> HttpClient,
) {
private val lock = Any()
private var client: HttpClient? = null
private val _readiness = MutableStateFlow(NetworkReadiness.NotInitialized)
val readiness: StateFlow<NetworkReadiness> = _readiness.asStateFlow()
@@ -17,15 +18,21 @@ class RealtimeTransportProvider(
}
fun getClient(): HttpClient {
return client ?: factory().also {
client = it
_readiness.value = NetworkReadiness.ClientReady
return synchronized(lock) {
client ?: factory().also {
client = it
_readiness.value = NetworkReadiness.ClientReady
}
}
}
fun close() {
client?.close()
client = null
_readiness.value = NetworkReadiness.NotInitialized
val clientToClose = synchronized(lock) {
client.also {
client = null
_readiness.value = NetworkReadiness.NotInitialized
}
}
clientToClose?.close()
}
}

View File

@@ -10,14 +10,17 @@ enum class RestClientProfile { AUTH, API }
class RestClientProvider(
private val factory: (RestClientProfile) -> HttpClient,
) {
private val lock = Any()
private val clients = mutableMapOf<RestClientProfile, HttpClient>()
private val _readiness = MutableStateFlow(NetworkReadiness.NotInitialized)
val readiness: StateFlow<NetworkReadiness> = _readiness.asStateFlow()
fun getClient(profile: RestClientProfile): HttpClient {
return clients.getOrPut(profile) {
factory(profile).also {
_readiness.value = NetworkReadiness.ClientReady
return synchronized(lock) {
clients.getOrPut(profile) {
factory(profile).also {
_readiness.value = NetworkReadiness.ClientReady
}
}
}
}
@@ -27,8 +30,12 @@ class RestClientProvider(
}
fun close() {
clients.values.forEach { it.close() }
clients.clear()
_readiness.value = NetworkReadiness.NotInitialized
val clientsToClose = synchronized(lock) {
clients.values.toList().also {
clients.clear()
_readiness.value = NetworkReadiness.NotInitialized
}
}
clientsToClose.forEach { it.close() }
}
}

View File

@@ -11,8 +11,11 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AuthViewModel(private val context: Context) : ViewModel() {
private val authService = AuthService()
class AuthViewModel(
context: Context,
private val authService: AuthService,
private val tokenInitializer: (Context) -> Unit = TokenManager::initialize,
) : ViewModel() {
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
@@ -48,7 +51,7 @@ class AuthViewModel(private val context: Context) : ViewModel() {
val smsCountdown: StateFlow<Int> = _smsCountdown.asStateFlow()
init {
TokenManager.initialize(context)
tokenInitializer(context)
checkAuthStatus()
}

View File

@@ -5,9 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.huaga.life_echo.app.AppContainer
import com.huaga.life_echo.app.LifeEchoApp
import com.huaga.life_echo.feature.conversation.adapters.ConversationApiAdapter
import com.huaga.life_echo.feature.conversation.adapters.ConversationRealtimeAdapter
import com.huaga.life_echo.feature.memoir.adapters.MemoirApiAdapter
class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
@@ -23,7 +21,7 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
chapterRepository = container.chapterRepository,
messageRepository = container.messageRepository,
context = context,
conversationApi = ConversationApiAdapter(container.apiService),
conversationApi = container.conversationApi,
conversationRealtime = ConversationRealtimeAdapter(container.webSocketClient),
) as T
}
@@ -39,7 +37,10 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
) as T
}
modelClass.isAssignableFrom(AuthViewModel::class.java) -> {
AuthViewModel(context = context) as T
AuthViewModel(
context = context,
authService = container.authService,
) as T
}
modelClass.isAssignableFrom(PaymentViewModel::class.java) -> {
PaymentViewModel(

View File

@@ -0,0 +1,142 @@
package com.huaga.life_echo.data.repository
import com.huaga.life_echo.data.database.Conversation
import com.huaga.life_echo.data.database.ConversationDao
import com.huaga.life_echo.data.database.ConversationSegment
import com.huaga.life_echo.data.database.ConversationSegmentDao
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.network.models.CreateConversationResponse
import com.huaga.life_echo.network.models.MessageDto
import com.huaga.life_echo.network.models.TasksStatusDto
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
class ConversationRepositoryTest {
@Test
fun sync_conversations_uses_conversation_port_and_persists_results() = runTest {
val dao = FakeConversationDao()
val api = FakeConversationApiPort(
conversationListResult = Result.success(
listOf(
ConversationListItemDto(
id = "conversation-1",
title = "Title",
avatarUrl = null,
latestMessagePreview = "Hello",
latestMessageTime = 123L,
)
)
)
)
val repository = ConversationRepository(
conversationDao = dao,
segmentDao = FakeConversationSegmentDao(),
conversationApi = api,
)
repository.syncConversations()
assertEquals(1, api.getConversationListCalls)
assertEquals(listOf("conversation-1"), dao.insertedConversations.map { it.id })
}
@Test
fun delete_conversation_uses_conversation_port_and_removes_local_row() = runTest {
val existing = Conversation(
id = "conversation-1",
userId = "user-1",
startedAt = 1L,
endedAt = null,
durationSeconds = 0,
summary = null,
currentTopic = null,
conversationStage = null,
)
val dao = FakeConversationDao(existingConversation = existing)
val api = FakeConversationApiPort(deleteConversationResult = Result.success(Unit))
val repository = ConversationRepository(
conversationDao = dao,
segmentDao = FakeConversationSegmentDao(),
conversationApi = api,
)
repository.deleteConversation("conversation-1")
assertEquals(listOf("conversation-1"), api.deletedConversationIds)
assertEquals(listOf("conversation-1"), dao.deletedConversations.map { it.id })
}
private class FakeConversationDao(
private var existingConversation: Conversation? = null,
) : ConversationDao {
val insertedConversations = mutableListOf<Conversation>()
val deletedConversations = mutableListOf<Conversation>()
override fun getAllConversations() = flowOf(emptyList<Conversation>())
override suspend fun getConversationById(id: String): Conversation? =
existingConversation?.takeIf { it.id == id }
override suspend fun getLatestEmptyConversation(): Conversation? = null
override suspend fun insertConversation(conversation: Conversation) {
insertedConversations += conversation
existingConversation = conversation
}
override suspend fun updateConversation(conversation: Conversation) = Unit
override suspend fun deleteConversation(conversation: Conversation) {
deletedConversations += conversation
if (existingConversation?.id == conversation.id) {
existingConversation = null
}
}
override suspend fun deleteOtherEmptyConversations(keepId: String) = Unit
}
private class FakeConversationSegmentDao : ConversationSegmentDao {
override fun getSegmentsByConversationId(conversationId: String) =
flowOf(emptyList<ConversationSegment>())
override suspend fun insertSegment(segment: ConversationSegment) = Unit
override suspend fun insertSegments(segments: List<ConversationSegment>) = Unit
override suspend fun updateSegment(segment: ConversationSegment) = Unit
override suspend fun deleteSegment(segment: ConversationSegment) = Unit
}
private class FakeConversationApiPort(
private val conversationListResult: Result<List<ConversationListItemDto>> =
Result.success(emptyList()),
private val deleteConversationResult: Result<Unit> =
Result.failure(Exception("not configured")),
) : ConversationApiPort {
var getConversationListCalls: Int = 0
private set
val deletedConversationIds = mutableListOf<String>()
override suspend fun createConversation(): Result<CreateConversationResponse> =
Result.failure(Exception("not configured"))
override suspend fun getMessages(conversationId: String): Result<List<MessageDto>> =
Result.failure(Exception("not configured"))
override suspend fun getTasksStatus(): Result<TasksStatusDto> =
Result.failure(Exception("not configured"))
override suspend fun getChapters(): Result<List<ChapterDto>> =
Result.failure(Exception("not configured"))
override suspend fun clearTasks(): Result<Unit> =
Result.failure(Exception("not configured"))
override suspend fun getConversationList(): Result<List<ConversationListItemDto>> {
getConversationListCalls += 1
return conversationListResult
}
override suspend fun deleteConversation(conversationId: String): Result<Unit> {
deletedConversationIds += conversationId
return deleteConversationResult
}
}
}

View File

@@ -0,0 +1,87 @@
package com.huaga.life_echo.data.repository
import com.huaga.life_echo.data.database.Message
import com.huaga.life_echo.data.database.MessageDao
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.network.models.CreateConversationResponse
import com.huaga.life_echo.network.models.MessageDto
import com.huaga.life_echo.network.models.TasksStatusDto
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
class MessageRepositoryTest {
@Test
fun sync_messages_uses_conversation_port_and_persists_messages() = runTest {
val dao = FakeMessageDao()
val api = FakeConversationApiPort(
messagesResult = Result.success(
listOf(
MessageDto(
id = "message-1",
conversationId = "conversation-1",
content = "Hello",
senderType = "user",
timestamp = 123L,
)
)
)
)
val repository = MessageRepository(
messageDao = dao,
conversationApi = api,
)
repository.syncMessages("conversation-1")
assertEquals(listOf("conversation-1"), api.requestedMessageConversationIds)
assertEquals(listOf("message-1"), dao.insertedMessages.flatten().map { it.id })
}
private class FakeMessageDao : MessageDao {
val insertedMessages = mutableListOf<List<Message>>()
override fun getMessagesByConversationId(conversationId: String) = flowOf(emptyList<Message>())
override suspend fun getMessageById(id: String): Message? = null
override suspend fun insertMessage(message: Message) = Unit
override suspend fun insertMessages(messages: List<Message>) {
insertedMessages += messages
}
override suspend fun updateMessage(message: Message) = Unit
override suspend fun deleteMessage(message: Message) = Unit
override suspend fun deleteMessagesByConversationId(conversationId: String) = Unit
}
private class FakeConversationApiPort(
private val messagesResult: Result<List<MessageDto>>,
) : ConversationApiPort {
val requestedMessageConversationIds = mutableListOf<String>()
override suspend fun createConversation(): Result<CreateConversationResponse> =
Result.failure(Exception("not configured"))
override suspend fun getConversationList(): Result<List<ConversationListItemDto>> =
Result.failure(Exception("not configured"))
override suspend fun deleteConversation(conversationId: String): Result<Unit> =
Result.failure(Exception("not configured"))
override suspend fun getMessages(conversationId: String): Result<List<MessageDto>> {
requestedMessageConversationIds += conversationId
return messagesResult
}
override suspend fun getTasksStatus(): Result<TasksStatusDto> =
Result.failure(Exception("not configured"))
override suspend fun getChapters(): Result<List<ChapterDto>> =
Result.failure(Exception("not configured"))
override suspend fun clearTasks(): Result<Unit> =
Result.failure(Exception("not configured"))
}
}

View File

@@ -38,6 +38,7 @@ class ConversationRealtimeAdapterTest {
adapter.prepare()
assertEquals(1, fakeSocketClient.prepareCalls)
assertEquals(ConversationRealtimePort.State.Preparing, adapter.state.value)
}
@@ -62,6 +63,12 @@ class ConversationRealtimeAdapterTest {
private var capturedOnMessage: ((WebSocketMessage) -> Unit)? = null
private var capturedOnError: ((String) -> Unit)? = null
private var connected = false
var prepareCalls: Int = 0
private set
override fun prepare() {
prepareCalls += 1
}
override suspend fun connect(
conversationId: String,

View File

@@ -0,0 +1,39 @@
package com.huaga.life_echo.network.runtime
import io.ktor.client.HttpClient
import org.mockito.Mockito
import java.util.concurrent.Callable
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
internal fun requestClientsConcurrently(
parallelism: Int,
request: () -> HttpClient,
): List<HttpClient> {
val executor = Executors.newFixedThreadPool(parallelism)
val startGate = CountDownLatch(1)
return try {
val futures: List<Future<HttpClient>> = List(parallelism) {
executor.submit(
Callable {
startGate.await(2, TimeUnit.SECONDS)
request()
}
)
}
startGate.countDown()
futures.map { it.get(2, TimeUnit.SECONDS) }
} finally {
executor.shutdownNow()
}
}
internal fun slowMockClientFactory(
onCreate: () -> Unit,
): () -> HttpClient = {
onCreate()
Thread.sleep(75)
Mockito.mock(HttpClient::class.java)
}

View File

@@ -3,8 +3,10 @@ package com.huaga.life_echo.network.runtime
import io.ktor.client.HttpClient
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.Mockito
import java.util.concurrent.atomic.AtomicInteger
class RealtimeTransportProviderTest {
@@ -40,4 +42,21 @@ class RealtimeTransportProviderTest {
assertEquals(NetworkReadiness.NotInitialized, provider.readiness.value)
}
@Test
fun concurrent_first_access_only_creates_one_client() {
val createCount = AtomicInteger(0)
val provider = RealtimeTransportProvider(
factory = slowMockClientFactory {
createCount.incrementAndGet()
}
)
val clients = requestClientsConcurrently(parallelism = 8) {
provider.getClient()
}
assertEquals(1, createCount.get())
assertTrue(clients.all { it === clients.first() })
}
}

View File

@@ -4,8 +4,10 @@ import io.ktor.client.HttpClient
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.Mockito
import java.util.concurrent.atomic.AtomicInteger
class RestClientProviderTest {
@@ -63,4 +65,21 @@ class RestClientProviderTest {
assertEquals(NetworkReadiness.NotInitialized, provider.readiness.value)
}
@Test
fun concurrent_first_access_only_creates_one_client_per_profile() {
val createCount = AtomicInteger(0)
val provider = RestClientProvider { _ ->
slowMockClientFactory {
createCount.incrementAndGet()
}.invoke()
}
val clients = requestClientsConcurrently(parallelism = 8) {
provider.getClient(RestClientProfile.AUTH)
}
assertEquals(1, createCount.get())
assertTrue(clients.all { it === clients.first() })
}
}

View File

@@ -0,0 +1,39 @@
package com.huaga.life_echo.ui.viewmodel
import android.content.Context
import com.huaga.life_echo.network.AuthService
import com.huaga.life_echo.network.runtime.RestClientProvider
import com.huaga.life_echo.testutil.MainDispatcherRule
import io.ktor.client.HttpClient
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assert.assertSame
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
@OptIn(ExperimentalCoroutinesApi::class)
class AuthViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun uses_injected_auth_service_instance() {
val context = Mockito.mock(Context::class.java)
val authService = AuthService(
provider = RestClientProvider { Mockito.mock(HttpClient::class.java) }
)
val viewModel = AuthViewModel(
context = context,
authService = authService,
tokenInitializer = {},
)
val authServiceField = AuthViewModel::class.java.getDeclaredField("authService").apply {
isAccessible = true
}
assertSame(authService, authServiceField.get(viewModel))
}
}

View File

@@ -19,9 +19,9 @@ import com.huaga.life_echo.feature.voice.RecorderEngine
import com.huaga.life_echo.feature.voice.RecorderStartResult
import com.huaga.life_echo.feature.voice.RecorderStopResult
import com.huaga.life_echo.feature.voice.RecordingCoordinator
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.WebSocketMessage
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.network.models.CreateConversationResponse
import com.huaga.life_echo.network.models.MessageDto
import com.huaga.life_echo.network.models.TasksStatusDto
@@ -155,22 +155,22 @@ class CreateMemoryViewModelRecordingCoordinatorTest {
context: Context,
recordingCoordinator: RecordingCoordinator,
): CreateMemoryViewModel {
val apiService = ApiService()
val conversationApi = NoOpConversationApiPort()
return CreateMemoryViewModel(
conversationRepository = ConversationRepository(
conversationDao = FakeConversationDao(),
segmentDao = FakeConversationSegmentDao(),
apiService = apiService,
conversationApi = conversationApi,
),
chapterRepository = ChapterRepository(
chapterDao = FakeChapterDao(),
),
messageRepository = MessageRepository(
messageDao = FakeMessageDao(),
apiService = apiService,
conversationApi = conversationApi,
),
context = context,
conversationApi = NoOpConversationApiPort(),
conversationApi = conversationApi,
conversationRealtime = NoOpConversationRealtimePort(),
recordingCoordinator = recordingCoordinator,
tokenInitializer = {},
@@ -287,6 +287,10 @@ class CreateMemoryViewModelRecordingCoordinatorTest {
private class NoOpConversationApiPort : ConversationApiPort {
override suspend fun createConversation(): Result<CreateConversationResponse> =
Result.failure(Exception("no-op"))
override suspend fun getConversationList(): Result<List<ConversationListItemDto>> =
Result.success(emptyList())
override suspend fun deleteConversation(conversationId: String): Result<Unit> =
Result.success(Unit)
override suspend fun getMessages(conversationId: String): Result<List<MessageDto>> =
Result.success(emptyList())
override suspend fun getTasksStatus(): Result<TasksStatusDto> =

View File

@@ -15,8 +15,8 @@ import com.huaga.life_echo.data.repository.MessageRepository
import com.huaga.life_echo.feature.conversation.ports.AudioSegmentRequest
import com.huaga.life_echo.feature.conversation.ports.ConversationApiPort
import com.huaga.life_echo.feature.conversation.ports.ConversationRealtimePort
import com.huaga.life_echo.network.ApiService
import com.huaga.life_echo.network.WebSocketMessage
import com.huaga.life_echo.network.models.ConversationListItemDto
import com.huaga.life_echo.network.models.ChapterDto
import com.huaga.life_echo.network.models.CreateConversationResponse
import com.huaga.life_echo.network.models.MessageDto
@@ -82,22 +82,22 @@ class CreateMemoryViewModelWarmupTest {
context: Context,
realtime: ConversationRealtimePort = FakeConversationRealtimePort(),
): CreateMemoryViewModel {
val apiService = ApiService()
val conversationApi = FakeConversationApiPort()
return CreateMemoryViewModel(
conversationRepository = ConversationRepository(
conversationDao = FakeConversationDao(),
segmentDao = FakeConversationSegmentDao(),
apiService = apiService,
conversationApi = conversationApi,
),
chapterRepository = ChapterRepository(
chapterDao = FakeChapterDao(),
),
messageRepository = MessageRepository(
messageDao = FakeMessageDao(),
apiService = apiService,
conversationApi = conversationApi,
),
context = context,
conversationApi = FakeConversationApiPort(),
conversationApi = conversationApi,
conversationRealtime = realtime,
tokenInitializer = {},
)
@@ -150,6 +150,10 @@ class CreateMemoryViewModelWarmupTest {
private class FakeConversationApiPort : ConversationApiPort {
override suspend fun createConversation(): Result<CreateConversationResponse> =
Result.failure(Exception("no-op"))
override suspend fun getConversationList(): Result<List<ConversationListItemDto>> =
Result.success(emptyList())
override suspend fun deleteConversation(conversationId: String): Result<Unit> =
Result.success(Unit)
override suspend fun getMessages(conversationId: String): Result<List<MessageDto>> =
Result.failure(Exception("test-offline"))
override suspend fun getTasksStatus(): Result<TasksStatusDto> =