signal-bot/tests/test_mute.py
Krzysztof kuhy Rudnicki 41df308393
Some checks are pending
CI / check (push) Waiting to run
CI / web (push) Waiting to run
feat: !cisza silences the bot in one group for up to ten minutes
Anybody may ask; ten quiet minutes is the whole downside, so there is no
trust gate on the way in and no cooldown. The confirmation is the last
thing said: below it the dispatcher drops every reply in that group --
commands, the near-miss suggestion, and the nightly report, which is
held back (never marked) rather than lost. Messages are still counted.

!odcisz is the one command that runs through a mute, and only for
TRUSTED_INVITERS; a stranger is refused without a reply, because a
refusal is a reply. Unset, anybody may -- the same reading InviteTrust
gives an unset variable, now shared as trust.is_trusted.

There is deliberately no !unmute alias: it is one edit from !mute, and
tests/test_nearest.py forbids two commands within reach of each other.

The state is a deadline in group_meta, so it survives a restart and a
reload and is cleared with the group. tests/test_help_command.py hit
the 250-line cap and its album half moved to tests/test_help_album.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 10:31:08 +02:00

89 lines
3.2 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""The mute state: a deadline per group, and the one question asked of it."""
from __future__ import annotations
from typing import TYPE_CHECKING
from signal_bot.mute import (
MUTED_UNTIL_KEY,
UNMUTE_NAME,
allows,
is_muted,
mute,
muted_until,
unmute,
)
from signal_bot.registry import CommandRegistry
from tests.conftest import SECOND_GROUP, WATCHED_GROUP
if TYPE_CHECKING:
from signal_bot.context import CommandContext
from signal_bot.store import Store
NOW = 1_000.0
async def _noop(ctx: CommandContext) -> None:
"""A handler that does nothing."""
def test_a_group_that_was_never_muted_has_no_deadline(store: Store) -> None:
assert muted_until(store, WATCHED_GROUP) is None
assert not is_muted(store, WATCHED_GROUP, NOW)
def test_a_mute_stands_until_its_deadline_and_not_a_moment_longer(store: Store) -> None:
mute(store, WATCHED_GROUP, NOW + 60)
assert muted_until(store, WATCHED_GROUP) == NOW + 60
assert is_muted(store, WATCHED_GROUP, NOW)
assert is_muted(store, WATCHED_GROUP, NOW + 59.9)
# The deadline itself is over: "until" is exclusive.
assert not is_muted(store, WATCHED_GROUP, NOW + 60)
def test_a_mute_is_per_group(store: Store) -> None:
"""Silence asked for in one group must not reach into another."""
mute(store, WATCHED_GROUP, NOW + 60)
assert is_muted(store, WATCHED_GROUP, NOW)
assert not is_muted(store, SECOND_GROUP, NOW)
def test_a_later_mute_replaces_the_deadline_rather_than_extending_it(store: Store) -> None:
"""The cap is a cap: ten minutes asked twice is still ten minutes from now."""
mute(store, WATCHED_GROUP, NOW + 600)
mute(store, WATCHED_GROUP, NOW + 60)
assert muted_until(store, WATCHED_GROUP) == NOW + 60
def test_unmute_ends_the_silence_now_and_is_idempotent(store: Store) -> None:
mute(store, WATCHED_GROUP, NOW + 600)
unmute(store, WATCHED_GROUP)
assert not is_muted(store, WATCHED_GROUP, NOW)
unmute(store, WATCHED_GROUP)
assert not is_muted(store, WATCHED_GROUP, NOW)
def test_the_deadline_lives_in_group_meta_as_epoch_seconds(store: Store) -> None:
"""Pinned because forget_group clears group_meta, which is the intended cleanup."""
mute(store, WATCHED_GROUP, NOW + 60)
assert store.meta(WATCHED_GROUP, MUTED_UNTIL_KEY) == repr(NOW + 60)
def test_everything_is_allowed_while_nobody_asked_for_silence(store: Store) -> None:
registry = CommandRegistry()
registry.register("cat", ["!kot"], _noop, "kot")
assert allows(store, WATCHED_GROUP, NOW, registry.resolve("!kot"))
assert allows(store, WATCHED_GROUP, NOW, None)
def test_only_the_unmute_command_is_allowed_through_a_mute(store: Store) -> None:
"""A command, an unknown word and the unmute: one of the three passes."""
registry = CommandRegistry()
registry.register("cat", ["!kot"], _noop, "kot")
registry.register(UNMUTE_NAME, ["!odcisz"], _noop, "odcisz")
mute(store, WATCHED_GROUP, NOW + 60)
assert not allows(store, WATCHED_GROUP, NOW, registry.resolve("!kot"))
assert not allows(store, WATCHED_GROUP, NOW, None)
assert allows(store, WATCHED_GROUP, NOW, registry.resolve("!odcisz"))