Make private-media deletion transactional (#1468)

Receiver-side private-media deletion (per-bubble delete and /clear) becomes a write-ahead transaction: a deletion journal in BLEPrivateMediaReceiptStore is the atomic commit point, materialization (tombstones + payload unlinks) is idempotent and retried on recovery, path reservations in BLEIncomingFileStore prevent delete racing an in-flight arrival, and overlapping /clear operations are serialized with panic-generation invalidation. Integrates with the receipt quarantine (a pending journal entry outranks quarantine; materialization never resurrects a quarantined ID).

Review fixes included: (1) /clear no longer deletes outgoing media mirrored into another conversation (alias protection now mirrors the incoming path, with a regression test that fails pre-fix); (2) explicit delete of legacy incoming media actually unlinks the decrypted payload when unreferenced — gated on pending-delivery/reservation state, restoring main's delete semantics safely instead of leaving plaintext for quota cleanup; (3) refused deletions surface a localized system message in the affected chat instead of failing silently (30-locale key). Full local suite 1876 green.
This commit is contained in:
jack
2026-07-26 14:33:12 +02:00
committed by GitHub
parent d6bd4f0681
commit c72bb4ca2e
14 changed files with 3954 additions and 194 deletions
+38
View File
@@ -1230,6 +1230,44 @@ struct BLEServiceCoreTests {
))
}
@Test @MainActor
func panicSuspension_finalizesStaleTransportEventsAsRejected() async {
let ble = makeService()
let delegate = TransportEventCaptureDelegate()
ble.eventDelegate = delegate
let message = BitchatMessage(
id: "pre-panic-finalization",
sender: "Peer",
content: "must be rejected",
timestamp: Date(),
isRelay: false,
isPrivate: true,
recipientNickname: "Me",
senderPeerID: PeerID(str: "1122334455667788")
)
var completions = 0
var outcomes: [TransportEventDeliveryOutcome] = []
ble._test_emitTransportEvent(
.messageReceived(message),
completion: { completions += 1 },
finalization: { outcomes.append($0) }
)
ble.suspendForPanicReset()
ble._test_emitTransportEvent(
.messageReceived(message),
completion: { completions += 1 },
finalization: { outcomes.append($0) }
)
for _ in 0..<4 {
await Task.yield()
}
#expect(delegate.messageIDs.isEmpty)
#expect(completions == 0)
#expect(outcomes == [.rejected, .rejected])
}
@Test
func modifiedServices_rediscoverWhenBitChatServiceIsInvalidated() async throws {
let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB")