mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 12:37:32 +01:00
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
136 lines
5.0 KiB
Python
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")
|