Files
Sportstime/SportsTimeTests/Planning/MustStopValidationTests.swift
Trey T 9b622f8bbb Harden planning test suite with realistic fixtures and output sanity checks
Adds messy/realistic data factories to TestFixtures, new PlannerOutputSanityTests,
and updates all scenario planner tests with improved coverage and assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:38:41 -05:00

255 lines
10 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)
guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return }
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)
guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return }
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)
guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return }
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)
// 2 teams windowDuration = 4 days. Games must be within 3 days to fit in a single window.
let day3 = TestClock.calendar.date(byAdding: .day, value: 3, 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: day3, homeTeamId: teamBOS)
let stadiums = TestFixtures.stadiumMap(for: [gameNYC, gameBOS])
let prefs = TripPreferences(
planningMode: .teamFirst,
sports: [.mlb],
startDate: baseDate,
endDate: day3,
mustStopLocations: [LocationInput(name: "Boston")],
selectedTeamIds: [teamNYC, teamBOS]
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [gameNYC, gameBOS],
teams: [
teamNYC: TestFixtures.team(city: "New York"),
teamBOS: TestFixtures.team(city: "Boston"),
],
stadiums: stadiums
)
let engine = TripPlanningEngine()
let result = engine.planItineraries(request: request)
guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return }
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() {
// Route: Chicago New York (eastward). Detroit is directionally between them.
let baseDate = TestClock.now
let endDate = TestClock.calendar.date(byAdding: .day, value: 10, to: baseDate)!
let chiCoord = TestFixtures.coordinates["Chicago"]!
let detCoord = TestFixtures.coordinates["Detroit"]!
let nycCoord = TestFixtures.coordinates["New York"]!
let chiStadium = TestFixtures.stadium(id: "chi", city: "Chicago")
let detStadium = TestFixtures.stadium(id: "det", city: "Detroit")
let nycStadium = TestFixtures.stadium(id: "nyc", city: "New York")
let gameCHI = TestFixtures.game(city: "Chicago", dateTime: TestClock.addingDays(1), stadiumId: "chi")
let gameDET = TestFixtures.game(city: "Detroit", dateTime: TestClock.addingDays(4), stadiumId: "det")
let gameNYC = TestFixtures.game(city: "New York", dateTime: TestClock.addingDays(7), stadiumId: "nyc")
let prefs = TripPreferences(
planningMode: .locations,
startLocation: LocationInput(name: "Chicago", coordinate: chiCoord),
endLocation: LocationInput(name: "New York", coordinate: nycCoord),
sports: [.mlb],
startDate: baseDate,
endDate: endDate,
leisureLevel: .moderate,
mustStopLocations: [LocationInput(name: "Detroit")],
lodgingType: .hotel,
numberOfDrivers: 2
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [gameCHI, gameDET, gameNYC],
teams: [:],
stadiums: ["chi": chiStadium, "det": detStadium, "nyc": nycStadium]
)
let engine = TripPlanningEngine()
let result = engine.planItineraries(request: request)
guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return }
for option in options {
let cities = option.stops.map { $0.city.lowercased() }
#expect(cities.contains("detroit"), "Must-stop filter should ensure Detroit is included in Chicago→NYC route")
}
}
}