- Phase 1: Verify broken filter fixes (route preference, region filtering, must-stop, segment validation) — all already implemented, 8 TDD tests added - Phase 2: Verify guard rails (no fallback distance, same-stadium gap, overnight rest, exclusion warnings) — all implemented, 12 TDD tests added - Phase 3: Fix 2 timezone edge case tests (use fixed ET calendar), verify driving constraints, filter cascades, anchors, interactions — 5 tests added - Phase 4: Add sortByRoutePreference() for post-planning re-sort, verify inverted date range rejection, empty sports warning, region boundaries — 8 tests - Travel Integrity: 32 tests verifying N stops → N-1 segments invariant across all 5 scenario planners, ItineraryBuilder, isValid, and engine gate New: sortByRoutePreference() on ItineraryOption (Direct/Scenic/Balanced) Fixed: TimezoneEdgeCaseTests now timezone-independent 1199 tests, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
255 lines
9.9 KiB
Swift
255 lines
9.9 KiB
Swift
//
|
|
// MustStopValidationTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Tests for must-stop location filtering across all scenario planners.
|
|
//
|
|
|
|
import Testing
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("Must-Stop Validation")
|
|
struct MustStopValidationTests {
|
|
|
|
// MARK: - Centralized Must-Stop Filter (TripPlanningEngine)
|
|
|
|
@Test("scenarioA: must stops filter routes to include required cities")
|
|
func scenarioA_mustStops_routesContainRequiredCities() {
|
|
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 gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate)
|
|
let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2)
|
|
let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day3)
|
|
|
|
let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS, gamePHL])
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .dateRange,
|
|
sports: [.mlb],
|
|
startDate: baseDate,
|
|
endDate: day3,
|
|
mustStopLocations: [LocationInput(name: "Boston")]
|
|
)
|
|
|
|
let request = PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: [gameNYC, gameBOS, gamePHL],
|
|
teams: [:],
|
|
stadiums: stadiums
|
|
)
|
|
|
|
let engine = TripPlanningEngine()
|
|
let result = engine.planItineraries(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
let cities = option.stops.map { $0.city.lowercased() }
|
|
#expect(cities.contains("boston"), "Every route must include Boston as a must-stop")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("must stop impossible city returns failure")
|
|
func mustStops_impossibleCity_returnsFailure() {
|
|
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 gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate)
|
|
let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2)
|
|
|
|
let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS])
|
|
|
|
// Require a city with no games
|
|
let prefs = TripPreferences(
|
|
planningMode: .dateRange,
|
|
sports: [.mlb],
|
|
startDate: baseDate,
|
|
endDate: day2,
|
|
mustStopLocations: [LocationInput(name: "Denver")]
|
|
)
|
|
|
|
let request = PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: [gameNYC, gameBOS],
|
|
teams: [:],
|
|
stadiums: stadiums
|
|
)
|
|
|
|
let engine = TripPlanningEngine()
|
|
let result = engine.planItineraries(request: request)
|
|
|
|
// Should fail because no route can include Denver
|
|
#expect(!result.isSuccess)
|
|
}
|
|
|
|
@Test("scenarioB: must stops enforced via centralized filter")
|
|
func scenarioB_mustStops_routesContainRequiredCities() {
|
|
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 gameNYC = TestFixtures.game(id: "must_see_1", city: "New York", dateTime: baseDate)
|
|
let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2)
|
|
let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day3)
|
|
|
|
let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS, gamePHL])
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .gameFirst,
|
|
sports: [.mlb],
|
|
mustSeeGameIds: [gameNYC.id],
|
|
startDate: baseDate,
|
|
endDate: day3,
|
|
mustStopLocations: [LocationInput(name: "Boston")]
|
|
)
|
|
|
|
let request = PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: [gameNYC, gameBOS, gamePHL],
|
|
teams: [:],
|
|
stadiums: stadiums
|
|
)
|
|
|
|
let engine = TripPlanningEngine()
|
|
let result = engine.planItineraries(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
let cities = option.stops.map { $0.city.lowercased() }
|
|
#expect(cities.contains("boston"), "Must-stop filter should ensure Boston is included")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("scenarioD: must stops enforced via centralized filter")
|
|
func scenarioD_mustStops_routesContainRequiredCities() {
|
|
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 teamId = "team_mlb_new_york"
|
|
let gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate, homeTeamId: teamId)
|
|
let gameBOS = TestFixtures.game(city: "Boston", dateTime: day2, homeTeamId: "team_mlb_boston", awayTeamId: teamId)
|
|
let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day3, homeTeamId: "team_mlb_philadelphia", awayTeamId: teamId)
|
|
|
|
let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS, gamePHL])
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .followTeam,
|
|
sports: [.mlb],
|
|
startDate: baseDate,
|
|
endDate: day3,
|
|
mustStopLocations: [LocationInput(name: "Boston")],
|
|
followTeamId: teamId
|
|
)
|
|
|
|
let request = PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: [gameNYC, gameBOS, gamePHL],
|
|
teams: [teamId: TestFixtures.team(city: "New York")],
|
|
stadiums: stadiums
|
|
)
|
|
|
|
let engine = TripPlanningEngine()
|
|
let result = engine.planItineraries(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
let cities = option.stops.map { $0.city.lowercased() }
|
|
#expect(cities.contains("boston"), "Must-stop filter should ensure Boston is included in follow-team mode")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("scenarioE: must stops enforced via centralized filter")
|
|
func scenarioE_mustStops_routesContainRequiredCities() {
|
|
let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19, minute: 0)
|
|
let day1 = TestClock.calendar.date(byAdding: .day, value: 1, to: baseDate)!
|
|
let day2 = TestClock.calendar.date(byAdding: .day, value: 2, to: baseDate)!
|
|
|
|
let teamNYC = "team_mlb_new_york"
|
|
let teamBOS = "team_mlb_boston"
|
|
|
|
let gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate, homeTeamId: teamNYC)
|
|
let gameBOS = TestFixtures.game(city: "Boston", dateTime: day1, homeTeamId: teamBOS)
|
|
let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day2, homeTeamId: "team_mlb_philadelphia")
|
|
|
|
let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS, gamePHL])
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .teamFirst,
|
|
sports: [.mlb],
|
|
startDate: baseDate,
|
|
endDate: day2,
|
|
mustStopLocations: [LocationInput(name: "Boston")],
|
|
selectedTeamIds: [teamNYC, teamBOS]
|
|
)
|
|
|
|
let request = PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: [gameNYC, gameBOS, gamePHL],
|
|
teams: [
|
|
teamNYC: TestFixtures.team(city: "New York"),
|
|
teamBOS: TestFixtures.team(city: "Boston"),
|
|
],
|
|
stadiums: stadiums
|
|
)
|
|
|
|
let engine = TripPlanningEngine()
|
|
let result = engine.planItineraries(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
let cities = option.stops.map { $0.city.lowercased() }
|
|
#expect(cities.contains("boston"), "Must-stop filter should ensure Boston is included in team-first mode")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("scenarioC: must stops enforced via centralized filter")
|
|
func scenarioC_mustStops_routesContainRequiredCities() {
|
|
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 nycCoord = TestFixtures.coordinates["New York"]!
|
|
let bosCoord = TestFixtures.coordinates["Boston"]!
|
|
|
|
let gameNYC = TestFixtures.game(city: "New York", dateTime: baseDate)
|
|
let gamePHL = TestFixtures.game(city: "Philadelphia", dateTime: day2)
|
|
let gameBOS = TestFixtures.game(city: "Boston", dateTime: day3)
|
|
|
|
let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gamePHL, gameBOS])
|
|
|
|
let prefs = TripPreferences(
|
|
planningMode: .locations,
|
|
startLocation: LocationInput(name: "New York", coordinate: nycCoord),
|
|
endLocation: LocationInput(name: "Boston", coordinate: bosCoord),
|
|
sports: [.mlb],
|
|
startDate: baseDate,
|
|
endDate: day3,
|
|
mustStopLocations: [LocationInput(name: "Philadelphia")]
|
|
)
|
|
|
|
let request = PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: [gameNYC, gamePHL, gameBOS],
|
|
teams: [:],
|
|
stadiums: stadiums
|
|
)
|
|
|
|
let engine = TripPlanningEngine()
|
|
let result = engine.planItineraries(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
let cities = option.stops.map { $0.city.lowercased() }
|
|
#expect(cities.contains("philadelphia"), "Must-stop filter should ensure Philadelphia is included in route mode")
|
|
}
|
|
}
|
|
}
|
|
}
|