Resurrect dead Noise vector tests; add CI perf floors; make tests hermetic

Package.swift's .process("Noise") resource claim silently excluded all
of bitchatTests/Noise/ from compilation since Oct 2025 - including a
complete official-vector runner (cacophony + snow XX transcripts,
transport messages, handshake hash, byte-identical to upstream).
Narrowing the resource to the JSON file and loading via Bundle.module
brings 51 Noise tests back to life, with a guard asserting each
vector's protocol name matches the app's.

CI gains a performance floor gate: perf-floors.json carries deliberately
generous floors (~25% of measured throughput) that catch algorithmic
regressions without flaking on runner variance; PERF lines reach the
gate via an O_APPEND side-channel file since swift test --parallel
swallows passing tests' stdout.

Tests are now hermetic: FavoritesPersistenceService uses an in-memory
keychain under test (fixes the securityd hang that blocked pipeline
benchmarks locally) and read-receipt persistence uses a wiped scratch
UserDefaults suite instead of .standard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-11 15:53:34 +02:00
co-authored by Claude Fable 5
parent 8a867a17a1
commit 22be3d6392
9 changed files with 285 additions and 12 deletions
+24 -4
View File
@@ -406,6 +406,23 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// Single-writer: mutate only via `setPublicBatching(_:)` below.
@Published private(set) var isBatchingPublic: Bool = false
// Backing store for `sentReadReceipts` persistence. `.standard` in
// production; injectable so tests can use a scratch suite that does not
// leak state between runs.
let readReceiptsDefaults: UserDefaults
/// Default read-receipt persistence store. Production uses `.standard`.
/// Under test, a dedicated scratch suite is used instead wiped at first
/// use per process so back-to-back local test runs never see each
/// other's persisted receipts (and tests never pollute `.standard`).
static let defaultReadReceiptsDefaults: UserDefaults = {
guard TestEnvironment.isRunningTests else { return .standard }
let suiteName = "chat.bitchat.tests.readReceipts"
guard let scratch = UserDefaults(suiteName: suiteName) else { return .standard }
scratch.removePersistentDomain(forName: suiteName)
return scratch
}()
// Track sent read receipts to avoid duplicates (persisted across launches)
// Note: Persistence happens automatically in didSet, no lifecycle observers needed
var sentReadReceipts: Set<String> = [] { // messageID set
@@ -413,9 +430,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// Only persist if there are changes
guard oldValue != sentReadReceipts else { return }
// Persist to UserDefaults whenever it changes (no manual synchronize/verify re-read)
// Persist whenever it changes (no manual synchronize/verify re-read)
if let data = try? JSONEncoder().encode(Array(sentReadReceipts)) {
UserDefaults.standard.set(data, forKey: "sentReadReceipts")
readReceiptsDefaults.set(data, forKey: "sentReadReceipts")
} else {
SecureLogger.error("❌ Failed to encode read receipts for persistence", category: .session)
}
@@ -763,7 +780,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
conversations: ConversationStore? = nil,
peerIdentityStore: PeerIdentityStore? = nil,
locationPresenceStore: LocationPresenceStore? = nil,
locationManager: LocationChannelManager = .shared
locationManager: LocationChannelManager = .shared,
readReceiptsDefaults: UserDefaults? = nil
) {
let conversations = conversations ?? ConversationStore()
let peerIdentityStore = peerIdentityStore ?? PeerIdentityStore()
@@ -790,7 +808,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
self.autocompleteService = services.autocompleteService
self.deduplicationService = services.deduplicationService
self.publicMessagePipeline = services.publicMessagePipeline
self.sentReadReceipts = ChatViewModelBootstrapper.loadPersistedReadReceipts()
let readReceiptsDefaults = readReceiptsDefaults ?? Self.defaultReadReceiptsDefaults
self.readReceiptsDefaults = readReceiptsDefaults
self.sentReadReceipts = ChatViewModelBootstrapper.loadPersistedReadReceipts(userDefaults: readReceiptsDefaults)
// Republish on every store change so SwiftUI observers of the
// view model refresh. This replaces the UI-update role of the old