| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // 🤖 The two-generation memo's contract (#1210): a fill never wipes the hot |
| 5 | // set (the removeAll() bug re-parsed whole feeds on thread open), cold hits |
| 6 | // promote, and only the stalest generation is ever dropped. |
| 7 | final class MemoTests: XCTestCase { |
| 8 | func testHitsDoNotRebuild() { |
| 9 | var memo = Memo<String, Int>(cap: 4) |
| 10 | var builds = 0 |
| 11 | XCTAssertEqual(memo.get("a") { builds += 1; return 1 }, 1) |
| 12 | XCTAssertEqual(memo.get("a") { builds += 1; return 99 }, 1) |
| 13 | XCTAssertEqual(builds, 1) |
| 14 | } |
| 15 | |
| 16 | func testFillKeepsRecentEntriesReachable() { |
| 17 | var memo = Memo<Int, Int>(cap: 2) |
| 18 | var builds = 0 |
| 19 | for k in 0..<4 { _ = memo.get(k) { builds += 1; return k } } |
| 20 | XCTAssertEqual(builds, 4) |
| 21 | // 2 and 3 straddle the last rotation (one hot, one cold) — both must |
| 22 | // still be served without rebuilding. THIS is what removeAll() broke. |
| 23 | _ = memo.get(3) { builds += 1; return -1 } |
| 24 | _ = memo.get(2) { builds += 1; return -1 } |
| 25 | XCTAssertEqual(builds, 4) |
| 26 | } |
| 27 | |
| 28 | func testOnlyStalestGenerationDrops() { |
| 29 | var memo = Memo<Int, Int>(cap: 2) |
| 30 | var builds = 0 |
| 31 | for k in 0..<2 { _ = memo.get(k) { builds += 1; return k } } // hot = {0,1} |
| 32 | _ = memo.get(2) { builds += 1; return 2 } // rotate: cold={0,1}, hot={2} |
| 33 | _ = memo.get(0) { builds += 1; return 0 } // cold hit → promoted, no rebuild |
| 34 | XCTAssertEqual(builds, 3) |
| 35 | // Fill until 1's generation is two rotations back — only THEN does it rebuild. |
| 36 | for k in 10..<14 { _ = memo.get(k) { builds += 1; return k } } |
| 37 | _ = memo.get(1) { builds += 1; return 1 } |
| 38 | XCTAssertEqual(builds, 8) |
| 39 | } |
| 40 | } |
| 41 | |