mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 14:25:19 +00:00
Both failed on main-adjacent CI runs during this session's PRs. Neither was a product bug; both asserted things about real time that a loaded runner is under no obligation to honour. **NetworkReachabilityGateTests: a wall-clock upper bound.** test_monitor_duplicateUpdatesDoNotPostponeOfflineCommit slept 500ms for real, then asserted total elapsed time was under 1.4s to prove a duplicate mid-window had not restarted the 1.0s debounce. One CI run took 3.75s. No wall-clock bound can separate "deadline preserved" from "runner is slow", because Task.sleep and the asyncAfter flush are both real time and neither is bounded above. The deadline property was already covered deterministically one level down: test_debounce_duplicateObservationsPreservePendingDeadline drives ReachabilityDebounce with injected timestamps and checks pendingRemaining directly. So the monitor test now asserts only what needs a real monitor — that a duplicate still yields exactly one committed false through the debounce — with an injected clock for the arithmetic and a generous liveness budget. Renamed to say what it actually checks. No coverage lost, and it runs in 0.14s instead of ~3.8s because the real sleep is gone. **NoiseEncryptionServiceTests: injected timeouts that also arm during setup.** #1483 diagnosed and fixed exactly this in the quarantine-restore test, but two sibling tests kept the shape. Their injected ordinaryResponderHandshakeTimeout (0.04 and 0.06) also arms during the establishSessions setup handshake, where bob is the responder — so a preempted runner fires it mid-setup, tears down the half-open responder, and message 3 gets answered as a fresh initiation. The CI failure named setup's own `#expect(finalMessage == nil)` seeing a 96-byte message 2, which is precisely the signature #1483 recorded. Raised both to 1.0s, matching #1483's remedy: the scenarios still need the responder timeout to fire, and it still does, just with room for setup to complete first. Thin 1-second waitUntil budgets in the file now use the shared TestConstants.longTimeout; every one of them backs a positive assertion, so waitUntil still returns the moment the condition holds and nothing gets slower in the passing case. No assertion weakened in either file. Verified 3x sequentially and 3x with all 18 cores saturated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Test Harness Guide
This test suite uses an in-memory networking harness to make end-to-end and integration tests deterministic, fast, and race-free without touching production code.
In-Memory Bus
- File:
bitchatTests/Mocks/MockBLEService.swift - Registry/Adjacency: Global
registrymapspeerIDto aMockBLEServiceinstance;adjacencyrecords simulated links between peers. - Setup: Call
MockBLEService.resetTestBus()insetUp()to clear state between tests. - Topology: Use
simulateConnectedPeer(_:)andsimulateDisconnectedPeer(_:)to add/remove links.connectFullMesh()helpers in tests build larger topologies. - Handlers: Tests can observe data via
messageDeliveryHandler(decodedBitchatMessage) andpacketDeliveryHandler(rawBitchatPacket). - De‑duplication: A thread-safe
seenMessageIDsprevents duplicate deliveries during flooding/relays.
Broadcast Flooding
- Flag:
MockBLEService.autoFloodEnabled - Intent: When
true, public broadcasts propagate across the entire connected component (ignores TTL for reach) while still de‑duping to prevent loops. - Usage: Enabled in Integration tests (
setUp) to simulate large-network broadcast; disabled in E2E tests to keep routing explicit and verify TTL behavior (seePublicChatE2ETests.testZeroTTLNotRelayed).
Rehandshake Flow (Noise)
- Why: The legacy NACK recovery path was removed; recovery now relies on Noise session rehandshake after decrypt failure or desync.
- Manager:
NoiseSessionManagermanages per-peer sessions. - Pattern: On decrypt failure, proactively clear the local session and re-initiate a handshake. The peer accepts and replaces their session.
- Test:
IntegrationTests.testRehandshakeAfterDecryptionFailure- Corrupts ciphertext to induce a decrypt error.
- Calls
removeSession(for:)on the initiator’s manager beforeinitiateHandshake(with:)to avoidalreadyEstablished. - Verifies encrypt/decrypt succeeds post-rehandshake.
Tips
- Determinism: Add small async delays only where handler installation/topology changes could race the first send.
- Scoping: Keep
autoFloodEnabledtoggled only within Integration tests; always reset intearDown()to avoid cross-test contamination. - Direct vs Relay: Private messages target a specific peer when adjacent; otherwise they are surfaced to neighbors for relay and, if known, also delivered to the target.
Quick Start
- Create nodes and connect them:
let svc = MockBLEService(); svc.myPeerID = "PEER1"svc.simulateConnectedPeer("PEER2")
- Observe messages:
svc.messageDeliveryHandler = { msg in /* asserts */ }
- Enable broadcast flooding for Integration suites only:
MockBLEService.autoFloodEnabled = true