| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // 🤖 The stored-tombstone contract (#581/#582), pinned by the tombstone_delete |
| 5 | // vector (canonical copy: test/event-vectors/chat.json — this fixture mirrors |
| 6 | // it, like the other *-vector.json files). A history/catch-up row carrying |
| 7 | // tombstone_of must evict that message exactly as the live "delete" frame |
| 8 | // does; old rows without the field decode unchanged. |
| 9 | final class TombstoneVectorTests: XCTestCase { |
| 10 | private struct Vector: Codable { |
| 11 | let name: String |
| 12 | let server_message: ServerMessage |
| 13 | let expected_fold_event: Fold |
| 14 | struct Fold: Codable { |
| 15 | let type: String |
| 16 | let msgId: Int64 |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | private func loadVector() throws -> Vector { |
| 21 | let url = try XCTUnwrap(Bundle.module.url(forResource: "Fixtures/tombstone-vector", withExtension: "json"), |
| 22 | "tombstone-vector.json missing from test fixtures") |
| 23 | return try JSONDecoder().decode(Vector.self, from: Data(contentsOf: url)) |
| 24 | } |
| 25 | |
| 26 | func testTombstoneRowDecodes() throws { |
| 27 | let v = try loadVector() |
| 28 | XCTAssertEqual(v.expected_fold_event.type, "chat.delete") |
| 29 | let row = v.server_message |
| 30 | XCTAssertEqual(row.tombstone_of, v.expected_fold_event.msgId, "the tombstone names the id to drop") |
| 31 | XCTAssertEqual(row.blob, "", "tombstones carry no ciphertext") |
| 32 | XCTAssertGreaterThan(row.id, row.tombstone_of ?? 0, |
| 33 | "tombstone ids are fresh — above every client's forward watermark") |
| 34 | } |
| 35 | |
| 36 | func testEvictionsMatchTheVector() throws { |
| 37 | let v = try loadVector() |
| 38 | // A page mixing an ordinary row with the tombstone: only the |
| 39 | // tombstone's target is evicted; plain rows are untouched. |
| 40 | let plain = ServerMessage(id: 12344, created_at: 1751899999, blob: "ciphertext", edited_at: nil, tombstone_of: nil) |
| 41 | let evict = ChatFeedModel.tombstoneEvictions([plain, v.server_message]) |
| 42 | XCTAssertEqual(evict, [v.expected_fold_event.msgId]) |
| 43 | } |
| 44 | |
| 45 | func testLegacyRowsWithoutTheFieldDecode() throws { |
| 46 | // Pre-#581 servers (and non-tombstone rows) omit the field entirely. |
| 47 | let json = #"{"id": 7, "created_at": 1700000000, "blob": "abc"}"# |
| 48 | let row = try JSONDecoder().decode(ServerMessage.self, from: Data(json.utf8)) |
| 49 | XCTAssertNil(row.tombstone_of) |
| 50 | XCTAssertTrue(ChatFeedModel.tombstoneEvictions([row]).isEmpty) |
| 51 | } |
| 52 | } |
| 53 | |