fix: anchor Day N to each round's first game instead of lazy first sighting
CI / Lint (push) Successful in 5s
CI / Test (push) Successful in 9s
CI / Build & Push (push) Successful in 19s

The banner read "Day 1 of ~60" on day 2 of the playoffs because the old
anchor recorded whatever date we first polled a playoff game as Day 1.
Now round start dates come from /v1/schedule/playoff-series, so Day N
is authoritative and resets at each round boundary. Drops the noisy
"of ~60" denominator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 13:03:08 -04:00
parent 930247b32f
commit a88e2edef0
7 changed files with 220 additions and 94 deletions
+97 -31
View File
@@ -144,46 +144,112 @@ class TestFetchSeries:
assert playoff_cache.fetch_series("2026-A") is None
class TestRecordStartDate:
def test_no_playoff_games_no_write(self, tmp_db):
result = playoff_cache.record_start_date_if_missing([{"gameType": 2}])
assert result is None
assert playoff_cache.get_playoff_start_date() is None
class TestRefreshRoundStartDates:
def test_no_bracket_returns_none(self, tmp_db):
assert playoff_cache.refresh_round_start_dates(2026) is None
def test_records_on_first_playoff_sighting(self, tmp_db):
now = datetime(2026, 4, 18, 20, 0, tzinfo=EASTERN)
result = playoff_cache.record_start_date_if_missing([{"gameType": 3}], now=now)
assert result == "2026-04-18"
assert playoff_cache.get_playoff_start_date().isoformat() == "2026-04-18"
def test_idempotent_after_first_write(self, tmp_db):
first_now = datetime(2026, 4, 18, 20, 0, tzinfo=EASTERN)
second_now = datetime(2026, 4, 25, 20, 0, tzinfo=EASTERN)
playoff_cache.record_start_date_if_missing([{"gameType": 3}], now=first_now)
# Second call should not overwrite
result = playoff_cache.record_start_date_if_missing(
[{"gameType": 3}], now=second_now
def test_anchors_round_to_earliest_game(self, tmp_db, monkeypatch):
playoff_cache._put(
playoff_cache.bracket_key(2026),
{"series": [{"seriesLetter": "A", "playoffRound": 1}]},
)
assert result == "2026-04-18"
series_payload = {
"games": [
{"startTimeUTC": "2026-04-19T23:00:00Z"},
{"startTimeUTC": "2026-04-18T23:00:00Z"},
{"startTimeUTC": "2026-04-21T23:00:00Z"},
]
}
monkeypatch.setattr(
"app.playoff_cache.requests.get",
lambda *a, **kw: _Resp(series_payload),
)
merged = playoff_cache.refresh_round_start_dates(2026)
assert merged == {"1": "2026-04-18"}
assert playoff_cache.get_round_start_date(1).isoformat() == "2026-04-18"
def test_merges_multiple_rounds_min_per_round(self, tmp_db, monkeypatch):
playoff_cache._put(
playoff_cache.bracket_key(2026),
{
"series": [
{"seriesLetter": "A", "playoffRound": 1},
{"seriesLetter": "B", "playoffRound": 1},
{"seriesLetter": "I", "playoffRound": 2},
]
},
)
payloads = {
"A": {"games": [{"startTimeUTC": "2026-04-19T23:00:00Z"}]},
"B": {"games": [{"startTimeUTC": "2026-04-18T23:00:00Z"}]},
"I": {"games": [{"startTimeUTC": "2026-04-29T23:00:00Z"}]},
}
def fake_get(url, *a, **kw):
letter = url.rstrip("/").rsplit("/", 1)[-1].upper()
return _Resp(payloads[letter])
monkeypatch.setattr("app.playoff_cache.requests.get", fake_get)
merged = playoff_cache.refresh_round_start_dates(2026)
assert merged == {"1": "2026-04-18", "2": "2026-04-29"}
def test_preserves_existing_rounds_on_merge(self, tmp_db, monkeypatch):
playoff_cache._put(
playoff_cache.ROUND_DATES_KEY, {"1": "2026-04-18", "2": "2026-04-29"}
)
playoff_cache._put(
playoff_cache.bracket_key(2026),
{"series": [{"seriesLetter": "M", "playoffRound": 3}]},
)
monkeypatch.setattr(
"app.playoff_cache.requests.get",
lambda *a, **kw: _Resp(
{"games": [{"startTimeUTC": "2026-05-15T23:00:00Z"}]}
),
)
merged = playoff_cache.refresh_round_start_dates(2026)
assert merged["1"] == "2026-04-18"
assert merged["2"] == "2026-04-29"
assert merged["3"] == "2026-05-15"
class TestDayN:
def test_no_start_date(self, tmp_db):
assert playoff_cache.day_n() == (None, None)
class TestDayNForRound:
def test_no_round_num(self, tmp_db):
assert playoff_cache.day_n_for_round(None) is None
def test_round_not_anchored(self, tmp_db):
assert playoff_cache.day_n_for_round(1) is None
def test_day_one(self, tmp_db):
playoff_cache._put(playoff_cache.ROUND_DATES_KEY, {"1": "2026-04-18"})
now = datetime(2026, 4, 18, 20, 0, tzinfo=EASTERN)
playoff_cache.record_start_date_if_missing([{"gameType": 3}], now=now)
n, total = playoff_cache.day_n(now=now)
assert n == 1
assert total == 60
assert playoff_cache.day_n_for_round(1, now=now) == 1
def test_day_two(self, tmp_db):
playoff_cache._put(playoff_cache.ROUND_DATES_KEY, {"1": "2026-04-18"})
now = datetime(2026, 4, 19, 10, 0, tzinfo=EASTERN)
assert playoff_cache.day_n_for_round(1, now=now) == 2
def test_day_five(self, tmp_db):
start = datetime(2026, 4, 18, 20, 0, tzinfo=EASTERN)
later = datetime(2026, 4, 22, 20, 0, tzinfo=EASTERN)
playoff_cache.record_start_date_if_missing([{"gameType": 3}], now=start)
n, _ = playoff_cache.day_n(now=later)
assert n == 5
playoff_cache._put(playoff_cache.ROUND_DATES_KEY, {"1": "2026-04-18"})
now = datetime(2026, 4, 22, 20, 0, tzinfo=EASTERN)
assert playoff_cache.day_n_for_round(1, now=now) == 5
def test_round_two_resets_to_day_one(self, tmp_db):
playoff_cache._put(
playoff_cache.ROUND_DATES_KEY,
{"1": "2026-04-18", "2": "2026-04-29"},
)
now = datetime(2026, 4, 29, 20, 0, tzinfo=EASTERN)
assert playoff_cache.day_n_for_round(2, now=now) == 1
def test_before_start_returns_none(self, tmp_db):
playoff_cache._put(playoff_cache.ROUND_DATES_KEY, {"2": "2026-04-29"})
now = datetime(2026, 4, 20, 20, 0, tzinfo=EASTERN)
assert playoff_cache.day_n_for_round(2, now=now) is None
class TestSchema: