operator help: complete + context-aware (ops-17): !bot help full grouped list, !help for everyone per-channel

This commit is contained in:
Oleksandr Kozachuk
2026-07-14 11:09:33 +02:00
parent 09871b9b95
commit 891fbdc101
3 changed files with 103 additions and 2 deletions
+25 -2
View File
@@ -182,6 +182,9 @@ class FjerkroaBot(commands.Bot):
if content.startswith("!privacy"): if content.startswith("!privacy"):
await message.channel.send(self.config.get("privacy-notice", DEFAULT_PRIVACY_NOTICE), suppress_embeds=True) await message.channel.send(self.config.get("privacy-notice", DEFAULT_PRIVACY_NOTICE), suppress_embeds=True)
return return
if content.startswith("!help"): # OPS-17: context-aware, works even while paused
await message.channel.send(self._help_text(staff=self.is_staff_channel(message.channel)), suppress_embeds=True)
return
if not self.replies_allowed(): if not self.replies_allowed():
return return
if str(message.content).startswith("!wichtel"): if str(message.content).startswith("!wichtel"):
@@ -263,15 +266,35 @@ class FjerkroaBot(commands.Bot):
return f"Cancelled {store.task_set_state(int(args[1]), 'cancelled')} task(s)." return f"Cancelled {store.task_set_state(int(args[1]), 'cancelled')} task(s)."
return None return None
def _help_text(self, staff: bool) -> str:
"""Context-aware command help (OPS-17): every channel lists the user commands; the staff channel also lists operator commands."""
everywhere = (
"Available to everyone, in any channel:\n"
"• `!help` — this help\n"
"• `!forgetme` — delete your messages and memory traces (works even while I'm paused)\n"
"• `!privacy` — how your data is handled (works even while I'm paused)\n"
"• `!wichtel @a @b @c …` — draw Secret Santa pairings (needs ≥2 mentions; only while I'm active)"
)
if not staff:
return everywhere
operator = (
"Staff commands — this channel only, prefixed `!bot`:\n"
"• Control: `pause`, `resume`, `quiet <minutes>`, `status`\n"
"• Cost: `spend`, `images on|off`\n"
"• Memory: `memory <user>`, `forget-fact <id>`, `pin <channel|global> <text>`, `unpin <id>`, `pins`\n"
"• Tasks: `tasks` (list), `tasks on|off`, `task-approve <id>`, `task-cancel <id>`"
)
return operator + "\n\n" + everywhere
async def handle_staff_command(self, message: Message) -> None: async def handle_staff_command(self, message: Message) -> None:
"""Operator kill-switches, staff channel only (OPS-01..05, OPS-09, MEM-07).""" """Operator kill-switches, staff channel only (OPS-01..05, OPS-09, OPS-17, MEM-07)."""
args = str(message.content).split()[1:] args = str(message.content).split()[1:]
for handler in (self._memory_command, self._task_command): for handler in (self._memory_command, self._task_command):
reply = handler(args) reply = handler(args)
if reply is not None: if reply is not None:
await message.channel.send(reply, suppress_embeds=True) await message.channel.send(reply, suppress_embeds=True)
return return
reply = "Commands: pause, resume, images on|off, tasks on|off, quiet <minutes>, status, spend, memory <user>, forget-fact <id>, pin <channel|global> <fact>, unpin <id>" reply = self._help_text(staff=True) # OPS-17: unknown/`help` -> full grouped help
if args[:1] == ["pause"]: if args[:1] == ["pause"]:
self.replies_enabled = False self.replies_enabled = False
reply = "Replies paused." reply = "Replies paused."
+12
View File
@@ -73,3 +73,15 @@ per-channel) — without it, `!bot unpin <id>` required guessing ids.
`!bot spend` answers in the staff channel with today's estimated `!bot spend` answers in the staff channel with today's estimated
spend in USD, token and image counts, and the configured budget. spend in USD, token and image counts, and the configured budget.
Management sees the cost, not just the cap. Management sees the cost, not just the cap.
### OPS-17 — Help is complete and context-aware (coverage: test)
Help reflects where each command actually works, because not every
command is allowed everywhere. `!help` answers in any channel and
lists only the commands usable there: in a normal channel the
everyone-commands (`!help`, `!forgetme`, `!privacy`, `!wichtel`); in
the staff channel it additionally lists the operator commands grouped
by purpose (control, cost, memory, tasks). `!bot help` — and any
unrecognised `!bot` command — answers with that same full staff help,
so the listing is exhaustive rather than the old hand-maintained
partial line. Help works even while the bot is paused.
+66
View File
@@ -133,3 +133,69 @@ class TestTasksKillSwitch(OpsBase):
"""OPS-09: bot-initiated posts respect pause/quiet.""" """OPS-09: bot-initiated posts respect pause/quiet."""
await self.bot.on_message(self.staff_msg("!bot pause")) await self.bot.on_message(self.staff_msg("!bot pause"))
self.assertFalse(self.bot.bot_initiated_allowed()) self.assertFalse(self.bot.bot_initiated_allowed())
class TestHelp(OpsBase):
STAFF_CMDS = (
"pause",
"resume",
"quiet <minutes>",
"status",
"spend",
"images on|off",
"memory <user>",
"forget-fact <id>",
"pin <channel|global>",
"unpin <id>",
"pins",
"task-approve <id>",
"task-cancel <id>",
"(list)",
)
def test_staff_help_is_complete_and_grouped(self):
"""OPS-17: staff help lists every operator command, grouped by purpose."""
text = self.bot._help_text(staff=True)
for cmd in self.STAFF_CMDS:
self.assertIn(cmd, text, f"missing {cmd!r} in staff help")
for group in ("Control:", "Cost:", "Memory:", "Tasks:"):
self.assertIn(group, text)
for cmd in ("!help", "!forgetme", "!privacy", "!wichtel"):
self.assertIn(cmd, text) # everywhere-commands shown too
def test_user_help_hides_operator_commands(self):
"""OPS-17: non-staff help shows only the everyone-commands."""
text = self.bot._help_text(staff=False)
for cmd in ("!help", "!forgetme", "!privacy", "!wichtel"):
self.assertIn(cmd, text)
for op in ("task-approve", "images on|off", "spend", "Staff commands", "Control:"):
self.assertNotIn(op, text)
async def test_bot_help_in_staff_channel_returns_full_help(self):
"""OPS-17: `!bot help` answers with the complete staff help."""
await self.bot.on_message(self.staff_msg("!bot help"))
text = self.bot.staff_channel.send.await_args.args[0]
self.assertIn("Staff commands", text)
self.assertIn("task-cancel <id>", text)
async def test_unknown_bot_command_falls_back_to_help(self):
"""OPS-17: an unrecognised `!bot` command shows the full help, not a partial line."""
await self.bot.on_message(self.staff_msg("!bot wat"))
text = self.bot.staff_channel.send.await_args.args[0]
self.assertIn("Control:", text)
async def test_help_in_public_channel_is_user_scoped(self):
"""OPS-17: `!help` in a normal channel lists only everyone-commands."""
msg = self.public_msg("!help")
await self.bot.on_message(msg)
text = msg.channel.send.await_args.args[0]
self.assertIn("!forgetme", text)
self.assertNotIn("Staff commands", text)
self.assertNotIn("task-approve", text)
async def test_help_works_while_paused(self):
"""OPS-17: help answers even when replies are paused."""
await self.bot.on_message(self.staff_msg("!bot pause"))
msg = self.public_msg("!help")
await self.bot.on_message(msg)
msg.channel.send.assert_awaited()