Inject notification/favorites singletons through coordinator contexts

NotificationService.shared and FavoritesPersistenceService.shared
accesses across nine coordinators/components consolidate into
ChatViewModel witnesses behind intent-named context members
(notifyPrivateMessage, notifyMention, favoriteRelationship(forNoiseKey:),
allFavoriteRelationships, postLocalNotification, ...). Singleton reach
from coordinators is now zero; nine new tests cover previously
untestable notification/favorites flows.

Also root-causes the long-flaky gift-wrap dedup test: parallel tests
share LocationChannelManager.shared, so channel-switch tests trigger
clearProcessedNostrEvents() on every live ChatViewModel, wiping the
dedup record mid-test. The invariant (forged-signature copies never
poison dedup) now lives as a deterministic NostrInboundPipeline
mock-context test; two Schnorr concurrency probes added along the way
stay as regression guards for the P256K shared-context assumptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-10 23:04:20 +02:00
co-authored by Claude Fable 5
parent 638f3f5005
commit cc76086615
19 changed files with 820 additions and 116 deletions
@@ -56,6 +56,10 @@ protocol ChatVerificationContext: AnyObject {
func triggerHandshake(with peerID: PeerID)
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
// MARK: Notifications (shared with `ChatNostrContext`)
/// Posts a generic local user notification.
func postLocalNotification(title: String, body: String, identifier: String)
}
extension ChatViewModel: ChatVerificationContext {
@@ -110,6 +114,10 @@ extension ChatViewModel: ChatVerificationContext {
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data) {
meshService.sendVerifyResponse(to: peerID, noiseKeyHex: noiseKeyHex, nonceA: nonceA)
}
func postLocalNotification(title: String, body: String, identifier: String) {
NotificationService.shared.sendLocalNotification(title: title, body: body, identifier: identifier)
}
}
@MainActor
@@ -313,7 +321,7 @@ final class ChatVerificationCoordinator {
let peerName = context.unifiedPeer(for: peerID)?.nickname
?? context.resolveNickname(for: peerID)
NotificationService.shared.sendLocalNotification(
context.postLocalNotification(
title: "Verified",
body: "You verified \(peerName)",
identifier: "verify-success-\(peerID)-\(UUID().uuidString)"
@@ -347,7 +355,7 @@ private extension ChatVerificationCoordinator {
guard now.timeIntervalSince(lastToast) > 60 else { return }
lastMutualToastAt[fingerprint] = now
NotificationService.shared.sendLocalNotification(
context.postLocalNotification(
title: title,
body: "You and \(bodyName) verified each other",
identifier: "\(notificationPrefix)-\(peerID)-\(UUID().uuidString)"