RemoteCamTests/SessionTestSupport.swift:25
var sentMessages: [(msg: Message, peers: [MCPeerID], mode: MCSessionSendDataMode)] = []
...
sentMessages.append((msg, peers, mode)) // :44 — from the coordinator's actor context
Appended from the coordinator's actor while tests read it from the test thread — the same data race that made LoopbackMultipeerService.sentMessages crash 5 of 10 runs under Thread Sanitizer (fixed in PR #157, commit 4d81746).
This one isn't what TSan currently trips on, so it was left alone deliberately — but it's the same latent shape and will bite as soon as a test reads it while the actor is still sending.
Fix (same shape as the one already landed)
private let sentMessagesStorage = Locked<[(msg: Message, peers: [MCPeerID], mode: MCSessionSendDataMode)]>([])
var sentMessages: [(msg: Message, peers: [MCPeerID], mode: MCSessionSendDataMode)] {
get { sentMessagesStorage.value }
set { sentMessagesStorage.value = newValue }
}
plus sentMessagesStorage.mutate { $0.append(...) } in send. Keeping the property's name and type means all existing reads and removeAll() calls compile untouched (~14 sites in RemoteCamSessionTests).
Locked<T> is in RemoteCam/Locked.swift, reachable from tests via @testable import RemoteShutter.
RemoteCamTests/SessionTestSupport.swift:25Appended from the coordinator's actor while tests read it from the test thread — the same data race that made
LoopbackMultipeerService.sentMessagescrash 5 of 10 runs under Thread Sanitizer (fixed in PR #157, commit4d81746).This one isn't what TSan currently trips on, so it was left alone deliberately — but it's the same latent shape and will bite as soon as a test reads it while the actor is still sending.
Fix (same shape as the one already landed)
plus
sentMessagesStorage.mutate { $0.append(...) }insend. Keeping the property's name and type means all existing reads andremoveAll()calls compile untouched (~14 sites inRemoteCamSessionTests).Locked<T>is inRemoteCam/Locked.swift, reachable from tests via@testable import RemoteShutter.