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:
Trey t
2026-02-13 08:55:23 -06:00
parent 1c97f35754
commit 9736773475
19 changed files with 928 additions and 171 deletions

View File

@@ -220,6 +220,55 @@ struct ScenarioAPlannerTests {
#expect(failure.reason == .noGamesInRange)
}
@Test("plan: multiple must-stop cities are required without excluding other route games")
func plan_multipleMustStops_requireCoverageWithoutExclusiveFiltering() {
let startDate = Date()
let endDate = startDate.addingTimeInterval(86400 * 10)
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: CLLocationCoordinate2D(latitude: 39.9526, longitude: -75.1652))
let nycGame = makeGame(id: "nyc-game", stadiumId: "nyc", dateTime: startDate.addingTimeInterval(86400 * 1))
let bostonGame = makeGame(id: "boston-game", stadiumId: "boston", dateTime: startDate.addingTimeInterval(86400 * 3))
let phillyGame = makeGame(id: "philly-game", stadiumId: "philly", dateTime: startDate.addingTimeInterval(86400 * 5))
let prefs = TripPreferences(
planningMode: .dateRange,
sports: [.mlb],
startDate: startDate,
endDate: endDate,
leisureLevel: .moderate,
mustStopLocations: [
LocationInput(name: "New York", coordinate: nycCoord),
LocationInput(name: "Boston", coordinate: bostonCoord)
],
lodgingType: .hotel,
numberOfDrivers: 2
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [nycGame, bostonGame, phillyGame],
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 with two feasible must-stop cities")
return
}
#expect(!options.isEmpty)
for option in options {
let gameIds = Set(option.stops.flatMap { $0.games })
#expect(gameIds.contains("nyc-game"), "Each option should satisfy New York must-stop")
#expect(gameIds.contains("boston-game"), "Each option should satisfy Boston must-stop")
}
}
// MARK: - Specification Tests: Successful Planning
@Test("plan: single game in range returns success with one option")