# 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.context import CommandContext 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_holder import PackHolder from signal_bot.pack_load import load from signal_bot.registry import 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, PackHolder(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, PackHolder(pack), decorate=decorate) assert len(marked) == len(pack.entries)