Stadium Progress & Achievements: - Add StadiumVisit and Achievement SwiftData models - Create Progress tab with interactive map view - Implement photo-based visit import with GPS/date matching - Add achievement badges (count-based, regional, journey) - Create shareable progress cards for social media - Add canonical data infrastructure (stadium identities, team aliases) - Implement score resolution from free APIs (MLB, NBA, NHL stats) UI Improvements: - Add ThemedSpinner and ThemedSpinnerCompact components - Replace all ProgressView() with themed spinners throughout app - Fix sport selection state not persisting when navigating away Bug Fixes: - Fix Coast to Coast trips showing only 1 city (validation issue) - Fix stadium progress showing 0/0 (filtering issue) - Remove "Stadium Quest" title from progress view 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2024 lines
67 KiB
Swift
2024 lines
67 KiB
Swift
//
|
|
// ScenarioCPlannerTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Comprehensive tests for ScenarioCPlanner: Directional route planning.
|
|
// Tests start/end location validation, directional stadium filtering,
|
|
// monotonic progress, and date range generation.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
@Suite("ScenarioCPlanner Tests")
|
|
struct ScenarioCPlannerTests {
|
|
|
|
// MARK: - Test Fixtures
|
|
|
|
private func makeStadium(
|
|
id: UUID = UUID(),
|
|
name: String,
|
|
city: String,
|
|
state: String,
|
|
latitude: Double,
|
|
longitude: Double,
|
|
sport: Sport = .mlb
|
|
) -> Stadium {
|
|
Stadium(
|
|
id: id,
|
|
name: name,
|
|
city: city,
|
|
state: state,
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
capacity: 40000,
|
|
sport: sport
|
|
)
|
|
}
|
|
|
|
private func makeGame(
|
|
id: UUID = UUID(),
|
|
stadiumId: UUID,
|
|
date: Date
|
|
) -> Game {
|
|
Game(
|
|
id: id,
|
|
homeTeamId: UUID(),
|
|
awayTeamId: UUID(),
|
|
stadiumId: stadiumId,
|
|
dateTime: date,
|
|
sport: .mlb,
|
|
season: "2026"
|
|
)
|
|
}
|
|
|
|
private func date(_ string: String) -> Date {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
|
formatter.timeZone = TimeZone(identifier: "America/Los_Angeles")
|
|
return formatter.date(from: string)!
|
|
}
|
|
|
|
private func makeRequest(
|
|
games: [Game],
|
|
stadiums: [UUID: Stadium],
|
|
startLocation: LocationInput?,
|
|
endLocation: LocationInput?,
|
|
startDate: Date,
|
|
endDate: Date,
|
|
tripDuration: Int? = nil,
|
|
numberOfDrivers: Int = 1
|
|
) -> PlanningRequest {
|
|
let prefs = TripPreferences(
|
|
startLocation: startLocation,
|
|
endLocation: endLocation,
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
tripDuration: tripDuration,
|
|
numberOfDrivers: numberOfDrivers
|
|
)
|
|
return PlanningRequest(
|
|
preferences: prefs,
|
|
availableGames: games,
|
|
teams: [:],
|
|
stadiums: stadiums
|
|
)
|
|
}
|
|
|
|
// West Coast stadiums for testing directional routes
|
|
private var sdStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Petco Park", city: "San Diego", state: "CA", latitude: 32.7076, longitude: -117.1570)
|
|
}
|
|
|
|
private var laStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Dodger Stadium", city: "Los Angeles", state: "CA", latitude: 34.0739, longitude: -118.2400)
|
|
}
|
|
|
|
private var sfStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Oracle Park", city: "San Francisco", state: "CA", latitude: 37.7786, longitude: -122.3893)
|
|
}
|
|
|
|
// Cross-country stadiums for directional testing
|
|
private var chicagoStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Wrigley Field", city: "Chicago", state: "IL", latitude: 41.9484, longitude: -87.6553)
|
|
}
|
|
|
|
private var detroitStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Comerica Park", city: "Detroit", state: "MI", latitude: 42.3390, longitude: -83.0485)
|
|
}
|
|
|
|
private var clevelandStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Progressive Field", city: "Cleveland", state: "OH", latitude: 41.4962, longitude: -81.6852)
|
|
}
|
|
|
|
private var pittsburghStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "PNC Park", city: "Pittsburgh", state: "PA", latitude: 40.4469, longitude: -80.0057)
|
|
}
|
|
|
|
private var nyStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Yankee Stadium", city: "New York", state: "NY", latitude: 40.8296, longitude: -73.9262)
|
|
}
|
|
|
|
private var minneapolisStadium: Stadium {
|
|
makeStadium(id: UUID(), name: "Target Field", city: "Minneapolis", state: "MN", latitude: 44.9817, longitude: -93.2776)
|
|
}
|
|
|
|
// MARK: - Basic Success Tests
|
|
|
|
@Test("Single game between start and end succeeds")
|
|
func plan_SingleGameBetweenStartAndEnd_Succeeds() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let ny = nyStadium
|
|
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, cleveland.id: cleveland, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Multiple games along route succeeds")
|
|
func plan_MultipleGamesAlongRoute_Succeeds() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let pittsburgh = pittsburghStadium
|
|
let ny = nyStadium
|
|
|
|
let game1 = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: pittsburgh.id, date: date("2026-06-12 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [
|
|
chicago.id: chicago,
|
|
cleveland.id: cleveland,
|
|
pittsburgh.id: pittsburgh,
|
|
ny.id: ny
|
|
],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Missing Location Tests
|
|
|
|
@Test("Missing start location fails")
|
|
func plan_MissingStartLocation_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let ny = nyStadium
|
|
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [ny.id: ny],
|
|
startLocation: nil,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for missing start location")
|
|
}
|
|
}
|
|
|
|
@Test("Missing end location fails")
|
|
func plan_MissingEndLocation_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago],
|
|
startLocation: startLoc,
|
|
endLocation: nil,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for missing end location")
|
|
}
|
|
}
|
|
|
|
@Test("Both locations missing fails")
|
|
func plan_BothLocationsMissing_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [:],
|
|
startLocation: nil,
|
|
endLocation: nil,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for missing locations")
|
|
}
|
|
}
|
|
|
|
@Test("Start location without coordinates fails")
|
|
func plan_StartLocationNoCoordinates_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let ny = nyStadium
|
|
|
|
let startLoc = LocationInput(name: "Chicago", coordinate: nil)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for start location without coordinates")
|
|
}
|
|
}
|
|
|
|
@Test("End location without coordinates fails")
|
|
func plan_EndLocationNoCoordinates_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(name: "New York", coordinate: nil)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .missingLocations)
|
|
} else {
|
|
Issue.record("Expected failure for end location without coordinates")
|
|
}
|
|
}
|
|
|
|
// MARK: - Stadium City Tests
|
|
|
|
@Test("No stadiums in start city fails")
|
|
func plan_NoStadiumsInStartCity_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let ny = nyStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Boston", // No Boston stadium in our list
|
|
coordinate: CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .noGamesInRange)
|
|
#expect(failure.violations.first?.description.contains("start city") == true)
|
|
} else {
|
|
Issue.record("Expected failure for no stadiums in start city")
|
|
}
|
|
}
|
|
|
|
@Test("No stadiums in end city fails")
|
|
func plan_NoStadiumsInEndCity_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Boston", // No Boston stadium
|
|
coordinate: CLLocationCoordinate2D(latitude: 42.3601, longitude: -71.0589)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(failure.reason == .noGamesInRange)
|
|
#expect(failure.violations.first?.description.contains("end city") == true)
|
|
} else {
|
|
Issue.record("Expected failure for no stadiums in end city")
|
|
}
|
|
}
|
|
|
|
@Test("City name matching is case insensitive")
|
|
func plan_CityNameMatchingCaseInsensitive() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let cleveland = clevelandStadium
|
|
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
|
|
// Use different case for city names
|
|
let startLoc = LocationInput(
|
|
name: "CHICAGO", // uppercase
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "new york", // lowercase
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, cleveland.id: cleveland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should still work despite case differences
|
|
if case .failure = result {
|
|
Issue.record("Should handle case-insensitive city names")
|
|
}
|
|
}
|
|
|
|
// MARK: - Directional Filtering Tests
|
|
|
|
@Test("Backtracking stadium is filtered out")
|
|
func plan_BacktrackingStadium_FilteredOut() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let minneapolis = minneapolisStadium // West of Chicago - wrong direction
|
|
let ny = nyStadium
|
|
|
|
// Game in Minneapolis - going wrong way from Chicago to NY
|
|
let game = makeGame(stadiumId: minneapolis.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, minneapolis.id: minneapolis, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should fail because Minneapolis is not directional (west of Chicago when going east to NY)
|
|
if case .failure = result {
|
|
// Expected - no valid directional routes
|
|
} else if case .success(let options) = result {
|
|
// If it succeeded, Minneapolis should not appear in stops
|
|
for option in options {
|
|
for stop in option.stops {
|
|
#expect(stop.city != "Minneapolis", "Minneapolis should be filtered out")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Directional stadiums included in route")
|
|
func plan_DirectionalStadiums_Included() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let pittsburgh = pittsburghStadium
|
|
let ny = nyStadium
|
|
|
|
// Cleveland and Pittsburgh are directional (east of Chicago, toward NY)
|
|
let game1 = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: pittsburgh.id, date: date("2026-06-12 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [
|
|
chicago.id: chicago,
|
|
cleveland.id: cleveland,
|
|
pittsburgh.id: pittsburgh,
|
|
ny.id: ny
|
|
],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
// At least one option should include Cleveland and/or Pittsburgh
|
|
let allCities = options.flatMap { $0.stops.map { $0.city } }
|
|
let hasDirectionalCities = allCities.contains("Cleveland") || allCities.contains("Pittsburgh")
|
|
#expect(hasDirectionalCities)
|
|
} else {
|
|
Issue.record("Expected success with directional stadiums")
|
|
}
|
|
}
|
|
|
|
// MARK: - Output Structure Tests
|
|
|
|
@Test("Max 5 options returned")
|
|
func plan_Max5OptionsReturned() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Create many games to potentially generate many options
|
|
var games: [Game] = []
|
|
for day in 1...15 {
|
|
games.append(makeGame(
|
|
stadiumId: la.id,
|
|
date: date("2026-06-\(String(format: "%02d", day)) 19:00")
|
|
))
|
|
}
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: games,
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(options.count <= 5, "Should return at most 5 options")
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Options ranked from 1")
|
|
func plan_OptionsRankedFromOne() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(options.first?.rank == 1)
|
|
for (index, option) in options.enumerated() {
|
|
#expect(option.rank == index + 1)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Options have valid structure")
|
|
func plan_OptionsHaveValidStructure() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.rank > 0)
|
|
#expect(!option.stops.isEmpty)
|
|
#expect(!option.geographicRationale.isEmpty)
|
|
#expect(option.totalDrivingHours >= 0)
|
|
#expect(option.totalDistanceMiles >= 0)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segments count equals stops count minus 1")
|
|
func plan_TravelSegmentsCountCorrect() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.travelSegments.count == option.stops.count - 1,
|
|
"Travel segments should be stops - 1")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segments are drive mode")
|
|
func plan_TravelSegmentsAreDriveMode() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(segment.travelMode == .drive)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Geographic Rationale Tests
|
|
|
|
@Test("Geographic rationale shows start and end cities")
|
|
func plan_GeographicRationale_ShowsStartAndEnd() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let rationale = options.first?.geographicRationale {
|
|
#expect(rationale.contains("San Diego"))
|
|
#expect(rationale.contains("San Francisco"))
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Geographic rationale shows game count")
|
|
func plan_GeographicRationale_ShowsGameCount() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-11 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let rationale = options.first?.geographicRationale {
|
|
#expect(rationale.contains("game") || rationale.contains("2"))
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Stop Tests
|
|
|
|
@Test("Stops include start city")
|
|
func plan_StopsIncludeStartCity() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let firstStop = options.first?.stops.first {
|
|
#expect(firstStop.city == "San Diego")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Stops include end city")
|
|
func plan_StopsIncludeEndCity() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let lastStop = options.first?.stops.last {
|
|
#expect(lastStop.city == "San Francisco")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Start stop has no games")
|
|
func plan_StartStopHasNoGames() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let firstStop = options.first?.stops.first {
|
|
#expect(firstStop.games.isEmpty, "Start stop should have no games")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("End stop has no games")
|
|
func plan_EndStopHasNoGames() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if let lastStop = options.first?.stops.last {
|
|
#expect(lastStop.games.isEmpty, "End stop should have no games")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Game stops have games")
|
|
func plan_GameStopsHaveGames() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Middle stops (not first or last) should have games
|
|
if let stops = options.first?.stops, stops.count > 2 {
|
|
let middleStops = stops.dropFirst().dropLast()
|
|
for stop in middleStops {
|
|
#expect(!stop.games.isEmpty, "Middle stops should have games")
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Stop has correct city from stadium")
|
|
func plan_StopHasCorrectCity() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
let allCities = options.first?.stops.map { $0.city } ?? []
|
|
#expect(allCities.contains("Los Angeles"))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Ranking Tests
|
|
|
|
@Test("More games ranked higher")
|
|
func plan_MoreGamesRankedHigher() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Create games that allow different route options
|
|
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-11 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if options.count >= 2 {
|
|
let firstGames = options[0].stops.flatMap { $0.games }.count
|
|
let secondGames = options[1].stops.flatMap { $0.games }.count
|
|
#expect(firstGames >= secondGames, "More games should be ranked first")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Less driving hours ranked higher for equal games")
|
|
func plan_LessDrivingRankedHigher() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
if options.count >= 2 {
|
|
let firstGames = options[0].stops.flatMap { $0.games }.count
|
|
let secondGames = options[1].stops.flatMap { $0.games }.count
|
|
if firstGames == secondGames {
|
|
#expect(options[0].totalDrivingHours <= options[1].totalDrivingHours)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// MARK: - Date Range Tests
|
|
|
|
@Test("Explicit date range used when provided")
|
|
func plan_ExplicitDateRangeUsed() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Games outside and inside date range
|
|
let outsideGame = makeGame(stadiumId: la.id, date: date("2026-05-10 19:00"))
|
|
let insideGame = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [outsideGame, insideGame],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Only inside game should be in results
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(allGameIds.contains(insideGame.id))
|
|
#expect(!allGameIds.contains(outsideGame.id))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("No games in date range fails")
|
|
func plan_NoGamesInDateRange_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let cleveland = clevelandStadium
|
|
|
|
// Game outside date range
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-05-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, cleveland.id: cleveland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure = result {
|
|
// Expected - no games within date range
|
|
} else {
|
|
Issue.record("Expected failure for no games in date range")
|
|
}
|
|
}
|
|
|
|
// MARK: - Driving Constraints Tests
|
|
|
|
@Test("Two drivers allows longer routes")
|
|
func plan_TwoDrivers_AllowsLongerRoutes() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let cleveland = clevelandStadium
|
|
|
|
let game = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
// With 2 drivers, longer routes should be feasible
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, cleveland.id: cleveland],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59"),
|
|
numberOfDrivers: 2
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should succeed with 2 drivers
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test("Trip duration generates date ranges from games")
|
|
func plan_TripDurationGeneratesDateRanges() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Games at start (SD) and end (SF) cities to generate date ranges
|
|
let startCityGame = makeGame(stadiumId: sd.id, date: date("2026-06-05 19:00"))
|
|
let middleGame = makeGame(stadiumId: la.id, date: date("2026-06-07 19:00"))
|
|
let endCityGame = makeGame(stadiumId: sf.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
// Use trip duration instead of explicit dates
|
|
let request = makeRequest(
|
|
games: [startCityGame, middleGame, endCityGame],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59"),
|
|
tripDuration: 7
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should succeed with trip duration mode
|
|
switch result {
|
|
case .success, .failure:
|
|
// Just verify it handles trip duration without crashing
|
|
break
|
|
}
|
|
}
|
|
|
|
// MARK: - Failure Tests
|
|
|
|
@Test("Failure contains violation details")
|
|
func plan_Failure_ContainsViolationDetails() {
|
|
let planner = ScenarioCPlanner()
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [:],
|
|
startLocation: nil,
|
|
endLocation: nil,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .failure(let failure) = result {
|
|
#expect(!failure.violations.isEmpty)
|
|
#expect(failure.violations.first?.severity == .error)
|
|
} else {
|
|
Issue.record("Expected failure")
|
|
}
|
|
}
|
|
|
|
@Test("No valid directional routes fails")
|
|
func plan_NoValidDirectionalRoutes_Fails() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
let minneapolis = minneapolisStadium
|
|
|
|
// Only game is in wrong direction (Minneapolis is west of Chicago)
|
|
let game = makeGame(stadiumId: minneapolis.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [chicago.id: chicago, ny.id: ny, minneapolis.id: minneapolis],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should fail because Minneapolis is not directional
|
|
if case .failure = result {
|
|
// Expected
|
|
} else {
|
|
Issue.record("Expected failure for non-directional route")
|
|
}
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
@Test("Same city for start and end")
|
|
func plan_SameCityStartAndEnd() {
|
|
let planner = ScenarioCPlanner()
|
|
let la = laStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "Los Angeles",
|
|
coordinate: CLLocationCoordinate2D(latitude: la.latitude, longitude: la.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [la.id: la],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// May succeed or fail depending on implementation
|
|
// Just verify it doesn't crash
|
|
switch result {
|
|
case .success, .failure:
|
|
break
|
|
}
|
|
}
|
|
|
|
@Test("Very short date range with game")
|
|
func plan_ShortDateRangeWithGame() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
// Just one day
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-10 00:00"),
|
|
endDate: date("2026-06-10 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
#expect(!options.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test("Multiple games at same stadium")
|
|
func plan_MultipleGamesAtSameStadium() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game1 = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: la.id, date: date("2026-06-11 19:00"))
|
|
let game3 = makeGame(stadiumId: la.id, date: date("2026-06-12 13:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2, game3],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Games at same stadium should be grouped
|
|
for option in options {
|
|
let laStops = option.stops.filter { $0.city == "Los Angeles" }
|
|
#expect(laStops.count <= 1, "Same stadium games should be grouped")
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Empty games array")
|
|
func plan_EmptyGamesArray() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let ny = nyStadium
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [],
|
|
stadiums: [chicago.id: chicago, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should fail - no games
|
|
if case .failure = result {
|
|
// Expected
|
|
} else {
|
|
Issue.record("Expected failure for empty games")
|
|
}
|
|
}
|
|
|
|
@Test("Handles long cross-country route")
|
|
func plan_LongCrossCountryRoute() {
|
|
let planner = ScenarioCPlanner()
|
|
let sf = sfStadium
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let ny = nyStadium
|
|
|
|
// SF to NY is a long route
|
|
let game1 = makeGame(stadiumId: chicago.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: cleveland.id, date: date("2026-06-15 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [sf.id: sf, chicago.id: chicago, cleveland.id: cleveland, ny.id: ny],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59"),
|
|
numberOfDrivers: 2 // More drivers for longer route
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should handle without crashing
|
|
switch result {
|
|
case .success, .failure:
|
|
break
|
|
}
|
|
}
|
|
|
|
@Test("Stop coordinates match stadium")
|
|
func plan_StopCoordinatesMatchStadium() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Find LA stop
|
|
if let laStop = options.first?.stops.first(where: { $0.city == "Los Angeles" }) {
|
|
if let coord = laStop.coordinate {
|
|
#expect(abs(coord.latitude - la.latitude) < 0.01)
|
|
#expect(abs(coord.longitude - la.longitude) < 0.01)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Total distance is positive")
|
|
func plan_TotalDistanceIsPositive() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.totalDistanceMiles > 0)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Total driving hours is positive")
|
|
func plan_TotalDrivingHoursIsPositive() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
#expect(option.totalDrivingHours > 0)
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segment has valid origin and destination")
|
|
func plan_TravelSegmentHasValidOriginDestination() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(!segment.fromLocation.name.isEmpty)
|
|
#expect(!segment.toLocation.name.isEmpty)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segment has positive distance")
|
|
func plan_TravelSegmentHasPositiveDistance() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(segment.distanceMeters > 0)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Travel segment has positive duration")
|
|
func plan_TravelSegmentHasPositiveDuration() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for segment in option.travelSegments {
|
|
#expect(segment.durationSeconds > 0)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
// Note: "Departure time before arrival time" test removed
|
|
// Travel segments are now location-based, not time-based
|
|
// The user decides when to travel; segments only describe route info
|
|
|
|
@Test("Games filtered to date range")
|
|
func plan_GamesFilteredToDateRange() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Game outside range
|
|
let earlyGame = makeGame(stadiumId: la.id, date: date("2026-05-01 19:00"))
|
|
// Game inside range
|
|
let validGame = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
// Game outside range
|
|
let lateGame = makeGame(stadiumId: la.id, date: date("2026-07-01 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [earlyGame, validGame, lateGame],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(allGameIds.contains(validGame.id))
|
|
#expect(!allGameIds.contains(earlyGame.id))
|
|
#expect(!allGameIds.contains(lateGame.id))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Many games handles efficiently")
|
|
func plan_ManyGames_HandlesEfficiently() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// Create many games
|
|
var games: [Game] = []
|
|
for day in 1...30 {
|
|
games.append(makeGame(
|
|
stadiumId: la.id,
|
|
date: date("2026-06-\(String(format: "%02d", day)) 19:00")
|
|
))
|
|
}
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: games,
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
// Should complete without timeout
|
|
switch result {
|
|
case .success, .failure:
|
|
break
|
|
}
|
|
}
|
|
|
|
@Test("Route progresses toward end")
|
|
func plan_RouteProgressesTowardEnd() {
|
|
let planner = ScenarioCPlanner()
|
|
let chicago = chicagoStadium
|
|
let cleveland = clevelandStadium
|
|
let pittsburgh = pittsburghStadium
|
|
let ny = nyStadium
|
|
|
|
let game1 = makeGame(stadiumId: cleveland.id, date: date("2026-06-10 19:00"))
|
|
let game2 = makeGame(stadiumId: pittsburgh.id, date: date("2026-06-12 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "Chicago",
|
|
coordinate: CLLocationCoordinate2D(latitude: chicago.latitude, longitude: chicago.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "New York",
|
|
coordinate: CLLocationCoordinate2D(latitude: ny.latitude, longitude: ny.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game1, game2],
|
|
stadiums: [
|
|
chicago.id: chicago,
|
|
cleveland.id: cleveland,
|
|
pittsburgh.id: pittsburgh,
|
|
ny.id: ny
|
|
],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
// Verify each stop gets closer to (or doesn't significantly move away from) NY
|
|
for option in options {
|
|
var lastDistance = Double.infinity
|
|
for stop in option.stops {
|
|
if let coord = stop.coordinate {
|
|
let distance = sqrt(pow(coord.latitude - ny.latitude, 2) +
|
|
pow(coord.longitude - ny.longitude, 2))
|
|
// Allow small tolerance for backtracking
|
|
#expect(distance <= lastDistance * 1.2,
|
|
"Route should generally progress toward end")
|
|
lastDistance = distance
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Stop location has name")
|
|
func plan_StopLocationHasName() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
for option in options {
|
|
for stop in option.stops {
|
|
#expect(!stop.location.name.isEmpty)
|
|
}
|
|
}
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
|
|
@Test("Games at directional stadium included")
|
|
func plan_GamesAtDirectionalStadium_Included() {
|
|
let planner = ScenarioCPlanner()
|
|
let sd = sdStadium
|
|
let la = laStadium
|
|
let sf = sfStadium
|
|
|
|
// LA is between SD and SF (directional)
|
|
let game = makeGame(stadiumId: la.id, date: date("2026-06-10 19:00"))
|
|
|
|
let startLoc = LocationInput(
|
|
name: "San Diego",
|
|
coordinate: CLLocationCoordinate2D(latitude: sd.latitude, longitude: sd.longitude)
|
|
)
|
|
let endLoc = LocationInput(
|
|
name: "San Francisco",
|
|
coordinate: CLLocationCoordinate2D(latitude: sf.latitude, longitude: sf.longitude)
|
|
)
|
|
|
|
let request = makeRequest(
|
|
games: [game],
|
|
stadiums: [sd.id: sd, la.id: la, sf.id: sf],
|
|
startLocation: startLoc,
|
|
endLocation: endLoc,
|
|
startDate: date("2026-06-01 00:00"),
|
|
endDate: date("2026-06-30 23:59")
|
|
)
|
|
|
|
let result = planner.plan(request: request)
|
|
|
|
if case .success(let options) = result {
|
|
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
|
|
#expect(allGameIds.contains(game.id))
|
|
} else {
|
|
Issue.record("Expected success")
|
|
}
|
|
}
|
|
}
|