NYTPuzzleUpgraderTests.swift (4833B)
1 import Foundation 2 import Testing 3 4 @testable import Crossmate 5 6 @Suite("NYTPuzzleUpgrader") 7 struct NYTPuzzleUpgraderTests { 8 9 // MARK: - Fixtures 10 11 /// 3×3 open grid with default solutions ABC/DEF/GHI. Override `gridRows` 12 /// for solution-diff tests; clue text overrides are independent so the 13 /// verifier sees a clue-only change. 14 private func openGridSource( 15 clueAcross1: String = "Across one", 16 clueAcross4: String = "Across four", 17 clueAcross5: String = "Across five", 18 clueDown1: String = "Down one", 19 clueDown2: String = "Down two", 20 clueDown3: String = "Down three", 21 gridRows: [String] = ["ABC", "DEF", "GHI"] 22 ) -> String { 23 precondition(gridRows.count == 3 && gridRows.allSatisfy { $0.count == 3 }) 24 let metadata = """ 25 Title: Test 26 Publisher: New York Times 27 Date: 2025-01-26 28 """ 29 let grid = gridRows.joined(separator: "\n") 30 func col(_ idx: Int) -> String { 31 String(gridRows.map { Array($0)[idx] }) 32 } 33 let clues = """ 34 A1. \(clueAcross1) ~ \(gridRows[0]) 35 A4. \(clueAcross4) ~ \(gridRows[1]) 36 A5. \(clueAcross5) ~ \(gridRows[2]) 37 38 D1. \(clueDown1) ~ \(col(0)) 39 D2. \(clueDown2) ~ \(col(1)) 40 D3. \(clueDown3) ~ \(col(2)) 41 """ 42 return [metadata, grid, clues].joined(separator: "\n\n\n") 43 } 44 45 /// 2×2 open grid for the dimensions-diff test. 46 private func smallGridSource() -> String { 47 let metadata = """ 48 Title: Small 49 Publisher: New York Times 50 Date: 2025-01-26 51 """ 52 let grid = "AB\nCD" 53 let clues = """ 54 A1. Top ~ AB 55 A3. Bottom ~ CD 56 57 D1. Left ~ AC 58 D2. Right ~ BD 59 """ 60 return [metadata, grid, clues].joined(separator: "\n\n\n") 61 } 62 63 /// 3×3 grid with a single block at (0,2). Manually written because the 64 /// open-grid helper assumes every cell is open. 65 private func blockedSource() -> String { 66 let metadata = """ 67 Title: Blocked 68 Publisher: New York Times 69 Date: 2025-01-26 70 """ 71 let grid = "AB#\nDEF\nGHI" 72 let clues = """ 73 A1. Top ~ AB 74 A3. Middle ~ DEF 75 A5. Bottom ~ GHI 76 77 D1. Left ~ ADG 78 D2. Mid ~ BEH 79 D4. Right ~ FI 80 """ 81 return [metadata, grid, clues].joined(separator: "\n\n\n") 82 } 83 84 // MARK: - Outcomes 85 86 @Test("Clue-only diff is .upgraded — grid + solutions match") 87 func clueOnlyDiffIsUpgraded() async { 88 let old = openGridSource(clueAcross1: "[aria-label] Old prefix") 89 let new = openGridSource(clueAcross1: "Old prefix") 90 let outcome = await NYTPuzzleUpgrader.upgrade( 91 date: Date(), 92 currentSource: old, 93 fetch: { _ in new } 94 ) 95 guard case .upgraded(let returned) = outcome else { 96 Issue.record("Expected .upgraded but got \(outcome)") 97 return 98 } 99 #expect(returned == new) 100 } 101 102 @Test("Solution diff is .mismatched") 103 func solutionDiffIsMismatched() async { 104 let old = openGridSource() 105 let new = openGridSource(gridRows: ["ABC", "DEF", "GHJ"]) 106 let outcome = await NYTPuzzleUpgrader.upgrade( 107 date: Date(), 108 currentSource: old, 109 fetch: { _ in new } 110 ) 111 if case .mismatched = outcome { return } 112 Issue.record("Expected .mismatched but got \(outcome)") 113 } 114 115 @Test("Block-pattern diff is .mismatched") 116 func blockPatternDiffIsMismatched() async { 117 let old = openGridSource() 118 let new = blockedSource() 119 let outcome = await NYTPuzzleUpgrader.upgrade( 120 date: Date(), 121 currentSource: old, 122 fetch: { _ in new } 123 ) 124 if case .mismatched = outcome { return } 125 Issue.record("Expected .mismatched but got \(outcome)") 126 } 127 128 @Test("Dimensions diff is .mismatched") 129 func dimensionsDiffIsMismatched() async { 130 let old = openGridSource() 131 let new = smallGridSource() 132 let outcome = await NYTPuzzleUpgrader.upgrade( 133 date: Date(), 134 currentSource: old, 135 fetch: { _ in new } 136 ) 137 if case .mismatched = outcome { return } 138 Issue.record("Expected .mismatched but got \(outcome)") 139 } 140 141 @Test("Fetcher throwing is .failed") 142 func fetcherThrowingIsFailed() async { 143 struct StubError: Error {} 144 let old = openGridSource() 145 let outcome = await NYTPuzzleUpgrader.upgrade( 146 date: Date(), 147 currentSource: old, 148 fetch: { _ in throw StubError() } 149 ) 150 if case .failed = outcome { return } 151 Issue.record("Expected .failed but got \(outcome)") 152 } 153 }