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

@@ -19,6 +19,7 @@ struct ScenarioDPlannerTests {
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
private let chicagoCoord = CLLocationCoordinate2D(latitude: 41.8827, longitude: -87.6233)
private let phillyCoord = CLLocationCoordinate2D(latitude: 39.9526, longitude: -75.1652)
// MARK: - Specification Tests: Missing Team
@@ -155,13 +156,16 @@ struct ScenarioDPlannerTests {
let result = planner.plan(request: request)
if case .success(let options) = result {
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
// Both home and away games should be includable
let hasHomeGame = allGameIds.contains("home-game")
let hasAwayGame = allGameIds.contains("away-game")
#expect(hasHomeGame || hasAwayGame, "Should include at least one team game")
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
// At least one option should include BOTH home and away games
let hasOptionWithBoth = options.contains { option in
let gameIds = Set(option.stops.flatMap { $0.games })
return gameIds.contains("home-game") && gameIds.contains("away-game")
}
#expect(hasOptionWithBoth, "At least one option should include both home and away games")
}
// MARK: - Specification Tests: Region Filtering
@@ -219,10 +223,13 @@ struct ScenarioDPlannerTests {
let result = planner.plan(request: request)
if case .success(let options) = result {
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
#expect(!allGameIds.contains("central-game"), "Central game should be filtered by East region")
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
#expect(allGameIds.contains("east-game"), "East game should be included when East region is selected")
#expect(!allGameIds.contains("central-game"), "Central game should be filtered by East region")
}
// MARK: - Specification Tests: Successful Planning
@@ -277,8 +284,8 @@ struct ScenarioDPlannerTests {
let startDate = TestClock.now
let endDate = startDate.addingTimeInterval(86400 * 10)
let homeCoord = CLLocationCoordinate2D(latitude: 39.7392, longitude: -104.9903) // Denver
let homeLocation = LocationInput(name: "Denver", coordinate: homeCoord)
let homeCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855) // NYC
let homeLocation = LocationInput(name: "New York", coordinate: homeCoord)
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
let game = Game(
@@ -322,8 +329,8 @@ struct ScenarioDPlannerTests {
#expect(!options.isEmpty)
for option in options {
#expect(option.stops.first?.city == "Denver")
#expect(option.stops.last?.city == "Denver")
#expect(option.stops.first?.city == "New York")
#expect(option.stops.last?.city == "New York")
#expect(option.stops.first?.games.isEmpty == true)
#expect(option.stops.last?.games.isEmpty == true)
}
@@ -396,29 +403,70 @@ struct ScenarioDPlannerTests {
let result = planner.plan(request: request)
if case .success(let options) = result {
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
#expect(!allGameIds.contains("other"), "Games not involving the followed team should be excluded")
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
let allGameIds = options.flatMap { $0.stops.flatMap { $0.games } }
#expect(!allGameIds.contains("other"), "Games not involving the followed team should be excluded")
// Full invariant: ALL returned games must involve the followed team
let allGames = [homeGame, awayGame, otherGame]
for gameId in allGameIds {
let game = allGames.first { $0.id == gameId }
#expect(game != nil, "Game ID \(gameId) should be in the available games list")
if let game = game {
#expect(game.homeTeamId == teamId || game.awayTeamId == teamId,
"Game \(gameId) should involve followed team \(teamId)")
}
}
}
@Test("Invariant: duplicate routes are removed")
func invariant_duplicateRoutesRemoved() {
let startDate = TestClock.now
let endDate = startDate.addingTimeInterval(86400 * 7)
let endDate = startDate.addingTimeInterval(86400 * 14)
let stadium = makeStadium(id: "stadium1", city: "New York", coordinate: nycCoord)
let game = Game(
id: "game1",
// 3 games for the followed team at nearby cities the DAG router may
// produce multiple routes (e.g. [NYC, BOS], [NYC, PHI], [NYC, BOS, PHI])
// which makes the uniqueness check meaningful.
let nycStadium = makeStadium(id: "nyc", city: "New York", coordinate: nycCoord)
let bostonStadium = makeStadium(id: "boston", city: "Boston", coordinate: bostonCoord)
let phillyStadium = makeStadium(id: "philly", city: "Philadelphia", coordinate: phillyCoord)
let game1 = Game(
id: "game-nyc",
homeTeamId: "yankees",
awayTeamId: "opponent",
stadiumId: "stadium1",
stadiumId: "nyc",
dateTime: startDate.addingTimeInterval(86400 * 2),
sport: .mlb,
season: "2026",
isPlayoff: false
)
let game2 = Game(
id: "game-bos",
homeTeamId: "red-sox",
awayTeamId: "yankees",
stadiumId: "boston",
dateTime: startDate.addingTimeInterval(86400 * 5),
sport: .mlb,
season: "2026",
isPlayoff: false
)
let game3 = Game(
id: "game-phi",
homeTeamId: "phillies",
awayTeamId: "yankees",
stadiumId: "philly",
dateTime: startDate.addingTimeInterval(86400 * 8),
sport: .mlb,
season: "2026",
isPlayoff: false
)
let prefs = TripPreferences(
planningMode: .followTeam,
sports: [.mlb],
@@ -426,27 +474,29 @@ struct ScenarioDPlannerTests {
endDate: endDate,
leisureLevel: .moderate,
lodgingType: .hotel,
numberOfDrivers: 1,
numberOfDrivers: 2,
followTeamId: "yankees"
)
let request = PlanningRequest(
preferences: prefs,
availableGames: [game],
availableGames: [game1, game2, game3],
teams: [:],
stadiums: ["stadium1": stadium]
stadiums: ["nyc": nycStadium, "boston": bostonStadium, "philly": phillyStadium]
)
let result = planner.plan(request: request)
if case .success(let options) = result {
// Verify no duplicate game combinations
var seenGameCombinations = Set<String>()
for option in options {
let gameIds = option.stops.flatMap { $0.games }.sorted().joined(separator: "-")
#expect(!seenGameCombinations.contains(gameIds), "Duplicate route found: \(gameIds)")
seenGameCombinations.insert(gameIds)
}
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
// Verify no two options have identical game-ID sets
var seenGameCombinations = Set<String>()
for option in options {
let gameIds = option.stops.flatMap { $0.games }.sorted().joined(separator: "-")
#expect(!seenGameCombinations.contains(gameIds), "Duplicate route found: \(gameIds)")
seenGameCombinations.insert(gameIds)
}
}
@@ -489,11 +539,13 @@ struct ScenarioDPlannerTests {
let result = planner.plan(request: request)
if case .success(let options) = result {
#expect(!options.isEmpty, "Success must have at least one option")
for option in options {
#expect(!option.stops.isEmpty, "Each option must have stops")
}
guard case .success(let options) = result else {
Issue.record("Expected .success, got \(result)")
return
}
#expect(!options.isEmpty, "Success must have at least one option")
for option in options {
#expect(!option.stops.isEmpty, "Each option must have stops")
}
}