225 lines
6.7 KiB
Go
225 lines
6.7 KiB
Go
package bot
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestBuildSessionKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
src SessionSource
|
|
// DM 同 chat 不同 user 应返回相同 key
|
|
wantSame bool
|
|
src2 SessionSource
|
|
}{
|
|
{
|
|
name: "dm same chat different user",
|
|
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user123", UserID: "a"},
|
|
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user123", UserID: "b"},
|
|
wantSame: true,
|
|
},
|
|
{
|
|
name: "dm different chat",
|
|
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user123", UserID: "a"},
|
|
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user456", UserID: "a"},
|
|
wantSame: false,
|
|
},
|
|
{
|
|
name: "direct same chat different user",
|
|
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDirect, ChatID: "guild123", UserID: "a"},
|
|
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDirect, ChatID: "guild123", UserID: "b"},
|
|
wantSame: true,
|
|
},
|
|
{
|
|
name: "direct distinct from dm",
|
|
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDirect, ChatID: "shared", UserID: "a"},
|
|
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "shared", UserID: "a"},
|
|
wantSame: false,
|
|
},
|
|
{
|
|
name: "group same chat different user",
|
|
src: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group1", UserID: "a"},
|
|
src2: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group1", UserID: "b"},
|
|
wantSame: false,
|
|
},
|
|
{
|
|
name: "group same user different chat",
|
|
src: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group1", UserID: "a"},
|
|
src2: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group2", UserID: "a"},
|
|
wantSame: false,
|
|
},
|
|
{
|
|
name: "thread shared",
|
|
src: SessionSource{Platform: PlatformQQ, ChatType: ChatThread, ChatID: "ch1", ThreadID: "th1", UserID: "a"},
|
|
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatThread, ChatID: "ch1", ThreadID: "th1", UserID: "b"},
|
|
wantSame: true,
|
|
},
|
|
{
|
|
name: "different platform same ids",
|
|
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "123", UserID: "u1"},
|
|
src2: SessionSource{Platform: PlatformFeishu, ChatType: ChatDM, ChatID: "123", UserID: "u1"},
|
|
wantSame: false,
|
|
},
|
|
{
|
|
name: "same platform different connection",
|
|
src: SessionSource{Platform: PlatformFeishu, ConnectionID: "feishu-feishu", ChatType: ChatDM, ChatID: "123", UserID: "u1"},
|
|
src2: SessionSource{Platform: PlatformFeishu, ConnectionID: "feishu-lark", ChatType: ChatDM, ChatID: "123", UserID: "u1"},
|
|
wantSame: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
k1 := BuildSessionKey(tt.src)
|
|
k2 := BuildSessionKey(tt.src2)
|
|
if tt.wantSame && k1 != k2 {
|
|
t.Errorf("want same key, got %s != %s", k1, k2)
|
|
}
|
|
if !tt.wantSame && k1 == k2 {
|
|
t.Errorf("want different keys, got %s == %s", k1, k2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSlashBypass(t *testing.T) {
|
|
tests := []struct {
|
|
text string
|
|
bypass bool
|
|
}{
|
|
{"/stop", true},
|
|
{"/stop extra args", true},
|
|
{"/new", true},
|
|
{"/reset", true},
|
|
{"/approve", true},
|
|
{"/deny", true},
|
|
{"/yolo", true},
|
|
{"/yolo on", true},
|
|
{"/mode yolo", true},
|
|
{"/status", true},
|
|
{"/help", true},
|
|
{"hello", false},
|
|
{"/unknown", false},
|
|
{"", false},
|
|
{" /stop", false}, // leading space means not a slash command
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := IsSlashBypass(tt.text)
|
|
if got != tt.bypass {
|
|
t.Errorf("IsSlashBypass(%q) = %v, want %v", tt.text, got, tt.bypass)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionManager_TryAcquire(t *testing.T) {
|
|
sm := NewSessionManager(100 * time.Millisecond)
|
|
|
|
msg := InboundMessage{Text: "hello", Platform: PlatformQQ, ChatType: ChatDM, ChatID: "c1", UserID: "u1"}
|
|
key := BuildSessionKey(msg.Session())
|
|
|
|
// 第一次获取成功
|
|
acquired, merged := sm.TryAcquire(key, msg)
|
|
if !acquired || merged {
|
|
t.Error("first acquire should succeed")
|
|
}
|
|
|
|
// 第二次获取应该排队
|
|
acquired, merged = sm.TryAcquire(key, InboundMessage{Text: "world"})
|
|
if acquired || !merged {
|
|
t.Error("second acquire should merge into queue")
|
|
}
|
|
|
|
// slash bypass 命令应绕过
|
|
acquired, merged = sm.TryAcquire(key, InboundMessage{Text: "/stop"})
|
|
if !acquired || merged {
|
|
t.Error("slash bypass should acquire immediately")
|
|
}
|
|
|
|
// 第一次 Release 返回排队消息
|
|
next := sm.Release(key)
|
|
if next == nil {
|
|
t.Fatal("expected queued message after first release")
|
|
}
|
|
if next.Text != "world" {
|
|
t.Errorf("merged text = %q, want %q", next.Text, "world")
|
|
}
|
|
}
|
|
|
|
func TestSessionManager_Debounce(t *testing.T) {
|
|
sm := NewSessionManager(200 * time.Millisecond)
|
|
|
|
msg := InboundMessage{Text: "first", Platform: PlatformQQ, ChatType: ChatDM, ChatID: "c1", UserID: "u1"}
|
|
key := BuildSessionKey(msg.Session())
|
|
|
|
acquired, _ := sm.TryAcquire(key, msg)
|
|
if !acquired {
|
|
t.Fatal("first acquire should succeed")
|
|
}
|
|
|
|
// 同 session 消息应合并
|
|
sm.TryAcquire(key, InboundMessage{Text: "second"})
|
|
// 在 debounce 窗口内发第三条
|
|
sm.TryAcquire(key, InboundMessage{Text: "third"})
|
|
|
|
next := sm.Release(key)
|
|
if next == nil {
|
|
t.Fatal("expected queued message after release")
|
|
}
|
|
// "second" 和 "third" 合并在队列里("first" 已作为 active 被处理)
|
|
if next.Text != "second\nthird" {
|
|
t.Errorf("merged = %q, want %q", next.Text, "second\nthird")
|
|
}
|
|
}
|
|
|
|
func TestSessionManager_ForceRelease(t *testing.T) {
|
|
sm := NewSessionManager(100 * time.Millisecond)
|
|
|
|
msg := InboundMessage{Text: "test", Platform: PlatformQQ, ChatType: ChatDM, ChatID: "c1", UserID: "u1"}
|
|
key := BuildSessionKey(msg.Session())
|
|
|
|
sm.TryAcquire(key, msg)
|
|
if !sm.IsActive(key) {
|
|
t.Error("should be active")
|
|
}
|
|
|
|
sm.ForceRelease(key)
|
|
if sm.IsActive(key) {
|
|
t.Error("should not be active after force release")
|
|
}
|
|
}
|
|
|
|
func TestHashID(t *testing.T) {
|
|
h1 := hashID("user_12345")
|
|
h2 := hashID("user_12345")
|
|
h3 := hashID("user_67890")
|
|
|
|
if h1 != h2 {
|
|
t.Error("same input should produce same hash")
|
|
}
|
|
if h1 == h3 {
|
|
t.Error("different inputs should produce different hashes")
|
|
}
|
|
if hashID("") != "" {
|
|
t.Error("empty input should produce empty hash")
|
|
}
|
|
}
|
|
|
|
func TestInboundMessage_Session(t *testing.T) {
|
|
msg := InboundMessage{
|
|
Platform: PlatformQQ,
|
|
ConnectionID: "qq-main",
|
|
Domain: "qq",
|
|
ChatType: ChatDM,
|
|
ChatID: "chat1",
|
|
UserID: "user1",
|
|
ThreadID: "thread1",
|
|
}
|
|
|
|
src := msg.Session()
|
|
if src.Platform != PlatformQQ || src.ConnectionID != "qq-main" || src.Domain != "qq" || src.ChatType != ChatDM || src.ChatID != "chat1" || src.UserID != "user1" || src.ThreadID != "thread1" {
|
|
t.Error("Session() should copy all fields")
|
|
}
|
|
}
|