mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:17:02 +01:00
Per-person photo commands, where the mechanism is public and the album is not. Nothing identifying is in this repository: who the people are, what fires their photos and which file shows whom all live in one directory outside it, and the tests, the docs and the offline e2e run use three invented people instead. Several names in one message intersect -- "!alfa beta" is a photo with both -- and where an entry offers a choice the bot picks among the readings that actually have a photo, so a question the album could answer is not refused. A combination nobody photographed gets one line rather than silence, which from the group's side is indistinguishable from a broken bot. Scoping is enforced in the dispatcher, not in the handler: a command the group is not allowed resolves to nothing, so it is not answered, not counted as a command, and never named in a log line outside its own groups. A handler that quietly returned would leak all three. Commands are named by pseudonym (pack:p1), so the journal and command_uses say somebody asked for a photo and nothing about whose. Video works: the attachment goes out as data:video/mp4;filename=...;base64, which is the container's own spelling, read off its /swagger/doc.json. Told it is a photo, Signal renders a clip as an unplayable file. tool/tagger.py serves one page on loopback to fill the album in -- names are typed into the page, never as arguments, and it prints how many photos each command would have when it stops, because a command with zero is dead on arrival. Also renames the real people who were still sitting in the stats test fixtures. They remain in the history; the point is that nothing new is added to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
119 lines
4.5 KiB
Python
119 lines
4.5 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""The crown: who holds it, for how long, and what a tie does to a run."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from signal_bot.stats.streaks import (
|
|
current_streaks,
|
|
daily_winners,
|
|
held_days,
|
|
longest_ever,
|
|
names_of,
|
|
)
|
|
from tests.stats_seed import ALA, BASIA, GROUP, KUHY, wrote
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.store import Store
|
|
|
|
|
|
def test_no_history_has_no_crown(store: Store) -> None:
|
|
winners = daily_winners(store.events, GROUP)
|
|
assert winners == {}
|
|
assert current_streaks(winners) == []
|
|
assert longest_ever(winners) is None
|
|
assert held_days(winners, "u-kuhy") == 0
|
|
|
|
|
|
def test_the_busiest_person_holds_the_day(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 10)
|
|
wrote(store, "2026-09-01", KUHY, 5)
|
|
winners = daily_winners(store.events, GROUP)
|
|
assert names_of(winners["2026-09-01"]) == ["Ala"]
|
|
|
|
|
|
def test_a_tie_shares_the_crown(store: Store) -> None:
|
|
"""kuhy's call: shared, because the alternative starts arguments."""
|
|
wrote(store, "2026-09-01", ALA, 4)
|
|
wrote(store, "2026-09-01", KUHY, 4)
|
|
wrote(store, "2026-09-01", BASIA, 1)
|
|
winners = daily_winners(store.events, GROUP)
|
|
assert names_of(winners["2026-09-01"]) == ["Ala", "kuhy"]
|
|
|
|
|
|
def test_a_tie_keeps_every_tied_run_alive(store: Store) -> None:
|
|
"""Three days, both tied throughout: both hold a three-day streak."""
|
|
for day in ("2026-09-01", "2026-09-02", "2026-09-03"):
|
|
wrote(store, day, ALA, 3)
|
|
wrote(store, day, KUHY, 3)
|
|
streaks = current_streaks(daily_winners(store.events, GROUP))
|
|
assert [(s.display_name, s.length) for s in streaks] == [("Ala", 3), ("kuhy", 3)]
|
|
|
|
|
|
def test_a_run_survives_a_day_nobody_spoke(store: Store) -> None:
|
|
"""There was no crown to lose on a silent day, so the run continues."""
|
|
wrote(store, "2026-09-01", KUHY, 5)
|
|
# 09-02 and 09-03 silent.
|
|
wrote(store, "2026-09-04", KUHY, 5)
|
|
streaks = current_streaks(daily_winners(store.events, GROUP))
|
|
assert [(s.display_name, s.length) for s in streaks] == [("kuhy", 2)]
|
|
|
|
|
|
def test_losing_a_day_ends_a_run_but_keeps_what_it_reached(store: Store) -> None:
|
|
"""The bug this test exists for reported every streak as zero."""
|
|
wrote(store, "2026-09-01", ALA, 10)
|
|
wrote(store, "2026-09-02", ALA, 10)
|
|
wrote(store, "2026-09-03", ALA, 4)
|
|
wrote(store, "2026-09-03", KUHY, 4)
|
|
wrote(store, "2026-09-06", KUHY, 9)
|
|
streaks = current_streaks(daily_winners(store.events, GROUP))
|
|
assert [(s.display_name, s.length) for s in streaks] == [("kuhy", 2)]
|
|
assert streaks[0].last_day == "2026-09-06"
|
|
|
|
|
|
def test_longest_ever_survives_the_run_that_set_it_ending(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 10)
|
|
wrote(store, "2026-09-02", ALA, 10)
|
|
wrote(store, "2026-09-03", ALA, 10)
|
|
wrote(store, "2026-09-04", KUHY, 10)
|
|
best = longest_ever(daily_winners(store.events, GROUP))
|
|
assert (best.display_name, best.length, best.last_day) == ("Ala", 3, "2026-09-03")
|
|
|
|
|
|
def test_held_days_counts_shared_days_for_everyone_tied(store: Store) -> None:
|
|
wrote(store, "2026-09-01", ALA, 10)
|
|
wrote(store, "2026-09-02", ALA, 4)
|
|
wrote(store, "2026-09-02", KUHY, 4)
|
|
winners = daily_winners(store.events, GROUP)
|
|
assert held_days(winners, "u-ala") == 2
|
|
assert held_days(winners, "u-kuhy") == 1
|
|
assert held_days(winners, "u-basia") == 0
|
|
|
|
|
|
def test_somebody_who_merely_took_part_holds_nothing(store: Store) -> None:
|
|
"""Neither leader nor tied: the common case, and the one worth pinning."""
|
|
wrote(store, "2026-09-01", ALA, 10)
|
|
wrote(store, "2026-09-01", KUHY, 4)
|
|
wrote(store, "2026-09-01", BASIA, 1)
|
|
winners = daily_winners(store.events, GROUP)
|
|
assert names_of(winners["2026-09-01"]) == ["Ala"]
|
|
assert held_days(winners, "u-basia") == 0
|
|
|
|
|
|
def test_streak_order_is_stable(store: Store) -> None:
|
|
"""Longest first, then by name -- not whichever the dict happened to yield."""
|
|
wrote(store, "2026-09-01", ALA, 3)
|
|
wrote(store, "2026-09-01", KUHY, 3)
|
|
wrote(store, "2026-09-01", BASIA, 3)
|
|
streaks = current_streaks(daily_winners(store.events, GROUP))
|
|
assert [s.display_name for s in streaks] == ["Ala", "Basia", "kuhy"]
|
|
|
|
|
|
def test_the_backfilled_day_can_be_left_out(store: Store) -> None:
|
|
wrote(store, "2026-08-30", ALA, 98)
|
|
wrote(store, "2026-09-01", KUHY, 5)
|
|
winners = daily_winners(store.events, GROUP, exclude="2026-08-30")
|
|
assert "2026-08-30" not in winners
|
|
assert [s.display_name for s in current_streaks(winners)] == ["kuhy"]
|