Compare commits

..

1 Commits

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"):
await message.channel.send(self.config.get("privacy-notice", DEFAULT_PRIVACY_NOTICE), suppress_embeds=True)
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():
return
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 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:
"""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:]
for handler in (self._memory_command, self._task_command):
reply = handler(args)
if reply is not None:
await message.channel.send(reply, suppress_embeds=True)
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"]:
self.replies_enabled = False
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
spend in USD, token and image counts, and the configured budget.
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."""
await self.bot.on_message(self.staff_msg("!bot pause"))
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()