# Test Suite Implementation Plan **Scope:** TO-DOS items 20.0 - 20.10 **Approach:** TDD — tests define expected behavior, then app code is updated to pass **Framework:** Swift Testing (`@Test`, `#expect`, `@Suite`) with XCTest fallback if needed **Status:** Use checkboxes to track progress --- ## Pre-Implementation Setup ### Phase 0: Test Infrastructure > Set up foundations before writing any feature tests. - [x] **0.1** Create `SportsTimeTests/` directory structure: ``` SportsTimeTests/ ├── Fixtures/ │ └── FixtureGenerator.swift ├── Mocks/ │ ├── MockCloudKitService.swift │ ├── MockLocationService.swift │ └── MockAppDataProvider.swift ├── Helpers/ │ ├── BruteForceRouteVerifier.swift │ └── TestConstants.swift ├── Planning/ │ ├── GameDAGRouterTests.swift │ ├── ScenarioAPlannerTests.swift │ ├── ScenarioBPlannerTests.swift │ ├── ScenarioCPlannerTests.swift │ ├── TravelEstimatorTests.swift │ ├── ItineraryBuilderTests.swift │ └── TripPlanningEngineTests.swift └── Performance/ └── PlannerScaleTests.swift ``` - [x] **0.2** Implement `FixtureGenerator.swift`: - Generate synthetic `Game` objects with configurable: - Count (5, 50, 500, 2000, 10000) - Date range - Geographic spread - Stadium/team distribution - Generate synthetic `Stadium` dictionary - Generate synthetic `Trip` with configurable stops - Deterministic seeding for reproducible tests - [x] **0.3** Implement `MockCloudKitService.swift`: - Stub all public methods - Return fixture data - Configurable error injection - [x] **0.4** Implement `MockLocationService.swift`: - Stub geocoding - Stub routing (return pre-calculated distances) - Configurable latency simulation - [x] **0.5** Implement `MockAppDataProvider.swift`: - Return fixture stadiums/teams/games - Configurable to simulate empty data - [x] **0.6** Implement `BruteForceRouteVerifier.swift`: - For inputs with ≤8 stops, exhaustively enumerate all permutations - Compare engine result against true optimal - Used to validate "no obviously better route exists" - [x] **0.7** Implement `TestConstants.swift`: ```swift enum TestConstants { static let nearbyRadiusMiles: Double = 50.0 static let performanceTimeout: TimeInterval = 300.0 // 5 min static let hangTimeout: TimeInterval = 30.0 // Baselines TBD after initial runs static var baseline500Games: TimeInterval = 0 static var baseline2000Games: TimeInterval = 0 static var baseline10000Games: TimeInterval = 0 } ``` --- ## Feature Test Phases ### Phase 1: TravelEstimator Tests > Foundation — all planners depend on this. **File:** `TravelEstimatorTests.swift` - [x] **1.1** `test_haversineDistanceMiles_KnownDistance` NYC to LA = ~2,451 miles (verify within 1% tolerance) - [x] **1.2** `test_haversineDistanceMiles_SamePoint_ReturnsZero` - [x] **1.3** `test_haversineDistanceMiles_Antipodal_ReturnsHalfEarthCircumference` - [x] **1.4** `test_estimate_NilCoordinates_ReturnsNil` - [x] **1.5** `test_estimate_ExceedsMaxDailyHours_ReturnsNil` Distance requiring 3+ days of driving should return nil - [x] **1.6** `test_estimate_ValidTrip_ReturnsSegment` Verify `distanceMeters`, `durationSeconds`, `travelMode` - [x] **1.7** `test_calculateTravelDays_SingleDayDrive` 4 hours driving = 1 day - [x] **1.8** `test_calculateTravelDays_MultiDayDrive` 20 hours driving = 3 days (ceil(20/8)) - [x] **1.9** `test_estimateFallbackDistance_SameCity_ReturnsZero` - [x] **1.10** `test_estimateFallbackDistance_DifferentCity_Returns300` --- ### Phase 2: GameDAGRouter Tests — Core Logic > The "scary to touch" component — extensive edge case coverage. **File:** `GameDAGRouterTests.swift` #### 2A: Empty & Single-Element Cases - [x] **2.1** `test_findRoutes_EmptyGames_ReturnsEmptyArray` - [x] **2.2** `test_findRoutes_SingleGame_ReturnsSingleRoute` - [x] **2.3** `test_findRoutes_SingleGame_WithMatchingAnchor_ReturnsSingleRoute` - [x] **2.4** `test_findRoutes_SingleGame_WithNonMatchingAnchor_ReturnsEmpty` #### 2B: Two-Game Cases - [x] **2.5** `test_findRoutes_TwoGames_FeasibleTransition_ReturnsBothInOrder` - [x] **2.6** `test_findRoutes_TwoGames_InfeasibleTransition_ReturnsSeparateRoutes` - [x] **2.7** `test_findRoutes_TwoGames_SameStadiumSameDay_Succeeds` (Doubleheader scenario) #### 2C: Anchor Game Constraints - [x] **2.8** `test_findRoutes_WithAnchors_OnlyReturnsRoutesContainingAllAnchors` - [x] **2.9** `test_findRoutes_ImpossibleAnchors_ReturnsEmpty` Anchors are geographically/temporally impossible to connect - [x] **2.10** `test_findRoutes_MultipleAnchors_RouteMustContainAll` #### 2D: Repeat Cities Toggle - [x] **2.11** `test_findRoutes_AllowRepeatCities_SameCityMultipleDays_Allowed` - [x] **2.12** `test_findRoutes_DisallowRepeatCities_SkipsSecondVisit` - [x] **2.13** `test_findRoutes_DisallowRepeatCities_OnlyOptionIsRepeat_OverridesWithWarning` **TDD Note:** This test defines a new `Trip.warnings: [PlanningWarning]` property #### 2E: Driving Constraints - [x] **2.14** `test_findRoutes_ExceedsMaxDailyDriving_TransitionRejected` - [x] **2.15** `test_findRoutes_MultiDayDrive_Allowed_IfWithinDailyLimits` - [x] **2.16** `test_findRoutes_SameDayDifferentStadiums_ChecksAvailableTime` #### 2F: Calendar Day Logic - [x] **2.17** `test_findRoutes_MaxDayLookahead_RespectsLimit` Games > 5 days apart should not connect directly - [x] **2.18** `test_findRoutes_DSTTransition_HandlesCorrectly` Spring forward / fall back edge case - [x] **2.19** `test_findRoutes_MidnightGame_AssignsToCorrectDay` Game at 12:05 AM belongs to new day #### 2G: Diversity Selection - [x] **2.20** `test_selectDiverseRoutes_ShortAndLongTrips_BothRepresented` - [x] **2.21** `test_selectDiverseRoutes_HighAndLowMileage_BothRepresented` - [x] **2.22** `test_selectDiverseRoutes_FewAndManyCities_BothRepresented` - [x] **2.23** `test_selectDiverseRoutes_DuplicateRoutes_Deduplicated` #### 2H: Cycle Handling - [x] **2.24** `test_findRoutes_GraphWithPotentialCycle_HandlesSilently` Verify no infinite loop, returns valid routes #### 2I: Beam Search Behavior - [x] **2.25** `test_findRoutes_LargeDataset_ScalesBeamWidth` Verify `effectiveBeamWidth` kicks in for 5000+ games - [x] **2.26** `test_findRoutes_EarlyTermination_TriggersWhenBeamFull` --- ### Phase 3: GameDAGRouter Tests — Scale & Performance > Stress tests for 10K+ objects. Periodic/manual execution. **File:** `GameDAGRouterTests.swift` (continued) or separate `GameDAGRouterScaleTests.swift` #### 3A: Scale Tests - [x] **3.1** `test_findRoutes_5Games_CompletesWithin5Minutes` - [x] **3.2** `test_findRoutes_50Games_CompletesWithin5Minutes` - [x] **3.3** `test_findRoutes_500Games_CompletesWithin5Minutes` - [x] **3.4** `test_findRoutes_2000Games_CompletesWithin5Minutes` - [x] **3.5** `test_findRoutes_10000Games_CompletesWithin5Minutes` - [x] **3.6** `test_findRoutes_50000Nodes_CompletesWithin5Minutes` Stress test — may need timeout adjustment #### 3B: Performance Baselines - [x] **3.7** Record baseline times for 500/2000/10000 games (first run) - [x] **3.8** After baselines established, add regression assertions #### 3C: Memory Tests - [x] **3.9** `test_findRoutes_RepeatedCalls_NoMemoryLeak` Run 100 iterations, verify allocation/deallocation balance - [x] **3.10** `test_findRoutes_LargeDataset_MemoryBounded` 10K games should not exceed reasonable memory --- ### Phase 4: ScenarioAPlanner Tests (Plan by Dates) > User provides dates, planner finds games. **File:** `ScenarioAPlannerTests.swift` #### 4A: Valid Inputs - [x] **4.1** `test_planByDates_ValidDateRange_ReturnsGamesInRange` - [x] **4.2** `test_planByDates_SingleDayRange_ReturnsGamesOnThatDay` - [x] **4.3** `test_planByDates_MultiWeekRange_ReturnsMultipleGames` #### 4B: Edge Cases - [x] **4.4** `test_planByDates_NoGamesInRange_ThrowsError` **TDD Note:** Uses existing `PlanningFailure.FailureReason.noGamesInRange` - [x] **4.5** `test_planByDates_EndDateBeforeStartDate_ThrowsError` - [x] **4.6** `test_planByDates_SingleGameInRange_ReturnsSingleGameRoute` - [x] **4.7** `test_planByDates_MaxGamesInRange_HandlesGracefully` 10K games in range — verify no crash/hang #### 4C: Integration with DAG - [x] **4.8** `test_planByDates_UsesDAGRouterForRouting` - [x] **4.9** `test_planByDates_RespectsDriverConstraints` --- ### Phase 5: ScenarioBPlanner Tests (Must-See Games) > User selects specific games, planner builds route. **File:** `ScenarioBPlannerTests.swift` #### 5A: Valid Inputs - [x] **5.1** `test_mustSeeGames_SingleGame_ReturnsTripWithThatGame` - [x] **5.2** `test_mustSeeGames_MultipleGames_ReturnsOptimalRoute` - [x] **5.3** `test_mustSeeGames_GamesInDifferentCities_ConnectsThem` #### 5B: Edge Cases - [x] **5.4** `test_mustSeeGames_EmptySelection_ThrowsError` - [x] **5.5** `test_mustSeeGames_ImpossibleToConnect_ThrowsError` Games on same day in cities 850+ miles apart (same region) - [x] **5.6** `test_mustSeeGames_MaxGamesSelected_HandlesGracefully` #### 5C: Optimality Verification - [x] **5.7** `test_mustSeeGames_SmallInput_MatchesBruteForceOptimal` ≤8 games — verify against `BruteForceRouteVerifier` - [x] **5.8** `test_mustSeeGames_LargeInput_NoObviouslyBetterRoute` --- ### Phase 6: ScenarioCPlanner Tests (Depart/Return Cities) > User specifies origin and destination. **File:** `ScenarioCPlannerTests.swift` #### 6A: Valid Inputs - [x] **6.1** `test_departReturn_SameCity_ReturnsRoundTrip` - [x] **6.2** `test_departReturn_DifferentCities_ReturnsOneWayRoute` - [x] **6.3** `test_departReturn_GamesAlongCorridor_IncludesNearbyGames` Uses 50-mile radius #### 6B: Edge Cases - [x] **6.4** `test_departReturn_NoGamesAlongRoute_ThrowsError` - [x] **6.5** `test_departReturn_InvalidCity_ThrowsError` - [x] **6.6** `test_departReturn_ExtremeDistance_RespectsConstraints` NYC to LA — verify driving constraints applied #### 6C: Must-Stop Locations - [x] **6.7** `test_departReturn_WithMustStopLocation_IncludesStop` - [x] **6.8** `test_departReturn_MustStopNoNearbyGames_IncludesStopAnyway` Stop is included but may not have games - [x] **6.9** `test_departReturn_MultipleMustStops_AllIncluded` - [x] **6.10** `test_departReturn_MustStopConflictsWithRoute_FindsCompromise` --- ### Phase 7: TripPlanningEngine Integration Tests > Main orchestrator — tests all scenarios together. **File:** `TripPlanningEngineTests.swift` #### 7A: Scenario Routing - [x] **7.1** `test_engine_ScenarioA_DelegatesCorrectly` - [x] **7.2** `test_engine_ScenarioB_DelegatesCorrectly` - [x] **7.3** `test_engine_ScenarioC_DelegatesCorrectly` - [x] **7.4** `test_engine_ScenariosAreMutuallyExclusive` Cannot mix scenarios in single request #### 7B: Result Structure - [x] **7.5** `test_engine_Result_ContainsTravelSegments` - [x] **7.6** `test_engine_Result_ContainsItineraryDays` - [x] **7.7** `test_engine_Result_IncludesWarnings_WhenApplicable` **TDD Note:** Validates `Trip.warnings` property exists #### 7C: Constraint Application - [x] **7.8** `test_engine_NumberOfDrivers_AffectsMaxDailyDriving` More drivers = can drive more per day - [x] **7.9** `test_engine_MaxDrivingPerDay_Respected` - [x] **7.10** `test_engine_AllowRepeatCities_PropagatedToDAG` #### 7D: Error Handling - [x] **7.11** `test_engine_ImpossibleConstraints_ReturnsNoResult` - [x] **7.12** `test_engine_EmptyInput_ThrowsError` --- ### Phase 8: ItineraryBuilder Tests > Builds day-by-day itinerary from route. **File:** `ItineraryBuilderTests.swift` - [x] **8.1** `test_builder_SingleGame_CreatesSingleDay` - [x] **8.2** `test_builder_MultiCity_CreatesTravelSegmentsBetween` - [x] **8.3** `test_builder_SameCity_MultipleGames_GroupsOnSameDay` - [x] **8.4** `test_builder_TravelDays_InsertedWhenDrivingExceeds8Hours` - [x] **8.5** `test_builder_ArrivalTimeBeforeGame_Calculated` - [x] **8.6** `test_builder_EmptyRoute_ReturnsEmptyItinerary` --- ### Phase 9: RouteFilters Tests > Filtering on All Trips list. **File:** `RouteFiltersTests.swift` #### 9A: Single Filters - [x] **9.1** `test_filterBySport_SingleSport_ReturnsMatching` - [x] **9.2** `test_filterBySport_MultipleSports_ReturnsUnion` - [x] **9.3** `test_filterBySport_AllSports_ReturnsAll` - [x] **9.4** `test_filterByDateRange_ReturnsTripsInRange` (+ bonus: `test_filterByDateRange_IncludesOverlappingTrips`) - [x] **9.5** `test_filterByStatus_Planned_ReturnsPlanned` **TDD Note:** `Trip.status: TripStatus` property added - [x] **9.6** `test_filterByStatus_InProgress_ReturnsInProgress` - [x] **9.7** `test_filterByStatus_Completed_ReturnsCompleted` #### 9B: Combined Filters - [x] **9.8** `test_combinedFilters_SportAndDate_ReturnsIntersection` - [x] **9.9** `test_combinedFilters_AllFilters_ReturnsIntersection` #### 9C: Edge Cases - [x] **9.10** `test_filter_NoMatches_ReturnsEmptyArray` - [x] **9.11** `test_filter_AllMatch_ReturnsAll` - [x] **9.12** `test_filter_EmptyInput_ReturnsEmptyArray` --- ### Phase 10: Concurrency Tests > Prove current implementation is NOT thread-safe (for future work). **File:** `ConcurrencyTests.swift` - [x] **10.1** `test_engine_ConcurrentRequests_CurrentlyUnsafe` Document existing behavior (may crash/race) - [x] **10.2** `test_engine_SequentialRequests_Succeeds` **Future (out of scope for now):** - [ ] `test_engine_ConcurrentRequests_ThreadSafe` (after refactor) --- ### Phase 11: Edge Case Omnibus > Catch-all for extreme/unusual inputs. **File:** `EdgeCaseTests.swift` #### 11A: Data Edge Cases - [x] **11.1** `test_nilStadium_HandlesGracefully` - [x] **11.2** `test_malformedDate_HandlesGracefully` - [x] **11.3** `test_invalidCoordinates_HandlesGracefully` Lat > 90, Lon > 180 - [x] **11.4** `test_missingRequiredFields_HandlesGracefully` #### 11B: Boundary Conditions - [x] **11.5** `test_exactlyAtDrivingLimit_Succeeds` - [x] **11.6** `test_oneMileOverLimit_Fails` - [x] **11.7** `test_exactlyAtRadiusBoundary_IncludesGame` Game at exactly 50 miles - [x] **11.8** `test_oneFootOverRadius_ExcludesGame` #### 11C: Time Zone Cases - [x] **11.9** `test_gameInDifferentTimeZone_NormalizesToUTC` - [x] **11.10** `test_dstSpringForward_HandlesCorrectly` - [x] **11.11** `test_dstFallBack_HandlesCorrectly` --- ## API Proposals (TDD Discoveries) These APIs will be defined by tests and need implementation: | Property/Type | Defined In | Description | |--------------|------------|-------------| | `Trip.warnings: [PlanningWarning]` | 2.13 | Warnings when planner overrides preferences | | `PlanningWarning` enum | 2.13 | `.repeatCityOverridden(city: String)`, etc. | | `PlanningError.noGamesFound` | 4.4 | Thrown when date range has no games | | `Trip.status: TripStatus` | 9.5 | `.planned`, `.inProgress`, `.completed` | --- ## Execution Order **Recommended batch order for TDD cycle:** 1. **Phase 0** — Infrastructure (no app changes) 2. **Phase 1** — TravelEstimator (foundation, likely passes) 3. **Phase 2** — GameDAGRouter core (highest risk, most edge cases) 4. **Phase 3** — GameDAGRouter scale (set baselines) 5. **Phase 4-6** — Scenario planners (A, B, C) 6. **Phase 7** — TripPlanningEngine integration 7. **Phase 8** — ItineraryBuilder 8. **Phase 9** — RouteFilters 9. **Phase 10** — Concurrency (documentation tests) 10. **Phase 11** — Edge cases --- ## Progress Tracking | Phase | Tests | Passing | Status | |-------|-------|---------|--------| | 0 | 7 | 7 | Complete | | 1 | 10 | 10 | Complete | | 2 | 26 | 26 | Complete | | 3 | 10 | 10 | Complete | | 4 | 9 | 9 | Complete | | 5 | 8 | 8 | Complete | | 6 | 10 | 10 | Complete | | 7 | 12 | 12 | Complete | | 8 | 6 | 6 | Complete | | 9 | 13 | 13 | Complete | | 10 | 2 | 2 | Complete | | 11 | 11 | 11 | Complete | | **Total** | **124** | **124** | | --- ## Notes - **Hang timeout:** 30 seconds (tests marked as hanging if exceeded) - **Performance timeout:** 5 minutes per test (scale tests) - **Nearby radius:** 50 miles driving distance (`TestConstants.nearbyRadiusMiles`) - **Brute force threshold:** ≤8 stops for exact optimality verification - **Baselines:** Will be recorded after initial runs, then hardcoded --- *Last updated: 2026-01-11 (Phase 11 complete — All tests passing)*