fix(planning): gameFirst mode now uses full date range and shows correct month

Two bugs fixed in "By Games" trip planning mode:

1. Calendar navigation: DateRangePicker now navigates to the selected
   game's month when startDate changes externally, instead of staying
   on the current month.

2. Date range calculation: Fixed race condition where date range was
   calculated before games were loaded. Now updateDateRangeForSelectedGames()
   is called after loadSummaryGames() completes.

3. Bonus games: planTrip() now uses the UI-selected 7-day date range
   instead of overriding it with just the anchor game dates. This allows
   ScenarioBPlanner to find additional games within the trip window.

Added regression tests to verify gameFirst mode includes bonus games.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-21 16:37:19 -06:00
parent 4d097883a6
commit d97dec44b2
4 changed files with 153 additions and 30 deletions

View File

@@ -138,6 +138,127 @@ struct ScenarioBPlannerTests {
}
}
// MARK: - Regression Tests: Bonus Games in Date Range
@Test("plan: gameFirst mode includes bonus games within date range")
func plan_gameFirstMode_includesBonusGamesInDateRange() {
// Regression test: When a single anchor game is selected, the planner should
// find additional "bonus" games within the date range that fit geographically.
// Bug: planTrip() was overriding the 7-day date range with just anchor dates,
// causing only the anchor game to appear in results.
let startDate = Date()
let endDate = startDate.addingTimeInterval(86400 * 7) // 7-day span
// NYC and Boston are geographically close (drivable)
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
// Anchor game on day 4
let anchorGame = makeGame(id: "anchor-game", stadiumId: "nyc", dateTime: startDate.addingTimeInterval(86400 * 3))
// Bonus game on day 2 (within date range, geographically sensible)
let bonusGame = makeGame(id: "bonus-game", stadiumId: "boston", dateTime: startDate.addingTimeInterval(86400 * 1))
let prefs = TripPreferences(
planningMode: .gameFirst,
sports: [.mlb],
mustSeeGameIds: ["anchor-game"], // Only anchor is selected
startDate: startDate,
endDate: endDate,
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 2
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [anchorGame, bonusGame], // Both games available
teams: [:],
stadiums: ["nyc": nycStadium, "boston": bostonStadium]
)
let result = planner.plan(request: request)
guard case .success(let options) = result else {
Issue.record("Expected success with bonus game available")
return
}
#expect(!options.isEmpty, "Should have trip options")
// At least one option should include the bonus game
let optionsWithBonus = options.filter { option in
option.stops.flatMap { $0.games }.contains("bonus-game")
}
#expect(!optionsWithBonus.isEmpty, "At least one route should include bonus game from date range")
// ALL options must still contain the anchor game
for option in options {
let gameIds = option.stops.flatMap { $0.games }
#expect(gameIds.contains("anchor-game"), "Anchor game must be in every route")
}
}
@Test("plan: gameFirst mode uses full date range not just anchor dates")
func plan_gameFirstMode_usesFullDateRange() {
// Regression test: Verify that the planner considers games across the entire
// date range, not just on the anchor game dates.
let startDate = Date()
// 7-day date range
let day1 = startDate
let day3 = startDate.addingTimeInterval(86400 * 2)
let day4 = startDate.addingTimeInterval(86400 * 3) // Anchor game day
let day6 = startDate.addingTimeInterval(86400 * 5)
let endDate = startDate.addingTimeInterval(86400 * 7)
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
let phillyStadium = makeStadium(id: "philly", city: "Philadelphia", coordinate: phillyCoord)
// Anchor game on day 4
let anchorGame = makeGame(id: "anchor", stadiumId: "nyc", dateTime: day4)
// Games on other days
let day1Game = makeGame(id: "day1-game", stadiumId: "philly", dateTime: day1)
let day3Game = makeGame(id: "day3-game", stadiumId: "boston", dateTime: day3)
let day6Game = makeGame(id: "day6-game", stadiumId: "philly", dateTime: day6)
let prefs = TripPreferences(
planningMode: .gameFirst,
sports: [.mlb],
mustSeeGameIds: ["anchor"],
startDate: startDate,
endDate: endDate,
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 2
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [anchorGame, day1Game, day3Game, day6Game],
teams: [:],
stadiums: ["nyc": nycStadium, "boston": bostonStadium, "philly": phillyStadium]
)
let result = planner.plan(request: request)
guard case .success(let options) = result else {
Issue.record("Expected success")
return
}
// Collect all game IDs across all options
let allGameIdsInOptions = Set(options.flatMap { $0.stops.flatMap { $0.games } })
// At least some non-anchor games should appear in the results
// (we don't require ALL because geographic constraints may exclude some)
let bonusGamesFound = allGameIdsInOptions.subtracting(["anchor"])
#expect(!bonusGamesFound.isEmpty, "Planner should find bonus games from full date range, not just anchor date")
}
// MARK: - Specification Tests: Sliding Window
@Test("plan: gameFirst mode uses sliding window")