Files
Sportstime/SportsTimeTests/Planning/ScenarioPlannerFactoryTests.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

335 lines
12 KiB
Swift

//
// ScenarioPlannerFactoryTests.swift
// SportsTimeTests
//
// TDD specification tests for ScenarioPlannerFactory.
//
import Testing
import CoreLocation
@testable import SportsTime
@Suite("ScenarioPlannerFactory")
@MainActor
struct ScenarioPlannerFactoryTests {
// MARK: - Specification Tests: planner(for:)
@Test("planner: followTeamId set returns ScenarioDPlanner")
func planner_followTeamId_returnsScenarioD() {
let prefs = TripPreferences(
planningMode: .followTeam,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1,
followTeamId: "team-123"
)
let request = makeRequest(preferences: prefs)
let planner = ScenarioPlannerFactory.planner(for: request)
#expect(planner is ScenarioDPlanner)
}
@Test("planner: selectedGames not empty returns ScenarioBPlanner")
func planner_selectedGames_returnsScenarioB() {
let game = makeGame()
let prefs = TripPreferences(
planningMode: .gameFirst,
sports: [.mlb],
mustSeeGameIds: [game.id],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
)
let request = makeRequest(preferences: prefs, games: [game])
let planner = ScenarioPlannerFactory.planner(for: request)
#expect(planner is ScenarioBPlanner)
}
@Test("planner: start and end locations returns ScenarioCPlanner")
func planner_startEndLocations_returnsScenarioC() {
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
let prefs = TripPreferences(
planningMode: .locations,
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
endLocation: LocationInput(name: "LA", coordinate: laCoord),
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
)
let request = makeRequest(preferences: prefs)
let planner = ScenarioPlannerFactory.planner(for: request)
#expect(planner is ScenarioCPlanner)
}
@Test("planner: date range only returns ScenarioAPlanner")
func planner_dateRangeOnly_returnsScenarioA() {
let prefs = TripPreferences(
planningMode: .dateRange,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
)
let request = makeRequest(preferences: prefs)
let planner = ScenarioPlannerFactory.planner(for: request)
#expect(planner is ScenarioAPlanner)
}
@Test("planner: priority is D > B > C > A")
func planner_priority_DoverBoverCoverA() {
// If all conditions are met, followTeamId wins
let game = makeGame()
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
let prefs = TripPreferences(
planningMode: .followTeam,
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
sports: [.mlb],
mustSeeGameIds: [game.id], // B condition
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1,
followTeamId: "team-123" // D condition
)
let request = makeRequest(preferences: prefs, games: [game])
let planner = ScenarioPlannerFactory.planner(for: request)
#expect(planner is ScenarioDPlanner, "D should take priority")
}
@Test("planner: B takes priority over C")
func planner_priority_BoverC() {
// If B and C conditions met but not D, B wins
let game = makeGame()
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
let prefs = TripPreferences(
planningMode: .gameFirst,
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
endLocation: LocationInput(name: "LA", coordinate: laCoord), // C condition
sports: [.mlb],
mustSeeGameIds: [game.id], // B condition
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
// followTeamId: nil by default - Not D
)
let request = makeRequest(preferences: prefs, games: [game])
let planner = ScenarioPlannerFactory.planner(for: request)
#expect(planner is ScenarioBPlanner, "B should take priority over C")
}
@Test("planner: teamFirst with 2+ teams returns ScenarioEPlanner")
func planner_teamFirst_returnsScenarioE() {
let prefs = TripPreferences(
planningMode: .teamFirst,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1,
selectedTeamIds: ["team-1", "team-2"]
)
let request = makeRequest(preferences: prefs)
let planner = ScenarioPlannerFactory.planner(for: request)
#expect(planner is ScenarioEPlanner)
}
@Test("classify: teamFirst with 2+ teams returns scenarioE")
func classify_teamFirst_returnsScenarioE() {
let prefs = TripPreferences(
planningMode: .teamFirst,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1,
selectedTeamIds: ["team-1", "team-2"]
)
let request = makeRequest(preferences: prefs)
let scenario = ScenarioPlannerFactory.classify(request)
#expect(scenario == .scenarioE)
}
// MARK: - Specification Tests: classify()
@Test("classify: followTeamId returns scenarioD")
func classify_followTeamId_returnsScenarioD() {
let prefs = TripPreferences(
planningMode: .followTeam,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1,
followTeamId: "team-123"
)
let request = makeRequest(preferences: prefs)
let scenario = ScenarioPlannerFactory.classify(request)
#expect(scenario == .scenarioD)
}
@Test("classify: selectedGames returns scenarioB")
func classify_selectedGames_returnsScenarioB() {
let game = makeGame()
let prefs = TripPreferences(
planningMode: .gameFirst,
sports: [.mlb],
mustSeeGameIds: [game.id],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
)
let request = makeRequest(preferences: prefs, games: [game])
let scenario = ScenarioPlannerFactory.classify(request)
#expect(scenario == .scenarioB)
}
@Test("classify: startEndLocations returns scenarioC")
func classify_startEndLocations_returnsScenarioC() {
let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
let laCoord = CLLocationCoordinate2D(latitude: 34.0430, longitude: -118.2673)
let prefs = TripPreferences(
planningMode: .locations,
startLocation: LocationInput(name: "NYC", coordinate: nycCoord),
endLocation: LocationInput(name: "LA", coordinate: laCoord),
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
)
let request = makeRequest(preferences: prefs)
let scenario = ScenarioPlannerFactory.classify(request)
#expect(scenario == .scenarioC)
}
@Test("classify: dateRangeOnly returns scenarioA")
func classify_dateRangeOnly_returnsScenarioA() {
let prefs = TripPreferences(
planningMode: .dateRange,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
)
let request = makeRequest(preferences: prefs)
let scenario = ScenarioPlannerFactory.classify(request)
#expect(scenario == .scenarioA)
}
// MARK: - Property Tests
@Test("Property: planner and classify are consistent")
func property_plannerAndClassifyConsistent() {
// Scenario A
let prefsA = TripPreferences(
planningMode: .dateRange,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1
)
let requestA = makeRequest(preferences: prefsA)
let plannerA = ScenarioPlannerFactory.planner(for: requestA)
let classifyA = ScenarioPlannerFactory.classify(requestA)
#expect(plannerA is ScenarioAPlanner && classifyA == .scenarioA)
// Scenario D
let prefsD = TripPreferences(
planningMode: .followTeam,
sports: [.mlb],
startDate: TestClock.now,
endDate: TestClock.now.addingTimeInterval(86400 * 7),
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1,
followTeamId: "team-123"
)
let requestD = makeRequest(preferences: prefsD)
let plannerD = ScenarioPlannerFactory.planner(for: requestD)
let classifyD = ScenarioPlannerFactory.classify(requestD)
#expect(plannerD is ScenarioDPlanner && classifyD == .scenarioD)
}
// MARK: - Helper Methods
private func makeRequest(
preferences: TripPreferences,
games: [Game] = []
) -> PlanningRequest {
PlanningRequest(
preferences: preferences,
availableGames: games,
teams: [:],
stadiums: [:]
)
}
private func makeGame() -> Game {
Game(
id: "game-\(UUID().uuidString)",
homeTeamId: "team1",
awayTeamId: "team2",
stadiumId: "stadium1",
dateTime: TestClock.now,
sport: .mlb,
season: "2026",
isPlayoff: false
)
}
}