diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordLifecycleAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordLifecycleAndroidTest.kt index 1c32100817..489f03dc25 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordLifecycleAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/MindRecordLifecycleAndroidTest.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.afternote.afternote_fe.test.FailureArtifactRule +import com.afternote.afternote_fe.test.FakeErrorReporter import com.afternote.core.domain.repository.UserRepository import com.afternote.core.model.user.User import com.afternote.core.ui.theme.AfternoteTheme @@ -156,6 +157,7 @@ class MindRecordLifecycleAndroidTest { loader = MindRecordDraftLoader(diaryRepository, dailyQuestionRepository), diaryRepository = diaryRepository, dailyQuestionRepository = dailyQuestionRepository, + errorReporter = FakeErrorReporter(), ) composeRule.setContent { @@ -281,7 +283,7 @@ class MindRecordLifecycleAndroidTest { ), ), ) - val viewModel = ReceiverMindRecordViewModel(repository) + val viewModel = ReceiverMindRecordViewModel(repository, FakeErrorReporter()) composeRule.setContent { AfternoteTheme { diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt index b92d97415e..9ef7a7928f 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt @@ -29,6 +29,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.test.ext.junit.runners.AndroidJUnit4 import com.afternote.afternote_fe.test.FailureArtifactRule +import com.afternote.afternote_fe.test.FakeErrorReporter import com.afternote.afternote_fe.test.FakeUserRepository import com.afternote.afternote_fe.test.testReceiver import com.afternote.core.domain.repository.PhotoUploadRepository @@ -468,6 +469,7 @@ class TimeLetterMindRecordCompletionAndroidTest { loader = MindRecordDraftLoader(repository, draftDailyQuestionRepository), diaryRepository = repository, dailyQuestionRepository = draftDailyQuestionRepository, + errorReporter = FakeErrorReporter(), ) var routedArguments: Pair? = null var writeViewModel by mutableStateOf(null) diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/reporting/MindRecordFailureReporting.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/reporting/MindRecordFailureReporting.kt new file mode 100644 index 0000000000..55f061595e --- /dev/null +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/reporting/MindRecordFailureReporting.kt @@ -0,0 +1,56 @@ +package com.afternote.feature.mindrecord.presentation.reporting + +import com.afternote.core.common.reporting.ErrorReporter + +/** + * 마음의 기록 흐름에서 실패가 발생한 지점 (#964). + * + * 이 모듈은 종전에 `Log.e` 도 `ErrorReporter` 도 0건이었다 — 실패가 UI 상태로만 흡수돼 + * 릴리즈에서 무슨 일이 있었는지 알 방법이 없었다. + * + * [reportingName] 은 리포팅 콘솔의 필터 값이다. 바꾸면 그 시점 이후 기록이 기존 이슈 그룹과 + * 끊겨 추이를 잃으므로, 단계 자체가 사라지지 않는 한 유지한다. + * + * 흐름마다 전용 enum 과 `<흐름>_stage` 키를 따로 둔다 — 키를 합치면 콘솔에서 한 키에 서로 + * 다른 값 목록이 섞인다(애프터노트·수신자·홈이 각각 자기 키를 쓰는 것과 같은 이유다). + */ +enum class MindRecordFailureStage( + val reportingName: String, +) { + /** + * 임시저장 목록 조회. 사용자가 «쓰다 만 글» 을 찾으러 들어오는 화면이라, 실패하면 + * 작성물이 사라진 것처럼 보인다 — #519 가 실제로 그 형태의 결함이었다. + */ + DRAFT_LIST_LOAD("draft_list_load"), + + /** + * 임시저장 삭제. 되돌릴 수 없는 동작이고, 부분 실패(일부만 지워짐)가 목록과 서버 상태를 + * 어긋나게 둔다. + */ + DRAFT_DELETE("draft_delete"), + + /** + * 수신자가 받은 기록 열람. 유가족이 «지금 못 여는» 상황이라 읽기 실패지만 승격 가치가 + * 높다 — 재현할 계정도 조건도 우리 손에 없어 실기 QA 로 잡히지 않는다. + */ + RECEIVER_RECORD_LOAD("receiver_record_load"), +} + +/** + * 마음의 기록 흐름의 handled 실패를 공통 키 규격으로 기록한다. + * + * 사용자 입력 오류·사용자 취소처럼 오류가 아닌 경로는 호출부에서 걸러 넘기지 않는다. + * 코루틴 취소는 [ErrorReporter] 가 창구에서 거르므로 호출부가 다시 막지 않아도 된다 — + * 이 모듈의 저장소는 `runCatching` 으로 취소까지 실패로 바꿔 돌려주는 자리가 있다. + */ +fun ErrorReporter.recordMindRecordFailure( + stage: MindRecordFailureStage, + throwable: Throwable, +) { + recordFailure( + throwable = throwable, + attributes = mapOf(KEY_MIND_RECORD_STAGE to stage.reportingName), + ) +} + +private const val KEY_MIND_RECORD_STAGE = "mind_record_stage" diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListViewModel.kt index 2cc8368739..1c5753168f 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListViewModel.kt @@ -2,11 +2,14 @@ package com.afternote.feature.mindrecord.presentation.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.afternote.core.common.reporting.ErrorReporter import com.afternote.core.ui.UiText import com.afternote.feature.mindrecord.domain.repository.DailyQuestionRepository import com.afternote.feature.mindrecord.domain.repository.DiaryRepository import com.afternote.feature.mindrecord.presentation.R import com.afternote.feature.mindrecord.presentation.mapper.toUi +import com.afternote.feature.mindrecord.presentation.reporting.MindRecordFailureStage +import com.afternote.feature.mindrecord.presentation.reporting.recordMindRecordFailure import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.async @@ -32,6 +35,7 @@ class DraftListViewModel private val loader: MindRecordDraftLoader, private val diaryRepository: DiaryRepository, private val dailyQuestionRepository: DailyQuestionRepository, + private val errorReporter: ErrorReporter, ) : ViewModel() { private val _uiState = MutableStateFlow(DraftListUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() @@ -74,7 +78,17 @@ class DraftListViewModel .map { item -> async { item to deleteOne(item) } } .awaitAll() } - val failed = results.filter { (_, result) -> result.isFailure }.map { (item, _) -> item } + // 되돌릴 수 없는 동작이고, 부분 실패는 목록과 서버 상태를 어긋나게 둔다 — + // 화면은 사용자에게 알리지만 콘솔에는 아무 흔적도 남지 않았다 (#964). + // 계측과 «실패 목록» 을 한 순회에 둔다 — 갈라 두면 나중에 한쪽만 고친다. + val failed = + results + .onEach { (_, result) -> + result.exceptionOrNull()?.let { throwable -> + errorReporter.recordMindRecordFailure(MindRecordFailureStage.DRAFT_DELETE, throwable) + } + }.filter { (_, result) -> result.isFailure } + .map { (item, _) -> item } val refreshed = collectDrafts() _uiState.value = @@ -143,7 +157,11 @@ class DraftListViewModel private suspend fun collectDrafts(): List? = loader .load() - .map { drafts -> + // 사용자가 «쓰다 만 글» 을 찾으러 들어오는 화면이라, 실패하면 작성물이 사라진 + // 것처럼 보인다 — #519 가 실제로 그 형태의 결함이었다 (#964). + .onFailure { throwable -> + errorReporter.recordMindRecordFailure(MindRecordFailureStage.DRAFT_LIST_LOAD, throwable) + }.map { drafts -> val diaryItems = // 날짜를 못 정한 항목은 toUi() 가 null 을 돌린다 — 정렬 키가 없어 뺀다. drafts.diaries.mapNotNull { diary -> diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverMindRecordViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverMindRecordViewModel.kt index 960957042e..eadacd8cb6 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverMindRecordViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverMindRecordViewModel.kt @@ -2,12 +2,15 @@ package com.afternote.feature.mindrecord.presentation.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.afternote.core.common.reporting.ErrorReporter import com.afternote.core.ui.UiText import com.afternote.feature.mindrecord.domain.error.DeliveryNotReadyException import com.afternote.feature.mindrecord.domain.model.MindRecordSummary import com.afternote.feature.mindrecord.domain.model.ReceiverMindRecords import com.afternote.feature.mindrecord.domain.repository.MindRecordReceiverRepository import com.afternote.feature.mindrecord.presentation.R +import com.afternote.feature.mindrecord.presentation.reporting.MindRecordFailureStage +import com.afternote.feature.mindrecord.presentation.reporting.recordMindRecordFailure import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -28,6 +31,7 @@ class ReceiverMindRecordViewModel @Inject constructor( private val repository: MindRecordReceiverRepository, + private val errorReporter: ErrorReporter, ) : ViewModel() { private val rawRecords = MutableStateFlow(null) private val _uiState = MutableStateFlow(ReceiverMindRecordUiState.Loading) @@ -89,6 +93,17 @@ class ReceiverMindRecordViewModel filter = currentFilter, ).withDerived() }.onFailure { e -> + // 유가족이 «지금 못 여는» 상황이라 읽기 실패지만 승격 대상이다 — + // 재현할 계정도 조건도 우리 손에 없어 실기 QA 로는 잡히지 않는다 (#964). + // 재진입 갱신 실패도 기록한다. 화면은 유지되지만 서버 상태는 실패다. + // + // 다만 전달 조건 미충족은 **정상 상태**다 — 서버도 기기도 멀쩡하고 + // 발신자가 조건을 정해야 풀린다. 전달을 기다리는 수신자는 앱을 열 + // 때마다 이 경로를 타므로, 기록하면 보관 한도(최근 8건)를 그 잡음이 + // 채워 정작 잡아야 할 열람 실패를 밀어낸다 (#964 판단 기준: 유지). + if (e !is DeliveryNotReadyException) { + errorReporter.recordMindRecordFailure(MindRecordFailureStage.RECEIVER_RECORD_LOAD, e) + } // 재진입 갱신 실패는 보고 있던 화면을 유지한다. if (showsLoading) { // 서버 원문을 화면 문구로 쓰지 않는다 — 타입에서 막는다 (#614). diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/reporting/MindRecordFailureReportingTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/reporting/MindRecordFailureReportingTest.kt new file mode 100644 index 0000000000..72f1ee3018 --- /dev/null +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/reporting/MindRecordFailureReportingTest.kt @@ -0,0 +1,63 @@ +package com.afternote.feature.mindrecord.presentation.reporting + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.IOException +import kotlin.coroutines.cancellation.CancellationException + +/** + * 마음의 기록 계측 규격 (#964). + * + * 이 모듈은 종전에 `Log.e` 도 `ErrorReporter` 도 0건이라, 실패가 UI 상태로만 흡수되고 + * 릴리즈에서는 아무 흔적도 남지 않았다. + */ +class MindRecordFailureReportingTest { + private val reporter = RecordingErrorReporter() + + @Test + fun `단계는 전용 키로 기록된다`() { + // 키를 다른 흐름과 합치면 콘솔에서 한 키에 서로 다른 값 목록이 섞인다. + reporter.recordMindRecordFailure(MindRecordFailureStage.DRAFT_LIST_LOAD, IOException("offline")) + + val recorded = reporter.failures.single() + assertEquals("draft_list_load", recorded.attributes["mind_record_stage"]) + } + + @Test + fun `예외 원문은 콘솔로 나가지 않는다`() { + // 서버 응답 본문이 message 에 그대로 담겨 오는 경우가 있어 창구가 redact 한다. + reporter.recordMindRecordFailure( + MindRecordFailureStage.RECEIVER_RECORD_LOAD, + IllegalStateException("user@example.com 의 열람 권한이 없습니다"), + ) + + val recorded = reporter.failures.single() + assertTrue( + "예외 원문이 그대로 실렸다: ${recorded.throwable.message}", + recorded.throwable.message?.contains("user@example.com") != true, + ) + // 대신 타입은 남아야 어떤 실패인지 가릴 수 있다. + assertEquals( + "java.lang.IllegalStateException", + recorded.attributes.values.firstOrNull { it.startsWith("java.lang.IllegalStateException") }, + ) + } + + @Test + fun `코루틴 취소는 기록하지 않는다`() { + // 이 모듈의 저장소는 runCatching 으로 취소까지 실패로 바꿔 돌려준다 — 화면 이탈이 + // 잦은 목록에서 그 취소가 전부 기록되면 보관 한도(최근 8건)를 잡음이 채운다. + reporter.recordMindRecordFailure(MindRecordFailureStage.DRAFT_LIST_LOAD, CancellationException("화면 이탈")) + + assertTrue("취소가 기록됐다", reporter.failures.isEmpty()) + } + + @Test + fun `단계 이름은 서로 겹치지 않는다`() { + // reportingName 은 콘솔 필터 값이라 겹치면 다른 화면의 실패가 한 그룹으로 묶인다. + val names = MindRecordFailureStage.entries.map { it.reportingName } + + assertEquals(names.size, names.toSet().size) + } +} diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/reporting/RecordingErrorReporter.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/reporting/RecordingErrorReporter.kt new file mode 100644 index 0000000000..b358efca50 --- /dev/null +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/reporting/RecordingErrorReporter.kt @@ -0,0 +1,28 @@ +package com.afternote.feature.mindrecord.presentation.reporting + +import com.afternote.core.common.reporting.ErrorReporter + +/** + * 기록된 실패를 들여다보는 테스트용 [ErrorReporter]. + * + * `writeFailure` 를 구현한다 — `recordFailure` 는 인터페이스가 취소 제외·원문 redact 를 + * 처리한 뒤 이 함수를 부르므로, 그 정책까지 함께 태워야 실제 동작과 같아진다. + */ +class RecordingErrorReporter : ErrorReporter { + data class Failure( + val throwable: Throwable, + val attributes: Map, + ) + + val failures = mutableListOf() + + val stages: List + get() = failures.mapNotNull { it.attributes["mind_record_stage"] } + + override fun writeFailure( + throwable: Throwable, + attributes: Map, + ) { + failures += Failure(throwable, attributes) + } +} diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt index e047f4990f..e487320945 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt @@ -10,6 +10,7 @@ import com.afternote.feature.mindrecord.domain.model.DiaryUpdatePayload import com.afternote.feature.mindrecord.domain.model.TodayDailyQuestion import com.afternote.feature.mindrecord.domain.repository.DailyQuestionRepository import com.afternote.feature.mindrecord.domain.repository.DiaryRepository +import com.afternote.feature.mindrecord.presentation.reporting.RecordingErrorReporter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -39,6 +40,24 @@ class DraftListDeleteTest { Dispatchers.resetMain() } + @Test + fun `삭제 실패는 텔레메트리에도 남는다`() { + // 화면은 사용자에게 알리지만, 종전에는 콘솔에 아무 흔적도 남지 않아 릴리즈에서 + // «지워지지 않는다» 는 제보를 받아도 재현 지점을 찾을 수 없었다 (#964). + val repository = + FakeDailyQuestionDraftRepository( + drafts = listOf(dailyQuestion(id = 1L)), + deleteResult = Result.failure(IllegalStateException("서버 거절")), + removesOnDelete = false, + ) + val reporter = RecordingErrorReporter() + val viewModel = viewModel(repository, reporter) + + viewModel.delete((viewModel.uiState.value as DraftListUiState.Success).items) + + assertEquals(listOf("draft_delete"), reporter.stages) + } + @Test fun `삭제에 실패하고 항목이 남아 있으면 완료가 아니라 실패로 알린다`() { val stubborn = dailyQuestion(id = 1L) @@ -98,7 +117,10 @@ class DraftListDeleteTest { assertTrue(state.items.isEmpty()) } - private fun viewModel(dailyQuestionRepository: DailyQuestionRepository): DraftListViewModel = + private fun viewModel( + dailyQuestionRepository: DailyQuestionRepository, + errorReporter: RecordingErrorReporter = RecordingErrorReporter(), + ): DraftListViewModel = DraftListViewModel( // #769 가 목록 조회를 loader 로 뽑아낸 뒤 생성자가 3개로 늘었다. 같은 두 // 저장소를 넘겨 종전과 같은 조회 경로를 그대로 태운다. @@ -109,6 +131,7 @@ class DraftListDeleteTest { ), diaryRepository = EmptyDiaryDraftRepository, dailyQuestionRepository = dailyQuestionRepository, + errorReporter = errorReporter, ) private fun dailyQuestion(id: Long) = diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverFailureReportingTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverFailureReportingTest.kt new file mode 100644 index 0000000000..b6384ae5c1 --- /dev/null +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverFailureReportingTest.kt @@ -0,0 +1,67 @@ +package com.afternote.feature.mindrecord.presentation.viewmodel + +import com.afternote.feature.mindrecord.domain.error.DeliveryNotReadyException +import com.afternote.feature.mindrecord.domain.model.ReceiverMindRecords +import com.afternote.feature.mindrecord.domain.repository.MindRecordReceiverRepository +import com.afternote.feature.mindrecord.presentation.reporting.RecordingErrorReporter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.io.IOException + +/** + * 수신자 열람 실패 계측이 «정상 상태» 까지 올리지 않는지 (#964 리뷰). + * + * 전달 조건 미충족(`DeliveryNotReadyException`)은 장애가 아니다 — 서버도 기기도 멀쩡하고 + * 발신자가 조건을 정해야 풀린다. 전달을 기다리는 수신자는 앱을 열 때마다 이 경로를 타므로, + * 기록하면 Crashlytics 보관 한도(최근 8건)를 그 잡음이 채워 **정작 잡아야 할 열람 실패를 + * 밀어낸다** — 이 stage 를 승격한 이유 자체가 무력해진다. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ReceiverFailureReportingTest { + private val dispatcher = StandardTestDispatcher() + + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `전달 조건 미충족은 기록하지 않는다`() = + runTest(dispatcher) { + val reporter = RecordingErrorReporter() + ReceiverMindRecordViewModel(FailingReceiverRepository(DeliveryNotReadyException(null)), reporter) + advanceUntilIdle() + + assertEquals(emptyList(), reporter.stages) + } + + @Test + fun `그 밖의 열람 실패는 기록한다`() = + runTest(dispatcher) { + val reporter = RecordingErrorReporter() + ReceiverMindRecordViewModel(FailingReceiverRepository(IOException("offline")), reporter) + advanceUntilIdle() + + assertEquals(listOf("receiver_record_load"), reporter.stages) + } +} + +private class FailingReceiverRepository( + private val failure: Throwable, +) : MindRecordReceiverRepository { + override suspend fun getAll(): Result = Result.failure(failure) +} diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverRefreshFilterTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverRefreshFilterTest.kt index 65dd4ab0a5..e884973e71 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverRefreshFilterTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/ReceiverRefreshFilterTest.kt @@ -4,6 +4,7 @@ import com.afternote.feature.mindrecord.domain.model.MindRecordSummary import com.afternote.feature.mindrecord.domain.model.MindRecordType import com.afternote.feature.mindrecord.domain.model.ReceiverMindRecords import com.afternote.feature.mindrecord.domain.repository.MindRecordReceiverRepository +import com.afternote.feature.mindrecord.presentation.reporting.RecordingErrorReporter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -41,7 +42,7 @@ class ReceiverRefreshFilterTest { @Test fun `재진입 갱신이 정렬과 기간 필터를 지운다면 안 된다`() = runTest(dispatcher) { - val viewModel = ReceiverMindRecordViewModel(repository = FakeReceiverRepository()) + val viewModel = ReceiverMindRecordViewModel(repository = FakeReceiverRepository(), errorReporter = RecordingErrorReporter()) advanceUntilIdle() val applied =