mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 12:47:48 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
152 lines
5.5 KiB
Python
152 lines
5.5 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""The !papież command: a random pope, captioned and linked."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from signal_bot.decorators import DISPLAY_MARGIN_SECONDS, cooldown
|
|
from signal_bot.http_client import ImageFetcher
|
|
from signal_bot.models import parse_envelope
|
|
from signal_bot.popes_command import (
|
|
COMMAND_NAME,
|
|
article_url,
|
|
caption_for,
|
|
random_pope,
|
|
register_pope_command,
|
|
)
|
|
from signal_bot.portraits import PortraitCache
|
|
from signal_bot.registry import CommandContext, CommandRefusedError, CommandRegistry
|
|
from signal_bot.versioning import POPES
|
|
from tests.conftest import WATCHED_GROUP, envelope
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.config import Config
|
|
from signal_bot.store import Store
|
|
from signal_bot.transport_fake import FakeTransport
|
|
|
|
IMAGE = b"portrait-bytes"
|
|
ALIASES = {COMMAND_NAME: ["!papież", "!papiez", "!pope"]}
|
|
|
|
|
|
def _cache(tmp_path: Path) -> PortraitCache:
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
if "commons" in str(request.url):
|
|
page = {"imageinfo": [{"thumburl": "https://cdn/portrait.jpg"}]}
|
|
return httpx.Response(200, json={"query": {"pages": [page]}})
|
|
return httpx.Response(200, content=IMAGE)
|
|
|
|
fetcher = ImageFetcher(httpx.AsyncClient(transport=httpx.MockTransport(handler)))
|
|
return PortraitCache(fetcher, "https://commons.example/w/api.php", tmp_path)
|
|
|
|
|
|
def _ctx(transport: FakeTransport, store: Store, config: Config, text: str) -> CommandContext:
|
|
message = parse_envelope(envelope(text=text))
|
|
assert message is not None
|
|
return CommandContext(
|
|
message=message,
|
|
group_id=WATCHED_GROUP,
|
|
transport=transport,
|
|
store=store,
|
|
config=config,
|
|
command_name=COMMAND_NAME,
|
|
now=100.0,
|
|
day="2026-08-30",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("trigger", ["!papież", "!papiez", "!pope"])
|
|
def test_every_alias_resolves(trigger: str) -> None:
|
|
registry = CommandRegistry()
|
|
register_pope_command(registry, _cache(Path("/nonexistent")), aliases=ALIASES)
|
|
|
|
command = registry.resolve(trigger)
|
|
assert command is not None
|
|
assert command.name == COMMAND_NAME
|
|
|
|
|
|
def test_the_real_alias_file_carries_the_command() -> None:
|
|
registry = CommandRegistry()
|
|
register_pope_command(registry, _cache(Path("/nonexistent")))
|
|
# The Polish spelling is the one kuhy asked for; folding makes the rest
|
|
# of the keyboard layouts work, but this one must be listed outright.
|
|
assert registry.resolve("!papież") is not None
|
|
|
|
|
|
async def test_it_posts_a_portrait_with_the_caption_attached(
|
|
tmp_path: Path, transport: FakeTransport, store: Store, config: Config
|
|
) -> None:
|
|
registry = CommandRegistry()
|
|
register_pope_command(registry, _cache(tmp_path), aliases=ALIASES)
|
|
command = registry.resolve("!papież")
|
|
assert command is not None
|
|
|
|
await command.handler(_ctx(transport, store, config, "!papież"))
|
|
|
|
sent = transport.sent[0]
|
|
assert sent.recipient == WATCHED_GROUP
|
|
assert sent.image_base64 is not None
|
|
assert base64.b64decode(sent.image_base64) == IMAGE
|
|
# One message, not two: the caption rides in the same payload.
|
|
assert len(transport.sent) == 1
|
|
assert sent.caption.startswith(tuple(str(p.ordinal) for p in POPES))
|
|
assert "https://pl.wikipedia.org/wiki/" in sent.caption
|
|
|
|
|
|
async def test_the_cooldown_decorator_applies(
|
|
tmp_path: Path, transport: FakeTransport, store: Store, config: Config
|
|
) -> None:
|
|
registry = CommandRegistry()
|
|
register_pope_command(
|
|
registry, _cache(tmp_path), decorate=cooldown(10, "WAIT {seconds}"), aliases=ALIASES
|
|
)
|
|
store.record_use(WATCHED_GROUP, "uuid-alice", COMMAND_NAME, 100.0, "2026-08-30")
|
|
command = registry.resolve("!pope")
|
|
assert command is not None
|
|
|
|
with pytest.raises(CommandRefusedError):
|
|
await command.handler(_ctx(transport, store, config, "!pope"))
|
|
|
|
assert transport.texts == [f"WAIT {10 + DISPLAY_MARGIN_SECONDS}"]
|
|
|
|
|
|
def test_the_caption_names_the_pope_in_both_languages() -> None:
|
|
pope = POPES[263]
|
|
assert pope.name == "Jan Paweł II"
|
|
assert caption_for(pope) == (
|
|
"264. Jan Paweł II — Ioannes Paulus Secundus (1978-2005)\n"
|
|
"https://pl.wikipedia.org/wiki/Jan_Pawe%C5%82_II"
|
|
)
|
|
|
|
|
|
def test_the_latin_name_never_contradicts_the_polish_number() -> None:
|
|
"""Both names come from the same table row, so they cannot disagree.
|
|
|
|
la.wikipedia titles #154 "Stephanus X" while every other source calls
|
|
him the ninth, and "Stefan IX — Stephanus X" in one caption reads as a
|
|
bug. The Latin column of Poczet papieży says "Stephanus Nonus".
|
|
"""
|
|
assert POPES[153].name == "Stefan IX"
|
|
assert POPES[153].latin == "Stephanus Nonus"
|
|
assert POPES[266].latin == "Leo Decimus Quartus"
|
|
|
|
|
|
def test_the_url_uses_the_title_so_disambiguated_popes_still_resolve() -> None:
|
|
telesfor = POPES[7]
|
|
assert telesfor.name == "Telesfor"
|
|
# Half the early popes live at "<name> (papież)"; name alone 404s.
|
|
assert article_url(telesfor) == "https://pl.wikipedia.org/wiki/Telesfor_%28papie%C5%BC%29"
|
|
|
|
|
|
def test_the_draw_actually_varies() -> None:
|
|
# 0.0.3 shipped a !gekon that posted one identical photo forever because
|
|
# nobody drew twice. Twenty draws from 267 popes repeating is a 1-in-267^19
|
|
# event; anything that pins the choice fails here immediately.
|
|
drawn = {random_pope().ordinal for _ in range(20)}
|
|
assert len(drawn) > 1
|