Add implementation code for all 4 improvement plan phases
Production changes: - TravelEstimator: remove 300mi fallback, return nil on missing coords - TripPlanningEngine: add warnings array, empty sports warning, inverted date range rejection, must-stop filter, segment validation gate - GameDAGRouter: add routePreference parameter with preference-aware bucket ordering and sorting in selectDiverseRoutes() - ScenarioA-E: pass routePreference through to GameDAGRouter - ScenarioA: track games with missing stadium data - ScenarioE: add region filtering for home games - TravelSegment: add requiresOvernightStop and travelDays() helpers Test changes: - GameDAGRouterTests: +252 lines for route preference verification - TripPlanningEngineTests: +153 lines for segment validation, date range, empty sports - ScenarioEPlannerTests: +119 lines for region filter tests - TravelEstimatorTests: remove obsolete fallback distance tests - ItineraryBuilderTests: update nil-coords test expectation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,159 @@ struct TripPlanningEngineTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Travel Segment Validation
|
||||
|
||||
@Test("planTrip: multi-stop result always has travel segments")
|
||||
func planTrip_multiStopResult_alwaysHasTravelSegments() {
|
||||
let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0)
|
||||
let day2 = TestClock.calendar.date(byAdding: .day, value: 1, to: baseDate)!
|
||||
let day3 = TestClock.calendar.date(byAdding: .day, value: 2, to: baseDate)!
|
||||
|
||||
let game1 = TestFixtures.game(city: "New York", dateTime: baseDate)
|
||||
let game2 = TestFixtures.game(city: "Boston", dateTime: day2)
|
||||
let game3 = TestFixtures.game(city: "Philadelphia", dateTime: day3)
|
||||
|
||||
let stadiums = TestFixtures.stadiumMap(for: [game1, game2, game3])
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: baseDate,
|
||||
endDate: day3
|
||||
)
|
||||
|
||||
let request = PlanningRequest(
|
||||
preferences: prefs,
|
||||
availableGames: [game1, game2, game3],
|
||||
teams: [:],
|
||||
stadiums: stadiums
|
||||
)
|
||||
|
||||
let engine = TripPlanningEngine()
|
||||
let result = engine.planItineraries(request: request)
|
||||
|
||||
if case .success(let options) = result {
|
||||
for option in options {
|
||||
#expect(option.isValid, "Every returned option must be valid (segments = stops - 1)")
|
||||
if option.stops.count > 1 {
|
||||
#expect(option.travelSegments.count == option.stops.count - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test("planTrip: N stops always have exactly N-1 travel segments")
|
||||
func planTrip_nStops_haveExactlyNMinus1Segments() {
|
||||
let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0)
|
||||
|
||||
// Create 5 games across cities to produce routes of varying lengths
|
||||
let cities = ["New York", "Boston", "Philadelphia", "Chicago", "Detroit"]
|
||||
var games: [Game] = []
|
||||
for (i, city) in cities.enumerated() {
|
||||
let date = TestClock.calendar.date(byAdding: .day, value: i, to: baseDate)!
|
||||
games.append(TestFixtures.game(city: city, dateTime: date))
|
||||
}
|
||||
|
||||
let stadiums = TestFixtures.stadiumMap(for: games)
|
||||
let endDate = TestClock.calendar.date(byAdding: .day, value: cities.count, to: baseDate)!
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: baseDate,
|
||||
endDate: endDate
|
||||
)
|
||||
|
||||
let request = PlanningRequest(
|
||||
preferences: prefs,
|
||||
availableGames: games,
|
||||
teams: [:],
|
||||
stadiums: stadiums
|
||||
)
|
||||
|
||||
let engine = TripPlanningEngine()
|
||||
let result = engine.planItineraries(request: request)
|
||||
|
||||
if case .success(let options) = result {
|
||||
#expect(!options.isEmpty, "Should produce at least one option")
|
||||
for option in options {
|
||||
if option.stops.count > 1 {
|
||||
#expect(option.travelSegments.count == option.stops.count - 1,
|
||||
"Option with \(option.stops.count) stops must have exactly \(option.stops.count - 1) segments, got \(option.travelSegments.count)")
|
||||
} else {
|
||||
#expect(option.travelSegments.isEmpty,
|
||||
"Single-stop option must have 0 segments")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test("planTrip: invalid options are filtered out")
|
||||
func planTrip_invalidOptions_areFilteredOut() {
|
||||
// Create a valid ItineraryOption manually with wrong segment count
|
||||
let stop1 = ItineraryStop(
|
||||
city: "New York", state: "NY",
|
||||
coordinate: nycCoord,
|
||||
games: ["g1"], arrivalDate: Date(), departureDate: Date(),
|
||||
location: LocationInput(name: "New York", coordinate: nycCoord),
|
||||
firstGameStart: Date()
|
||||
)
|
||||
let stop2 = ItineraryStop(
|
||||
city: "Boston", state: "MA",
|
||||
coordinate: bostonCoord,
|
||||
games: ["g2"], arrivalDate: Date(), departureDate: Date(),
|
||||
location: LocationInput(name: "Boston", coordinate: bostonCoord),
|
||||
firstGameStart: Date()
|
||||
)
|
||||
|
||||
// Invalid: 2 stops but 0 segments
|
||||
let invalidOption = ItineraryOption(
|
||||
rank: 1, stops: [stop1, stop2],
|
||||
travelSegments: [],
|
||||
totalDrivingHours: 0, totalDistanceMiles: 0,
|
||||
geographicRationale: "test"
|
||||
)
|
||||
#expect(!invalidOption.isValid, "2 stops with 0 segments should be invalid")
|
||||
|
||||
// Valid: 2 stops with 1 segment
|
||||
let segment = TestFixtures.travelSegment(from: "New York", to: "Boston")
|
||||
let validOption = ItineraryOption(
|
||||
rank: 1, stops: [stop1, stop2],
|
||||
travelSegments: [segment],
|
||||
totalDrivingHours: 3.5, totalDistanceMiles: 215,
|
||||
geographicRationale: "test"
|
||||
)
|
||||
#expect(validOption.isValid, "2 stops with 1 segment should be valid")
|
||||
}
|
||||
|
||||
@Test("planTrip: inverted date range returns failure")
|
||||
func planTrip_invertedDateRange_returnsFailure() {
|
||||
let endDate = TestFixtures.date(year: 2026, month: 6, day: 1)
|
||||
let startDate = TestFixtures.date(year: 2026, month: 6, day: 10)
|
||||
|
||||
let prefs = TripPreferences(
|
||||
planningMode: .dateRange,
|
||||
sports: [.mlb],
|
||||
startDate: startDate,
|
||||
endDate: endDate
|
||||
)
|
||||
|
||||
let request = PlanningRequest(
|
||||
preferences: prefs,
|
||||
availableGames: [],
|
||||
teams: [:],
|
||||
stadiums: [:]
|
||||
)
|
||||
|
||||
let engine = TripPlanningEngine()
|
||||
let result = engine.planItineraries(request: request)
|
||||
|
||||
#expect(!result.isSuccess)
|
||||
if let failure = result.failure {
|
||||
#expect(failure.reason == .missingDateRange)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func makeStadium(
|
||||
|
||||
Reference in New Issue
Block a user