feat: improve planning engine travel handling, itinerary reordering, and scenario planners
Add TravelInfo initializers and city normalization helpers to fix repeat city-pair disambiguation. Improve drag-and-drop reordering with segment index tracking and source-row-aware zone calculation. Enhance all five scenario planners with better next-day departure handling and travel segment placement. Add comprehensive tests across all planners. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -406,6 +406,146 @@ struct ScenarioEPlannerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test("plan: falls back when earliest per-team anchors are infeasible")
|
||||
func plan_fallbackWhenEarliestAnchorsInfeasible() {
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let chicagoStadium = makeStadium(id: "chi", city: "Chicago", coordinate: chicagoCoord)
|
||||
|
||||
// Team A has one early game in NYC.
|
||||
let teamAGame = makeGame(
|
||||
id: "team-a-day1",
|
||||
homeTeamId: "teamA",
|
||||
awayTeamId: "opp",
|
||||
stadiumId: "nyc",
|
||||
dateTime: calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))!
|
||||
)
|
||||
|
||||
// Team B has an early game (day 2, infeasible from NYC with 1 driver),
|
||||
// and a later game (day 4, feasible and should be selected by fallback).
|
||||
let teamBEarly = makeGame(
|
||||
id: "team-b-day2",
|
||||
homeTeamId: "teamB",
|
||||
awayTeamId: "opp",
|
||||
stadiumId: "chi",
|
||||
dateTime: calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 2))!
|
||||
)
|
||||
let teamBLate = makeGame(
|
||||
id: "team-b-day4",
|
||||
homeTeamId: "teamB",
|
||||
awayTeamId: "opp",
|
||||
stadiumId: "chi",
|
||||
dateTime: calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 4))!
|
||||
)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .teamFirst,
|
||||
sports: [.mlb],
|
||||
startDate: baseDate,
|
||||
endDate: baseDate.addingTimeInterval(86400 * 30),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 1,
|
||||
selectedTeamIds: ["teamA", "teamB"]
|
||||
)
|
||||
|
||||
let request = PlanningRequest(
|
||||
preferences: prefs,
|
||||
availableGames: [teamAGame, teamBEarly, teamBLate],
|
||||
teams: [
|
||||
"teamA": makeTeam(id: "teamA", name: "Team A"),
|
||||
"teamB": makeTeam(id: "teamB", name: "Team B")
|
||||
],
|
||||
stadiums: ["nyc": nycStadium, "chi": chicagoStadium]
|
||||
)
|
||||
|
||||
let result = planner.plan(request: request)
|
||||
|
||||
guard case .success(let options) = result else {
|
||||
Issue.record("Expected fallback success when earliest anchor combo is infeasible")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(!options.isEmpty)
|
||||
|
||||
let optionGameIds = options.map { Set($0.stops.flatMap { $0.games }) }
|
||||
#expect(optionGameIds.contains { $0.contains("team-a-day1") && $0.contains("team-b-day4") },
|
||||
"Expected at least one route that uses the later feasible Team B game")
|
||||
}
|
||||
|
||||
@Test("plan: keeps date-distinct options even when city order is identical")
|
||||
func plan_keepsDistinctGameSetsWithSameCityOrder() {
|
||||
let calendar = Calendar.current
|
||||
let baseDate = calendar.startOfDay(for: Date())
|
||||
|
||||
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
|
||||
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
|
||||
|
||||
let teamAFirst = makeGame(
|
||||
id: "team-a-day1",
|
||||
homeTeamId: "teamA",
|
||||
awayTeamId: "opp",
|
||||
stadiumId: "nyc",
|
||||
dateTime: calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 1))!
|
||||
)
|
||||
let teamBFirst = makeGame(
|
||||
id: "team-b-day4",
|
||||
homeTeamId: "teamB",
|
||||
awayTeamId: "opp",
|
||||
stadiumId: "boston",
|
||||
dateTime: calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 4))!
|
||||
)
|
||||
let teamASecond = makeGame(
|
||||
id: "team-a-day10",
|
||||
homeTeamId: "teamA",
|
||||
awayTeamId: "opp",
|
||||
stadiumId: "nyc",
|
||||
dateTime: calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 10))!
|
||||
)
|
||||
let teamBSecond = makeGame(
|
||||
id: "team-b-day13",
|
||||
homeTeamId: "teamB",
|
||||
awayTeamId: "opp",
|
||||
stadiumId: "boston",
|
||||
dateTime: calendar.date(bySettingHour: 19, minute: 0, second: 0, of: baseDate.addingTimeInterval(86400 * 13))!
|
||||
)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .teamFirst,
|
||||
sports: [.mlb],
|
||||
startDate: baseDate,
|
||||
endDate: baseDate.addingTimeInterval(86400 * 30),
|
||||
leisureLevel: .moderate,
|
||||
lodgingType: .hotel,
|
||||
numberOfDrivers: 2,
|
||||
selectedTeamIds: ["teamA", "teamB"]
|
||||
)
|
||||
|
||||
let request = PlanningRequest(
|
||||
preferences: prefs,
|
||||
availableGames: [teamAFirst, teamBFirst, teamASecond, teamBSecond],
|
||||
teams: [
|
||||
"teamA": makeTeam(id: "teamA", name: "Team A"),
|
||||
"teamB": makeTeam(id: "teamB", name: "Team B")
|
||||
],
|
||||
stadiums: ["nyc": nycStadium, "boston": bostonStadium]
|
||||
)
|
||||
|
||||
let result = planner.plan(request: request)
|
||||
|
||||
guard case .success(let options) = result else {
|
||||
Issue.record("Expected success for repeated city-order windows")
|
||||
return
|
||||
}
|
||||
|
||||
let uniqueGameSets = Set(options.map { option in
|
||||
option.stops.flatMap { $0.games }.sorted().joined(separator: "-")
|
||||
})
|
||||
#expect(uniqueGameSets.count >= 2, "Expected distinct date/game combinations to survive deduplication")
|
||||
}
|
||||
|
||||
@Test("plan: routes sorted by duration ascending")
|
||||
func plan_routesSortedByDurationAscending() {
|
||||
let baseDate = Date()
|
||||
|
||||
Reference in New Issue
Block a user