Files
Sportstime/SportsTimeTests/Planning/TripPlanningEngineTests.swift
Trey T a6f538dfed Audit and fix 52 test correctness issues across 22 files
Systematic audit of 1,191 tests found tests written to pass rather than
verify correctness. Key fixes:

Infrastructure:
- TestClock: fixed timezone from .current to America/New_York (deterministic)
- TestFixtures: added 1.3x road routing factor to match production
- ItineraryTestHelpers: real per-city coordinates instead of hardcoded (40,-80)

Planning tests:
- Added missing Scenario E factory dispatch tests
- Tightened 12 loose assertions (>= 1 → == 8.0, > 0 → range checks)
- Fixed 4 no-op tests that accepted both success and failure
- Fixed wrong repeat-city invariant (was checking same-day, not different-day)
- Fixed tautological assertion in missing-stadium edge case

Services/Domain/Export tests:
- Replaced 4 placeholder tests (#expect(true)) with real assertions
- Fixed tautological assertions in POISearchServiceTests
- Fixed Chicago coordinate in RegionMapSelectorTests (-89 → -87.6553)
- Added sort order verification to ItineraryRowFlatteningTests

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

299 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// TripPlanningEngineTests.swift
// SportsTimeTests
//
// TDD specification tests for TripPlanningEngine.
//
import Testing
import Foundation
import CoreLocation
@testable import SportsTime
@Suite("TripPlanningEngine")
struct TripPlanningEngineTests {
// MARK: - Test Data
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
private let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
// MARK: - Specification Tests: Driving Constraints
@Test("DrivingConstraints: calculates maxDailyDrivingHours correctly")
func drivingConstraints_maxDailyHours() {
let constraints = DrivingConstraints(numberOfDrivers: 2, maxHoursPerDriverPerDay: 6.0)
#expect(constraints.maxDailyDrivingHours == 12.0)
}
@Test("DrivingConstraints: clamps negative drivers to 1")
func drivingConstraints_clampsNegativeDrivers() {
let constraints = DrivingConstraints(numberOfDrivers: -5, maxHoursPerDriverPerDay: 8.0)
#expect(constraints.numberOfDrivers == 1)
#expect(constraints.maxDailyDrivingHours == 8.0)
}
@Test("DrivingConstraints: clamps zero hours to minimum")
func drivingConstraints_clampsZeroHours() {
let constraints = DrivingConstraints(numberOfDrivers: 1, maxHoursPerDriverPerDay: 0)
#expect(constraints.maxHoursPerDriverPerDay == 1.0)
}
// MARK: - Specification Tests: Trip Preferences Computed Properties
@Test("totalDriverHoursPerDay: defaults to 8 hours when nil")
func totalDriverHoursPerDay_default() {
let prefs = TripPreferences(
numberOfDrivers: 1,
maxDrivingHoursPerDriver: nil
)
#expect(prefs.totalDriverHoursPerDay == 8.0)
}
@Test("totalDriverHoursPerDay: multiplies by number of drivers")
func totalDriverHoursPerDay_multipleDrivers() {
let prefs = TripPreferences(
numberOfDrivers: 2,
maxDrivingHoursPerDriver: 6.0
)
#expect(prefs.totalDriverHoursPerDay == 12.0)
}
@Test("effectiveTripDuration: uses explicit tripDuration when set")
func effectiveTripDuration_explicit() {
let prefs = TripPreferences(
tripDuration: 5
)
#expect(prefs.effectiveTripDuration == 5)
}
@Test("effectiveTripDuration: calculates from date range when tripDuration is nil")
func effectiveTripDuration_calculated() {
let calendar = TestClock.calendar
let startDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
let endDate = calendar.date(from: DateComponents(year: 2026, month: 6, day: 22))!
let prefs = TripPreferences(
startDate: startDate,
endDate: endDate,
tripDuration: nil
)
#expect(prefs.effectiveTripDuration == 8) // 8 days inclusive (15th through 22nd)
}
// MARK: - Invariant Tests
@Test("Invariant: totalDriverHoursPerDay > 0")
func invariant_totalDriverHoursPositive() {
let prefs1 = TripPreferences(numberOfDrivers: 1)
#expect(prefs1.totalDriverHoursPerDay > 0)
#expect(prefs1.totalDriverHoursPerDay == 8.0) // 1 driver × 8 hrs
let prefs2 = TripPreferences(numberOfDrivers: 3, maxDrivingHoursPerDriver: 4)
#expect(prefs2.totalDriverHoursPerDay > 0)
#expect(prefs2.totalDriverHoursPerDay == 12.0) // 3 drivers × 4 hrs
}
@Test("Invariant: effectiveTripDuration >= 1")
func invariant_effectiveTripDurationMinimum() {
let testCases: [Int?] = [nil, 1, 5, 10]
for duration in testCases {
let prefs = TripPreferences(tripDuration: duration)
#expect(prefs.effectiveTripDuration >= 1)
}
// Verify specific value for nil duration with default dates
let prefsNil = TripPreferences(tripDuration: nil)
#expect(prefsNil.effectiveTripDuration == 8) // Default 7-day range = 8 days inclusive
}
// 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)
guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return }
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)
guard case .success(let options) = result else { Issue.record("Expected .success, got \(result)"); return }
#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("ItineraryOption.isValid: correctly validates segment count")
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(
id: String,
city: String,
coordinate: CLLocationCoordinate2D
) -> Stadium {
Stadium(
id: id,
name: "\(city) Stadium",
city: city,
state: "XX",
latitude: coordinate.latitude,
longitude: coordinate.longitude,
capacity: 40000,
sport: .mlb
)
}
private func makeGame(
id: String,
stadiumId: String,
dateTime: Date
) -> Game {
Game(
id: id,
homeTeamId: "team1",
awayTeamId: "team2",
stadiumId: stadiumId,
dateTime: dateTime,
sport: .mlb,
season: "2026"
)
}
}