mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 12:37:32 +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>
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""The switchboard proxy: forwarded to the bot, answered in the bot's words."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
import httpx
|
|
|
|
from tests.manage_fixture import bot_is_down, client_for, deps_at
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
ROW = {
|
|
"name": "cat",
|
|
"trigger": "!kot",
|
|
"description": "kot",
|
|
"enabled": True,
|
|
"disabled_until": None,
|
|
"cooldown_seconds": None,
|
|
"default_cooldown_seconds": 30,
|
|
}
|
|
|
|
|
|
def test_the_list_is_the_bots_list(tmp_path: Path) -> None:
|
|
def bot(request: httpx.Request) -> httpx.Response:
|
|
assert request.url.path == "/_commands"
|
|
return httpx.Response(200, json={"commands": [ROW]})
|
|
|
|
with client_for(deps_at(tmp_path, bot)) as client:
|
|
answer = client.get("/api/commands")
|
|
assert answer.status_code == 200
|
|
assert answer.json() == {"commands": [ROW]}
|
|
|
|
|
|
def test_a_put_is_forwarded_with_the_body_intact(tmp_path: Path) -> None:
|
|
seen: list[dict[str, object]] = []
|
|
|
|
def bot(request: httpx.Request) -> httpx.Response:
|
|
assert request.method == "PUT"
|
|
assert request.url.path == "/_commands/cat"
|
|
seen.append(json.loads(request.content))
|
|
return httpx.Response(200, json={**ROW, "enabled": False})
|
|
|
|
body = {"enabled": False, "disabled_until": None, "cooldown_seconds": 5}
|
|
with client_for(deps_at(tmp_path, bot)) as client:
|
|
answer = client.put("/api/commands/cat", json=body)
|
|
assert answer.status_code == 200
|
|
assert answer.json()["enabled"] is False
|
|
assert seen == [body]
|
|
|
|
|
|
def test_the_bots_404_and_503_are_passed_through_unchanged(tmp_path: Path) -> None:
|
|
def bot(request: httpx.Request) -> httpx.Response:
|
|
if request.method == "PUT":
|
|
return httpx.Response(404, json={"detail": "no command named 'nosuch'"})
|
|
return httpx.Response(503, json={"detail": {"status": "connecting"}})
|
|
|
|
with client_for(deps_at(tmp_path, bot)) as client:
|
|
put = client.put("/api/commands/nosuch", json={"enabled": False})
|
|
listed = client.get("/api/commands")
|
|
assert put.status_code == 404
|
|
assert "nosuch" in put.json()["detail"]
|
|
assert listed.status_code == 503
|
|
assert listed.json()["detail"]["status"] == "connecting"
|
|
|
|
|
|
def test_a_bot_that_does_not_answer_is_a_bad_gateway(tmp_path: Path) -> None:
|
|
with client_for(deps_at(tmp_path, bot_is_down)) as client:
|
|
answer = client.get("/api/commands")
|
|
assert answer.status_code == 502
|