signal-bot/tests/test_stats_records.py
Krzysztof kuhy Rudnicki a9ffd02f33 feat: periods, records and the crown
The queries the statistics are made of, all derived rather than cached --
two sources of truth for one number is how a leaderboard comes to
disagree with itself, and this history is small enough that recomputing
is free.

Periods take an inclusive span of stored Warsaw day strings, never a
range of timestamps, so a daylight-saving change cannot move a boundary.
Trend reports None rather than 0 when there is no previous period:
"nothing to compare against yet" and "no change" are different
statements and the group will read them differently.

A tie shares the crown and keeps every tied run alive, per kuhy's call.
Streaks count *recorded* days, so a day nobody spoke does not end a run
-- there was no crown to lose -- and neither does a day the bot was
offline for, which it could not have judged.

Every record excludes the backfilled day. It is a 22-hour window filed
as a day, and leaving it in would hand somebody a personal best they
never set.

Two bugs found by running the code rather than reading it:

- current_streaks reported every streak as zero. Ending somebody's run
  dropped them from the tally along with the length they had already
  accumulated, so the surviving dict was empty by the time it returned.
  Now the run ends and the count it reached is kept.
- daily_winners depended on SQLite's grouping order to find a day's
  leader. It happened to work because uuids sorted conveniently in the
  fixtures. Ordering by messages DESC makes the first row per day the
  leader by construction, which is both correct and obviously so.

No suppressions: the period queries compare an exclusion parameter
against '' instead of pasting half a WHERE clause into an f-string,
which is a SQL-injection shape even when every input is a constant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
2026-08-30 19:38:54 +02:00

136 lines
5.0 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""All-time records, and the silences between them."""
from __future__ import annotations
from typing import TYPE_CHECKING
from signal_bot.stats.queries_period import every_day
from signal_bot.stats.records import (
busiest_day,
longest_silence,
most_media_day,
personal_best,
previous_best,
quietest_day,
silent_since,
)
from tests.stats_seed import GROUP, IGA, KUHY, MITHRA, Shape, wrote
if TYPE_CHECKING:
from signal_bot.store import Store
def history(store: Store) -> None:
"""Four days, a two-day gap, ties, and somebody who stops writing."""
wrote(store, "2026-09-01", MITHRA, 10, Shape(media=3))
wrote(store, "2026-09-01", KUHY, 5)
wrote(store, "2026-09-02", MITHRA, 4)
wrote(store, "2026-09-02", KUHY, 4)
wrote(store, "2026-09-03", IGA, 2)
# 09-04 and 09-05 silent.
wrote(store, "2026-09-06", KUHY, 9, Shape(media=1))
def test_no_history_yields_no_records(store: Store) -> None:
"""Day one of this release: everything empty, nothing crashing."""
days = every_day(store.events, GROUP)
assert busiest_day(days) is None
assert quietest_day(days) is None
assert longest_silence(days, "2026-09-06") is None
assert most_media_day(store.events, GROUP) is None
assert personal_best(store.events, GROUP, "u-kuhy") is None
def test_busiest_and_quietest(store: Store) -> None:
history(store)
days = every_day(store.events, GROUP)
assert busiest_day(days).day == "2026-09-01"
assert busiest_day(days).value == 15
assert quietest_day(days).day == "2026-09-03"
assert quietest_day(days).value == 2
def test_the_quietest_day_is_one_somebody_spoke_on(store: Store) -> None:
"""A silent day is a silence, not a quiet day, and is measured separately."""
history(store)
days = every_day(store.events, GROUP)
assert quietest_day(days).value > 0
assert "2026-09-04" not in days
def test_a_tie_on_the_busiest_day_goes_to_the_earlier(store: Store) -> None:
wrote(store, "2026-09-02", KUHY, 7)
wrote(store, "2026-09-01", MITHRA, 7)
assert busiest_day(every_day(store.events, GROUP)).day == "2026-09-01"
def test_personal_best_and_what_it_beat(store: Store) -> None:
history(store)
best = personal_best(store.events, GROUP, "u-kuhy")
assert (best.day, best.value) == ("2026-09-06", 9)
previous = previous_best(store.events, GROUP, "u-kuhy", "2026-09-06")
assert (previous.day, previous.value) == ("2026-09-01", 5)
def test_a_first_ever_day_has_nothing_to_beat(store: Store) -> None:
"""The "poprzedni rekord" clause has to be omitted, not printed as 0."""
wrote(store, "2026-09-01", IGA, 3)
assert previous_best(store.events, GROUP, "u-iga", "2026-09-01") is None
def test_most_media_day(store: Store) -> None:
history(store)
record = most_media_day(store.events, GROUP)
assert (record.day, record.value) == ("2026-09-01", 3)
def test_a_history_with_no_media_has_no_media_record(store: Store) -> None:
wrote(store, "2026-09-01", KUHY, 4)
assert most_media_day(store.events, GROUP) is None
def test_longest_silence(store: Store) -> None:
history(store)
gap = longest_silence(every_day(store.events, GROUP), "2026-09-06")
assert (gap.start, gap.end, gap.days) == ("2026-09-04", "2026-09-05", 2)
def test_a_silence_running_to_today_counts(store: Store) -> None:
"""The gap the group is currently in is the one worth mentioning."""
wrote(store, "2026-09-01", KUHY, 1)
gap = longest_silence(every_day(store.events, GROUP), "2026-09-05")
assert (gap.start, gap.end, gap.days) == ("2026-09-02", "2026-09-05", 4)
def test_an_unbroken_history_has_no_silence(store: Store) -> None:
wrote(store, "2026-09-01", KUHY, 1)
wrote(store, "2026-09-02", KUHY, 1)
assert longest_silence(every_day(store.events, GROUP), "2026-09-02") is None
def test_silent_since_counts_recorded_days_only(store: Store) -> None:
"""Bounded by the group's own history, so a newcomer is not "silent for a year"."""
history(store)
days = every_day(store.events, GROUP)
ala = {d: n for d, n in days.items() if d in {"2026-09-01", "2026-09-02"}}
assert silent_since(days, ala, "2026-09-06") == 2
assert silent_since(days, {"2026-09-06": 9}, "2026-09-06") == 0
def test_somebody_who_never_wrote_is_silent_for_the_whole_history(store: Store) -> None:
history(store)
days = every_day(store.events, GROUP)
assert silent_since(days, {}, "2026-09-06") == len(days)
assert silent_since({}, {}, "2026-09-06") == 0
def test_the_backfilled_day_is_kept_out_of_records(store: Store) -> None:
"""A 22-hour window filed as a day must not become somebody's best."""
wrote(store, "2026-08-30", MITHRA, 98)
wrote(store, "2026-09-01", MITHRA, 10)
assert personal_best(store.events, GROUP, "u-ala").value == 98
excluded = personal_best(store.events, GROUP, "u-ala", exclude="2026-08-30")
assert (excluded.day, excluded.value) == ("2026-09-01", 10)
assert "2026-08-30" not in every_day(store.events, GROUP, exclude="2026-08-30")