Files
bitchat/bitchatTests/PublicMessagePipelineTests.swift
T
ef848857b7 Remove dead code found by full Periphery audit; add scan config + advisory CI (#1410)
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so
platform-specific code is never touched), with test targets indexed and
the share extension built. 277 dead declarations removed or demoted:
dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed-
feature remnants (autocomplete command suggestions, back-swipe tuning,
MediaSendError, GeohashParticipantTracker), unused Tor dormancy
bindings, assign-only properties, unused parameters (renamed to _), and
redundant public accessibility. 13 orphaned localization keys deleted
across all 29 locales (old pre-#1392 location-notes UI, app_info
warnings).

Two real tests were flagged as unused because they never ran: Swift
Testing methods missing @Test (NostrProtocolTests.
testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests.
testAssemblesCompressedLargeFrame). Re-armed both; they pass.

Deliberately kept, now recorded in .periphery.baseline.json: iOS-only
code invisible to the CI macOS scan, C FFI signatures, keep-alive
NWPathMonitor reference, InboundEventKey.eventID (dedup semantics),
wifiBulk capability bit (reserved for Wi-Fi bulk work, used by
BitFoundation package tests), and the String secureClear cluster
(exercised by package tests).

New: .periphery.yml config and an advisory Dead Code CI job (mirrors
the SwiftLint precedent from #1361) that fails on findings not in the
committed baseline.

Verified: full macOS app suite, BitFoundation (119) and BitLogger (13)
package tests green; periphery scan --strict exits clean.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:24:19 +02:00

132 lines
4.9 KiB
Swift

//
// PublicMessagePipelineTests.swift
// bitchatTests
//
// Tests for PublicMessagePipeline batching, content dedup, and per-message
// conversation routing. Ordering and ID dedup live in the ConversationStore
// the flush commits into (the old late-insert threshold is gone; see
// ConversationStoreTests for ordered-insert coverage).
//
import Testing
import Foundation
import BitFoundation
@testable import bitchat
@MainActor
private final class TestPipelineDelegate: PublicMessagePipelineDelegate {
private let dedupService = MessageDeduplicationService()
/// Commits in arrival-at-commit order, per conversation.
private(set) var committed: [(message: BitchatMessage, conversationID: ConversationID)] = []
/// Message IDs the commit rejects (simulates the store's ID dedup).
var rejectedMessageIDs: Set<String> = []
private(set) var recordedContentKeys: [String] = []
private(set) var batchingStates: [Bool] = []
func messages(in conversationID: ConversationID) -> [BitchatMessage] {
committed.filter { $0.conversationID == conversationID }.map(\.message)
}
func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String {
dedupService.normalizedContentKey(content)
}
func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
dedupService.contentTimestamp(forKey: key)
}
func pipeline(_: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
dedupService.recordContentKey(key, timestamp: timestamp)
recordedContentKeys.append(key)
}
func pipeline(_: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
guard !rejectedMessageIDs.contains(message.id) else { return false }
committed.append((message, conversationID))
return true
}
func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage) {}
func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool) {
batchingStates.append(isBatching)
}
}
@MainActor
private func makeMessage(id: String, content: String, timestamp: Date) -> BitchatMessage {
BitchatMessage(
id: id,
sender: "A",
content: content,
timestamp: timestamp,
isRelay: false
)
}
struct PublicMessagePipelineTests {
@Test @MainActor
func flush_commitsInTimestampOrder() async {
let pipeline = PublicMessagePipeline()
let delegate = TestPipelineDelegate()
pipeline.delegate = delegate
let earlier = Date().addingTimeInterval(-10)
let later = Date()
pipeline.enqueue(makeMessage(id: "a", content: "Later", timestamp: later), to: .mesh)
pipeline.enqueue(makeMessage(id: "b", content: "Earlier", timestamp: earlier), to: .mesh)
pipeline.flushIfNeeded()
#expect(delegate.messages(in: .mesh).map { $0.id } == ["b", "a"])
// Batching state wrapped the flush.
#expect(delegate.batchingStates == [true, false])
}
@Test @MainActor
func flush_deduplicatesByContentWithinWindow() async {
let pipeline = PublicMessagePipeline()
let delegate = TestPipelineDelegate()
pipeline.delegate = delegate
let now = Date()
pipeline.enqueue(makeMessage(id: "a", content: "Same", timestamp: now), to: .mesh)
pipeline.enqueue(makeMessage(id: "b", content: "Same", timestamp: now.addingTimeInterval(0.2)), to: .mesh)
pipeline.flushIfNeeded()
#expect(delegate.messages(in: .mesh).count == 1)
#expect(delegate.messages(in: .mesh).first?.content == "Same")
}
@Test @MainActor
func flush_routesEachMessageToItsConversation() async {
let pipeline = PublicMessagePipeline()
let delegate = TestPipelineDelegate()
pipeline.delegate = delegate
let base = Date()
pipeline.enqueue(makeMessage(id: "mesh-1", content: "mesh hello", timestamp: base), to: .mesh)
// A channel switch mid-batch must not misroute already-buffered messages.
pipeline.enqueue(makeMessage(id: "geo-1", content: "geo hello", timestamp: base.addingTimeInterval(1)), to: .geohash("u4pruydq"))
pipeline.flushIfNeeded()
#expect(delegate.messages(in: .mesh).map { $0.id } == ["mesh-1"])
#expect(delegate.messages(in: .geohash("u4pruydq")).map { $0.id } == ["geo-1"])
}
@Test @MainActor
func flush_rejectedCommitDoesNotRecordContentKey() async {
let pipeline = PublicMessagePipeline()
let delegate = TestPipelineDelegate()
pipeline.delegate = delegate
delegate.rejectedMessageIDs = ["dup"]
pipeline.enqueue(makeMessage(id: "dup", content: "already stored", timestamp: Date()), to: .mesh)
pipeline.flushIfNeeded()
#expect(delegate.messages(in: .mesh).isEmpty)
#expect(delegate.recordedContentKeys.isEmpty)
}
}