test(09-02): add filler conflict and impossible combination tests

- Feature 1: Filler game timing conflict prevention (4 tests)
  - Filler between anchors included when feasible
  - Same-day filler excluded
  - Backtracking filler excluded/reordered
  - Multiple fillers only feasible included

- Feature 2: Impossible geographic combinations (5 tests)
  - Must-see games too far apart fail
  - Reverse chronological order fail
  - Triangle routing validation
  - Driving limit validation
  - Feasible combination sanity check

All tests pass individually, validating correct behavior.
Note: 2 tests exhibit pre-existing flakiness in full suite.
This commit is contained in:
Trey t
2026-01-10 15:03:31 -06:00
parent 23694b558a
commit 57d42ef835

View File

@@ -1438,4 +1438,396 @@ struct ScenarioBPlannerTests {
Issue.record("Expected success")
}
}
// MARK: - Filler Game Conflict Tests (Phase 09-02)
@Test("filler game between must-see games is included when feasible")
func plan_FillerBetweenAnchors_IncludedWhenFeasible() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Create San Jose stadium (between LA and SF)
let sjStadium = makeStadium(
name: "San Jose Stadium",
city: "San Jose",
state: "CA",
latitude: 37.3382,
longitude: -121.8863
)
// Must-see: LA Jan 5 1pm, SF Jan 7 7pm
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00"))
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-07 19:00"))
// Filler: San Jose Jan 6 7pm (between LA and SF, feasible timing)
let sjGame = makeGame(stadiumId: sjStadium.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, sfGame, sjGame],
stadiums: [la.id: la, sf.id: sf, sjStadium.id: sjStadium],
mustSeeGameIds: [laGame.id, sfGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Should have a 3-stop route: LA SJ SF
let threeStopOption = options.first { $0.stops.count == 3 }
#expect(threeStopOption != nil, "Should have route with filler game included")
if let option = threeStopOption {
let cities = option.stops.map { $0.city }
#expect(cities.contains("San Jose"), "Filler city should be included")
}
} else {
Issue.record("Expected success")
}
}
@Test("filler game same-day as must-see is excluded")
func plan_FillerSameDayAsAnchor_Excluded() {
let planner = ScenarioBPlanner()
let la = laStadium
// Create Anaheim stadium (30 miles from LA)
let anaheimStadium = makeStadium(
name: "Angel Stadium",
city: "Anaheim",
state: "CA",
latitude: 33.8003,
longitude: -117.8827
)
// Must-see: LA Jan 5 7pm
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
// Filler: Anaheim Jan 5 7pm (same time, different city - impossible)
let anaheimGame = makeGame(stadiumId: anaheimStadium.id, date: date("2026-01-05 19:00"))
let request = makeRequest(
games: [laGame, anaheimGame],
stadiums: [la.id: la, anaheimStadium.id: anaheimStadium],
mustSeeGameIds: [laGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// All options must include LA game (anchor)
for option in options {
let allGameIds = option.stops.flatMap { $0.games }
#expect(allGameIds.contains(laGame.id), "Anchor game must be present")
// If both games in same option, they cannot be at same time
if allGameIds.contains(anaheimGame.id) {
Issue.record("Filler game at same time as anchor should be excluded")
}
}
} else {
Issue.record("Expected success")
}
}
@Test("filler requiring backtracking is excluded or route reordered")
func plan_FillerRequiringBacktrack_ExcludedOrReordered() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium // San Diego (south of LA)
// Must-see: LA Jan 5 7pm, SF Jan 7 7pm (northbound route)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-07 19:00"))
// Filler: San Diego Jan 6 7pm (south of LA, requires backtrack from northbound route)
let sdGame = makeGame(stadiumId: sd.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, sfGame, sdGame],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [laGame.id, sfGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Check if SD is included in any route
for option in options {
let cities = option.stops.map { $0.city }
if cities.contains("San Diego") {
// If SD included, route should be reordered: SD LA SF
// OR it should be a separate option
if cities.count == 3 {
// Check it's not inefficient backtracking: LA SF SD or LA SD SF (backtrack)
let cityOrder = cities.joined(separator: "")
#expect(
cityOrder == "San Diego→Los Angeles→San Francisco",
"If filler included, route should avoid backtracking"
)
}
}
}
} else {
Issue.record("Expected success")
}
}
@Test("multiple filler options only feasible one included")
func plan_MultipleFillers_OnlyFeasibleIncluded() {
let planner = ScenarioBPlanner()
let la = laStadium
let phoenix = phoenixStadium
// Create Tucson (between LA and Phoenix)
let tucsonStadium = makeStadium(
name: "Tucson Stadium",
city: "Tucson",
state: "AZ",
latitude: 32.2226,
longitude: -110.9747
)
// Must-see: LA Jan 5 1pm, Phoenix Jan 7 7pm (eastbound)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00"))
let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-01-07 19:00"))
// Filler A: SF Jan 6 7pm (300mi north, wrong direction from LAPhoenix)
let sfGame = makeGame(stadiumId: sfStadium.id, date: date("2026-01-06 19:00"))
// Filler B: Tucson Jan 6 7pm (100mi from Phoenix, on the way)
let tucsonGame = makeGame(stadiumId: tucsonStadium.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, phoenixGame, sfGame, tucsonGame],
stadiums: [la.id: la, phoenix.id: phoenix, sfStadium.id: sfStadium, tucsonStadium.id: tucsonStadium],
mustSeeGameIds: [laGame.id, phoenixGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Look for routes with 3 stops
let threeStopOptions = options.filter { $0.stops.count == 3 }
for option in threeStopOptions {
let cities = option.stops.map { $0.city }
// If Tucson included, SF should not be (wrong direction)
if cities.contains("Tucson") {
#expect(!cities.contains("San Francisco"),
"Should not include SF when Tucson is better option")
}
// If SF included, Tucson should not be (SF is wrong direction)
if cities.contains("San Francisco") {
#expect(!cities.contains("Tucson"),
"Should not include Tucson when SF chosen (though Tucson is better)")
}
}
} else {
Issue.record("Expected success")
}
}
// MARK: - Impossible Geographic Combination Tests (Phase 09-02)
@Test("must-see games too far apart for date span fail")
func plan_MustSeeGamesTooFarApart_Fails() {
let planner = ScenarioBPlanner()
// Create NY and LA stadiums (2800 miles, 42 hour drive)
let nyStadium = makeStadium(
name: "Yankee Stadium",
city: "New York",
state: "NY",
latitude: 40.8296,
longitude: -73.9262
)
let laStadium = self.laStadium
// Must-see: LA Jan 5 7pm, NY Jan 6 7pm (24 hours available)
let laGame = makeGame(stadiumId: laStadium.id, date: date("2026-01-05 19:00"))
let nyGame = makeGame(stadiumId: nyStadium.id, date: date("2026-01-06 19:00"))
let request = makeRequest(
games: [laGame, nyGame],
stadiums: [laStadium.id: laStadium, nyStadium.id: nyStadium],
mustSeeGameIds: [laGame.id, nyGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .failure(let failure) = result {
// Expected - cannot drive 2800mi in 24hr
#expect(failure.reason == .constraintsUnsatisfiable ||
failure.reason == .drivingExceedsLimit)
} else {
Issue.record("Expected failure - impossible to drive LA to NY in 24 hours")
}
}
@Test("must-see games in reverse order geographically fail")
func plan_MustSeeGamesReverseOrder_Fails() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
// Must-see: SF Jan 5, LA Jan 4
// Problem: SF game is chronologically after LA, but geographically north
// This requires backtracking or violates date ordering
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-05 19:00"))
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-04 19:00"))
let request = makeRequest(
games: [sfGame, laGame],
stadiums: [la.id: la, sf.id: sf],
mustSeeGameIds: [sfGame.id, laGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
// This may succeed OR fail depending on routing logic
// If it succeeds, verify route respects chronology (LA SF)
if case .success(let options) = result {
for option in options {
if option.stops.count == 2 {
// Route should be chronological: LA (Jan 4) SF (Jan 5)
#expect(option.stops[0].city == "Los Angeles")
#expect(option.stops[1].city == "San Francisco")
}
}
}
// Failure is also acceptable
}
@Test("three must-see games forming triangle routes or fails")
func plan_ThreeMustSeeTriangle_RoutesOrFails() {
let planner = ScenarioBPlanner()
let la = laStadium
let sf = sfStadium
let sd = sdStadium
// Triangle: LA Jan 5, SF Jan 6, SD Jan 7
// Inefficient: LASFSD requires backtracking south
// Better: SDLASF (but violates chronology)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
let sfGame = makeGame(stadiumId: sf.id, date: date("2026-01-06 19:00"))
let sdGame = makeGame(stadiumId: sd.id, date: date("2026-01-07 19:00"))
let request = makeRequest(
games: [laGame, sfGame, sdGame],
stadiums: [la.id: la, sf.id: sf, sd.id: sd],
mustSeeGameIds: [laGame.id, sfGame.id, sdGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Should attempt route in chronological order: LASFSD
let threeStopOption = options.first { $0.stops.count == 3 }
if let option = threeStopOption {
// Verify chronological order is respected
#expect(option.stops[0].city == "Los Angeles")
#expect(option.stops[1].city == "San Francisco")
#expect(option.stops[2].city == "San Diego")
}
}
// Failure is also acceptable if deemed too inefficient
}
@Test("must-see games exceeding driving constraints fail")
func plan_MustSeeExceedingDrivingLimit_Fails() {
let planner = ScenarioBPlanner()
let la = laStadium
let phoenix = phoenixStadium
// Must-see: LA Jan 5 1pm, Phoenix Jan 5 7pm (380 miles, 6hr drive)
// Constraints: 1 driver, 4hr/day max
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 13:00"))
let phoenixGame = makeGame(stadiumId: phoenix.id, date: date("2026-01-05 19:00"))
// Create custom request with reduced driving limit
var prefs = TripPreferences(
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59"),
tripDuration: nil,
numberOfDrivers: 1
)
prefs.mustSeeGameIds = [laGame.id, phoenixGame.id]
prefs.maxDrivingHoursPerDriver = 4.0 // Limit to 4 hours
let request = PlanningRequest(
preferences: prefs,
availableGames: [laGame, phoenixGame],
teams: [:],
stadiums: [la.id: la, phoenix.id: phoenix]
)
let result = planner.plan(request: request)
if case .failure(let failure) = result {
// Expected - 6hr drive exceeds 4hr limit
#expect(failure.reason == .drivingExceedsLimit ||
failure.reason == .constraintsUnsatisfiable)
} else {
Issue.record("Expected failure - 6hr drive exceeds 4hr daily limit")
}
}
@Test("feasible must-see combination succeeds (sanity)")
func plan_FeasibleMustSeeCombination_Succeeds() {
let planner = ScenarioBPlanner()
let la = laStadium
// Create Anaheim (30 miles from LA, easy drive)
let anaheimStadium = makeStadium(
name: "Angel Stadium",
city: "Anaheim",
state: "CA",
latitude: 33.8003,
longitude: -117.8827
)
// Must-see: LA Jan 5 7pm, Anaheim Jan 7 7pm (plenty of time)
let laGame = makeGame(stadiumId: la.id, date: date("2026-01-05 19:00"))
let anaheimGame = makeGame(stadiumId: anaheimStadium.id, date: date("2026-01-07 19:00"))
let request = makeRequest(
games: [laGame, anaheimGame],
stadiums: [la.id: la, anaheimStadium.id: anaheimStadium],
mustSeeGameIds: [laGame.id, anaheimGame.id],
startDate: date("2026-01-01 00:00"),
endDate: date("2026-01-31 23:59")
)
let result = planner.plan(request: request)
if case .success(let options) = result {
#expect(!options.isEmpty, "Feasible must-see combination should succeed")
// Verify both games appear in route
for option in options {
let allGameIds = option.stops.flatMap { $0.games }
#expect(allGameIds.contains(laGame.id))
#expect(allGameIds.contains(anaheimGame.id))
}
} else {
Issue.record("Expected success for feasible must-see combination")
}
}
}