mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 21:27:16 +01:00
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
196 lines
6.5 KiB
Python
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)
|