Merge branch 'main' into feat/sensitive-areas-guide

This commit is contained in:
Rod Bahmanyari 2026-07-26 16:28:32 -07:00 committed by GitHub
commit b6485ce9d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 414 additions and 132 deletions

View File

@ -39,7 +39,7 @@ struct BLEServiceCoreTests {
ble._test_handlePacket(packet, fromPeerID: sender, signingPublicKey: signingKey)
let receivedDuplicate = await TestHelpers.waitUntil(
{ delegate.publicMessagesSnapshot().count > 1 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!receivedDuplicate)
@ -117,7 +117,7 @@ struct BLEServiceCoreTests {
let unsignedRelayed = await TestHelpers.waitUntil(
{ outbound.count(ofType: .leave) > 0 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!unsignedRelayed)
#expect(ble.currentPeerSnapshots().contains { $0.peerID == alicePeerID })
@ -133,7 +133,7 @@ struct BLEServiceCoreTests {
let badSignatureRelayed = await TestHelpers.waitUntil(
{ outbound.count(ofType: .leave) > 0 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!badSignatureRelayed)
#expect(ble.currentPeerSnapshots().contains { $0.peerID == alicePeerID })
@ -1209,7 +1209,7 @@ struct BLEServiceCoreTests {
let didObservePanicClosure = await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let didObserveClosure = panicIngressObserver.waitUntilClosed(
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
gate.release()
continuation.resume(returning: didObserveClosure)
@ -1340,7 +1340,7 @@ struct BLEServiceCoreTests {
// rotated sender IDs never bought a sixth response.
let exceededBudget = await TestHelpers.waitUntil(
{ outbound.count(ofType: .pong) > budget },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!exceededBudget)
#expect(outbound.count(ofType: .pong) == budget)

View File

@ -43,14 +43,14 @@ struct ChatViewModelRefactoringTests {
transport.simulateConnect(peerID, nickname: "alice")
let didResolve = await TestHelpers.waitUntil({ viewModel.getPeerIDForNickname("alice") != nil },
timeout: TestConstants.shortTimeout)
timeout: TestConstants.settleTimeout)
#expect(didResolve)
// Action: User types /msg command
viewModel.sendMessage("/msg @alice Hello Private World")
let didSend = await TestHelpers.waitUntil({ transport.sentPrivateMessages.count == 1 },
timeout: TestConstants.shortTimeout)
timeout: TestConstants.settleTimeout)
#expect(didSend)
// Assert:
@ -74,7 +74,7 @@ struct ChatViewModelRefactoringTests {
transport.simulateConnect(peerID, nickname: "troll")
let didResolve = await TestHelpers.waitUntil({ viewModel.getPeerIDForNickname("troll") != nil },
timeout: TestConstants.shortTimeout)
timeout: TestConstants.settleTimeout)
#expect(didResolve)
// Action
@ -83,7 +83,7 @@ struct ChatViewModelRefactoringTests {
// Assert
// Verify identity manager was called to block "fingerprint_123"
let didBlock = await TestHelpers.waitUntil({ identity.isBlocked(fingerprint: "fingerprint_123") },
timeout: TestConstants.shortTimeout)
timeout: TestConstants.settleTimeout)
#expect(didBlock)
}
@ -114,7 +114,7 @@ struct ChatViewModelRefactoringTests {
// Wait for async processing with proper timeout
let found = await TestHelpers.waitUntil(
{ viewModel.privateChats[senderID]?.first?.content == "Secret" },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
// Assert
@ -140,7 +140,7 @@ struct ChatViewModelRefactoringTests {
{
viewModel.publicMessages(for: .mesh).contains(where: { $0.content == "Public Hi" })
},
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
// Assert

View File

@ -321,7 +321,7 @@ struct ChatViewModelCommandTests {
transport.simulateConnect(peerID, nickname: "Alice")
let resolved = await TestHelpers.waitUntil({
viewModel.getPeerIDForNickname("Alice") == peerID
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.negativeWaitWindow)
#expect(resolved)
viewModel.handleCommand("/msg Alice")
@ -422,7 +422,7 @@ struct ChatViewModelServiceLifecycleTests {
transport.sentReadReceipts.contains {
$0.peerID == peerID && $0.receipt.originalMessageID == "read-1"
}
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.negativeWaitWindow)
#expect(sentReadReceipt)
#expect(!viewModel.unreadPrivateMessages.contains(peerID))
@ -506,7 +506,7 @@ struct ChatViewModelReceivingTests {
let found = await TestHelpers.waitUntil({
viewModel.publicMessages(for: .mesh).contains { $0.content == "Public hello from Bob" }
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
#expect(found)
}
@ -535,11 +535,11 @@ struct ChatViewModelNoisePayloadTests {
let stored = await TestHelpers.waitUntil({
viewModel.privateChats[peerID]?.contains(where: { $0.id == "pm-noise-1" && $0.content == "Secret hello" }) == true
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
let acked = await TestHelpers.waitUntil({
transport.sentDeliveryAcks.contains { $0.messageID == "pm-noise-1" && $0.peerID == peerID }
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
#expect(stored)
#expect(acked)
@ -579,7 +579,7 @@ struct ChatViewModelNoisePayloadTests {
return name == "Bob"
}
return false
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
#expect(delivered)
}
@ -617,7 +617,7 @@ struct ChatViewModelNoisePayloadTests {
return true
}
return false
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
let conversationStoreUpdated = await TestHelpers.waitUntil({
let messages = viewModel.conversations.conversationsByID[.directPeer(peerID)]?.messages ?? []
@ -626,7 +626,7 @@ struct ChatViewModelNoisePayloadTests {
return true
}
return false
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
#expect(privateChatUpdated)
#expect(conversationStoreUpdated)
@ -730,7 +730,7 @@ struct ChatViewModelVerificationTests {
let bound = await TestHelpers.waitUntil({
viewModel.unifiedPeerService.peers.contains { $0.peerID == peerID }
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
#expect(bound)
let qr = VerificationService.VerificationQR(
@ -982,7 +982,7 @@ struct ChatViewModelPeerTests {
let cleaned = await TestHelpers.waitUntil({
!viewModel.unreadPrivateMessages.contains(stalePeer)
}, timeout: TestConstants.defaultTimeout)
}, timeout: TestConstants.settleTimeout)
#expect(cleaned)
}

View File

@ -142,7 +142,7 @@ struct CourierEndToEndTests {
))
let deposited = await TestHelpers.waitUntil(
{ aliceOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(deposited)
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
@ -151,7 +151,7 @@ struct CourierEndToEndTests {
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
let carried = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(carried)
@ -161,7 +161,7 @@ struct CourierEndToEndTests {
bob.sendBroadcastAnnounce()
let announced = await TestHelpers.waitUntil(
{ bobOut.first(ofType: .announce) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(announced)
let announcePacket = try #require(bobOut.first(ofType: .announce))
@ -169,7 +169,7 @@ struct CourierEndToEndTests {
let handedOver = await TestHelpers.waitUntil(
{ carolOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(handedOver)
// With CoreBluetooth disabled there is no physical link for the send
@ -183,7 +183,7 @@ struct CourierEndToEndTests {
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
let received = await TestHelpers.waitUntil(
{ !bobDelegate.snapshot().isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(received)
@ -229,7 +229,7 @@ struct CourierEndToEndTests {
))
let deposited = await TestHelpers.waitUntil(
{ aliceOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(deposited)
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
@ -237,7 +237,7 @@ struct CourierEndToEndTests {
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
let carried = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(carried)
@ -245,7 +245,7 @@ struct CourierEndToEndTests {
bob.sendBroadcastAnnounce()
let announced = await TestHelpers.waitUntil(
{ bobOut.first(ofType: .announce) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(announced)
let announcePacket = try #require(bobOut.first(ofType: .announce))
@ -253,7 +253,7 @@ struct CourierEndToEndTests {
let handedOver = await TestHelpers.waitUntil(
{ carolOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(handedOver)
let handoverPacket = try #require(carolOut.first(ofType: .courierEnvelope))
@ -265,7 +265,7 @@ struct CourierEndToEndTests {
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
let delivered = await TestHelpers.waitUntil(
{ !bobDelegate.snapshot().isEmpty },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!delivered)
}
@ -293,7 +293,7 @@ struct CourierEndToEndTests {
))
let deposited = await TestHelpers.waitUntil(
{ aliceOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(deposited)
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
@ -301,7 +301,7 @@ struct CourierEndToEndTests {
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
let carried = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(carried)
@ -310,7 +310,7 @@ struct CourierEndToEndTests {
let leakedOnUnverifiedAnnounce = await TestHelpers.waitUntil(
{ carolOut.count(ofType: .courierEnvelope) > 0 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!leakedOnUnverifiedAnnounce)
#expect(!carol.courierStore.isEmpty)
@ -318,7 +318,7 @@ struct CourierEndToEndTests {
bob.sendBroadcastAnnounce()
let announced = await TestHelpers.waitUntil(
{ bobOut.first(ofType: .announce) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(announced)
let verifiedAnnounce = try #require(bobOut.first(ofType: .announce))
@ -326,7 +326,7 @@ struct CourierEndToEndTests {
let handedOver = await TestHelpers.waitUntil(
{ carolOut.count(ofType: .courierEnvelope) == 1 },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(handedOver)
#expect(!carol.courierStore.isEmpty)
@ -355,7 +355,7 @@ struct CourierEndToEndTests {
))
let deposited = await TestHelpers.waitUntil(
{ aliceOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(deposited)
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
@ -363,14 +363,14 @@ struct CourierEndToEndTests {
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
let carried = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(carried)
bob.sendBroadcastAnnounce()
let announced = await TestHelpers.waitUntil(
{ bobOut.first(ofType: .announce) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(announced)
let directAnnounce = try #require(bobOut.first(ofType: .announce))
@ -385,7 +385,7 @@ struct CourierEndToEndTests {
let remoteHandover = await TestHelpers.waitUntil(
{ carolOut.count(ofType: .courierEnvelope) == 1 },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(remoteHandover)
#expect(!carol.courierStore.isEmpty)
@ -398,7 +398,7 @@ struct CourierEndToEndTests {
bob.sendBroadcastAnnounce()
let reannounced = await TestHelpers.waitUntil(
{ bobOut.all(ofType: .announce).contains { $0.timestamp != directAnnounce.timestamp } },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(reannounced)
let freshAnnounce = try #require(
@ -410,7 +410,7 @@ struct CourierEndToEndTests {
let refloodedInCooldown = await TestHelpers.waitUntil(
{ carolOut.count(ofType: .courierEnvelope) > 1 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!refloodedInCooldown)
#expect(!carol.courierStore.isEmpty)
@ -424,7 +424,7 @@ struct CourierEndToEndTests {
bob.sendBroadcastAnnounce()
let announcedAgain = await TestHelpers.waitUntil(
{ bobOut.all(ofType: .announce).contains { $0.timestamp != directAnnounce.timestamp && $0.timestamp != freshAnnounce.timestamp } },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(announcedAgain)
let directAgain = try #require(
@ -434,7 +434,7 @@ struct CourierEndToEndTests {
let handedOverWithoutLinkProof = await TestHelpers.waitUntil(
{ carolOut.count(ofType: .courierEnvelope) > 1 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!handedOverWithoutLinkProof)
#expect(!carol.courierStore.isEmpty)
@ -457,7 +457,7 @@ struct CourierEndToEndTests {
let queuedPacket = await TestHelpers.waitUntil(
{ aliceOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!queuedPacket)
}
@ -494,7 +494,7 @@ struct CourierEndToEndTests {
carol._test_handlePacket(packet, fromPeerID: alicePeerID, signingPublicKey: alice.getSigningPublicKeyData())
let stored = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!stored)
}
@ -532,7 +532,7 @@ struct CourierEndToEndTests {
carol._test_handlePacket(packet, fromPeerID: alicePeerID, signingPublicKey: alice.getSigningPublicKeyData())
let stored = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!stored)
}
@ -575,7 +575,7 @@ struct CourierEndToEndTests {
carol._test_handlePacket(packet, fromPeerID: mallory.myPeerID, preseedPeer: false)
let stored = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!stored)
}
@ -602,14 +602,14 @@ struct CourierEndToEndTests {
let delivered = await TestHelpers.waitUntil(
{ !bobDelegate.snapshot().isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(delivered)
// Give a duplicate delivery a chance to surface, then confirm the
// second copy never reached the delegate.
let duplicated = await TestHelpers.waitUntil(
{ bobDelegate.snapshot().count > 1 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!duplicated)
#expect(bobDelegate.snapshot().count == 1)
@ -629,7 +629,7 @@ struct CourierEndToEndTests {
let initiated = await TestHelpers.waitUntil(
{ outbound.count(ofType: .noiseHandshake) > 0 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!initiated)
@ -639,7 +639,7 @@ struct CourierEndToEndTests {
ble.sendDeliveryAck(for: "msg-2", to: present)
let initiatedForPresent = await TestHelpers.waitUntil(
{ outbound.count(ofType: .noiseHandshake) > 0 },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(initiatedForPresent)
}

View File

@ -87,7 +87,7 @@ struct PrekeyEndToEndTests {
peer.sendBroadcastAnnounce()
let published = await TestHelpers.waitUntil(
{ tap.first(ofType: .announce) != nil && tap.first(ofType: .prekeyBundle) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(published)
return (
@ -124,7 +124,7 @@ struct PrekeyEndToEndTests {
let cached = await TestHelpers.waitUntil(
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(cached)
@ -138,7 +138,7 @@ struct PrekeyEndToEndTests {
))
let deposited = await TestHelpers.waitUntil(
{ aliceOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(deposited)
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
@ -149,7 +149,7 @@ struct PrekeyEndToEndTests {
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
let carried = await TestHelpers.waitUntil(
{ !carol.courierStore.isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(carried)
@ -158,7 +158,7 @@ struct PrekeyEndToEndTests {
bob.sendBroadcastAnnounce()
let reannounced = await TestHelpers.waitUntil(
{ bobOut.first(ofType: .announce) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(reannounced)
let handoverTrigger = try #require(bobOut.first(ofType: .announce))
@ -166,7 +166,7 @@ struct PrekeyEndToEndTests {
let handedOver = await TestHelpers.waitUntil(
{ carolOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(handedOver)
let handoverPacket = try #require(carolOut.first(ofType: .courierEnvelope))
@ -178,7 +178,7 @@ struct PrekeyEndToEndTests {
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
let received = await TestHelpers.waitUntil(
{ !bobDelegate.snapshot().isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(received)
@ -207,7 +207,7 @@ struct PrekeyEndToEndTests {
bob._test_handlePacket(redelivery, fromPeerID: carol.myPeerID)
let redelivered = await TestHelpers.waitUntil(
{ bobDelegate.snapshot().count == 2 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!redelivered)
#expect(bobDelegate.snapshot().count == 1)
@ -235,7 +235,7 @@ struct PrekeyEndToEndTests {
))
let deposited = await TestHelpers.waitUntil(
{ aliceOut.first(ofType: .courierEnvelope) != nil },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(deposited)
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
@ -248,7 +248,7 @@ struct PrekeyEndToEndTests {
bob._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, preseedPeer: false)
let received = await TestHelpers.waitUntil(
{ !bobDelegate.snapshot().isEmpty },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(received)
let delivered = try #require(bobDelegate.snapshot().first)
@ -272,7 +272,7 @@ struct PrekeyEndToEndTests {
let cached = await TestHelpers.waitUntil(
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!cached)
}
@ -310,7 +310,7 @@ struct PrekeyEndToEndTests {
let cached = await TestHelpers.waitUntil(
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!cached)
}
@ -328,7 +328,7 @@ struct PrekeyEndToEndTests {
let cached = await TestHelpers.waitUntil(
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
timeout: TestConstants.defaultTimeout
timeout: TestConstants.settleTimeout
)
#expect(cached)
// The verified bundle now participates in Alice's sync rounds.
@ -364,7 +364,7 @@ struct PrekeyEndToEndTests {
let cached = await TestHelpers.waitUntil(
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!cached)
#expect(!alice._test_hasGossipPrekeyBundle(for: bob.myPeerID))
@ -396,7 +396,7 @@ struct PrekeyEndToEndTests {
let cached = await TestHelpers.waitUntil(
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!cached)
#expect(!alice._test_hasGossipPrekeyBundle(for: bob.myPeerID))

View File

@ -37,7 +37,7 @@ struct GossipSyncManagerTests {
}
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
try await TestHelpers.waitFor({ delegate.lastPacket != nil }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.lastPacket != nil }, timeout: TestConstants.settleTimeout)
}
let lastPacket = try #require(delegate.lastPacket, "Expected sync packet to be sent")
@ -394,7 +394,7 @@ struct GossipSyncManagerTests {
)
manager.handleRequestSync(from: peer, request: request)
try await TestHelpers.waitFor({ delegate.packets.count == 2 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 2 }, timeout: TestConstants.settleTimeout)
// Barrier: flush the sync queue so a late third packet would be visible.
manager._performMaintenanceSynchronously(now: Date())
let sentPackets = delegate.packets
@ -477,7 +477,7 @@ struct GossipSyncManagerTests {
manager.handleRequestSync(from: peer, request: request)
manager.handleRequestSync(from: peer, request: request)
try await TestHelpers.waitFor({ delegate.packets.count >= 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count >= 1 }, timeout: TestConstants.settleTimeout)
// Barrier: both requests have been processed once this returns.
manager._performMaintenanceSynchronously(now: Date())
#expect(delegate.packets.count == 1)
@ -498,7 +498,7 @@ struct GossipSyncManagerTests {
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
let packet = try #require(delegate.packets.first)
let request = try #require(RequestSyncPacket.decode(from: packet.payload))
let types = try #require(request.types)
@ -553,7 +553,7 @@ struct GossipSyncManagerTests {
let request = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .fragment)
manager.handleRequestSync(from: peer, request: request)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
let sentPackets = delegate.packets
#expect(sentPackets.count == 1)
#expect(sentPackets[0].type == MessageType.fragment.rawValue)
@ -615,7 +615,7 @@ struct GossipSyncManagerTests {
)
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
// Barrier: flush the sync queue so a late second packet would be visible.
manager._performMaintenanceSynchronously(now: Date())
let sentPackets = delegate.packets
@ -641,7 +641,7 @@ struct GossipSyncManagerTests {
let stalledID = try #require(Data(hexString: "0102030405060708"))
manager.requestMissingFragments(fragmentIDs: [stalledID])
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
let sent = try #require(delegate.packets.first)
#expect(sent.type == MessageType.requestSync.rawValue)
#expect(sent.ttl == 0)
@ -697,7 +697,7 @@ struct GossipSyncManagerTests {
// And a .prekeyBundle sync request is answered with the stored packet.
let request = RequestSyncPacket(p: 7, m: 1, data: Data(), types: .prekeyBundle)
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
let served = try #require(delegate.packets.first)
#expect(served.type == MessageType.prekeyBundle.rawValue)
#expect(served.isRSR)
@ -774,7 +774,7 @@ struct GossipSyncManagerTests {
)
let restored = await TestHelpers.waitUntil(
{ second._messageCount(for: PeerID(hexData: senderID)) == 1 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.settleTimeout
)
#expect(restored)
}
@ -844,7 +844,7 @@ struct GossipSyncManagerTests {
!FileManager.default.fileExists(atPath: fileURL.path)
&& manager._messageCount(for: PeerID(hexData: senderID)) == 0
},
timeout: TestConstants.shortTimeout
timeout: TestConstants.settleTimeout
)
#expect(erased)
}

View File

@ -392,7 +392,7 @@ final class NearbyNotesCounterTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping @MainActor () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -723,9 +723,9 @@ struct NoiseCoverageTests {
// A failed startup requirement must not strand a late thread in
// the blocking test double after the test has returned.
oldSession.resumeDecrypt()
_ = decryptResult.wait(timeout: 5)
_ = decryptResult.wait(timeout: TestConstants.settleTimeout)
if let promotionResultForCleanup {
_ = promotionResultForCleanup.wait(timeout: 5)
_ = promotionResultForCleanup.wait(timeout: TestConstants.settleTimeout)
}
}
@ -751,15 +751,19 @@ struct NoiseCoverageTests {
promotionThread.name = "NoiseCoverageTests.staleDecrypt.promote"
promotionThread.qualityOfService = .userInitiated
promotionThread.start()
try #require(promotionStarted.wait(timeout: .now() + 5) == .success)
try #require(promotionStarted.wait(timeout: .now() + TestConstants.settleTimeout) == .success)
#expect(
// test-timing-ok: a NEGATIVE wait it asserts the promotion has
// NOT completed yet, so a long deadline would only make the suite
// slow while still passing. A starved runner can only make this
// more likely to hold, never less.
promotionResult.wait(timeout: 0.05) == nil,
"Promotion must wait for the exact decrypting-session lease"
)
oldSession.resumeDecrypt()
let decrypted = try #require(decryptResult.wait(timeout: 5)).get()
_ = try #require(promotionResult.wait(timeout: 5)).get()
let decrypted = try #require(decryptResult.wait(timeout: TestConstants.settleTimeout)).get()
_ = try #require(promotionResult.wait(timeout: TestConstants.settleTimeout)).get()
#expect(decrypted.plaintext == Data("old session".utf8))
#expect(decrypted.sessionGeneration == oldGeneration)

View File

@ -580,8 +580,16 @@ final class GeoRelayDirectoryTests: XCTestCase {
/// constrained CI runners (2-core, serialized testing) can starve the
/// detached utility-priority fetch task for seconds before it runs, and
/// a successful wait returns as soon as the condition becomes true.
/// Default deliberately far larger than the work being awaited.
///
/// The directory performs its fetch in a `Task.detached(priority: .utility)`,
/// and utility priority competes with every other suite on a CI runner. At
/// ten seconds the retry-scheduling test timed out at exactly 10.06s with
/// the retry never scheduled which reads like a missing retry rather than
/// a starved background task. Returning as soon as the condition holds means
/// a longer deadline only extends the genuine-failure case.
private func waitUntil(
timeout: TimeInterval = 10.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping @MainActor () async -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -188,7 +188,7 @@ struct PTTBurstPlayerTests {
_ condition: () -> Bool,
sourceLocation: SourceLocation = #_sourceLocation
) async {
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
let deadline = ContinuousClock.now.advanced(by: .seconds(TestConstants.settleTimeout))
while !condition(), ContinuousClock.now < deadline {
await Task.yield()
try? await Task.sleep(nanoseconds: 1_000_000)

View File

@ -15,7 +15,7 @@ final class FavoritesPersistenceServiceTests: XCTestCase {
service.addFavorite(peerNoisePublicKey: peerKey, peerNostrPublicKey: "npub1alice", peerNickname: "Alice")
wait(for: [expectation], timeout: 1.0)
wait(for: [expectation], timeout: TestConstants.settleTimeout)
XCTAssertTrue(service.isFavorite(peerKey))
XCTAssertEqual(service.getFavoriteStatus(for: peerKey)?.peerNickname, "Alice")
XCTAssertNotNil(keychain.load(key: storageKey, service: serviceKey))

View File

@ -227,7 +227,7 @@ final class GeohashPresenceServiceTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping @MainActor () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -355,7 +355,7 @@ final class LocationStateManagerTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping @MainActor () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -63,7 +63,7 @@ final class NetworkActivationServiceTests: XCTestCase {
context.service.start()
context.service.setUserTorEnabled(false)
wait(for: [notified], timeout: 1.0)
wait(for: [notified], timeout: TestConstants.negativeWaitWindow)
context.notificationCenter.removeObserver(token)
XCTAssertFalse(context.service.userTorEnabled)
@ -243,7 +243,7 @@ final class NetworkActivationServiceTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping @MainActor () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -69,25 +69,45 @@ final class NetworkReachabilityGateTests: XCTestCase {
XCTAssertNil(d.pendingRemaining(at: t0.addingTimeInterval(2.5)))
}
func test_monitor_duplicateUpdatesDoNotPostponeOfflineCommit() async {
let monitor = NWPathReachabilityMonitor(debounceInterval: 1.0)
/// Wiring only: a duplicate mid-window still yields exactly one committed
/// `false`, published through the monitor's debounce.
///
/// This deliberately makes no assertion about *when* the flush fires. It
/// used to bound elapsed wall-clock time at 1.4 s to prove the deadline was
/// not restarted, which flaked on loaded CI runners one observed run took
/// 3.75 s, because `Task.sleep` and the `asyncAfter` flush are both real
/// time and neither is bounded above on a busy machine. No wall-clock bound
/// can distinguish "deadline preserved" from "runner is slow", so the timing
/// property is asserted where it is computable instead:
/// `test_debounce_duplicateObservationsPreservePendingDeadline` drives
/// `ReachabilityDebounce` with injected timestamps and checks
/// `pendingRemaining` directly.
///
/// The clock is injected here so the debounce arithmetic is deterministic
/// even though the flush itself is scheduled in real time.
func test_monitor_duplicateUpdatesCommitOnceThroughTheDebounce() async {
let clock = MutableDate(now: Date(timeIntervalSince1970: 1_784_000_000))
let monitor = NWPathReachabilityMonitor(
debounceInterval: 0.2,
now: { clock.now }
)
var received: [Bool] = []
let cancellable = monitor.reachabilityPublisher.sink { received.append($0) }
defer { cancellable.cancel() }
let start = Date()
monitor.ingest(reachable: false)
try? await Task.sleep(nanoseconds: 500_000_000)
// Duplicate unsatisfied update mid-window (e.g. interface detail change
// while still offline) must not restart the debounce window.
// Duplicate unsatisfied update mid-window (e.g. an interface detail
// change while still offline).
clock.now = clock.now.addingTimeInterval(0.1)
monitor.ingest(reachable: false)
// Past the original deadline, so the scheduled flush commits.
clock.now = clock.now.addingTimeInterval(0.2)
let committed = await waitUntil(timeout: 2.0) { !received.isEmpty }
// Generous: this is a liveness check, not a latency bound. A real
// regression never committing still fails, just later.
let committed = await waitUntil(timeout: 10.0) { !received.isEmpty }
XCTAssertTrue(committed)
XCTAssertEqual(received, [false])
// The flush must fire at the original ~1.0s deadline, not ~1.5s
// (a full interval after the duplicate).
XCTAssertLessThan(Date().timeIntervalSince(start), 1.4)
}
// MARK: - Service gating
@ -179,7 +199,7 @@ final class NetworkReachabilityGateTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping @MainActor () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)
@ -239,3 +259,13 @@ private final class GateMockProxyController: NetworkActivationProxyControlling {
private(set) var proxyModes: [Bool] = []
func setProxyMode(useTor: Bool) { proxyModes.append(useTor) }
}
/// Controllable clock, so debounce arithmetic is deterministic even where the
/// flush itself is scheduled in real time.
private final class MutableDate: @unchecked Sendable {
var now: Date
init(now: Date) {
self.now = now
}
}

View File

@ -105,11 +105,11 @@ struct NoiseEncryptionServiceTests {
try establishSessions(alice: alice, bob: bob)
let authenticated = await TestHelpers.waitUntil({ recorder.count >= 2 }, timeout: 5.0)
let authenticated = await TestHelpers.waitUntil({ recorder.count >= 2 }, timeout: TestConstants.settleTimeout)
#expect(authenticated)
let generationAuthenticated = await TestHelpers.waitUntil(
{ recorder.generationCount >= 1 },
timeout: 5.0
timeout: TestConstants.settleTimeout
)
#expect(generationAuthenticated)
#expect(alice.hasEstablishedSession(with: bobPeerID))
@ -166,7 +166,7 @@ struct NoiseEncryptionServiceTests {
#expect(!receiver.hasSession(with: claimedAlicePeerID))
let emittedAuthentication = await TestHelpers.waitUntil(
{ recorder.count > 0 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!emittedAuthentication)
}
@ -216,7 +216,7 @@ struct NoiseEncryptionServiceTests {
#expect(try receiver.decrypt(after, from: alicePeerID) == Data("after".utf8))
let emittedReplacementAuthentication = await TestHelpers.waitUntil(
{ recorder.count > 1 },
timeout: TestConstants.shortTimeout
timeout: TestConstants.negativeWaitWindow
)
#expect(!emittedReplacementAuthentication)
}
@ -652,12 +652,12 @@ struct NoiseEncryptionServiceTests {
)
let retried = await TestHelpers.waitUntil(
{ recorder.messages.count == 1 },
timeout: 1
timeout: TestConstants.longTimeout
)
#expect(retried)
let retryExpired = await TestHelpers.waitUntil(
{ !service.hasSession(with: peerID) },
timeout: 1
timeout: TestConstants.longTimeout
)
#expect(retryExpired)
#expect(recorder.timeoutCount == 1)
@ -710,7 +710,12 @@ struct NoiseEncryptionServiceTests {
let alice = NoiseEncryptionService(keychain: MockKeychain())
let bob = NoiseEncryptionService(
keychain: MockKeychain(),
ordinaryResponderHandshakeTimeout: 0.06,
// Generous for the same reason as the quarantine-restore test
// (#1483): this timeout also arms during the `establishSessions`
// setup handshake below, where bob is the responder. At 0.06 a
// preempted runner could fire it mid-setup, tear down the half-open
// responder, and make message 3 be answered as a fresh initiation.
ordinaryResponderHandshakeTimeout: 1.0,
ordinaryReconnectRollbackCooldown: 0.3
)
let mallory = NoiseEncryptionService(keychain: MockKeychain())
@ -741,12 +746,12 @@ struct NoiseEncryptionServiceTests {
let restored = await TestHelpers.waitUntil(
{ bob.hasEstablishedSession(with: alicePeerID) },
timeout: 1
timeout: TestConstants.longTimeout
)
#expect(restored)
let callbackArrived = await TestHelpers.waitUntil(
{ recovery.timeoutCount == 1 },
timeout: 1
timeout: TestConstants.longTimeout
)
#expect(callbackArrived)
@ -780,7 +785,13 @@ struct NoiseEncryptionServiceTests {
let bob = NoiseEncryptionService(
keychain: MockKeychain(),
ordinaryHandshakeTimeout: 0.04,
ordinaryResponderHandshakeTimeout: 0.04
// Also arms during the `establishSessions` setup handshake below,
// where bob is the responder. Observed failing on a loaded CI
// runner with exactly the signature #1483 documented: the setup's
// `#expect(finalMessage == nil)` saw a 96-byte message 2, because
// the half-open responder had already been torn down and message 3
// was answered as a fresh initiation.
ordinaryResponderHandshakeTimeout: 1.0
)
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
let bobPeerID = PeerID(publicKey: bob.getStaticPublicKeyData())
@ -814,12 +825,12 @@ struct NoiseEncryptionServiceTests {
// initiates one bounded convergence retry; drop that message 1 too.
let retryPrepared = await TestHelpers.waitUntil(
{ recovery.messages.count == 1 },
timeout: 1
timeout: TestConstants.longTimeout
)
#expect(retryPrepared)
let retryExpired = await TestHelpers.waitUntil(
{ !bob.hasSession(with: alicePeerID) },
timeout: 1
timeout: TestConstants.longTimeout
)
#expect(retryExpired)
#expect(recovery.timeoutCount == 1)
@ -1026,7 +1037,7 @@ struct NoiseEncryptionServiceTests {
let requested = await TestHelpers.waitUntil(
{ recovery.messages.count == 1 },
timeout: 1
timeout: TestConstants.longTimeout
)
#expect(requested)
let retryMessage1 = try #require(recovery.messages.first)

View File

@ -960,7 +960,7 @@ final class NostrRelayManagerTests: XCTestCase {
try context.sessionFactory.latestConnection(for: relayURL)?.emitEventMessage(subscriptionID: "ordered", event: event)
}
let allDelivered = await waitUntil(timeout: 5.0) {
let allDelivered = await waitUntil(timeout: TestConstants.settleTimeout) {
receivedIDs.count == events.count
}
XCTAssertTrue(allDelivered)
@ -1006,7 +1006,7 @@ final class NostrRelayManagerTests: XCTestCase {
}
try context.sessionFactory.latestConnection(for: quietRelayURL)?.emitEventMessage(subscriptionID: "quiet", event: quietEvent)
let quietDelivered = await waitUntil(timeout: 5.0) { quietDeliveredAfterBusyCount >= 0 }
let quietDelivered = await waitUntil(timeout: TestConstants.settleTimeout) { quietDeliveredAfterBusyCount >= 0 }
XCTAssertTrue(quietDelivered, "relay B's event was never delivered")
// The signal: B did not have to wait for A's entire backlog. If the two
@ -1019,7 +1019,7 @@ final class NostrRelayManagerTests: XCTestCase {
)
// Both relays still drain fully and in order.
let allDelivered = await waitUntil(timeout: 5.0) {
let allDelivered = await waitUntil(timeout: TestConstants.settleTimeout) {
busyDeliveredCount == busyEvents.count
}
XCTAssertTrue(allDelivered)
@ -1907,7 +1907,7 @@ final class NostrRelayManagerTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping @MainActor () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -164,7 +164,7 @@ struct NostrTransportTests {
transport.sendPrivateMessage("hello over nostr", to: shortPeerID, recipientNickname: "Carol", messageID: "pm-1")
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
#expect(didSend)
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
let privateMessage = try decodePrivateMessage(from: result.payload)
@ -209,7 +209,7 @@ struct NostrTransportTests {
transport.sendFavoriteNotification(to: fullPeerID, isFavorite: true)
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
#expect(didSend)
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
let privateMessage = try decodePrivateMessage(from: result.payload)
@ -250,7 +250,7 @@ struct NostrTransportTests {
transport.sendDeliveryAck(for: "ack-1", to: fullPeerID)
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
#expect(didSend)
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
@ -288,7 +288,7 @@ struct NostrTransportTests {
messageID: "geo-1"
)
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
#expect(didSend)
let event = probe.sentEvents[0]
let result = try decodeEmbeddedPayload(from: event, recipient: recipient)

View File

@ -562,7 +562,7 @@ final class SecureIdentityStateManagerTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -320,7 +320,7 @@ struct SecureIdentityStateManagerVouchTests {
// MARK: - Helpers
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -48,7 +48,7 @@ struct GossipSyncBoardTests {
let request = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .board)
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
let sent = try #require(delegate.packets.first)
#expect(sent.type == MessageType.boardPost.rawValue)
#expect(sent.isRSR)
@ -69,7 +69,7 @@ struct GossipSyncBoardTests {
let boardRequest = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .board)
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: boardRequest)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
#expect(delegate.packets.count == 1)
#expect(delegate.packets.first?.type == MessageType.boardPost.rawValue)
}

View File

@ -63,7 +63,7 @@ final class RequestSyncManagerTests: XCTestCase {
}
private func waitUntil(
timeout: TimeInterval = 1.0,
timeout: TimeInterval = TestConstants.settleTimeout,
condition: @escaping () -> Bool
) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)

View File

@ -11,14 +11,54 @@ import Foundation
struct TestConstants {
static let defaultTimeout: TimeInterval = 5.0
static let shortTimeout: TimeInterval = 1.0
/// For positive waits on work that hops through `Task.detached` or
/// background queues: those contend with every parallel test worker for
/// the global executor, so a loaded CI runner can exceed
/// `defaultTimeout`. `waitUntil` returns as soon as the condition holds,
/// so passing runs never pay the longer timeout.
static let longTimeout: TimeInterval = 10.0
/// **Default deadline for any "wait until this async thing settles" helper.**
///
/// Four separate tests flaked on CI during July 2026 with the same root
/// cause, and it is worth stating the rule rather than re-learning it a
/// fifth time: *a wait deadline is not a latency budget.* It exists so a
/// genuine hang eventually fails the suite. Size it for the worst-case
/// scheduler, never for how long the operation "should" take.
///
/// A CI runner executes many suites at once. Work behind `@MainActor`,
/// `Task.detached(priority: .utility)`, or a `DispatchQueue.asyncAfter` can
/// be starved for seconds one observed run took 3.75 s for a 1 s
/// operation. Deadlines sized to the operation (the old 1 s defaults) turn
/// that starvation into a red build that reads like a product bug.
///
/// This costs nothing when tests pass, because every helper returns as soon
/// as its condition holds. It only extends the genuine-failure case.
///
/// `TestTimingHygieneTests` enforces that wait helpers default to at least
/// `minimumSettleTimeout`.
static let settleTimeout: TimeInterval = 30.0
/// Floor enforced by `TestTimingHygieneTests`. Anything below this is a
/// latency assumption in disguise.
static let minimumSettleTimeout: TimeInterval = 10.0
/// For waits whose **expected outcome is `false`** "prove this does not
/// happen".
///
/// The floor above is wrong for these, and inverted: a negative wait always
/// runs its deadline out, so `settleTimeout` would spend 30 s per case
/// proving nothing extra. Starvation cannot cause a false failure here
/// either a starved runner only makes the thing *less* likely to happen,
/// so the assertion still holds. Short is correct, and naming it says the
/// polarity out loud instead of leaving a bare literal that reads like the
/// mistake this file exists to prevent.
///
/// `TestTimingHygieneTests` accepts this by name. Using it for a wait you
/// expect to succeed reintroduces exactly the flake class it sits next to.
static let negativeWaitWindow: TimeInterval = 1.0
static let testNickname1 = "Alice"
static let testNickname2 = "Bob"
static let testNickname3 = "Charlie"

View File

@ -0,0 +1,177 @@
import Foundation
import Testing
/// Guards the test suite against the flake class that produced four separate
/// red builds in July 2026: **treating a wait deadline as a latency budget.**
///
/// A CI runner executes many suites at once, so work behind `@MainActor`,
/// `Task.detached(priority: .utility)`, or `DispatchQueue.asyncAfter` can be
/// starved for seconds. One observed run took 3.75 s for a 1 s operation.
/// Deadlines sized to how long the operation "should" take turn that starvation
/// into a red build that reads like a product bug, and the debugging cost lands
/// on whoever opened an unrelated PR.
///
/// Two rules, both enforced below:
///
/// 1. A wait helper's default deadline must be at least
/// `TestConstants.minimumSettleTimeout`. Waits return as soon as their
/// condition holds, so a generous deadline is free in the passing case.
/// 2. No test asserts an *upper bound* on elapsed wall-clock time. Such an
/// assertion cannot distinguish the behaviour under test from a slow
/// machine, so it can only be flaky. Assert the property somewhere it is
/// computable with an injected clock, on the pure logic instead.
///
/// Both rules can be waived per line with `\(Self.waiver)` plus a reason, for
/// the rare case where the timing itself is genuinely the thing under test.
struct TestTimingHygieneTests {
/// Opt-out marker. Reviewers should expect a reason next to it.
static let waiver = "test-timing-ok:"
private static let testsRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent() // TestUtilities
.deletingLastPathComponent() // bitchatTests
private struct Line {
let file: String
let number: Int
let text: String
/// True when the waiver appears on this line or in the comment block
/// immediately above it, so a reason can be written at readable length
/// rather than crammed onto the end of the code line.
let waived: Bool
}
private static func swiftLines() throws -> [Line] {
let enumerator = FileManager.default.enumerator(
at: testsRoot,
includingPropertiesForKeys: nil
)
var out: [Line] = []
while let url = enumerator?.nextObject() as? URL {
guard url.pathExtension == "swift" else { continue }
// This file necessarily contains the patterns it bans.
guard url.lastPathComponent != "TestTimingHygieneTests.swift" else { continue }
let name = url.lastPathComponent
let texts = try String(contentsOf: url, encoding: .utf8)
.components(separatedBy: .newlines)
for (index, text) in texts.enumerated() {
// Scan back over an unbroken run of comment lines.
var waived = text.contains(waiver)
var back = index - 1
while !waived, back >= 0 {
let above = texts[back].trimmingCharacters(in: .whitespaces)
guard above.hasPrefix("//") else { break }
waived = above.contains(waiver)
back -= 1
}
out.append(Line(file: name, number: index + 1, text: text, waived: waived))
}
}
return out
}
private static func isWaived(_ line: Line) -> Bool {
line.waived
}
/// Rule 1: no wait helper may default to a deadline below the floor.
@Test func waitHelpersDoNotDefaultToShortDeadlines() throws {
let lines = try Self.swiftLines()
#expect(!lines.isEmpty, "hygiene scan found no test sources — check the path")
// Two shapes, both of which have flaked here:
// a declaration default `timeout: TimeInterval = 2.5`
// a wait call site `wait(for:, timeout: 1.0)`, `waitUntil(timeout: 5.0)`
//
// Deliberately NOT matched: a bare `timeout:` label on something that is
// not a wait, such as the injected production handshake timeouts in the
// Noise tests. Those are the behaviour under test, and a short value is
// correct there.
let patterns = [
#"(?:timeout|deadline)\s*:\s*TimeInterval\s*=\s*([0-9]+(?:\.[0-9]+)?)"#,
#"(?:wait|waitUntil|waitFor|fulfillment)\s*\([^)]*\btimeout:\s*([0-9]+(?:\.[0-9]+)?)"#
].map { try? NSRegularExpression(pattern: $0) }.compactMap { $0 }
#expect(patterns.count == 2, "hygiene regexes failed to compile")
// Named constants hide the same mistake behind a symbol, and did: the
// fifth flake of the session was `timeout: TestConstants.shortTimeout`
// (1 s) on a positive wait, which a literals-only scan cannot see.
// `shortTimeout` itself is deleted (Periphery flagged it dead once its
// last wait site converted); the ban stays so it cannot come back.
// `negativeWaitWindow` is deliberately absent short is correct there.
let bannedConstants = ["shortTimeout", "defaultTimeout"]
var offenders: [String] = []
for line in lines where !Self.isWaived(line) {
let range = NSRange(line.text.startIndex..., in: line.text)
var flagged = false
for pattern in patterns {
guard let match = pattern.firstMatch(in: line.text, range: range),
let valueRange = Range(match.range(at: 1), in: line.text),
let value = TimeInterval(line.text[valueRange]),
value < TestConstants.minimumSettleTimeout else { continue }
offenders.append("\(line.file):\(line.number)\(value)s: \(line.text.trimmingCharacters(in: .whitespaces))")
flagged = true
break
}
guard !flagged else { continue }
for name in bannedConstants
where line.text.contains("timeout: TestConstants.\(name)") {
offenders.append("\(line.file):\(line.number) — TestConstants.\(name): \(line.text.trimmingCharacters(in: .whitespaces))")
break
}
}
#expect(
offenders.isEmpty,
"""
Wait deadlines below \(TestConstants.minimumSettleTimeout)s are latency \
assumptions and will flake on a loaded runner. Use \
TestConstants.settleTimeout, or add "\(Self.waiver) <reason>" if the \
timing really is what the test asserts.
\(offenders.joined(separator: "\n"))
"""
)
}
/// Rule 2: no test bounds elapsed wall-clock time from above.
///
/// This is the assertion that started it all `XCTAssertLessThan(
/// Date().timeIntervalSince(start), 1.4)` proving a debounce deadline was
/// not restarted. It cannot separate "behaved correctly" from "runner was
/// busy", so it only ever fails for the wrong reason.
@Test func testsDoNotAssertUpperBoundsOnElapsedTime() throws {
let lines = try Self.swiftLines()
let elapsedAssertion = try NSRegularExpression(
pattern: #"(?:XCTAssertLessThan|XCTAssertLessThanOrEqual)\s*\(\s*(?:Date\(\)\.timeIntervalSince|[A-Za-z_][A-Za-z0-9_]*\.timeIntervalSince|ContinuousClock)"#
)
var offenders: [String] = []
for line in lines where !Self.isWaived(line) {
let range = NSRange(line.text.startIndex..., in: line.text)
guard elapsedAssertion.firstMatch(in: line.text, range: range) != nil else { continue }
offenders.append("\(line.file):\(line.number)\(line.text.trimmingCharacters(in: .whitespaces))")
}
#expect(
offenders.isEmpty,
"""
An upper bound on elapsed wall-clock time cannot distinguish the \
behaviour under test from a slow machine. Assert the property where \
it is computable inject a clock, or test the pure logic or add \
"\(Self.waiver) <reason>".
\(offenders.joined(separator: "\n"))
"""
)
}
/// The floor must stay meaningfully above the operations being waited on,
/// and the default must satisfy the rule this file enforces.
@Test func settleTimeoutsAreSelfConsistent() {
#expect(TestConstants.settleTimeout >= TestConstants.minimumSettleTimeout)
#expect(TestConstants.minimumSettleTimeout > TestConstants.defaultTimeout)
}
}

View File

@ -101,7 +101,7 @@ struct VoiceCaptureSessionTests {
_ condition: () -> Bool,
sourceLocation: SourceLocation = #_sourceLocation
) async {
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
let deadline = ContinuousClock.now.advanced(by: .seconds(TestConstants.settleTimeout))
while !condition(), ContinuousClock.now < deadline {
await Task.yield()
try? await Task.sleep(nanoseconds: 1_000_000)

View File

@ -59,11 +59,24 @@ struct VoiceNotePlaybackControllerTests {
return url
}
/// Waits for an async settle, then asserts.
///
/// The deadline is deliberately far larger than the work it waits on. Every
/// condition here depends on a `@MainActor` Task that playback schedules
/// (the session acquire and its failure path), and on a CI runner executing
/// many suites in parallel that Task can simply not be scheduled for
/// seconds. At five seconds this timed out on CI and reported *two*
/// failures the wait itself, and the `!isPlaying` that the un-run failure
/// path had not yet reset which reads like a playback bug rather than a
/// starved scheduler.
///
/// A generous deadline costs nothing when the condition holds, since this
/// returns as soon as it does; it only extends the genuine-failure case.
private func waitUntil(
_ condition: () -> Bool,
sourceLocation: SourceLocation = #_sourceLocation
) async {
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
let deadline = ContinuousClock.now.advanced(by: .seconds(TestConstants.settleTimeout))
while !condition(), ContinuousClock.now < deadline {
await Task.yield()
try? await Task.sleep(nanoseconds: 1_000_000)

View File

@ -11,7 +11,6 @@ import Foundation
// Kept local until the test-helper module is split out.
struct TestConstants {
static let defaultTimeout: TimeInterval = 5.0
static let shortTimeout: TimeInterval = 1.0
static let longTimeout: TimeInterval = 10.0
static let testNickname1 = "Alice"