From 55c15b8e290a029fda2f0a7ff13fec1e8bd792ab Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 08:14:43 +0000 Subject: [PATCH] test(whatsapp): add group trigger filtering tests Covers the four cases introduced by the group_trigger fix: - mention_only: group message without mention is dropped - mention_only: group message with mentioned:true is forwarded - mention_only: group message with non-empty mentions array is forwarded - mention_only: DMs are always forwarded regardless of config - prefix: matching prefix is forwarded with prefix stripped - prefix: non-matching message is dropped - no config: permissive default forwards everything https://claude.ai/code/session_01PfpBofWnnVaNf7uyBEJA4T --- .../whatsapp/whatsapp_command_test.go | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/pkg/channels/whatsapp/whatsapp_command_test.go b/pkg/channels/whatsapp/whatsapp_command_test.go index 2d85d74f8..6c62cffab 100644 --- a/pkg/channels/whatsapp/whatsapp_command_test.go +++ b/pkg/channels/whatsapp/whatsapp_command_test.go @@ -9,6 +9,32 @@ import ( "github.com/sipeed/picoclaw/pkg/config" ) +func newTestChannel(gt config.GroupTriggerConfig) (*WhatsAppChannel, *bus.MessageBus) { + messageBus := bus.NewMessageBus() + cfg := config.WhatsAppConfig{GroupTrigger: gt} + ch := &WhatsAppChannel{ + BaseChannel: channels.NewBaseChannel("whatsapp", cfg, messageBus, nil, + channels.WithGroupTrigger(cfg.GroupTrigger), + ), + ctx: context.Background(), + } + return ch, messageBus +} + +// drainInbound returns the next non-observe-only message from the bus, if any. +// ObserveOnly messages (recorded by ObserveGroupMessage) are not counted as "forwarded". +func drainInbound(mb *bus.MessageBus) (bus.InboundMessage, bool) { + select { + case msg := <-mb.InboundChan(): + if msg.ObserveOnly { + return bus.InboundMessage{}, false + } + return msg, true + default: + return bus.InboundMessage{}, false + } +} + func TestHandleIncomingMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T) { messageBus := bus.NewMessageBus() ch := &WhatsAppChannel{ @@ -35,3 +61,110 @@ func TestHandleIncomingMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T t.Fatalf("content=%q", inbound.Content) } } + +// TestGroupTrigger_MentionOnly verifies that with mention_only:true the bot +// ignores group messages unless the bridge signals a mention. +func TestGroupTrigger_MentionOnly(t *testing.T) { + gt := config.GroupTriggerConfig{MentionOnly: true} + + t.Run("group message without mention is dropped", func(t *testing.T) { + ch, mb := newTestChannel(gt) + ch.handleIncomingMessage(map[string]any{ + "from": "user1", + "chat": "group1", + "content": "hello everyone", + }) + if _, ok := drainInbound(mb); ok { + t.Fatal("expected message to be dropped, but it was forwarded") + } + }) + + t.Run("group message with mentioned:true is forwarded", func(t *testing.T) { + ch, mb := newTestChannel(gt) + ch.handleIncomingMessage(map[string]any{ + "from": "user1", + "chat": "group1", + "content": "hey bot", + "mentioned": true, + }) + msg, ok := drainInbound(mb) + if !ok { + t.Fatal("expected message to be forwarded, but it was dropped") + } + if msg.Content != "hey bot" { + t.Fatalf("content=%q", msg.Content) + } + }) + + t.Run("group message with non-empty mentions array is forwarded", func(t *testing.T) { + ch, mb := newTestChannel(gt) + ch.handleIncomingMessage(map[string]any{ + "from": "user1", + "chat": "group1", + "content": "hey bot", + "mentions": []any{"botjid@s.whatsapp.net"}, + }) + if _, ok := drainInbound(mb); !ok { + t.Fatal("expected message to be forwarded, but it was dropped") + } + }) + + t.Run("DM is always forwarded regardless of mention_only", func(t *testing.T) { + ch, mb := newTestChannel(gt) + ch.handleIncomingMessage(map[string]any{ + "from": "user1", + "chat": "user1", // chat == sender → DM + "content": "private message", + }) + if _, ok := drainInbound(mb); !ok { + t.Fatal("expected DM to be forwarded, but it was dropped") + } + }) +} + +// TestGroupTrigger_Prefix verifies prefix-based group trigger filtering. +func TestGroupTrigger_Prefix(t *testing.T) { + gt := config.GroupTriggerConfig{Prefixes: []string{"/ask"}} + + t.Run("group message with matching prefix is forwarded and prefix stripped", func(t *testing.T) { + ch, mb := newTestChannel(gt) + ch.handleIncomingMessage(map[string]any{ + "from": "user1", + "chat": "group1", + "content": "/ask what time is it", + }) + msg, ok := drainInbound(mb) + if !ok { + t.Fatal("expected message to be forwarded") + } + if msg.Content != "what time is it" { + t.Fatalf("content=%q, want %q", msg.Content, "what time is it") + } + }) + + t.Run("group message without prefix is dropped", func(t *testing.T) { + ch, mb := newTestChannel(gt) + ch.handleIncomingMessage(map[string]any{ + "from": "user1", + "chat": "group1", + "content": "hello", + }) + if _, ok := drainInbound(mb); ok { + t.Fatal("expected message to be dropped") + } + }) +} + +// TestGroupTrigger_NoConfig verifies that without any group_trigger config the +// bot responds to all group messages (permissive default). +func TestGroupTrigger_NoConfig(t *testing.T) { + ch, mb := newTestChannel(config.GroupTriggerConfig{}) + ch.handleIncomingMessage(map[string]any{ + "from": "user1", + "chat": "group1", + "content": "any message", + }) + if _, ok := drainInbound(mb); !ok { + t.Fatal("expected message to be forwarded with no group_trigger config") + } +}