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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user