Files
bitchat/bitchatTests/PublicTimelineStoreTests.swift
T
jackandClaude Fable 5 3a995e20b6 Extract BLE packet handlers and migrate coordinators to narrow contexts
BLEService's per-packet-type orchestration moves into owned, tested
components (BLEAnnounceHandler, BLEPublicMessageHandler,
BLENoisePacketHandler, BLEFileTransferHandler, BLEFragmentHandler),
each taking an environment struct of closures so every queue hop stays
in BLEService and the handlers are synchronously testable. Behavior is
preserved verbatim, including Noise session recovery on decrypt failure
and single-block UI event ordering. handleLeave/handleRequestSync stay
in place as already-thin delegations. BLEService drops to 3393 lines.

Four coordinators (delivery, private conversation, Nostr, public
conversation) drop their unowned/weak ChatViewModel back-references for
narrow @MainActor context protocols, with ChatViewModel conformances as
single shared witnesses for overlapping members. Their true coupling is
now an explicit, reviewable surface, and each gains a mock-context test
suite covering flows previously testable only through the full view
model. Delivery/read acks now also clear the router's retained-send
outbox via the delivery context.

New LargeTopologyTests exercise production-shaped meshes with the
in-memory harness: an 8-peer relay chain with per-hop TTL decay, a
14-peer cyclic mesh with exactly-once delivery, partition/heal, and
topology churn.

App-layer runtime/model files updated alongside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:22:52 +01:00

116 lines
4.7 KiB
Swift

import Foundation
import BitFoundation
import Testing
@testable import bitchat
@Suite("PublicTimelineStore Tests")
struct PublicTimelineStoreTests {
@Test("Mesh timeline deduplicates and trims to cap")
func meshTimelineDeduplicatesAndTrims() {
var store = PublicTimelineStore(meshCap: 2, geohashCap: 2)
let first = TestHelpers.createTestMessage(content: "one")
let second = TestHelpers.createTestMessage(content: "two")
let third = TestHelpers.createTestMessage(content: "three")
store.append(first, to: .mesh)
store.append(second, to: .mesh)
store.append(first, to: .mesh)
store.append(third, to: .mesh)
let messages = store.messages(for: .mesh)
#expect(messages.map(\.content) == ["two", "three"])
}
@Test("Timeline indexes allow trimmed message IDs to return")
func timelineIndexesAllowTrimmedMessageIDsToReturn() {
var store = PublicTimelineStore(meshCap: 2, geohashCap: 2)
let first = timelineMessage(id: "one", content: "one", timestamp: 1)
let second = timelineMessage(id: "two", content: "two", timestamp: 2)
let third = timelineMessage(id: "three", content: "three", timestamp: 3)
store.append(first, to: .mesh)
store.append(second, to: .mesh)
store.append(third, to: .mesh)
store.append(first, to: .mesh)
#expect(store.messages(for: .mesh).map(\.content) == ["three", "one"])
let geohash = "u4pruydq"
let channel = ChannelID.location(GeohashChannel(level: .city, geohash: geohash))
let geoFirst = timelineMessage(id: "geo-one", content: "geo one", timestamp: 1)
let geoSecond = timelineMessage(id: "geo-two", content: "geo two", timestamp: 2)
let geoThird = timelineMessage(id: "geo-three", content: "geo three", timestamp: 3)
let didAppendGeoFirst = store.appendIfAbsent(geoFirst, toGeohash: geohash)
let didAppendGeoSecond = store.appendIfAbsent(geoSecond, toGeohash: geohash)
let didAppendGeoThird = store.appendIfAbsent(geoThird, toGeohash: geohash)
let didReappendGeoFirst = store.appendIfAbsent(geoFirst, toGeohash: geohash)
#expect(didAppendGeoFirst)
#expect(didAppendGeoSecond)
#expect(didAppendGeoThird)
#expect(didReappendGeoFirst)
#expect(store.messages(for: channel).map(\.content) == ["geo one", "geo three"])
}
@Test("Geohash appendIfAbsent remove and clear work together")
func geohashStoreSupportsAppendRemoveAndClear() {
var store = PublicTimelineStore(meshCap: 2, geohashCap: 3)
let geohash = "u4pruydq"
let channel = ChannelID.location(GeohashChannel(level: .city, geohash: geohash))
let first = TestHelpers.createTestMessage(content: "geo one")
let second = TestHelpers.createTestMessage(content: "geo two")
let didAppendFirst = store.appendIfAbsent(first, toGeohash: geohash)
let didAppendDuplicate = store.appendIfAbsent(first, toGeohash: geohash)
#expect(didAppendFirst)
#expect(!didAppendDuplicate)
store.append(second, toGeohash: geohash)
let removed = store.removeMessage(withID: first.id)
#expect(removed?.id == first.id)
#expect(store.messages(for: channel).map(\.content) == ["geo two"])
store.clear(channel: channel)
#expect(store.messages(for: channel).isEmpty)
}
@Test("Mutate geohash updates stored messages in place")
func mutateGeohashAppliesTransformation() {
var store = PublicTimelineStore(meshCap: 2, geohashCap: 3)
let geohash = "u4pruydq"
let channel = ChannelID.location(GeohashChannel(level: .city, geohash: geohash))
let first = TestHelpers.createTestMessage(content: "geo one")
store.append(first, toGeohash: geohash)
store.mutateGeohash(geohash) { timeline in
timeline.append(TestHelpers.createTestMessage(content: "geo two"))
}
#expect(store.messages(for: channel).map(\.content) == ["geo one", "geo two"])
}
@Test("Queued geohash system messages drain once")
func pendingGeohashSystemMessagesDrainOnce() {
var store = PublicTimelineStore(meshCap: 1, geohashCap: 1)
store.queueGeohashSystemMessage("first")
store.queueGeohashSystemMessage("second")
#expect(store.drainPendingGeohashSystemMessages() == ["first", "second"])
#expect(store.drainPendingGeohashSystemMessages().isEmpty)
}
private func timelineMessage(id: String, content: String, timestamp: TimeInterval) -> BitchatMessage {
BitchatMessage(
id: id,
sender: "alice",
content: content,
timestamp: Date(timeIntervalSince1970: timestamp),
isRelay: false
)
}
}