Harden planning test suite with realistic fixtures and output sanity checks

Adds messy/realistic data factories to TestFixtures, new PlannerOutputSanityTests,
and updates all scenario planner tests with improved coverage and assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-04 13:38:41 -05:00
parent 188076717b
commit 9b622f8bbb
13 changed files with 2174 additions and 446 deletions

View File

@@ -212,18 +212,19 @@ struct Bug4_ScenarioDRationaleTests {
let planner = ScenarioDPlanner()
let result = planner.plan(request: request)
if case .success(let options) = result {
// Bug #4: rationale was using stops.count instead of actual game count.
// Verify that for each option, the game count in the rationale matches
// the actual total games across stops.
for option in options {
let actualGameCount = option.stops.reduce(0) { $0 + $1.games.count }
let rationale = option.geographicRationale
#expect(rationale.contains("\(actualGameCount) games"),
"Rationale game count should match actual games (\(actualGameCount)). Got: \(rationale)")
}
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
// Bug #4: rationale was using stops.count instead of actual game count.
// Verify that for each option, the game count in the rationale matches
// the actual total games across stops.
for option in options {
let actualGameCount = option.stops.reduce(0) { $0 + $1.games.count }
let rationale = option.geographicRationale
#expect(rationale.contains("\(actualGameCount) games"),
"Rationale game count should match actual games (\(actualGameCount)). Got: \(rationale)")
}
// If planning fails, that's OK this test focuses on rationale text when it succeeds
}
}
@@ -261,15 +262,21 @@ struct Bug5_ScenarioDDepartureDateTests {
let planner = ScenarioDPlanner()
let result = planner.plan(request: request)
if case .success(let options) = result, let option = options.first {
// Find the game stop (not the home start/end waypoints)
let gameStops = option.stops.filter { $0.hasGames }
if let gameStop = gameStops.first {
let gameDayStart = calendar.startOfDay(for: gameDate)
let departureDayStart = calendar.startOfDay(for: gameStop.departureDate)
#expect(departureDayStart > gameDayStart,
"Departure should be after game day, not same day. Game: \(gameDayStart), Departure: \(departureDayStart)")
}
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
guard let option = options.first else {
Issue.record("Expected at least one option, got empty array")
return
}
// Find the game stop (not the home start/end waypoints)
let gameStops = option.stops.filter { $0.hasGames }
if let gameStop = gameStops.first {
let gameDayStart = calendar.startOfDay(for: gameDate)
let departureDayStart = calendar.startOfDay(for: gameStop.departureDate)
#expect(departureDayStart > gameDayStart,
"Departure should be after game day, not same day. Game: \(gameDayStart), Departure: \(departureDayStart)")
}
}
}
@@ -321,11 +328,12 @@ struct Bug6_ScenarioCDateRangeTests {
let result = planner.plan(request: request)
// Should find at least one option games exactly span the trip duration
if case .failure(let failure) = result {
let reason = failure.reason
#expect(reason != PlanningFailure.FailureReason.noGamesInRange,
"Games spanning exactly daySpan should not be excluded. Failure: \(failure.message)")
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
#expect(!options.isEmpty,
"Games spanning exactly daySpan should produce at least one option")
}
}
@@ -634,9 +642,11 @@ struct Bug13_MissingStadiumTests {
// Currently: silently excluded noGamesInRange.
// This test documents the current behavior (missing stadiums are excluded).
if case .failure(let failure) = result {
#expect(failure.reason == .noGamesInRange)
guard case .failure(let failure) = result else {
Issue.record("Expected .failure, got \(result)")
return
}
#expect(failure.reason == .noGamesInRange)
}
}
@@ -647,14 +657,7 @@ struct Bug13_MissingStadiumTests {
@Suite("Bug #14: Drag drop feedback")
struct Bug14_DragDropTests {
@Test("documented: drag state should not be cleared before validation")
func documented_dragStateShouldPersistDuringValidation() {
// This bug is in TripDetailView.swift:1508-1525 (UI layer).
// Drag state is cleared synchronously before async validation runs.
// If validation fails, no visual feedback is shown.
// Fix: Move drag state clearing AFTER validation succeeds.
#expect(true, "UI bug documented — drag state should persist during validation")
}
// Bug #14 (drag state) is a UI-layer issue tracked separately no unit test possible here.
}
// MARK: - Bug #15: ScenarioB force unwraps on date arithmetic
@@ -699,8 +702,14 @@ struct Bug15_DateArithmeticTests {
)
let planner = ScenarioBPlanner()
// Should not crash just verifying safety
let _ = planner.plan(request: request)
let result = planner.plan(request: request)
// Should not crash verify we get a valid result (success or failure, not a crash)
switch result {
case .success(let options):
#expect(!options.isEmpty, "If success, should have at least one option")
case .failure:
break // Failure is acceptable the point is it didn't crash
}
}
}
@@ -709,14 +718,7 @@ struct Bug15_DateArithmeticTests {
@Suite("Bug #16: Sort order accumulation")
struct Bug16_SortOrderTests {
@Test("documented: repeated before-games moves should use midpoint not subtraction")
func documented_sortOrderShouldNotGoExtremelyNegative() {
// This bug is in ItineraryReorderingLogic.swift:420-428.
// Each "move before first item" subtracts 1.0 instead of using midpoint.
// After many moves, sortOrder becomes -10, -20, etc.
// Fix: Use midpoint (n/2.0) instead of subtraction (n-1.0).
#expect(true, "Documented: sortOrder should use midpoint insertion")
}
// Bug #16 (sortOrder accumulation) is in ItineraryReorderingLogic tracked separately.
}
// MARK: - Cross-cutting: TravelEstimator consistency