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