signal-bot/tests/test_pack_command.py
Krzysztof kuhy Rudnicki 3b3a78820a fix: the tagger pre-selected a group, which is how an album reaches the wrong chat
The first group in the account's listing was checked by default, and the
first group in this account's listing is a test group. Somebody who typed
the names, hit save and never looked at the radios would have scoped a
private album to the wrong chat -- silently, because the commands then
simply do nothing in the chat they were meant for. Nothing is pre-selected
now, and saving without a group is refused.

Also replaces a test of mine that failed one run in sixteen: it drew a
two-way random choice five times and demanded both outcomes. The property
worth pinning is that a choice lands on a reading that *has* photos, which
is deterministic when only one of them does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
2026-08-30 22:41:42 +02:00

196 lines
6.5 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Answering one photo request: what gets sent, and what gets refused."""
from __future__ import annotations
import base64
from typing import TYPE_CHECKING
import pytest
from signal_bot.models import parse_envelope
from signal_bot.pack_command import (
EITHER,
EVERYBODY,
NO_PHOTO,
ONE_PERSON,
description_for,
register_pack_commands,
)
from signal_bot.pack_load import load
from signal_bot.registry import CommandContext, CommandRegistry
from tests import pack_fixture
from tests.conftest import WATCHED_GROUP, envelope
if TYPE_CHECKING:
from pathlib import Path
from signal_bot.config import Config
from signal_bot.pack import Pack
from signal_bot.registry import Handler
from signal_bot.store import Store
from signal_bot.transport_fake import FakeTransport
@pytest.fixture
def pack(tmp_path: Path) -> Pack:
return load(pack_fixture.build(tmp_path / "pack"))
def _registry(pack: Pack) -> CommandRegistry:
registry = CommandRegistry()
register_pack_commands(registry, pack)
return registry
def _ctx(
transport: FakeTransport,
store: Store,
config: Config,
text: str,
args: tuple[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="pack:p1",
now=0.0,
day="2026-08-30",
args=args,
)
async def _run(
pack: Pack, transport: FakeTransport, store: Store, config: Config, text: str
) -> None:
"""Type one line at the pack, the way the dispatcher would."""
registry = _registry(pack)
resolved = registry.resolve_with_args(text, WATCHED_GROUP)
assert resolved is not None
await resolved.command.handler(_ctx(transport, store, config, text, resolved.args))
def test_each_entry_shape_explains_itself(pack: Pack) -> None:
by_key = {entry.key: entry for entry in pack.entries}
assert description_for(by_key["p1"]) == ONE_PERSON
assert description_for(by_key["c1"]) == EVERYBODY
assert description_for(by_key["c2"]) == EITHER
def test_a_description_never_names_anybody(pack: Pack) -> None:
"""!pomoc prints it; the trigger is the only place a name may appear."""
for entry in pack.entries:
described = description_for(entry)
assert described in {ONE_PERSON, EVERYBODY, EITHER}
assert not any(trigger.strip("!") in described for trigger in entry.triggers)
async def test_a_photo_is_sent_with_its_own_type_and_name(
pack: Pack, transport: FakeTransport, store: Store, config: Config
) -> None:
await _run(pack, transport, store, config, "!gamma")
sent = transport.sent[0]
assert sent.filename == "gamma.png"
assert sent.media_type == "image/png"
assert sent.image_base64 is not None
assert base64.b64decode(sent.image_base64) == pack_fixture.PIXEL
assert sent.quote is not None
async def test_two_names_want_a_photo_with_both(
pack: Pack, transport: FakeTransport, store: Store, config: Config
) -> None:
"""both.png is the only file tagged with p1 and p2 at once."""
await _run(pack, transport, store, config, "!alfa beta")
assert transport.sent[0].filename == "both.png"
async def test_the_second_name_may_carry_its_own_bang(
pack: Pack, transport: FakeTransport, store: Store, config: Config
) -> None:
await _run(pack, transport, store, config, "!alfa !beta")
assert transport.sent[0].filename == "both.png"
async def test_a_combination_nobody_photographed_is_answered_in_one_line(
pack: Pack, transport: FakeTransport, store: Store, config: Config
) -> None:
await _run(pack, transport, store, config, "!beta gamma")
assert transport.texts == [NO_PHOTO]
assert transport.sent[0].image_base64 is None
async def test_a_name_nobody_has_gets_the_same_answer(
pack: Pack, transport: FakeTransport, store: Store, config: Config
) -> None:
"""From the group's side it is the same question: is there such a photo?"""
await _run(pack, transport, store, config, "!alfa nikt")
assert transport.texts == [NO_PHOTO]
async def test_a_choice_lands_on_the_reading_that_has_a_photo(
tmp_path: Path, transport: FakeTransport, store: Store, config: Config
) -> None:
"""p2 has no photo at all, so "either of them" must land on p3 every time.
Deterministic on purpose. Asserting that a random choice eventually
produces both readings is a test that fails one run in sixteen, and this
repo has already shipped one bug because a random API was believed after
a single call.
"""
pack = load(pack_fixture.build(tmp_path / "pack", tags={"gamma.png": ["p3"]}))
for _ in range(5):
await _run(pack, transport, store, config, "!ktokolwiek")
assert [message.filename for message in transport.sent] == ["gamma.png"] * 5
async def test_a_choice_with_nothing_behind_either_reading_is_answered(
tmp_path: Path, transport: FakeTransport, store: Store, config: Config
) -> None:
pack = load(pack_fixture.build(tmp_path / "pack", tags={"alfa.png": ["p1"]}))
await _run(pack, transport, store, config, "!ktokolwiek")
assert transport.texts == [NO_PHOTO]
async def test_a_video_keeps_being_a_video(
tmp_path: Path, transport: FakeTransport, store: Store, config: Config
) -> None:
"""Told it is a jpeg, Signal renders an unplayable file."""
pack = load(pack_fixture.build(tmp_path / "pack", tags={"clip.mp4": ["p1"]}))
await _run(pack, transport, store, config, "!alfa")
assert transport.sent[0].media_type == "video/mp4"
assert transport.sent[0].filename == "clip.mp4"
def test_every_command_is_named_by_key_and_scoped_to_the_pack(pack: Pack) -> None:
"""A log line or a command_uses row must not say who was asked for."""
registry = _registry(pack)
assert [command.name for command in registry.commands] == [
"pack:p1",
"pack:p2",
"pack:p3",
"pack:c1",
"pack:c2",
]
for command in registry.commands:
assert command.groups == pack.groups
assert command.takes_args
def test_the_gate_is_applied_to_every_entry(pack: Pack) -> None:
registry = CommandRegistry()
marked: list[str] = []
def decorate(handler: Handler) -> Handler:
"""Count the wrap and change nothing else."""
marked.append("wrapped")
return handler
register_pack_commands(registry, pack, decorate=decorate)
assert len(marked) == len(pack.entries)