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>
299 lines
11 KiB
Swift
299 lines
11 KiB
Swift
//
|
||
// 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"
|
||
)
|
||
}
|
||
}
|