// // PlanningPipelineBugRegressionTests.swift // SportsTimeTests // // Regression tests for bugs found in the planning pipeline code review. // Each test is numbered to match the bug report (Bug #1 through #16). // import Testing import Foundation import CoreLocation @testable import SportsTime // MARK: - Bug #1: teamFirst with 1 team falls through to ScenarioA @Suite("Bug #1: ScenarioPlannerFactory teamFirst single team") struct Bug1_TeamFirstSingleTeamTests { @Test("teamFirst with 1 team should not fall through to ScenarioA") func teamFirst_singleTeam_shouldNotFallThroughToScenarioA() { let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], travelMode: .drive, startDate: Date(), endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!, leisureLevel: .moderate, routePreference: .balanced, selectedTeamIds: ["team_mlb_boston"] ) let request = PlanningRequest( preferences: prefs, availableGames: [], teams: [:], stadiums: [:] ) let scenario = ScenarioPlannerFactory.classify(request) // With 1 team in teamFirst, should dispatch to ScenarioD (follow team) or ScenarioE, NOT ScenarioA #expect(scenario != .scenarioA, "1-team teamFirst should not fall through to ScenarioA (date range)") } @Test("teamFirst with 2 teams dispatches to ScenarioE") func teamFirst_twoTeams_dispatchesToScenarioE() { let prefs = TripPreferences( planningMode: .teamFirst, sports: [.mlb], travelMode: .drive, startDate: Date(), endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!, leisureLevel: .moderate, routePreference: .balanced, selectedTeamIds: ["team_mlb_boston", "team_mlb_new_york"] ) let request = PlanningRequest( preferences: prefs, availableGames: [], teams: [:], stadiums: [:] ) let scenario = ScenarioPlannerFactory.classify(request) #expect(scenario == .scenarioE) } } // MARK: - Bug #2: Infinite loop in calculateRestDays / allDates @Suite("Bug #2: Infinite loop safety in date iteration") struct Bug2_InfiniteLoopTests { @Test("calculateRestDays does not hang for normal multi-day stop") func calculateRestDays_normalMultiDay_terminates() { let calendar = Calendar.current let arrival = TestFixtures.date(year: 2026, month: 6, day: 10, hour: 12) let departure = TestFixtures.date(year: 2026, month: 6, day: 14, hour: 12) let stop = ItineraryStop( city: "Boston", state: "MA", coordinate: TestFixtures.coordinates["Boston"], games: ["game_1"], arrivalDate: arrival, departureDate: departure, location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]), firstGameStart: arrival ) let option = ItineraryOption( rank: 1, stops: [stop], travelSegments: [], totalDrivingHours: 0, totalDistanceMiles: 0, geographicRationale: "Test" ) // Should complete without hanging. Rest days = days between arrival and departure // (excluding both endpoints) = Jun 11, 12, 13 = 3 rest days let timeline = option.generateTimeline() let restItems = timeline.filter { $0.isRest } #expect(restItems.count == 3) } @Test("allDates does not hang for normal date range") func allDates_normalRange_terminates() { let arrival = TestFixtures.date(year: 2026, month: 6, day: 10, hour: 12) let departure = TestFixtures.date(year: 2026, month: 6, day: 13, hour: 12) let stop = ItineraryStop( city: "Boston", state: "MA", coordinate: TestFixtures.coordinates["Boston"], games: ["game_1"], arrivalDate: arrival, departureDate: departure, location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]), firstGameStart: arrival ) let option = ItineraryOption( rank: 1, stops: [stop], travelSegments: [], totalDrivingHours: 0, totalDistanceMiles: 0, geographicRationale: "Test" ) // Should return 4 dates: Jun 10, 11, 12, 13 let dates = option.allDates() #expect(dates.count == 4) } } // MARK: - Bug #3: Same-day trip rejection @Suite("Bug #3: Same-day trip date range") struct Bug3_SameDayTripTests { @Test("dateRange should be valid when startDate equals endDate") func dateRange_sameDayTrip_shouldBeValid() { let today = TestFixtures.date(year: 2026, month: 7, day: 4, hour: 8) let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], travelMode: .drive, startDate: today, endDate: today, leisureLevel: .moderate, routePreference: .balanced ) let request = PlanningRequest( preferences: prefs, availableGames: [], teams: [:], stadiums: [:] ) #expect(request.dateRange != nil, "Same-day trip should produce a valid dateRange, not nil") } } // MARK: - Bug #4: ScenarioD rationale shows stops.count not game count @Suite("Bug #4: ScenarioD rationale game count") struct Bug4_ScenarioDRationaleTests { @Test("geographicRationale should show game count not stop count") func rationale_shouldShowGameCount() { // Create 3 games across 2 cities (2 in Boston, 1 in NYC) let date1 = TestFixtures.date(year: 2026, month: 7, day: 1, hour: 19) let date2 = TestFixtures.date(year: 2026, month: 7, day: 2, hour: 19) let date3 = TestFixtures.date(year: 2026, month: 7, day: 4, hour: 19) let bostonStadium = TestFixtures.stadium(city: "Boston") let nycStadium = TestFixtures.stadium(city: "New York") let bostonTeamId = "team_mlb_boston" let games = [ TestFixtures.game(id: "g1", city: "Boston", dateTime: date1, homeTeamId: bostonTeamId, stadiumId: bostonStadium.id), TestFixtures.game(id: "g2", city: "Boston", dateTime: date2, homeTeamId: bostonTeamId, stadiumId: bostonStadium.id), TestFixtures.game(id: "g3", city: "New York", dateTime: date3, awayTeamId: bostonTeamId, stadiumId: nycStadium.id), ] let prefs = TripPreferences( planningMode: .followTeam, sports: [.mlb], travelMode: .drive, startDate: date1.addingTimeInterval(-86400), endDate: date3.addingTimeInterval(86400), leisureLevel: .moderate, routePreference: .balanced, followTeamId: bostonTeamId ) let stadiums: [String: Stadium] = [ bostonStadium.id: bostonStadium, nycStadium.id: nycStadium, ] let request = PlanningRequest( preferences: prefs, availableGames: games, teams: [:], stadiums: stadiums ) let planner = ScenarioDPlanner() let result = planner.plan(request: request) if case .success(let options) = result { // Bug #4: rationale was using stops.count instead of actual game count. // Verify that for each option, the game count in the rationale matches // the actual total games across stops. for option in options { let actualGameCount = option.stops.reduce(0) { $0 + $1.games.count } let rationale = option.geographicRationale #expect(rationale.contains("\(actualGameCount) games"), "Rationale game count should match actual games (\(actualGameCount)). Got: \(rationale)") } } // If planning fails, that's OK — this test focuses on rationale text when it succeeds } } // MARK: - Bug #5: ScenarioD departureDate not advanced @Suite("Bug #5: ScenarioD departure date") struct Bug5_ScenarioDDepartureDateTests { @Test("stop departureDate should be after last game, not same day") func departureDate_shouldBeAfterLastGame() { let gameDate = TestFixtures.date(year: 2026, month: 7, day: 5, hour: 19) let calendar = Calendar.current let bostonStadium = TestFixtures.stadium(city: "Boston") let game = TestFixtures.game(id: "g1", city: "Boston", dateTime: gameDate, stadiumId: bostonStadium.id) let prefs = TripPreferences( planningMode: .followTeam, sports: [.mlb], travelMode: .drive, startDate: gameDate.addingTimeInterval(-86400), endDate: gameDate.addingTimeInterval(86400 * 3), leisureLevel: .moderate, routePreference: .balanced, followTeamId: game.homeTeamId ) let request = PlanningRequest( preferences: prefs, availableGames: [game], teams: [:], stadiums: [bostonStadium.id: bostonStadium] ) let planner = ScenarioDPlanner() let result = planner.plan(request: request) if case .success(let options) = result, let option = options.first { // Find the game stop (not the home start/end waypoints) let gameStops = option.stops.filter { $0.hasGames } if let gameStop = gameStops.first { let gameDayStart = calendar.startOfDay(for: gameDate) let departureDayStart = calendar.startOfDay(for: gameStop.departureDate) #expect(departureDayStart > gameDayStart, "Departure should be after game day, not same day. Game: \(gameDayStart), Departure: \(departureDayStart)") } } } } // MARK: - Bug #6: ScenarioC date range off-by-one @Suite("Bug #6: ScenarioC date range boundary") struct Bug6_ScenarioCDateRangeTests { @Test("games spanning exactly daySpan should be included") func gamesSpanningExactDaySpan_shouldBeIncluded() { // If daySpan is 7, games exactly 7 days apart should be valid let calendar = Calendar.current let startDate = TestFixtures.date(year: 2026, month: 7, day: 1, hour: 19) let endDate = calendar.date(byAdding: .day, value: 7, to: startDate)! let startStadium = TestFixtures.stadium(city: "New York") let endStadium = TestFixtures.stadium(city: "Chicago") let startGame = TestFixtures.game(id: "g1", city: "New York", dateTime: startDate, stadiumId: startStadium.id) let endGame = TestFixtures.game(id: "g2", city: "Chicago", dateTime: endDate, stadiumId: endStadium.id) let prefs = TripPreferences( planningMode: .locations, startLocation: LocationInput(name: "New York", coordinate: TestFixtures.coordinates["New York"]), endLocation: LocationInput(name: "Chicago", coordinate: TestFixtures.coordinates["Chicago"]), sports: [.mlb], travelMode: .drive, startDate: startDate.addingTimeInterval(-86400), endDate: endDate.addingTimeInterval(86400), tripDuration: 7, leisureLevel: .moderate, routePreference: .balanced ) let stadiums: [String: Stadium] = [ startStadium.id: startStadium, endStadium.id: endStadium, ] let request = PlanningRequest( preferences: prefs, availableGames: [startGame, endGame], teams: [:], stadiums: stadiums ) let planner = ScenarioCPlanner() let result = planner.plan(request: request) // Should find at least one option — games exactly span the trip duration if case .failure(let failure) = result { let reason = failure.reason #expect(reason != PlanningFailure.FailureReason.noGamesInRange, "Games spanning exactly daySpan should not be excluded. Failure: \(failure.message)") } } } // MARK: - Bug #7: DrivingConstraints missing clamp @Suite("Bug #7: DrivingConstraints init(from:) clamp") struct Bug7_DrivingConstraintsClampTests { @Test("init(from:) should clamp maxDrivingHoursPerDriver to minimum 1.0") func initFromPreferences_clampsMaxHours() { let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], travelMode: .drive, startDate: Date(), endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!, leisureLevel: .moderate, routePreference: .balanced, maxDrivingHoursPerDriver: 0.5 ) let constraints = DrivingConstraints(from: prefs) #expect(constraints.maxHoursPerDriverPerDay >= 1.0, "maxHoursPerDriverPerDay should be clamped to at least 1.0, got \(constraints.maxHoursPerDriverPerDay)") } @Test("standard init clamps correctly") func standardInit_clampsMaxHours() { let constraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 0.5) #expect(constraints.maxHoursPerDriverPerDay >= 1.0) } @Test("nil maxDrivingHoursPerDriver defaults to 8.0") func nilMaxHours_defaultsTo8() { let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], travelMode: .drive, startDate: Date(), endDate: Calendar.current.date(byAdding: .day, value: 7, to: Date())!, leisureLevel: .moderate, routePreference: .balanced, maxDrivingHoursPerDriver: nil ) let constraints = DrivingConstraints(from: prefs) #expect(constraints.maxHoursPerDriverPerDay == 8.0) } } // MARK: - Bug #8: effectiveTripDuration off by one @Suite("Bug #8: effectiveTripDuration off-by-one") struct Bug8_EffectiveTripDurationTests { @Test("3-day trip (May 1-3) should return 3, not 2") func threeDayTrip_returns3() { let start = TestFixtures.date(year: 2026, month: 5, day: 1, hour: 8) let end = TestFixtures.date(year: 2026, month: 5, day: 3, hour: 8) let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], travelMode: .drive, startDate: start, endDate: end, leisureLevel: .moderate, routePreference: .balanced ) #expect(prefs.effectiveTripDuration == 3, "May 1-3 is 3 days inclusive, got \(prefs.effectiveTripDuration)") } @Test("1-day trip (same day) should return 1") func sameDayTrip_returns1() { let date = TestFixtures.date(year: 2026, month: 5, day: 1, hour: 8) let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], travelMode: .drive, startDate: date, endDate: date, leisureLevel: .moderate, routePreference: .balanced ) #expect(prefs.effectiveTripDuration == 1, "Same-day trip should be 1 day, got \(prefs.effectiveTripDuration)") } @Test("7-day trip returns 7") func sevenDayTrip_returns7() { let start = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 8) let end = TestFixtures.date(year: 2026, month: 6, day: 7, hour: 8) let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], travelMode: .drive, startDate: start, endDate: end, leisureLevel: .moderate, routePreference: .balanced ) #expect(prefs.effectiveTripDuration == 7, "Jun 1-7 is 7 days inclusive, got \(prefs.effectiveTripDuration)") } } // MARK: - Bug #9: TripWizardViewModel missing date validation // Note: ViewModel tests require @MainActor — tested via TripPreferences validation @Suite("Bug #9: Date validation") struct Bug9_DateValidationTests { @Test("PlanningRequest.dateRange returns nil for reversed dates") func reversedDates_returnsNil() { let start = TestFixtures.date(year: 2026, month: 7, day: 10, hour: 8) let end = TestFixtures.date(year: 2026, month: 7, day: 5, hour: 8) let prefs = TripPreferences( planningMode: .dateRange, sports: [.mlb], travelMode: .drive, startDate: start, endDate: end, leisureLevel: .moderate, routePreference: .balanced ) let request = PlanningRequest( preferences: prefs, availableGames: [], teams: [:], stadiums: [:] ) // Reversed dates should return nil dateRange (this is expected behavior) #expect(request.dateRange == nil) } } // MARK: - Bug #10: allDates assumes sorted stops @Suite("Bug #10: allDates with unsorted stops") struct Bug10_AllDatesUnsortedTests { @Test("allDates returns correct range even with properly ordered stops") func allDates_sortedStops_correctRange() { let arrival1 = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 12) let departure1 = TestFixtures.date(year: 2026, month: 6, day: 3, hour: 12) let arrival2 = TestFixtures.date(year: 2026, month: 6, day: 3, hour: 18) let departure2 = TestFixtures.date(year: 2026, month: 6, day: 5, hour: 12) let stop1 = ItineraryStop( city: "Boston", state: "MA", coordinate: TestFixtures.coordinates["Boston"], games: ["g1"], arrivalDate: arrival1, departureDate: departure1, location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]), firstGameStart: arrival1 ) let stop2 = ItineraryStop( city: "New York", state: "NY", coordinate: TestFixtures.coordinates["New York"], games: ["g2"], arrivalDate: arrival2, departureDate: departure2, location: LocationInput(name: "New York", coordinate: TestFixtures.coordinates["New York"]), firstGameStart: arrival2 ) let option = ItineraryOption( rank: 1, stops: [stop1, stop2], travelSegments: [], totalDrivingHours: 3, totalDistanceMiles: 200, geographicRationale: "Test" ) let dates = option.allDates() // Jun 1-5 = 5 dates #expect(dates.count == 5) } } // MARK: - Bug #11: Moderate leisure sorting degenerates for zero driving @Suite("Bug #11: sortByLeisure with zero driving") struct Bug11_SortByLeisureTests { @Test("moderate sorting differentiates zero-driving options meaningfully") func moderateSorting_zeroDriving_sortsByGameCount() { let stop1 = ItineraryStop( city: "Boston", state: "MA", coordinate: TestFixtures.coordinates["Boston"], games: ["g1", "g2", "g3"], arrivalDate: Date(), departureDate: Date().addingTimeInterval(86400), location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]), firstGameStart: Date() ) let stop2 = ItineraryStop( city: "Boston", state: "MA", coordinate: TestFixtures.coordinates["Boston"], games: ["g4"], arrivalDate: Date(), departureDate: Date().addingTimeInterval(86400), location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]), firstGameStart: Date() ) let option3Games = ItineraryOption( rank: 1, stops: [stop1], travelSegments: [], totalDrivingHours: 0, totalDistanceMiles: 0, geographicRationale: "3 games" ) let option1Game = ItineraryOption( rank: 2, stops: [stop2], travelSegments: [], totalDrivingHours: 0, totalDistanceMiles: 0, geographicRationale: "1 game" ) let sorted = ItineraryOption.sortByLeisure([option1Game, option3Games], leisureLevel: .moderate) // With zero driving, moderate should still rank options // (degenerates to game count, which is documented behavior — test confirms it works) #expect(sorted.first?.totalGames == 3, "With zero driving, more games should rank first in moderate mode") } } // MARK: - Bug #12: ItineraryBuilder validator ignores game end time @Suite("Bug #12: arrivalBeforeGameStart validator") struct Bug12_ValidatorGameEndTimeTests { @Test("validator checks arrival feasibility with buffer") func validator_checksArrivalBuffer() { // Use deterministic dates to avoid time-of-day sensitivity let calendar = Calendar.current let today = TestFixtures.date(year: 2026, month: 7, day: 10, hour: 14) // 2pm today let tomorrow = calendar.date(byAdding: .day, value: 1, to: today)! let tomorrowMorning = TestFixtures.date(year: 2026, month: 7, day: 11, hour: 8) // 8am departure let gameTimeTomorrow = TestFixtures.date(year: 2026, month: 7, day: 11, hour: 19) // 7pm game let fromStop = ItineraryStop( city: "Boston", state: "MA", coordinate: TestFixtures.coordinates["Boston"], games: ["g1"], arrivalDate: today, departureDate: tomorrowMorning, location: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]), firstGameStart: today // Game at fromStop starts at 2pm today, ends ~5pm ) let toStop = ItineraryStop( city: "New York", state: "NY", coordinate: TestFixtures.coordinates["New York"], games: ["g2"], arrivalDate: tomorrow, departureDate: tomorrow.addingTimeInterval(86400), location: LocationInput(name: "New York", coordinate: TestFixtures.coordinates["New York"]), firstGameStart: gameTimeTomorrow ) let shortSegment = TravelSegment( fromLocation: LocationInput(name: "Boston", coordinate: TestFixtures.coordinates["Boston"]), toLocation: LocationInput(name: "New York", coordinate: TestFixtures.coordinates["New York"]), travelMode: .drive, distanceMeters: 300000, durationSeconds: 3600 * 3 // 3 hours ) let validator = ItineraryBuilder.arrivalBeforeGameStart(bufferSeconds: 3600) let result = validator(shortSegment, fromStop, toStop) // Depart 8am + 3h travel = arrive 11am, game at 7pm - 1h buffer = 6pm deadline. 11am < 6pm → pass #expect(result == true, "3-hour drive with ample time should pass validation") } } // MARK: - Bug #13: Silent game exclusion with missing stadium @Suite("Bug #13: Missing stadium in region filter") struct Bug13_MissingStadiumTests { @Test("games with missing stadiums excluded when regions active") func missingStadium_excludedWithRegionFilter() { let gameDate = TestFixtures.date(year: 2026, month: 7, day: 5, hour: 19) let game = TestFixtures.game( id: "orphan_game", city: "New York", dateTime: gameDate, stadiumId: "stadium_that_does_not_exist" ) let prefs = TestFixtures.preferences( startDate: gameDate.addingTimeInterval(-86400), endDate: gameDate.addingTimeInterval(86400), regions: [.east] // Region filter is active ) let request = PlanningRequest( preferences: prefs, availableGames: [game], teams: [:], stadiums: [:] // No stadiums — game's stadium is missing ) let planner = ScenarioAPlanner() let result = planner.plan(request: request) // Currently: silently excluded → noGamesInRange. // This test documents the current behavior (missing stadiums are excluded). if case .failure(let failure) = result { #expect(failure.reason == .noGamesInRange) } } } // MARK: - Bug #14: Drag drop feedback (UI test — documented only) // This is a UI interaction bug that cannot be unit tested. // Documenting the expected behavior here. @Suite("Bug #14: Drag drop feedback") struct Bug14_DragDropTests { @Test("documented: drag state should not be cleared before validation") func documented_dragStateShouldPersistDuringValidation() { // This bug is in TripDetailView.swift:1508-1525 (UI layer). // Drag state is cleared synchronously before async validation runs. // If validation fails, no visual feedback is shown. // Fix: Move drag state clearing AFTER validation succeeds. #expect(true, "UI bug documented — drag state should persist during validation") } } // MARK: - Bug #15: ScenarioB force unwraps on date arithmetic @Suite("Bug #15: ScenarioB date arithmetic safety") struct Bug15_DateArithmeticTests { @Test("generateDateRanges handles normal date ranges safely") func generateDateRanges_normalDates_doesNotCrash() { let start = TestFixtures.date(year: 2026, month: 7, day: 1, hour: 19) let end = TestFixtures.date(year: 2026, month: 7, day: 10, hour: 19) let startStadium = TestFixtures.stadium(city: "Boston") let endStadium = TestFixtures.stadium(city: "New York") let games = [ TestFixtures.game(id: "g1", city: "Boston", dateTime: start, stadiumId: startStadium.id), TestFixtures.game(id: "g2", city: "New York", dateTime: end, stadiumId: endStadium.id), ] let prefs = TripPreferences( planningMode: .gameFirst, sports: [.mlb], mustSeeGameIds: Set(games.map { $0.id }), travelMode: .drive, startDate: start.addingTimeInterval(-86400 * 2), endDate: end.addingTimeInterval(86400 * 2), leisureLevel: .moderate, routePreference: .balanced ) let stadiums: [String: Stadium] = [ startStadium.id: startStadium, endStadium.id: endStadium, ] let request = PlanningRequest( preferences: prefs, availableGames: games, teams: [:], stadiums: stadiums ) let planner = ScenarioBPlanner() // Should not crash — just verifying safety let _ = planner.plan(request: request) } } // MARK: - Bug #16: Negative sortOrder accumulation @Suite("Bug #16: Sort order accumulation") struct Bug16_SortOrderTests { @Test("documented: repeated before-games moves should use midpoint not subtraction") func documented_sortOrderShouldNotGoExtremelyNegative() { // This bug is in ItineraryReorderingLogic.swift:420-428. // Each "move before first item" subtracts 1.0 instead of using midpoint. // After many moves, sortOrder becomes -10, -20, etc. // Fix: Use midpoint (n/2.0) instead of subtraction (n-1.0). #expect(true, "Documented: sortOrder should use midpoint insertion") } } // MARK: - Cross-cutting: TravelEstimator consistency @Suite("TravelEstimator consistency") struct TravelEstimatorConsistencyTests { @Test("ItineraryStop and LocationInput estimates produce consistent distances") func bothOverloads_produceConsistentDistances() { let bostonCoord = TestFixtures.coordinates["Boston"]! let nycCoord = TestFixtures.coordinates["New York"]! let constraints = DrivingConstraints.default // ItineraryStop overload let fromStop = ItineraryStop( city: "Boston", state: "MA", coordinate: bostonCoord, games: [], arrivalDate: Date(), departureDate: Date(), location: LocationInput(name: "Boston", coordinate: bostonCoord), firstGameStart: nil ) let toStop = ItineraryStop( city: "New York", state: "NY", coordinate: nycCoord, games: [], arrivalDate: Date(), departureDate: Date(), location: LocationInput(name: "New York", coordinate: nycCoord), firstGameStart: nil ) let stopResult = TravelEstimator.estimate(from: fromStop, to: toStop, constraints: constraints) // LocationInput overload let fromLoc = LocationInput(name: "Boston", coordinate: bostonCoord) let toLoc = LocationInput(name: "New York", coordinate: nycCoord) let locResult = TravelEstimator.estimate(from: fromLoc, to: toLoc, constraints: constraints) guard let s = stopResult, let l = locResult else { Issue.record("Both estimates should return non-nil results") return } // Distances should be within 1% of each other let ratio = s.distanceMeters / l.distanceMeters #expect(abs(ratio - 1.0) < 0.01, "Distance mismatch: stop=\(s.distanceMeters)m vs location=\(l.distanceMeters)m, ratio=\(ratio)") } }