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