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

@@ -150,18 +150,12 @@ struct Phase1B_ScenarioERegionTests {
let result = planner.plan(request: request)
// With only East region, LA team has no home games should fail
if case .failure(let failure) = result {
#expect(failure.reason == .noGamesInRange,
"Should fail because LA team has no East region games")
}
// If success, verify no LA stops
if case .success(let options) = result {
for option in options {
let cities = option.stops.map { $0.city }
#expect(!cities.contains("Los Angeles"),
"East-only filter should exclude LA")
}
guard case .failure(let failure) = result else {
Issue.record("Expected .failure, got \(result)")
return
}
#expect(failure.reason == .noGamesInRange,
"Should fail because LA team has no East region games")
}
@Test("ScenarioE with all regions includes all teams")
@@ -204,11 +198,14 @@ struct Phase1B_ScenarioERegionTests {
let planner = ScenarioEPlanner()
let result = planner.plan(request: request)
// Should succeed with both nearby East Coast teams
if case .success(let options) = result {
// Should succeed with both nearby East Coast teams.
// Failure is also OK if driving constraints prevent it.
switch result {
case .success(let options):
#expect(!options.isEmpty, "Should find routes for NYC + Boston")
case .failure:
break // Acceptable driving constraints may prevent a valid route
}
// Failure is also OK if driving constraints prevent it
}
}
@@ -246,11 +243,13 @@ struct Phase1C_MustStopTests {
let result = engine.planItineraries(request: request)
// If successful, all options must include Boston
if case .success(let options) = result {
for option in options {
let cities = option.stops.map { $0.city.lowercased() }
#expect(cities.contains("boston"), "All options must include Boston must-stop")
}
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
for option in options {
let cities = option.stops.map { $0.city.lowercased() }
#expect(cities.contains("boston"), "All options must include Boston must-stop")
}
}
}
@@ -330,10 +329,12 @@ struct Phase1D_TravelSegmentTests {
let result = engine.planItineraries(request: request)
// If successful, all returned options must be valid
if case .success(let options) = result {
for option in options {
#expect(option.isValid, "Engine should only return valid options")
}
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
for option in options {
#expect(option.isValid, "Engine should only return valid options")
}
}
}
@@ -553,7 +554,7 @@ struct Phase2C_OvernightRestTests {
@Suite("Phase 2D: Silent Exclusion Warnings")
struct Phase2D_ExclusionWarningTests {
@Test("Engine tracks warnings when options are excluded by repeat city filter")
@Test("Engine filters repeat-city routes when allowRepeatCities is false")
func engine_tracksRepeatCityWarnings() {
let baseDate = TestFixtures.date(year: 2026, month: 6, day: 1, hour: 19)
let day2 = TestClock.calendar.date(byAdding: .day, value: 1, to: baseDate)!
@@ -580,11 +581,24 @@ struct Phase2D_ExclusionWarningTests {
)
let engine = TripPlanningEngine()
_ = engine.planItineraries(request: request)
let result = engine.planItineraries(request: request)
// Engine should have warnings accessible (even if result is failure)
// The warnings property exists and is populated
#expect(engine.warnings is [ConstraintViolation], "Warnings should be an array of ConstraintViolation")
// With allowRepeatCities=false, engine should return only routes without repeat cities
guard case .success(let options) = result else {
// If all routes had repeat cities, failure is also acceptable
return
}
// Every returned route must have unique cities per calendar day
for option in options {
let calendar = Calendar.current
var cityDays: Set<String> = []
for stop in option.stops {
let day = calendar.startOfDay(for: stop.arrivalDate)
let key = "\(stop.city.lowercased())_\(day.timeIntervalSince1970)"
#expect(!cityDays.contains(key), "Route should not visit \(stop.city) on the same day twice")
cityDays.insert(key)
}
}
}
@Test("Engine tracks must-stop exclusion warnings")
@@ -615,10 +629,12 @@ struct Phase2D_ExclusionWarningTests {
let result = engine.planItineraries(request: request)
// Should fail with must-stop violation
if case .failure(let failure) = result {
let hasMustStopViolation = failure.violations.contains(where: { $0.type == .mustStop })
#expect(hasMustStopViolation, "Failure should include mustStop constraint violation")
guard case .failure(let failure) = result else {
Issue.record("Expected .failure, got \(result)")
return
}
let hasMustStopViolation = failure.violations.contains(where: { $0.type == .mustStop })
#expect(hasMustStopViolation, "Failure should include mustStop constraint violation")
}
@Test("Engine tracks segment validation warnings")
@@ -636,10 +652,15 @@ struct Phase2D_ExclusionWarningTests {
let request = PlanningRequest(preferences: prefs, availableGames: [], teams: [:], stadiums: [:])
_ = engine.planItineraries(request: request)
// After a second run, warnings should be reset
let warningsAfterFirst = engine.warnings
// After a second run, warnings should be reset (not accumulated)
_ = engine.planItineraries(request: request)
let warningsAfterSecond = engine.warnings
// Warnings from first run should not leak into second run
// (engine resets warnings at start of planItineraries)
#expect(warningsAfterSecond.count == warningsAfterFirst.count,
"Warnings should be reset between runs, not accumulated (\(warningsAfterFirst.count) vs \(warningsAfterSecond.count))")
}
}
@@ -845,14 +866,14 @@ struct Phase4B_InvertedDateRangeTests {
let engine = TripPlanningEngine()
let result = engine.planItineraries(request: request)
if case .failure(let failure) = result {
#expect(failure.reason == .missingDateRange,
"Inverted date range should return missingDateRange failure")
#expect(failure.violations.contains(where: { $0.type == .dateRange }),
"Should include dateRange violation")
} else {
#expect(Bool(false), "Inverted date range should not succeed")
guard case .failure(let failure) = result else {
Issue.record("Expected .failure, got \(result)")
return
}
#expect(failure.reason == .missingDateRange,
"Inverted date range should return missingDateRange failure")
#expect(failure.violations.contains(where: { $0.type == .dateRange }),
"Should include dateRange violation")
}
}
@@ -945,12 +966,14 @@ struct Phase4D_CrossCountryTests {
let result = engine.planItineraries(request: request)
// If successful, should NOT contain LA
if case .success(let options) = result {
for option in options {
let cities = option.stops.map { $0.city }
#expect(!cities.contains("Los Angeles"),
"East-only search should not include LA")
}
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
for option in options {
let cities = option.stops.map { $0.city }
#expect(!cities.contains("Los Angeles"),
"East-only search should not include LA")
}
}
}