- Scripts/release.sh archives and uploads the macOS app via the appstore-publish skill's API-driven pipeline (xcodebuild + App Store Connect key, no fastlane); ExportOptions-macOS.plist adapts the skill's iOS-centric export options for generic/platform=macOS. Scripts/metadata/ drafts the listing from the README. Canonical asc-*.swift scripts are referenced in place from indie-skills, matching Weight/HIIT. - Replace the all-rights-reserved LICENSE.md with the portfolio-standard ISC license (REVIEW2 D2b), preserving the existing copyright year/holder. - Add an IceGlassTests target (project.yml) with a real smoke test suite covering GameState's progression-rank ordering and polling-interval mapping — logic MainService relies on to reject stale API regressions.
66 lines
2.8 KiB
Swift
66 lines
2.8 KiB
Swift
//
|
|
// GameStateTests.swift
|
|
// IceGlassTests
|
|
//
|
|
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import Testing
|
|
@testable import IceGlass
|
|
|
|
struct GameStateTests {
|
|
@Test func decodesFromNHLAPIRawValues() {
|
|
#expect(GameState(rawValue: "FUT") == .future)
|
|
#expect(GameState(rawValue: "PRE") == .pre)
|
|
#expect(GameState(rawValue: "LIVE") == .live)
|
|
#expect(GameState(rawValue: "CRIT") == .crit)
|
|
#expect(GameState(rawValue: "OVER") == .over)
|
|
#expect(GameState(rawValue: "FINAL") == .final_)
|
|
#expect(GameState(rawValue: "OFF") == .official)
|
|
#expect(GameState(rawValue: "BOGUS") == nil)
|
|
}
|
|
|
|
@Test func isLiveIsOverIsFutureAreMutuallyConsistent() {
|
|
let allCases: [GameState] = [.future, .pre, .live, .crit, .over, .final_, .official]
|
|
for state in allCases {
|
|
// Each state falls into exactly one of the three buckets used to
|
|
// drive menu/notification behavior.
|
|
let bucketCount = [state.isFuture, state.isLive, state.isOver].filter { $0 }.count
|
|
#expect(bucketCount == 1, "\(state) should be in exactly one of future/live/over")
|
|
}
|
|
#expect(GameState.future.isFuture)
|
|
#expect(GameState.pre.isFuture)
|
|
#expect(GameState.live.isLive)
|
|
#expect(GameState.crit.isLive)
|
|
#expect(GameState.over.isOver)
|
|
#expect(GameState.final_.isOver)
|
|
#expect(GameState.official.isOver)
|
|
}
|
|
|
|
@Test func progressionRankIsMonotonicAlongTheAPILifecycle() {
|
|
// FUT → PRE → LIVE → CRIT → OVER → FINAL → OFF must never regress;
|
|
// MainService relies on this ordering to reject stale API responses
|
|
// that would otherwise flip a finished game back to "-:-".
|
|
let ordered: [GameState] = [.future, .pre, .live, .crit, .over, .final_, .official]
|
|
let ranks = ordered.map(\.progressionRank)
|
|
#expect(ranks == ranks.sorted())
|
|
#expect(Set(ranks).count == ranks.count, "ranks must be distinct")
|
|
}
|
|
|
|
@Test func progressionRankOfRawStringMatchesEnumRankAndHandlesUnknown() {
|
|
#expect(GameState.progressionRank(of: "LIVE") == GameState.live.progressionRank)
|
|
#expect(GameState.progressionRank(of: "OFF") == GameState.official.progressionRank)
|
|
#expect(GameState.progressionRank(of: "NOT_A_STATE") == -1)
|
|
}
|
|
|
|
@Test func pollingIntervalMapsEachStateToTheExpectedCadence() {
|
|
#expect(GameState.future.pollingInterval == .gameDay)
|
|
#expect(GameState.pre.pollingInterval == .preGame)
|
|
#expect(GameState.live.pollingInterval == .liveGame)
|
|
#expect(GameState.crit.pollingInterval == .liveGame)
|
|
#expect(GameState.over.pollingInterval == .everyMinute)
|
|
#expect(GameState.final_.pollingInterval == .everyMinute)
|
|
#expect(GameState.official.pollingInterval == .idle)
|
|
}
|
|
}
|