fix: CI flaky tests and dependency resolution retry (backport)#651
fix: CI flaky tests and dependency resolution retry (backport)#651torlando-tech merged 4 commits intorelease/v0.9.xfrom
Conversation
…ering Messages from senders with incorrect system clocks would appear out of order because sorting used the sender's claimed timestamp. This adds a receivedAt column that records System.currentTimeMillis() at reception, and sorts by COALESCE(receivedAt, timestamp) so existing messages gracefully fall back to sender time. - Add receivedAt nullable Long to MessageEntity (Room migration 40→41) - Set receivedAt in both ServicePersistenceManager and MessageCollector - Update all DAO sort queries to use COALESCE(receivedAt, timestamp) - Use receivedAt for conversation card ordering (lastMessageTimestamp) - Show both "Received" and "Sent by Sender" on message detail screen - Display receivedAt in conversation bubble timestamps - Add user preference to toggle sort by sent vs received time - Wire sort toggle in Settings > Message Delivery & Retrieval card Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The new sortMessagesBySentTime property on SettingsRepository is read during SettingsViewModel init, but SettingsViewModelTest did not stub it, causing all tests in the class to fail with MockKException. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shard 2 failed when dl.google.com returned 502 while resolving kotlin-build-tools-impl:2.2.21. The transform isolation error was a symptom of the unresolved dependency, not a Gradle bug. Pre-resolving dependencies in a separate step with 3 retries isolates transient Maven repository failures from actual test results, so a network blip doesn't mask or retry real test failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ugViewModelEventDrivenTest ApkSharingViewModelTest: Pass testDispatcher to runTest() so the test body and Dispatchers.Main share the same StandardTestDispatcher. Without this, runTest creates its own scheduler and advanceUntilIdle() only advances that one — viewModelScope coroutines on the class-level dispatcher are never drained, causing timing-dependent failures on CI. DebugViewModelEventDrivenTest: The SHUTDOWN reset test races with parseAndUpdateDebugInfo's withContext(Dispatchers.IO) block. On slow CI runners, the IO work hasn't completed when SHUTDOWN fires, so SHUTDOWN resets debugInfo first, then parseAndUpdateDebugInfo overwrites it with initialized=true. Fix by awaiting the state update with withTimeout/first before triggering SHUTDOWN. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| // Use sender's timestamp for display; receivedAt for sort ordering | ||
| timestamp = receivedMessage.timestamp, |
There was a problem hiding this comment.
Bug: The deleteMessage method uses the sender's timestamp (lastMessage.timestamp) for conversation ordering, which can be incorrect and cause reordering issues after a message is deleted.
Severity: MEDIUM
Suggested Fix
In ConversationRepository.kt at line 496, update the lastMessageTimestamp assignment to use lastMessage.receivedAt ?: lastMessage.timestamp. This ensures the local reception time is used for ordering, consistent with the saveMessage method.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: app/src/main/java/com/lxmf/messenger/service/MessageCollector.kt#L196-L197
Potential issue: The pull request changes the `timestamp` field to represent the
sender's claimed timestamp. However, the `deleteMessage` method in
`ConversationRepository.kt` was not updated accordingly. When a message is deleted, it
updates the conversation's `lastMessageTimestamp` using `lastMessage.timestamp`, which
is the sender's potentially incorrect time. This can lead to incorrect conversation
ordering after a deletion, as it undermines the clock-skew resilience the PR aims to
introduce. This logic is inconsistent with `saveMessage`, which correctly uses the local
reception time for ordering.
Did we get this right? 👍 / 👎 to inform future reviews.
Greptile SummaryThis PR is a cherry-pick of #645 onto The core feature — adding a Key items to note:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Peer as Remote Peer
participant MC as MessageCollector
participant SPM as ServicePersistenceManager
participant DB as Room DB (messages)
participant MVM as MessagingViewModel
participant UI as MessagingScreen
Peer->>MC: LXMF message (timestamp = sender clock)
MC->>MC: now = System.currentTimeMillis()
MC->>SPM: DataMessage(timestamp=sender_ts, receivedAt=now)
SPM->>SPM: receivedAt = System.currentTimeMillis()
SPM->>DB: INSERT MessageEntity(timestamp, receivedAt)
SPM->>DB: UPDATE Conversation(lastMessageTimestamp = receivedAt)
MVM->>MVM: combine(peerHash, refreshTrigger, sortMessagesBySentTime)
alt sortBySent = false (default)
MVM->>DB: getMessagesForConversationPaged\nORDER BY COALESCE(receivedAt, timestamp) DESC
else sortBySent = true
MVM->>DB: getMessagesForConversationPagedBySentTime\nORDER BY timestamp DESC
end
DB-->>MVM: PagingData<MessageEntity>
MVM-->>UI: PagingData<MessageUi>(receivedAt, timestamp)
UI->>UI: bubble: formatTimestamp(receivedAt ?: timestamp)
UI->>UI: detail: "Received" card → receivedAt\n"Sent by Sender" card → timestamp
Last reviewed commit: 9f17a5f |
| import androidx.compose.ui.platform.LocalHapticFeedback | ||
| import androidx.compose.ui.platform.LocalSoftwareKeyboardController | ||
| import androidx.compose.ui.platform.LocalViewConfiguration | ||
| import androidx.compose.ui.res.stringResource |
There was a problem hiding this comment.
Unused import added in this PR
stringResource is imported here but is never referenced anywhere in MessagingScreen.kt. The only changes in this file are the two formatTimestamp(message.receivedAt ?: message.timestamp) call-site updates, neither of which uses stringResource. This import should be removed.
| import androidx.compose.ui.res.stringResource | |
| <details><summary>Prompt To Fix With AI</summary> | |
| `````markdown | |
| This is a comment left during a code review. | |
| Path: app/src/main/java/com/lxmf/messenger/ui/screens/MessagingScreen.kt | |
| Line: 136 | |
| Comment: | |
| **Unused import added in this PR** | |
| `stringResource` is imported here but is never referenced anywhere in `MessagingScreen.kt`. The only changes in this file are the two `formatTimestamp(message.receivedAt ?: message.timestamp)` call-site updates, neither of which uses `stringResource`. This import should be removed. | |
| ```suggestion | |
| How can I resolve this? If you propose a fix, please make it concise. |
| withTimeout(2000) { | ||
| viewModel.debugInfo.first { it.initialized } | ||
| } |
There was a problem hiding this comment.
withTimeout uses virtual clock, not wall-clock time
withTimeout inside runTest uses the TestCoroutineScheduler's virtual clock, which only advances when advanceTimeBy(…) or advanceUntilIdle() is called. Since parseAndUpdateDebugInfo dispatches to the real Dispatchers.IO pool, the virtual clock will never automatically advance to the 2 000 ms deadline. In practice the test will pass when IO finishes quickly, but if the IO work ever hangs the test will hang indefinitely rather than failing cleanly after 2 seconds — the timeout gives a false safety net.
A more robust alternative is to use a real-time boundary via kotlinx.coroutines.test.timeout (available as a runTest parameter), or to replace Dispatchers.IO with the testDispatcher in the ViewModel under test so that advanceUntilIdle() is sufficient. For example:
runTest(testDispatcher, timeout = 5.seconds) {
// ...
debugInfoFlow.emit(debugJson)
advanceUntilIdle() // drains everything once IO is on testDispatcher
// no withTimeout needed
}If replacing the dispatcher is not feasible, consider replacing the withTimeout call with withTimeout(2000.milliseconds) using kotlin.time.Duration so the intent is at least clearly documented, and adding a comment that this relies on real-time IO completing quickly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/test/java/com/lxmf/messenger/viewmodel/DebugViewModelEventDrivenTest.kt
Line: 499-501
Comment:
**`withTimeout` uses virtual clock, not wall-clock time**
`withTimeout` inside `runTest` uses the `TestCoroutineScheduler`'s virtual clock, which only advances when `advanceTimeBy(…)` or `advanceUntilIdle()` is called. Since `parseAndUpdateDebugInfo` dispatches to the real `Dispatchers.IO` pool, the virtual clock will never automatically advance to the 2 000 ms deadline. In practice the test will pass when IO finishes quickly, but if the IO work ever hangs the test will hang indefinitely rather than failing cleanly after 2 seconds — the timeout gives a false safety net.
A more robust alternative is to use a real-time boundary via `kotlinx.coroutines.test.timeout` (available as a `runTest` parameter), or to replace `Dispatchers.IO` with the `testDispatcher` in the ViewModel under test so that `advanceUntilIdle()` is sufficient. For example:
```kotlin
runTest(testDispatcher, timeout = 5.seconds) {
// ...
debugInfoFlow.emit(debugJson)
advanceUntilIdle() // drains everything once IO is on testDispatcher
// no withTimeout needed
}
```
If replacing the dispatcher is not feasible, consider replacing the `withTimeout` call with `withTimeout(2000.milliseconds)` using `kotlin.time.Duration` so the intent is at least clearly documented, and adding a comment that this relies on real-time IO completing quickly.
How can I resolve this? If you propose a fix, please make it concise.
Summary
Cherry-pick of fixes from #645 onto
release/v0.9.x:sortMessagesBySentTimestub to SettingsViewModelTest (MockK strict mock failure)Test plan
🤖 Generated with Claude Code