Systematic audit of 1,191 tests found tests written to pass rather than verify correctness. Key fixes: Infrastructure: - TestClock: fixed timezone from .current to America/New_York (deterministic) - TestFixtures: added 1.3x road routing factor to match production - ItineraryTestHelpers: real per-city coordinates instead of hardcoded (40,-80) Planning tests: - Added missing Scenario E factory dispatch tests - Tightened 12 loose assertions (>= 1 → == 8.0, > 0 → range checks) - Fixed 4 no-op tests that accepted both success and failure - Fixed wrong repeat-city invariant (was checking same-day, not different-day) - Fixed tautological assertion in missing-stadium edge case Services/Domain/Export tests: - Replaced 4 placeholder tests (#expect(true)) with real assertions - Fixed tautological assertions in POISearchServiceTests - Fixed Chicago coordinate in RegionMapSelectorTests (-89 → -87.6553) - Added sort order verification to ItineraryRowFlatteningTests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
6.6 KiB
Swift
187 lines
6.6 KiB
Swift
//
|
|
// ScoreResolutionCacheTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification tests for ScoreResolutionCache types.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import SportsTime
|
|
|
|
// MARK: - CacheStats Tests
|
|
|
|
@Suite("CacheStats")
|
|
struct CacheStatsTests {
|
|
|
|
// MARK: - Test Data
|
|
|
|
private func makeStats(
|
|
totalEntries: Int = 100,
|
|
entriesWithScores: Int = 80,
|
|
entriesWithoutScores: Int = 20,
|
|
expiredEntries: Int = 5,
|
|
entriesBySport: [Sport: Int] = [.mlb: 50, .nba: 30, .nhl: 20]
|
|
) -> CacheStats {
|
|
CacheStats(
|
|
totalEntries: totalEntries,
|
|
entriesWithScores: entriesWithScores,
|
|
entriesWithoutScores: entriesWithoutScores,
|
|
expiredEntries: expiredEntries,
|
|
entriesBySport: entriesBySport
|
|
)
|
|
}
|
|
|
|
// MARK: - Specification Tests: Properties
|
|
|
|
@Test("CacheStats: stores totalEntries")
|
|
func cacheStats_totalEntries() {
|
|
let stats = makeStats(totalEntries: 150)
|
|
#expect(stats.totalEntries == 150)
|
|
}
|
|
|
|
@Test("CacheStats: stores entriesWithScores")
|
|
func cacheStats_entriesWithScores() {
|
|
let stats = makeStats(entriesWithScores: 75)
|
|
#expect(stats.entriesWithScores == 75)
|
|
}
|
|
|
|
@Test("CacheStats: stores entriesWithoutScores")
|
|
func cacheStats_entriesWithoutScores() {
|
|
let stats = makeStats(entriesWithoutScores: 25)
|
|
#expect(stats.entriesWithoutScores == 25)
|
|
}
|
|
|
|
@Test("CacheStats: stores expiredEntries")
|
|
func cacheStats_expiredEntries() {
|
|
let stats = makeStats(expiredEntries: 10)
|
|
#expect(stats.expiredEntries == 10)
|
|
}
|
|
|
|
@Test("CacheStats: stores entriesBySport")
|
|
func cacheStats_entriesBySport() {
|
|
let bySport: [Sport: Int] = [.mlb: 40, .nba: 60]
|
|
let stats = makeStats(entriesBySport: bySport)
|
|
#expect(stats.entriesBySport[.mlb] == 40)
|
|
#expect(stats.entriesBySport[.nba] == 60)
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
@Test("CacheStats: handles empty cache")
|
|
func cacheStats_emptyCache() {
|
|
let stats = makeStats(
|
|
totalEntries: 0,
|
|
entriesWithScores: 0,
|
|
entriesWithoutScores: 0,
|
|
expiredEntries: 0,
|
|
entriesBySport: [:]
|
|
)
|
|
#expect(stats.totalEntries == 0)
|
|
#expect(stats.entriesBySport.isEmpty)
|
|
}
|
|
|
|
@Test("CacheStats: handles all expired")
|
|
func cacheStats_allExpired() {
|
|
let stats = makeStats(totalEntries: 100, expiredEntries: 100)
|
|
#expect(stats.expiredEntries == stats.totalEntries)
|
|
}
|
|
|
|
@Test("CacheStats: handles all with scores")
|
|
func cacheStats_allWithScores() {
|
|
let stats = makeStats(totalEntries: 100, entriesWithScores: 100, entriesWithoutScores: 0)
|
|
#expect(stats.entriesWithScores == stats.totalEntries)
|
|
#expect(stats.entriesWithoutScores == 0)
|
|
}
|
|
|
|
@Test("CacheStats: handles single sport")
|
|
func cacheStats_singleSport() {
|
|
let stats = makeStats(entriesBySport: [.mlb: 100])
|
|
#expect(stats.entriesBySport.count == 1)
|
|
#expect(stats.entriesBySport[.mlb] == 100)
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: entriesWithScores + entriesWithoutScores == totalEntries
|
|
@Test("Invariant: scores split equals total")
|
|
func invariant_scoresSplitEqualsTotal() {
|
|
let stats = makeStats(totalEntries: 100, entriesWithScores: 80, entriesWithoutScores: 20)
|
|
#expect(stats.entriesWithScores + stats.entriesWithoutScores == stats.totalEntries)
|
|
}
|
|
|
|
/// - Invariant: expiredEntries <= totalEntries
|
|
@Test("Invariant: expired entries cannot exceed total")
|
|
func invariant_expiredCannotExceedTotal() {
|
|
let stats = makeStats(totalEntries: 100, expiredEntries: 50)
|
|
#expect(stats.expiredEntries <= stats.totalEntries)
|
|
}
|
|
|
|
/// - Invariant: sum of entriesBySport <= totalEntries
|
|
// NOTE: CacheStats is a plain data struct — this test documents the expected
|
|
// relationship between sport entries and total, not enforcement by the cache.
|
|
// The struct does not validate or clamp values; callers are responsible for
|
|
// providing consistent data.
|
|
@Test("Invariant: sport entries sum does not exceed total")
|
|
func invariant_sportEntriesSumDoesNotExceedTotal() {
|
|
let bySport: [Sport: Int] = [.mlb: 30, .nba: 40, .nhl: 30]
|
|
let stats = makeStats(totalEntries: 100, entriesBySport: bySport)
|
|
let sportSum = stats.entriesBySport.values.reduce(0, +)
|
|
#expect(sportSum <= stats.totalEntries)
|
|
}
|
|
}
|
|
|
|
// MARK: - Cache Expiration Behavior Tests
|
|
|
|
@Suite("Cache Expiration Behavior")
|
|
struct CacheExpirationBehaviorTests {
|
|
|
|
// These tests document the expected cache expiration behavior
|
|
// based on ScoreResolutionCache.calculateExpiration
|
|
|
|
// MARK: - Specification Tests: Cache Durations
|
|
|
|
/// - Expected Behavior: Recent games (< 30 days old) expire after 24 hours
|
|
@Test("Expiration: recent games expire after 24 hours")
|
|
func expiration_recentGames() {
|
|
// Games less than 30 days old should expire after 24 hours
|
|
let recentGameCacheDuration: TimeInterval = 24 * 60 * 60
|
|
#expect(recentGameCacheDuration == 86400) // 24 hours in seconds
|
|
}
|
|
|
|
/// - Expected Behavior: Historical games (> 30 days old) never expire (nil)
|
|
@Test("Expiration: historical games never expire")
|
|
func expiration_historicalGames() {
|
|
// Games older than 30 days should have nil expiration (never expire)
|
|
let historicalAgeThreshold: TimeInterval = 30 * 24 * 60 * 60
|
|
#expect(historicalAgeThreshold == 2592000) // 30 days in seconds
|
|
}
|
|
|
|
/// - Expected Behavior: Failed lookups expire after 7 days
|
|
@Test("Expiration: failed lookups expire after 7 days")
|
|
func expiration_failedLookups() {
|
|
let failedLookupCacheDuration: TimeInterval = 7 * 24 * 60 * 60
|
|
#expect(failedLookupCacheDuration == 604800) // 7 days in seconds
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: Historical threshold > recent cache duration
|
|
@Test("Invariant: historical threshold exceeds recent cache duration")
|
|
func invariant_historicalExceedsRecent() {
|
|
let recentCacheDuration: TimeInterval = 24 * 60 * 60
|
|
let historicalThreshold: TimeInterval = 30 * 24 * 60 * 60
|
|
|
|
#expect(historicalThreshold > recentCacheDuration)
|
|
}
|
|
|
|
/// - Invariant: Failed lookup duration > recent cache duration
|
|
@Test("Invariant: failed lookup duration exceeds recent cache duration")
|
|
func invariant_failedLookupExceedsRecent() {
|
|
let recentCacheDuration: TimeInterval = 24 * 60 * 60
|
|
let failedLookupDuration: TimeInterval = 7 * 24 * 60 * 60
|
|
|
|
#expect(failedLookupDuration > recentCacheDuration)
|
|
}
|
|
}
|