Complete rewrite of unit test suite using TDD methodology: Planning Engine Tests: - GameDAGRouterTests: Beam search, anchor games, transitions - ItineraryBuilderTests: Stop connection, validators, EV enrichment - RouteFiltersTests: Region, time window, scoring filters - ScenarioA/B/C/D PlannerTests: All planning scenarios - TravelEstimatorTests: Distance, duration, travel days - TripPlanningEngineTests: Orchestration, caching, preferences Domain Model Tests: - AchievementDefinitionsTests, AnySportTests, DivisionTests - GameTests, ProgressTests, RegionTests, StadiumTests - TeamTests, TravelSegmentTests, TripTests, TripPollTests - TripPreferencesTests, TripStopTests, SportTests Service Tests: - FreeScoreAPITests, RouteDescriptionGeneratorTests - SuggestedTripsGeneratorTests Export Tests: - ShareableContentTests (card types, themes, dimensions) Bug fixes discovered through TDD: - ShareCardDimensions: mapSnapshotSize exceeded available width (960x480) - ScenarioBPlanner: Added anchor game validation filter All tests include: - Specification tests (expected behavior) - Invariant tests (properties that must always hold) - Edge case tests (boundary conditions) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
76 lines
2.7 KiB
Swift
76 lines
2.7 KiB
Swift
//
|
|
// TripPlanningEngine.swift
|
|
// SportsTime
|
|
//
|
|
// Thin orchestrator that delegates to scenario-specific planners.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Main entry point for trip planning.
|
|
/// Delegates to scenario-specific planners via the ScenarioPlanner protocol.
|
|
///
|
|
/// - Expected Behavior:
|
|
/// - Uses ScenarioPlannerFactory.planner(for:) to select the right planner
|
|
/// - Delegates entirely to the selected scenario planner
|
|
/// - Applies repeat city filter to successful results
|
|
/// - If all options violate repeat city constraint → .failure with .repeatCityViolation
|
|
/// - Passes through failures from scenario planners unchanged
|
|
///
|
|
/// - Invariants:
|
|
/// - Never modifies the logic of scenario planners
|
|
/// - Always returns a result (success or failure), never throws
|
|
/// - Repeat city filter only applied when allowRepeatCities is false
|
|
///
|
|
final class TripPlanningEngine {
|
|
|
|
/// Plans itineraries based on the request inputs.
|
|
/// Automatically detects which scenario applies and delegates to the appropriate planner.
|
|
///
|
|
/// - Parameter request: The planning request containing all inputs
|
|
/// - Returns: Ranked itineraries on success, or explicit failure with reason
|
|
func planItineraries(request: PlanningRequest) -> ItineraryResult {
|
|
|
|
// Detect scenario and get the appropriate planner
|
|
let planner = ScenarioPlannerFactory.planner(for: request)
|
|
|
|
// Delegate to the scenario planner
|
|
let result = planner.plan(request: request)
|
|
|
|
// Apply preference filters to successful results
|
|
return applyPreferenceFilters(to: result, request: request)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
/// Applies allowRepeatCities filter after scenario planners return.
|
|
/// Note: Region filtering is done during game selection in scenario planners.
|
|
private func applyPreferenceFilters(
|
|
to result: ItineraryResult,
|
|
request: PlanningRequest
|
|
) -> ItineraryResult {
|
|
guard case .success(let originalOptions) = result else {
|
|
return result
|
|
}
|
|
|
|
var options = originalOptions
|
|
|
|
// Filter repeat cities (this is enforced during beam search, but double-check here)
|
|
options = RouteFilters.filterRepeatCities(
|
|
options,
|
|
allow: request.preferences.allowRepeatCities
|
|
)
|
|
|
|
if options.isEmpty && !request.preferences.allowRepeatCities {
|
|
let violatingCities = RouteFilters.findRepeatCities(in: originalOptions)
|
|
return .failure(PlanningFailure(
|
|
reason: .repeatCityViolation(cities: violatingCities)
|
|
))
|
|
}
|
|
|
|
// Region filtering is applied during game selection in scenario planners
|
|
|
|
return .success(options)
|
|
}
|
|
}
|