4e5fab654d
seriesStatus updates with the just-played game's win, so once a card goes FINAL the is_clincher / is_game7 / is_pivotal predicates point at the upcoming game. Gate the stake badge, stake blurb, and elimination_count tally on a non-FINAL gameState so a completed Game 3 that left the series 3-0 reads "Flyers lead 3-0" instead of "Flyers can close it out — Game 3." Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
332 lines
12 KiB
Python
332 lines
12 KiB
Python
import pytest
|
||
|
||
from app.playoff import (
|
||
is_pinned,
|
||
is_playoff_game,
|
||
is_playoff_ot,
|
||
ot_label,
|
||
series_badges,
|
||
series_blurb,
|
||
series_state,
|
||
series_summary,
|
||
today_meta,
|
||
)
|
||
from tests.conftest import make_game, make_playoff_game
|
||
|
||
|
||
class TestSeriesState:
|
||
def test_empty_returns_defaults(self):
|
||
state = series_state({})
|
||
assert state["is_opener"] is True
|
||
assert state["game_number"] == 1
|
||
assert state["round"] == 1
|
||
assert state["leader"] is None
|
||
|
||
@pytest.mark.parametrize(
|
||
"top,bot,expected_game",
|
||
[(0, 0, 1), (1, 0, 2), (1, 1, 3), (2, 1, 4), (2, 2, 5), (3, 2, 6), (3, 3, 7)],
|
||
)
|
||
def test_game_number_computation(self, top, bot, expected_game):
|
||
state = series_state({"round": 1, "topSeedWins": top, "bottomSeedWins": bot})
|
||
assert state["game_number"] == expected_game
|
||
|
||
def test_game7_predicate(self):
|
||
state = series_state({"round": 1, "topSeedWins": 3, "bottomSeedWins": 3})
|
||
assert state["is_game7"] is True
|
||
assert state["is_clincher"] is False
|
||
assert state["is_pivotal"] is False
|
||
|
||
def test_clincher_predicate(self):
|
||
state = series_state({"round": 1, "topSeedWins": 3, "bottomSeedWins": 1})
|
||
assert state["is_clincher"] is True
|
||
assert state["is_elimination"] is True
|
||
assert state["is_game7"] is False
|
||
|
||
def test_pivotal_predicate(self):
|
||
state = series_state({"round": 2, "topSeedWins": 2, "bottomSeedWins": 2})
|
||
assert state["is_pivotal"] is True
|
||
assert state["is_game7"] is False
|
||
assert state["is_clincher"] is False
|
||
|
||
def test_opener_predicate(self):
|
||
state = series_state({"round": 1, "topSeedWins": 0, "bottomSeedWins": 0})
|
||
assert state["is_opener"] is True
|
||
|
||
def test_leader_top(self):
|
||
state = series_state({"round": 1, "topSeedWins": 2, "bottomSeedWins": 1})
|
||
assert state["leader"] == "top"
|
||
assert state["hi"] == 2 and state["lo"] == 1
|
||
|
||
def test_leader_bottom(self):
|
||
state = series_state({"round": 1, "topSeedWins": 1, "bottomSeedWins": 2})
|
||
assert state["leader"] == "bottom"
|
||
|
||
def test_no_leader_when_tied(self):
|
||
state = series_state({"round": 1, "topSeedWins": 1, "bottomSeedWins": 1})
|
||
assert state["leader"] is None
|
||
|
||
|
||
class TestSeriesBlurb:
|
||
def test_opener_blurb(self):
|
||
game = make_playoff_game(top_wins=0, bottom_wins=0)
|
||
assert series_blurb(game) == "Series opener"
|
||
|
||
def test_game7_blurb(self):
|
||
game = make_playoff_game(top_wins=3, bottom_wins=3)
|
||
assert series_blurb(game) == "Win-or-go-home \u2014 Game 7."
|
||
|
||
def test_clincher_blurb_names_leader(self):
|
||
game = make_playoff_game(
|
||
top_wins=3,
|
||
bottom_wins=1,
|
||
top_abbrev="TOR",
|
||
bottom_abbrev="BOS",
|
||
top_is_home=True,
|
||
)
|
||
blurb = series_blurb(game)
|
||
assert "Top Seeds" in blurb
|
||
assert "close it out" in blurb
|
||
assert "Game 5" in blurb
|
||
|
||
def test_pivotal_blurb(self):
|
||
game = make_playoff_game(top_wins=2, bottom_wins=2)
|
||
assert "pivotal" in series_blurb(game).lower()
|
||
assert "Game 5" in series_blurb(game)
|
||
|
||
def test_leader_trailer_blurb(self):
|
||
game = make_playoff_game(top_wins=2, bottom_wins=1)
|
||
blurb = series_blurb(game)
|
||
assert "lead" in blurb
|
||
assert "2\u20111" in blurb
|
||
|
||
def test_tied_mid_series_blurb(self):
|
||
game = make_playoff_game(top_wins=1, bottom_wins=1)
|
||
blurb = series_blurb(game)
|
||
assert "1" in blurb
|
||
assert "Game 3" in blurb
|
||
|
||
def test_final_clincher_falls_through_to_leader_blurb(self):
|
||
# Post-game seriesStatus (3-0) would trigger the clincher branch, but
|
||
# the FINAL card is already decided — that stake belongs to Game 4.
|
||
game = make_playoff_game(
|
||
top_wins=3,
|
||
bottom_wins=0,
|
||
top_abbrev="PHI",
|
||
bottom_abbrev="PIT",
|
||
top_is_home=True,
|
||
game_state="OFF",
|
||
)
|
||
blurb = series_blurb(game)
|
||
assert "close it out" not in blurb
|
||
assert "lead" in blurb
|
||
assert "3‑0" in blurb
|
||
|
||
|
||
class TestSeriesBadges:
|
||
def test_round_1_always_first(self):
|
||
game = make_playoff_game(round_num=1, top_wins=1, bottom_wins=0)
|
||
assert series_badges(game)[0] == "R1"
|
||
|
||
def test_cup_final_label(self):
|
||
game = make_playoff_game(round_num=4, top_wins=0, bottom_wins=0)
|
||
assert series_badges(game)[0] == "CUP FINAL"
|
||
|
||
def test_conf_final_label(self):
|
||
game = make_playoff_game(round_num=3, top_wins=0, bottom_wins=0)
|
||
assert series_badges(game)[0] == "CONF FINAL"
|
||
|
||
def test_game7_badge(self):
|
||
game = make_playoff_game(top_wins=3, bottom_wins=3)
|
||
assert "GAME 7" in series_badges(game)
|
||
|
||
def test_clincher_badge(self):
|
||
game = make_playoff_game(top_wins=3, bottom_wins=1)
|
||
assert "CLINCHER" in series_badges(game)
|
||
|
||
def test_pivotal_badge(self):
|
||
game = make_playoff_game(top_wins=2, bottom_wins=2)
|
||
assert "PIVOTAL" in series_badges(game)
|
||
|
||
def test_opener_has_no_stake_badge(self):
|
||
badges = series_badges(make_playoff_game(top_wins=0, bottom_wins=0))
|
||
assert badges == ["R1"]
|
||
|
||
def test_no_stake_badge_on_final(self):
|
||
# Post-game seriesStatus shows is_clincher true, but CLINCHER refers to
|
||
# the upcoming Game 4, not the completed card.
|
||
game = make_playoff_game(top_wins=3, bottom_wins=0, game_state="OFF")
|
||
assert series_badges(game) == ["R1"]
|
||
|
||
|
||
class TestSeriesSummary:
|
||
def test_opener_summary(self):
|
||
game = make_playoff_game(top_wins=0, bottom_wins=0, round_num=1)
|
||
assert series_summary(game) == "Game 1 of 7"
|
||
|
||
def test_leader_summary(self):
|
||
game = make_playoff_game(top_wins=2, bottom_wins=1)
|
||
assert series_summary(game) == "Game 4 of 7"
|
||
|
||
def test_tied_mid_series_summary(self):
|
||
game = make_playoff_game(top_wins=1, bottom_wins=1)
|
||
assert series_summary(game) == "Game 3 of 7"
|
||
|
||
def test_finished_game_uses_pre_advance_number(self):
|
||
# Scoreboard payloads don't carry gameNumber. Once a game goes FINAL,
|
||
# seriesStatus already includes this game's result, so the card's game
|
||
# number is hi+lo, not hi+lo+1.
|
||
game = make_playoff_game(top_wins=1, bottom_wins=0, game_state="FINAL")
|
||
assert series_summary(game) == "Game 1 of 7"
|
||
|
||
def test_finished_game_honors_explicit_game_number(self):
|
||
game = make_playoff_game(
|
||
top_wins=2, bottom_wins=0, game_state="FINAL", game_number=2
|
||
)
|
||
assert series_summary(game) == "Game 2 of 7"
|
||
|
||
|
||
class TestIsPinned:
|
||
def test_game7_live_is_pinned(self):
|
||
game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="LIVE")
|
||
assert is_pinned(game) is True
|
||
|
||
def test_game7_pre_is_pinned(self):
|
||
game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="FUT")
|
||
assert is_pinned(game) is True
|
||
|
||
def test_game7_final_not_pinned(self):
|
||
game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="OFF")
|
||
assert is_pinned(game) is False
|
||
|
||
def test_non_game7_not_pinned(self):
|
||
game = make_playoff_game(top_wins=2, bottom_wins=1)
|
||
assert is_pinned(game) is False
|
||
|
||
def test_regular_season_not_pinned(self):
|
||
game = make_game() # game_type=2, no series
|
||
assert is_pinned(game) is False
|
||
|
||
|
||
class TestIsPlayoffOt:
|
||
def test_playoff_period_4_live(self):
|
||
game = make_playoff_game(period=4, game_state="LIVE")
|
||
assert is_playoff_ot(game) is True
|
||
|
||
def test_playoff_period_5_live(self):
|
||
game = make_playoff_game(period=5, game_state="LIVE")
|
||
assert is_playoff_ot(game) is True
|
||
|
||
def test_playoff_period_3_not_ot(self):
|
||
game = make_playoff_game(period=3, game_state="LIVE")
|
||
assert is_playoff_ot(game) is False
|
||
|
||
def test_regular_season_ot_not_playoff_ot(self):
|
||
game = make_game(period=4, game_state="LIVE", game_type=2)
|
||
assert is_playoff_ot(game) is False
|
||
|
||
def test_crit_state_counts_as_live(self):
|
||
game = make_playoff_game(period=4, game_state="CRIT")
|
||
assert is_playoff_ot(game) is True
|
||
|
||
def test_final_state_not_playoff_ot(self):
|
||
game = make_playoff_game(period=4, game_state="OFF")
|
||
assert is_playoff_ot(game) is False
|
||
|
||
|
||
class TestOtLabel:
|
||
def test_period_4_is_ot(self):
|
||
assert ot_label(4) == "OT"
|
||
|
||
def test_period_5_is_2ot(self):
|
||
assert ot_label(5) == "2OT"
|
||
|
||
def test_period_6_is_3ot(self):
|
||
assert ot_label(6) == "3OT"
|
||
|
||
def test_pre_ot_returns_empty(self):
|
||
assert ot_label(3) == ""
|
||
assert ot_label(0) == ""
|
||
|
||
|
||
class TestIsPlayoffGame:
|
||
def test_playoff_raw_shape(self):
|
||
assert is_playoff_game(make_playoff_game()) is True
|
||
|
||
def test_regular_raw_shape(self):
|
||
assert is_playoff_game(make_game(game_type=2)) is False
|
||
|
||
def test_parsed_shape(self):
|
||
# parse_games produces {"Game Type": 3} — is_playoff_game should handle both
|
||
assert is_playoff_game({"Game Type": 3}) is True
|
||
assert is_playoff_game({"Game Type": 2}) is False
|
||
|
||
|
||
class TestTodayMeta:
|
||
def test_no_playoff_games_off_mode(self):
|
||
meta = today_meta([make_game(game_type=2)])
|
||
assert meta["playoff_mode"] is False
|
||
assert meta["round_label"] is None
|
||
|
||
def test_playoff_games_on_mode(self):
|
||
games = [
|
||
make_playoff_game(series_letter="A"),
|
||
make_playoff_game(series_letter="B"),
|
||
]
|
||
meta = today_meta(games)
|
||
assert meta["playoff_mode"] is True
|
||
assert meta["series_active"] == 2
|
||
assert meta["round_label"] == "First Round"
|
||
|
||
def test_counts_game7(self):
|
||
games = [
|
||
make_playoff_game(top_wins=3, bottom_wins=3, series_letter="A"),
|
||
make_playoff_game(top_wins=1, bottom_wins=0, series_letter="B"),
|
||
]
|
||
meta = today_meta(games)
|
||
assert meta["game7_count"] == 1
|
||
assert meta["elimination_count"] == 0
|
||
|
||
def test_counts_elimination_games(self):
|
||
games = [
|
||
make_playoff_game(top_wins=3, bottom_wins=1, series_letter="A"),
|
||
make_playoff_game(top_wins=3, bottom_wins=2, series_letter="B"),
|
||
]
|
||
meta = today_meta(games)
|
||
assert meta["elimination_count"] == 2
|
||
assert meta["game7_count"] == 0
|
||
|
||
def test_round_label_reflects_highest_active_round(self):
|
||
games = [
|
||
make_playoff_game(round_num=1, series_letter="A"),
|
||
make_playoff_game(round_num=2, series_letter="I"),
|
||
]
|
||
meta = today_meta(games)
|
||
assert meta["round_label"] == "Second Round"
|
||
|
||
def test_cup_final_label(self):
|
||
games = [make_playoff_game(round_num=4, series_letter="P")]
|
||
meta = today_meta(games)
|
||
assert meta["round_label"] == "Stanley Cup Final"
|
||
|
||
def test_does_not_count_final_games_as_elimination(self):
|
||
games = [
|
||
make_playoff_game(
|
||
top_wins=3, bottom_wins=0, series_letter="A", game_state="OFF"
|
||
),
|
||
make_playoff_game(
|
||
top_wins=3, bottom_wins=1, series_letter="B", game_state="LIVE"
|
||
),
|
||
]
|
||
meta = today_meta(games)
|
||
# Only the LIVE card counts; the FINAL card describes a completed game.
|
||
assert meta["elimination_count"] == 1
|
||
|
||
def test_does_not_count_final_game7(self):
|
||
games = [
|
||
make_playoff_game(
|
||
top_wins=3, bottom_wins=3, series_letter="A", game_state="OFF"
|
||
)
|
||
]
|
||
meta = today_meta(games)
|
||
assert meta["game7_count"] == 0
|