mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:27:20 +01:00
Seven numbers the group finds funny get a marker wherever they show up as
a count: the nightly table, every !stats_* view, !ja, the records and the
highlights. Percentages and shares stay bare (a 69% share is not a 69), and
so does the (N 📷) aside, so one line never carries two jokes. One table,
FUNNY in stats/text.py, and one helper, marked(), so adding a number is one
line. Documented in DOCS-statistics.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
238 lines
8.7 KiB
Python
238 lines
8.7 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""What the bot notices about a day, and what it stays quiet about."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from signal_bot.stats import curiosities
|
|
from signal_bot.stats.facts import DayFacts, gather
|
|
from signal_bot.stats.highlights import Highlight, ratio, select
|
|
from signal_bot.stats.queries_day import day_rows, with_silent
|
|
from tests.stats_seed import ALA, BASIA, GROUP, KUHY, Shape, wrote
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.store import Store
|
|
|
|
|
|
def facts_for(store: Store, day: str, roster: list[tuple[str, str]] | None = None) -> DayFacts:
|
|
"""Gather a day the way publish_report does."""
|
|
rows = with_silent(day_rows(store.events, GROUP, day), roster or [])
|
|
return gather(store.events, GROUP, day, rows)
|
|
|
|
|
|
def test_day_one_notices_nothing(store: Store) -> None:
|
|
"""Almost every candidate has no history to stand against yet."""
|
|
wrote(store, "2026-09-01", KUHY, 3)
|
|
fired = [c for c in curiosities.for_day(facts_for(store, "2026-09-01")) if c]
|
|
assert [c.kind for c in fired] == ["only_one_spoke"]
|
|
|
|
|
|
def test_facts_expose_the_days_crown_holders(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 4)
|
|
wrote(store, "2026-09-01", KUHY, 4)
|
|
wrote(store, "2026-09-01", BASIA, 1)
|
|
assert sorted(n for _, n in facts_for(store, "2026-09-01").leaders) == ["Ala", "kuhy"]
|
|
assert facts_for(store, "2026-09-09").leaders == []
|
|
|
|
|
|
def test_a_personal_record_reports_what_it_beat(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 5)
|
|
wrote(store, "2026-09-02", KUHY, 1)
|
|
wrote(store, "2026-09-02", ALA, 9)
|
|
found = curiosities.personal_record(facts_for(store, "2026-09-02"))
|
|
assert found is not None
|
|
assert "Ala 9 — rekord życiowy (poprzedni 5, 1 września)" in found.text
|
|
|
|
|
|
def test_a_first_ever_day_is_not_announced_as_a_record(store: Store) -> None:
|
|
"""Everybody's first day is their best; saying so every time is noise."""
|
|
wrote(store, "2026-09-01", ALA, 5)
|
|
assert curiosities.personal_record(facts_for(store, "2026-09-01")) is None
|
|
|
|
|
|
def test_the_busiest_day_ever(store: Store) -> None:
|
|
wrote(store, "2026-09-01", KUHY, 3)
|
|
wrote(store, "2026-09-02", KUHY, 9)
|
|
found = curiosities.group_record_day(facts_for(store, "2026-09-02"))
|
|
assert found is not None
|
|
assert "najgadatliwszy dzień w historii: 9 wiadomości" in found.text
|
|
|
|
|
|
def test_a_quiet_day_is_not_a_record(store: Store) -> None:
|
|
wrote(store, "2026-09-01", KUHY, 9)
|
|
wrote(store, "2026-09-02", KUHY, 3)
|
|
assert curiosities.group_record_day(facts_for(store, "2026-09-02")) is None
|
|
|
|
|
|
def test_a_media_record_needs_more_than_one_photo(store: Store) -> None:
|
|
wrote(store, "2026-09-01", KUHY, 3, Shape(media=1))
|
|
assert curiosities.media_record(facts_for(store, "2026-09-01")) is None
|
|
wrote(store, "2026-09-02", KUHY, 4, Shape(media=3))
|
|
found = curiosities.media_record(facts_for(store, "2026-09-02"))
|
|
assert found is not None
|
|
assert "rekord zdjęć: 3" in found.text
|
|
|
|
|
|
def test_a_crown_streak_needs_two_days(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 5)
|
|
wrote(store, "2026-09-01", KUHY, 1)
|
|
assert curiosities.crown_streak(facts_for(store, "2026-09-01")) is None
|
|
wrote(store, "2026-09-02", ALA, 5)
|
|
wrote(store, "2026-09-02", KUHY, 1)
|
|
found = curiosities.crown_streak(facts_for(store, "2026-09-02"))
|
|
assert found is not None
|
|
assert "Ala z koroną 2 dni z rzędu" in found.text
|
|
|
|
|
|
def test_a_shared_crown_names_everyone(store: Store) -> None:
|
|
for day in ("2026-09-01", "2026-09-02"):
|
|
wrote(store, day, ALA, 4)
|
|
wrote(store, day, KUHY, 4)
|
|
found = curiosities.crown_streak(facts_for(store, "2026-09-02"))
|
|
assert found is not None
|
|
assert "Ala i kuhy z koroną" in found.text
|
|
|
|
|
|
def test_breaking_a_silence(store: Store) -> None:
|
|
wrote(store, "2026-09-01", BASIA, 2)
|
|
for day in ("2026-09-02", "2026-09-03", "2026-09-04"):
|
|
wrote(store, day, KUHY, 3)
|
|
wrote(store, "2026-09-05", BASIA, 1)
|
|
wrote(store, "2026-09-05", KUHY, 9)
|
|
found = curiosities.silence_broken(facts_for(store, "2026-09-05"))
|
|
assert found is not None
|
|
assert "Basia odezwał się po 3 dni ciszy" in found.text
|
|
|
|
|
|
def test_a_short_gap_is_not_worth_mentioning(store: Store) -> None:
|
|
wrote(store, "2026-09-01", BASIA, 2)
|
|
wrote(store, "2026-09-02", KUHY, 3)
|
|
wrote(store, "2026-09-03", BASIA, 2)
|
|
assert curiosities.silence_broken(facts_for(store, "2026-09-03")) is None
|
|
|
|
|
|
def test_a_monologue(store: Store) -> None:
|
|
wrote(store, "2026-09-01", KUHY, 7)
|
|
found = curiosities.only_one_spoke(facts_for(store, "2026-09-01"))
|
|
assert found is not None
|
|
assert "cały dzień mówił tylko kuhy" in found.text
|
|
|
|
|
|
def test_one_single_message_is_not_a_monologue(store: Store) -> None:
|
|
wrote(store, "2026-09-01", KUHY, 1)
|
|
assert curiosities.only_one_spoke(facts_for(store, "2026-09-01")) is None
|
|
|
|
|
|
def test_dominating_the_day(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 8)
|
|
wrote(store, "2026-09-01", KUHY, 2)
|
|
found = curiosities.dominant_share(facts_for(store, "2026-09-01"))
|
|
assert found is not None
|
|
assert "Ala to 80% dzisiejszych wiadomości" in found.text
|
|
|
|
|
|
def test_an_evenly_shared_day_is_not_dominated(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 5)
|
|
wrote(store, "2026-09-01", KUHY, 4)
|
|
wrote(store, "2026-09-01", BASIA, 4)
|
|
assert curiosities.dominant_share(facts_for(store, "2026-09-01")) is None
|
|
|
|
|
|
def test_a_weekday_needs_a_baseline_before_it_can_be_unusual(store: Store) -> None:
|
|
"""Two Tuesdays are not an average worth quoting."""
|
|
wrote(store, "2026-09-01", KUHY, 5)
|
|
wrote(store, "2026-09-08", KUHY, 50)
|
|
assert curiosities.weekday_average(facts_for(store, "2026-09-08")) is None
|
|
|
|
|
|
def test_an_unusual_weekday(store: Store) -> None:
|
|
for day in ("2026-09-01", "2026-09-08", "2026-09-15"):
|
|
wrote(store, day, KUHY, 10)
|
|
wrote(store, "2026-09-22", KUHY, 30)
|
|
found = curiosities.weekday_average(facts_for(store, "2026-09-22"))
|
|
assert found is not None
|
|
assert "wtorki mają średnio 10 — dziś o 200% więcej" in found.text
|
|
|
|
|
|
def test_a_typical_weekday_says_nothing(store: Store) -> None:
|
|
for day in ("2026-09-01", "2026-09-08", "2026-09-15", "2026-09-22"):
|
|
wrote(store, day, KUHY, 10)
|
|
assert curiosities.weekday_average(facts_for(store, "2026-09-22")) is None
|
|
|
|
|
|
def test_a_quiet_weekday_reads_as_fewer(store: Store) -> None:
|
|
for day in ("2026-09-01", "2026-09-08", "2026-09-15"):
|
|
wrote(store, day, KUHY, 20)
|
|
wrote(store, "2026-09-22", KUHY, 5)
|
|
found = curiosities.weekday_average(facts_for(store, "2026-09-22"))
|
|
assert found is not None
|
|
assert "mniej" in found.text
|
|
|
|
|
|
def test_a_night_owl(store: Store) -> None:
|
|
wrote(store, "2026-09-01", KUHY, 3, Shape(hour=1))
|
|
found = curiosities.night_owl(facts_for(store, "2026-09-01"))
|
|
assert found is not None
|
|
assert "kuhy pisał o 01:00" in found.text
|
|
|
|
|
|
def test_an_ordinary_evening_is_not_a_night_owl(store: Store) -> None:
|
|
wrote(store, "2026-09-01", KUHY, 3, Shape(hour=22))
|
|
assert curiosities.night_owl(facts_for(store, "2026-09-01")) is None
|
|
|
|
|
|
def test_one_line_per_family(store: Store) -> None:
|
|
"""Three sentences about the same person are one fact, not three."""
|
|
picks = select(
|
|
[
|
|
Highlight("a", "voice", 60, 1.0, "A"),
|
|
Highlight("b", "voice", 50, 1.0, "B"),
|
|
Highlight("c", "record", 40, 1.0, "C"),
|
|
],
|
|
3,
|
|
)
|
|
assert [p.kind for p in picks] == ["a", "c"]
|
|
|
|
|
|
def test_selection_is_ordered_and_bounded() -> None:
|
|
picks = select(
|
|
[
|
|
Highlight("low", "one", 10, 1.0, ""),
|
|
None,
|
|
Highlight("high", "two", 90, 1.0, ""),
|
|
Highlight("mid", "three", 50, 1.0, ""),
|
|
],
|
|
2,
|
|
)
|
|
assert [p.kind for p in picks] == ["high", "mid"]
|
|
|
|
|
|
def test_ties_break_on_name_so_a_day_renders_the_same_twice() -> None:
|
|
picks = select(
|
|
[
|
|
Highlight("zebra", "one", 50, 1.0, ""),
|
|
Highlight("alpha", "two", 50, 1.0, ""),
|
|
],
|
|
2,
|
|
)
|
|
assert [p.kind for p in picks] == ["alpha", "zebra"]
|
|
|
|
|
|
def test_nothing_to_say_is_an_empty_list() -> None:
|
|
assert select([None, None], 3) == []
|
|
|
|
|
|
def test_ratio_survives_a_zero_baseline() -> None:
|
|
assert ratio(5, 0) == 5.0
|
|
assert ratio(10, 5) == 1.0
|
|
|
|
|
|
def test_a_record_that_lands_on_a_funny_number_carries_its_marker(store: Store) -> None:
|
|
"""The count is what is funny; the marker rides on it wherever the day's notice prints it."""
|
|
wrote(store, "2026-09-01", ALA, 69)
|
|
wrote(store, "2026-09-02", ALA, 2137)
|
|
found = curiosities.personal_record(facts_for(store, "2026-09-02"))
|
|
assert found is not None
|
|
assert "Ala 2137 🙏 — rekord życiowy (poprzedni 69 (nice), 1 września)" in found.text
|