mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:17:02 +01:00
A compliment gets a reaction drawn from a small warm set (❤️ 🥰 😊 👍 🎉 🙏 😸) put on the compliment itself, and nothing in the group -- the one answer that adds no line. Same cooldown as every other command. The transport learns reactions: send_reaction on the Protocol, POST /v1/reactions/<number> on the REST adapter with the four fields api.SendReactionRequest requires (204 back), a recorded list on the fake. CommandContext.react() names the target the way a quote does, and falls back to the emoji as text for an envelope with no timestamp, so a shape this bot has never seen is visible rather than swallowed. The fake container gains the endpoint, strict about the schema, and records reactions in order with the sends; a scenario can now expect {kind: reaction, quotes: true}. First two-word trigger: fold() keeps the inner space and the registry matches whole text first, so "!good bot" resolves and "!good" does not; one-word spellings cover a double space. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""!good bot: a reaction on the compliment, and not a word in the group."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
from signal_bot.animals import ALIAS_FILE
|
|
from signal_bot.context import CommandContext
|
|
from signal_bot.decorators import cooldown
|
|
from signal_bot.dispatcher import Dispatcher
|
|
from signal_bot.good_bot_command import (
|
|
COMMAND_NAME,
|
|
EMOJI,
|
|
pick,
|
|
register_good_bot_command,
|
|
)
|
|
from signal_bot.help_command import GROUPS
|
|
from signal_bot.models import parse_envelope
|
|
from signal_bot.registry import CommandRegistry
|
|
from signal_bot.registry_holder import RegistryHolder
|
|
from tests.conftest import WATCHED_GROUP, envelope
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.config import Config
|
|
from signal_bot.discovery import GroupDirectory
|
|
from signal_bot.store import Store
|
|
from signal_bot.transport_fake import FakeTransport
|
|
|
|
ALIASES = {COMMAND_NAME: ["!good bot", "!dobry bot", "!goodbot"]}
|
|
SENT_AT = 1719177917717
|
|
DRAWS = 200
|
|
NOW = dt.datetime(2026, 9, 4, 12, 0, tzinfo=dt.UTC)
|
|
|
|
|
|
def _registry() -> CommandRegistry:
|
|
registry = CommandRegistry()
|
|
register_good_bot_command(registry, aliases=ALIASES)
|
|
return registry
|
|
|
|
|
|
async def _run(
|
|
store: Store, transport: FakeTransport, config: Config, text: str, *, timestamp: int
|
|
) -> None:
|
|
"""Type one line at the bot through the real resolver and handler."""
|
|
message = parse_envelope(envelope(text=text, timestamp=timestamp))
|
|
assert message is not None
|
|
resolved = _registry().resolve_with_args(text, WATCHED_GROUP)
|
|
assert resolved is not None
|
|
ctx = CommandContext(
|
|
message=message,
|
|
group_id=WATCHED_GROUP,
|
|
transport=transport,
|
|
store=store,
|
|
config=config,
|
|
command_name=resolved.command.name,
|
|
now=1_000.0,
|
|
day="2026-09-04",
|
|
args=resolved.args,
|
|
)
|
|
await resolved.command.handler(ctx)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"text", ["!good bot", "!Good Bot", "!dobry bot", "!goodbot", " !good bot "]
|
|
)
|
|
async def test_the_compliment_gets_an_emoji_on_itself_and_no_reply(
|
|
store: Store, transport: FakeTransport, config: Config, text: str
|
|
) -> None:
|
|
await _run(store, transport, config, text, timestamp=SENT_AT)
|
|
|
|
(reaction,) = transport.reactions
|
|
assert reaction.emoji in EMOJI
|
|
# On the asker's message, in the asker's group: what a quote would name.
|
|
assert (reaction.recipient, reaction.target_author) == (WATCHED_GROUP, "uuid-alice")
|
|
assert reaction.timestamp == SENT_AT
|
|
assert transport.sent == []
|
|
|
|
|
|
async def test_a_message_that_cannot_be_pointed_at_gets_the_emoji_as_text(
|
|
store: Store, transport: FakeTransport, config: Config
|
|
) -> None:
|
|
"""The visible degradation, the same one reply() makes when it cannot quote."""
|
|
await _run(store, transport, config, "!good bot", timestamp=0)
|
|
|
|
assert transport.reactions == []
|
|
(sent,) = transport.sent
|
|
assert sent.text in EMOJI
|
|
assert sent.quote is None
|
|
|
|
|
|
def test_only_the_two_words_together_are_the_command() -> None:
|
|
"""The bare first word is nothing, and so is a compliment with a tail."""
|
|
registry = _registry()
|
|
assert registry.resolve_with_args("!good", WATCHED_GROUP) is None
|
|
assert registry.resolve_with_args("!good bot!!!", WATCHED_GROUP) is None
|
|
assert registry.resolve_with_args("!good bot thanks", WATCHED_GROUP) is None
|
|
assert registry.resolve_with_args("!goodbot", WATCHED_GROUP) is not None
|
|
|
|
|
|
def test_every_emoji_in_the_set_comes_up() -> None:
|
|
"""A draw over seven; two hundred draws that miss one would be a broken draw."""
|
|
assert {pick() for _ in range(DRAWS)} == set(EMOJI)
|
|
assert pick(("x",)) == "x"
|
|
|
|
|
|
def test_the_shipped_aliases_are_command_words_and_the_first_is_the_two_word_one() -> None:
|
|
"""_repo_vocabulary() subtracts these from the personal-data gate.
|
|
|
|
The first alias is what !pomoc prints, and "!good bot" is the spelling
|
|
the group asked for; the one-word ones exist for a double space.
|
|
"""
|
|
document = json.loads(ALIAS_FILE.read_text(encoding="utf-8"))
|
|
triggers = document[COMMAND_NAME]
|
|
assert triggers[0] == "!good bot"
|
|
for trigger in triggers:
|
|
assert trigger.startswith("!")
|
|
assert trigger[1:].replace(" ", "").replace("_", "").isalpha()
|
|
|
|
|
|
def test_help_files_it_under_its_own_heading() -> None:
|
|
assert any(COMMAND_NAME in names for _, names in GROUPS)
|
|
|
|
|
|
async def test_through_the_dispatcher_it_is_a_command_with_a_cooldown(
|
|
config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport
|
|
) -> None:
|
|
"""Served like any other command: recorded, timed, and rate-limited."""
|
|
registry = CommandRegistry()
|
|
register_good_bot_command(registry, aliases=ALIASES, decorate=cooldown(10, "wait {seconds}"))
|
|
dispatcher = Dispatcher(
|
|
config=config,
|
|
directory=directory,
|
|
holder=RegistryHolder(lambda: registry),
|
|
store=store,
|
|
transport=transport,
|
|
clock=lambda: NOW,
|
|
)
|
|
|
|
first = await dispatcher.handle(envelope(text="!good bot"))
|
|
second = await dispatcher.handle(envelope(text="!good bot"))
|
|
|
|
assert first is not None
|
|
assert (first.command, first.outcome) == (COMMAND_NAME, "ok")
|
|
assert first.send_ms >= 0
|
|
assert second is not None
|
|
assert second.outcome == "refused_cooldown"
|
|
assert len(transport.reactions) == 1
|
|
assert transport.texts[0].startswith("wait ")
|