# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """A command switched off from the page: answered, filed, and live at once.""" from __future__ import annotations import datetime as dt from typing import TYPE_CHECKING import pytest from signal_bot.command_settings import CommandSetting from signal_bot.command_switch import off_message from signal_bot.decorators import cooldown from signal_bot.registry import CommandRegistry from tests.conftest import WATCHED_GROUP, envelope from tests.test_dispatcher_recording import SENDER, build, history if TYPE_CHECKING: from signal_bot.config import Config from signal_bot.context import CommandContext from signal_bot.discovery import GroupDirectory from signal_bot.store import Store from signal_bot.transport_fake import FakeTransport # 2026-09-04 12:00 UTC, which Warsaw (CEST) reads as 14:00. NOON_UTC = dt.datetime(2026, 9, 4, 12, 0, tzinfo=dt.UTC).timestamp() def _registry(runs: list[str], *, cooldown_seconds: float = 0) -> CommandRegistry: """!cat, wrapped in a cooldown when asked, that records each run.""" async def handler(ctx: CommandContext) -> None: runs.append(ctx.command_name) reg = CommandRegistry() wrapped = ( cooldown(cooldown_seconds, "poczekaj {seconds} {unit}")(handler) if cooldown_seconds else handler ) reg.register("cat", ["!cat"], wrapped, "kot") return reg @pytest.mark.parametrize( ("disabled_until", "text"), [ (None, "BEEP BOOP !cat WYŁĄCZONE."), (NOON_UTC + 3600, "BEEP BOOP !cat WYŁĄCZONE DO 15:00."), # A deadline that has passed is not the reason the command is off. (NOON_UTC - 1, "BEEP BOOP !cat WYŁĄCZONE."), (NOON_UTC, "BEEP BOOP !cat WYŁĄCZONE."), ], ) def test_the_refusal_names_the_deadline_in_warsaw_time( disabled_until: float | None, text: str ) -> None: assert off_message("!cat", disabled_until, NOON_UTC) == text async def test_a_command_that_is_off_answers_and_is_filed_as_a_refusal( config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport ) -> None: """Every attempt gets the word, none of them counts as a use.""" runs: list[str] = [] dispatcher = build(config, directory, _registry(runs), store, transport) store.command_settings.put(CommandSetting("cat", enabled=False)) await dispatcher.handle(envelope(text="!cat", timestamp=1000)) await dispatcher.handle(envelope(text="!cat", timestamp=2000)) assert runs == [] assert [m.text for m in transport.sent] == ["BEEP BOOP !cat WYŁĄCZONE."] * 2 assert [row["outcome"] for row in history(store)] == ["refused_off"] * 2 assert store.last_used(WATCHED_GROUP, SENDER, "cat") is None async def test_the_switch_is_live_in_both_directions( config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport ) -> None: """Off, then on again, between two frames: no reload in between.""" runs: list[str] = [] dispatcher = build(config, directory, _registry(runs), store, transport) await dispatcher.handle(envelope(text="!cat", timestamp=1000)) store.command_settings.put(CommandSetting("cat", enabled=False)) await dispatcher.handle(envelope(text="!cat", timestamp=2000)) store.command_settings.put(CommandSetting("cat")) await dispatcher.handle(envelope(text="!cat", timestamp=3000)) assert runs == ["cat", "cat"] assert [row["outcome"] for row in history(store)] == ["ok", "refused_off", "ok"] async def test_a_timed_switch_lifts_itself( config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport ) -> None: runs: list[str] = [] clock = [dt.datetime(2026, 9, 4, 12, 0, tzinfo=dt.UTC)] dispatcher = build(config, directory, _registry(runs), store, transport, clock=lambda: clock[0]) store.command_settings.put(CommandSetting("cat", disabled_until=NOON_UTC + 60)) await dispatcher.handle(envelope(text="!cat", timestamp=1000)) clock[0] += dt.timedelta(seconds=60) await dispatcher.handle(envelope(text="!cat", timestamp=2000)) assert runs == ["cat"] assert transport.sent[0].text == "BEEP BOOP !cat WYŁĄCZONE DO 14:01." async def test_the_operator_cooldown_is_read_on_every_attempt( config: Config, directory: GroupDirectory, store: Store, transport: FakeTransport ) -> None: """A deployment cooldown of an hour, cut to nothing from the page, live.""" runs: list[str] = [] dispatcher = build(config, directory, _registry(runs, cooldown_seconds=3600), store, transport) await dispatcher.handle(envelope(text="!cat", timestamp=1000)) await dispatcher.handle(envelope(text="!cat", timestamp=2000)) assert runs == ["cat"] assert transport.sent[-1].text.startswith("poczekaj") store.command_settings.put(CommandSetting("cat", cooldown_seconds=0)) await dispatcher.handle(envelope(text="!cat", timestamp=3000)) assert runs == ["cat", "cat"] # And back: a longer one than the deployment's bites where it did not. store.command_settings.put(CommandSetting("cat", cooldown_seconds=7200)) await dispatcher.handle(envelope(text="!cat", timestamp=4000)) assert runs == ["cat", "cat"] assert [row["outcome"] for row in history(store)][-1] == "refused_cooldown"