mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 18:07:29 +01:00
One row per live command on a new Commands tab of the management page,
each with an on/off switch, an optional "off until" moment and a cooldown
that overrides the bot's default for that command alone. Every switch is
live: the bot keeps them in its own SQLite (command_settings, a permanent
table) and reads the row on every attempt, so nothing goes through
/_reload and nothing restarts.
A command that is off answers "BEEP BOOP !kot WYŁĄCZONE." (with "DO HH:MM"
for a timed switch) rather than going quiet, so nobody reports it broken;
the refusal is filed as refused_off, which the analytics count with the
other refusals, and it neither starts the cooldown nor counts as a use.
The bot grows GET /_commands and PUT /_commands/{name}; the management app
proxies them under /api/commands and passes the bot's 404/503 through
unwrapped. !cisza and !odcisz carry no cooldown and the page says so
instead of accepting a number the bot would ignore (test_wiring_ungated
keeps that set honest against the real table).
Proven end to end: scripts/e2e.sh now brackets a PUT with switch_off.yaml
and switch_on.yaml, the same actor refused and then served, with no reload
between them. Also bumps vitest and @vitest/coverage-v8 to 5.0.0 and
@types/react-dom to 19.2.7, which the freshness gate asked for.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
63 lines
2.4 KiB
Python
63 lines
2.4 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""The operator's switches: defaults, overrides, and what "off" means."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
from signal_bot.command_settings import CommandSetting
|
|
|
|
if TYPE_CHECKING:
|
|
from signal_bot.store import Store
|
|
|
|
NOW = 1_000.0
|
|
|
|
|
|
def test_a_command_that_was_never_touched_is_on_with_no_override(store: Store) -> None:
|
|
setting = store.command_settings.get("cat")
|
|
assert setting == CommandSetting("cat")
|
|
assert not setting.off_at(NOW)
|
|
assert store.command_settings.cooldown_for("cat", 30.0) == 30.0
|
|
assert store.command_settings.all() == {}
|
|
|
|
|
|
def test_a_setting_round_trips_through_the_database(store: Store) -> None:
|
|
setting = CommandSetting("cat", enabled=False, disabled_until=NOW + 60, cooldown_seconds=5)
|
|
store.command_settings.put(setting)
|
|
assert store.command_settings.get("cat") == setting
|
|
assert store.command_settings.all() == {"cat": setting}
|
|
|
|
|
|
def test_a_later_put_replaces_the_row(store: Store) -> None:
|
|
"""One row per command: switching back on must not leave the old row behind."""
|
|
store.command_settings.put(CommandSetting("cat", enabled=False))
|
|
store.command_settings.put(CommandSetting("cat", cooldown_seconds=1))
|
|
assert store.command_settings.get("cat") == CommandSetting("cat", cooldown_seconds=1)
|
|
assert len(store.command_settings.all()) == 1
|
|
|
|
|
|
def test_the_operator_cooldown_wins_over_the_deployment_one(store: Store) -> None:
|
|
store.command_settings.put(CommandSetting("cat", cooldown_seconds=0))
|
|
assert store.command_settings.cooldown_for("cat", 30.0) == 0
|
|
store.command_settings.put(CommandSetting("cat", cooldown_seconds=None))
|
|
assert store.command_settings.cooldown_for("cat", 30.0) == 30.0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("setting", "off"),
|
|
[
|
|
(CommandSetting("cat"), False),
|
|
(CommandSetting("cat", enabled=False), True),
|
|
(CommandSetting("cat", disabled_until=NOW + 1), True),
|
|
# Exclusive at the deadline, like a mute.
|
|
(CommandSetting("cat", disabled_until=NOW), False),
|
|
(CommandSetting("cat", disabled_until=NOW - 1), False),
|
|
# The hard switch is not lifted by a deadline that has passed.
|
|
(CommandSetting("cat", enabled=False, disabled_until=NOW - 1), True),
|
|
],
|
|
)
|
|
def test_off_is_either_switch(setting: CommandSetting, *, off: bool) -> None:
|
|
assert setting.off_at(NOW) is off
|