Files
rzen a755cce3e2 Add App Store release pipeline, adopt ISC license, add unit test target
- 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.
2026-07-05 10:27:36 -04:00

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)
}
}