mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
Centralize PTT and voice audio-session ownership, harden courier/bridge/outbox delivery and recovery, correct location and delivery-state races, add privacy/release metadata, and ship reproducible universal Arti slices with Release CI coverage. Validated by the full iOS suite, repeated audio/fragment/performance regressions, BitFoundation tests, strict lint and dead-code analysis, universal iOS Release builds, and iOS/macOS archives.
146 lines
5.4 KiB
Swift
146 lines
5.4 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)
|
|
}
|
|
|
|
@Test @MainActor
|
|
func removeMessage_discardsBridgeAliasBeforeBatchFlush() {
|
|
let pipeline = PublicMessagePipeline()
|
|
let delegate = TestPipelineDelegate()
|
|
pipeline.delegate = delegate
|
|
|
|
pipeline.enqueue(makeMessage(id: "bridge-event", content: "same radio payload", timestamp: Date()), to: .mesh)
|
|
pipeline.removeMessage(withID: "bridge-event")
|
|
pipeline.flushIfNeeded()
|
|
|
|
#expect(delegate.committed.isEmpty)
|
|
#expect(delegate.recordedContentKeys.isEmpty)
|
|
}
|
|
}
|