Add Slack as a messaging channel using Socket Mode (WebSocket), bringing the total supported channels to 8. Features include bidirectional messaging, thread support with per-thread session context, @mention handling, ack reactions (👀/✅), slash commands, file/attachment support with Groq Whisper audio transcription, and allowlist filtering by Slack user ID. Closes #31 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
194 lines
4.1 KiB
Go
194 lines
4.1 KiB
Go
package channels
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
)
|
|
|
|
func TestParseSlackChatID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
chatID string
|
|
wantChanID string
|
|
wantThread string
|
|
}{
|
|
{
|
|
name: "channel only",
|
|
chatID: "C123456",
|
|
wantChanID: "C123456",
|
|
wantThread: "",
|
|
},
|
|
{
|
|
name: "channel with thread",
|
|
chatID: "C123456/1234567890.123456",
|
|
wantChanID: "C123456",
|
|
wantThread: "1234567890.123456",
|
|
},
|
|
{
|
|
name: "DM channel",
|
|
chatID: "D987654",
|
|
wantChanID: "D987654",
|
|
wantThread: "",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
chatID: "",
|
|
wantChanID: "",
|
|
wantThread: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
chanID, threadTS := parseSlackChatID(tt.chatID)
|
|
if chanID != tt.wantChanID {
|
|
t.Errorf("parseSlackChatID(%q) channelID = %q, want %q", tt.chatID, chanID, tt.wantChanID)
|
|
}
|
|
if threadTS != tt.wantThread {
|
|
t.Errorf("parseSlackChatID(%q) threadTS = %q, want %q", tt.chatID, threadTS, tt.wantThread)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStripBotMention(t *testing.T) {
|
|
ch := &SlackChannel{botUserID: "U12345BOT"}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{
|
|
name: "mention at start",
|
|
input: "<@U12345BOT> hello there",
|
|
want: "hello there",
|
|
},
|
|
{
|
|
name: "mention in middle",
|
|
input: "hey <@U12345BOT> can you help",
|
|
want: "hey can you help",
|
|
},
|
|
{
|
|
name: "no mention",
|
|
input: "hello world",
|
|
want: "hello world",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "only mention",
|
|
input: "<@U12345BOT>",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ch.stripBotMention(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("stripBotMention(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSlackChannel(t *testing.T) {
|
|
msgBus := bus.NewMessageBus()
|
|
|
|
t.Run("missing bot token", func(t *testing.T) {
|
|
cfg := config.SlackConfig{
|
|
BotToken: "",
|
|
AppToken: "xapp-test",
|
|
}
|
|
_, err := NewSlackChannel(cfg, msgBus)
|
|
if err == nil {
|
|
t.Error("expected error for missing bot_token, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("missing app token", func(t *testing.T) {
|
|
cfg := config.SlackConfig{
|
|
BotToken: "xoxb-test",
|
|
AppToken: "",
|
|
}
|
|
_, err := NewSlackChannel(cfg, msgBus)
|
|
if err == nil {
|
|
t.Error("expected error for missing app_token, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("valid config", func(t *testing.T) {
|
|
cfg := config.SlackConfig{
|
|
BotToken: "xoxb-test",
|
|
AppToken: "xapp-test",
|
|
AllowFrom: []string{"U123"},
|
|
}
|
|
ch, err := NewSlackChannel(cfg, msgBus)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if ch.Name() != "slack" {
|
|
t.Errorf("Name() = %q, want %q", ch.Name(), "slack")
|
|
}
|
|
if ch.IsRunning() {
|
|
t.Error("new channel should not be running")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSlackChannelIsAllowed(t *testing.T) {
|
|
msgBus := bus.NewMessageBus()
|
|
|
|
t.Run("empty allowlist allows all", func(t *testing.T) {
|
|
cfg := config.SlackConfig{
|
|
BotToken: "xoxb-test",
|
|
AppToken: "xapp-test",
|
|
AllowFrom: []string{},
|
|
}
|
|
ch, _ := NewSlackChannel(cfg, msgBus)
|
|
if !ch.IsAllowed("U_ANYONE") {
|
|
t.Error("empty allowlist should allow all users")
|
|
}
|
|
})
|
|
|
|
t.Run("allowlist restricts users", func(t *testing.T) {
|
|
cfg := config.SlackConfig{
|
|
BotToken: "xoxb-test",
|
|
AppToken: "xapp-test",
|
|
AllowFrom: []string{"U_ALLOWED"},
|
|
}
|
|
ch, _ := NewSlackChannel(cfg, msgBus)
|
|
if !ch.IsAllowed("U_ALLOWED") {
|
|
t.Error("allowed user should pass allowlist check")
|
|
}
|
|
if ch.IsAllowed("U_BLOCKED") {
|
|
t.Error("non-allowed user should be blocked")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTruncateStringSlack(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
maxLen int
|
|
want string
|
|
}{
|
|
{"hello", 10, "hello"},
|
|
{"hello world", 5, "hello"},
|
|
{"", 5, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := truncateStringSlack(tt.input, tt.maxLen)
|
|
if got != tt.want {
|
|
t.Errorf("truncateStringSlack(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
|
|
}
|
|
}
|
|
}
|