mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 10:45:20 +00:00
Make lifecycle delayed read-pass deterministic in tests
The coordinator scheduled its delayed owner-level read pass via DispatchQueue.main.asyncAfter, which a busy CI runner's main queue can delay past any reasonable polling deadline. Scheduling is now an injected context member (scheduleOnMainAfter); the ChatViewModel witness keeps the exact asyncAfter behavior while the test mock runs the work synchronously, eliminating the wall-clock poll entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,9 @@ protocol ChatLifecycleContext: AnyObject {
|
|||||||
func markPrivateMessagesAsRead(from peerID: PeerID)
|
func markPrivateMessagesAsRead(from peerID: PeerID)
|
||||||
/// Marks the chat read in the private chat manager (sends pending mesh READ acks).
|
/// Marks the chat read in the private chat manager (sends pending mesh READ acks).
|
||||||
func markChatAsRead(from peerID: PeerID)
|
func markChatAsRead(from peerID: PeerID)
|
||||||
|
/// Schedules main-actor work after a UI-timing delay. Injected so tests
|
||||||
|
/// can run the work synchronously instead of polling wall-clock queues.
|
||||||
|
func scheduleOnMainAfter(_ delay: TimeInterval, _ work: @escaping @MainActor () -> Void)
|
||||||
func synchronizePrivateConversationStore()
|
func synchronizePrivateConversationStore()
|
||||||
func addSystemMessage(_ content: String)
|
func addSystemMessage(_ content: String)
|
||||||
|
|
||||||
@@ -83,6 +86,14 @@ extension ChatViewModel: ChatLifecycleContext {
|
|||||||
privateChatManager.markAsRead(from: peerID)
|
privateChatManager.markAsRead(from: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scheduleOnMainAfter(_ delay: TimeInterval, _ work: @escaping @MainActor () -> Void) {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||||
|
Task { @MainActor in
|
||||||
|
work()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func stopMeshServices() {
|
func stopMeshServices() {
|
||||||
meshService.stopServices()
|
meshService.stopServices()
|
||||||
}
|
}
|
||||||
@@ -119,10 +130,8 @@ final class ChatLifecycleCoordinator {
|
|||||||
markPrivateMessagesAsRead(from: peerID)
|
markPrivateMessagesAsRead(from: peerID)
|
||||||
|
|
||||||
let context = self.context
|
let context = self.context
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiAnimationMediumSeconds) { [weak context] in
|
context.scheduleOnMainAfter(TransportConfig.uiAnimationMediumSeconds) { [weak context] in
|
||||||
Task { @MainActor in
|
context?.markPrivateMessagesAsRead(from: peerID)
|
||||||
context?.markPrivateMessagesAsRead(from: peerID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ private final class MockChatLifecycleContext: ChatLifecycleContext {
|
|||||||
managerReadMarks.append(peerID)
|
managerReadMarks.append(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scheduled work runs synchronously so tests never poll wall-clock queues.
|
||||||
|
private(set) var scheduledDelays: [TimeInterval] = []
|
||||||
|
func scheduleOnMainAfter(_ delay: TimeInterval, _ work: @escaping @MainActor () -> Void) {
|
||||||
|
scheduledDelays.append(delay)
|
||||||
|
work()
|
||||||
|
}
|
||||||
|
|
||||||
func synchronizePrivateConversationStore() { privateStoreSyncCount += 1 }
|
func synchronizePrivateConversationStore() { privateStoreSyncCount += 1 }
|
||||||
func addSystemMessage(_ content: String) { systemMessages.append(content) }
|
func addSystemMessage(_ content: String) { systemMessages.append(content) }
|
||||||
|
|
||||||
@@ -299,10 +306,9 @@ struct ChatLifecycleCoordinatorContextTests {
|
|||||||
#expect(context.refreshBluetoothStateCount == 2)
|
#expect(context.refreshBluetoothStateCount == 2)
|
||||||
#expect(context.managerReadMarks == [peerID])
|
#expect(context.managerReadMarks == [peerID])
|
||||||
|
|
||||||
let deadline = Date().addingTimeInterval(2)
|
// The mock executes scheduled work synchronously, so the delayed
|
||||||
while context.ownerLevelReadPasses.isEmpty && Date() < deadline {
|
// owner-level pass has already run - no wall-clock polling.
|
||||||
try? await Task.sleep(nanoseconds: 20_000_000)
|
#expect(context.scheduledDelays == [TransportConfig.uiAnimationMediumSeconds])
|
||||||
}
|
|
||||||
#expect(context.ownerLevelReadPasses == [peerID])
|
#expect(context.ownerLevelReadPasses == [peerID])
|
||||||
}
|
}
|
||||||
@Test @MainActor
|
@Test @MainActor
|
||||||
|
|||||||
Reference in New Issue
Block a user