mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-08-28 14:01:38 +00:00
177 lines
6.3 KiB
Go
177 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-telegram/bot"
|
|
"github.com/go-telegram/bot/models"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// bufferOnly parks a message in the intake buffer without letting the flush run,
|
|
// by using a window long enough that no test waits it out.
|
|
const bufferOnly = 10 * time.Second
|
|
|
|
func bufferText(b *Bot, chatID int64, isEmojiOnly bool) {
|
|
b.bufferIntake(context.Background(), chatID, 555,
|
|
"tester", "Test", "User", false, "en", int(time.Now().Unix()), "", isEmojiOnly)
|
|
}
|
|
|
|
func TestBufferIntake_CoalescesIntoSingleTurn(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, _ := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = int(bufferOnly / time.Millisecond)
|
|
|
|
// The burst this whole feature exists for: one real question, then filler.
|
|
for i := 0; i < 5; i++ {
|
|
bufferText(b, 900, false)
|
|
}
|
|
|
|
b.intakeBuffersMu.Lock()
|
|
defer b.intakeBuffersMu.Unlock()
|
|
assert.Len(t, b.intakeBuffers, 1, "rapid messages must share one buffer entry")
|
|
assert.Equal(t, 5, b.intakeBuffers[900].count, "all five messages land in the same batch")
|
|
}
|
|
|
|
// Separate chats must not share a window; one user's burst cannot delay another's.
|
|
func TestBufferIntake_IsolatesChats(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, _ := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = int(bufferOnly / time.Millisecond)
|
|
|
|
bufferText(b, 910, false)
|
|
bufferText(b, 911, false)
|
|
bufferText(b, 911, false)
|
|
|
|
b.intakeBuffersMu.Lock()
|
|
defer b.intakeBuffersMu.Unlock()
|
|
assert.Equal(t, 1, b.intakeBuffers[910].count)
|
|
assert.Equal(t, 2, b.intakeBuffers[911].count)
|
|
}
|
|
|
|
func TestBufferIntake_ResetsWindowAndKeepsLatestMetadata(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, _ := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = int(bufferOnly / time.Millisecond)
|
|
|
|
b.bufferIntake(context.Background(), 901, 1, "first", "First", "", false, "en", 1000, "", true)
|
|
b.bufferIntake(context.Background(), 901, 2, "second", "Second", "", true, "de", 2000, "biz-42", false)
|
|
|
|
b.intakeBuffersMu.Lock()
|
|
defer b.intakeBuffersMu.Unlock()
|
|
pending := b.intakeBuffers[901]
|
|
require.NotNil(t, pending)
|
|
|
|
assert.Equal(t, 2, pending.count)
|
|
// OpenClaw semantics: reply metadata follows the most recent message.
|
|
assert.Equal(t, "second", pending.username)
|
|
assert.Equal(t, int64(2), pending.userID)
|
|
assert.Equal(t, "de", pending.languageCode)
|
|
assert.Equal(t, 2000, pending.messageTime)
|
|
assert.Equal(t, "biz-42", pending.businessConnectionID)
|
|
assert.True(t, pending.isPremium)
|
|
// One non-emoji message makes the whole coalesced turn non-emoji.
|
|
assert.False(t, pending.allEmojiOnly)
|
|
}
|
|
|
|
func TestBufferIntake_AllEmojiOnlySurvivesWhenEveryMessageIsEmoji(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, _ := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = int(bufferOnly / time.Millisecond)
|
|
|
|
bufferText(b, 902, true)
|
|
bufferText(b, 902, true)
|
|
|
|
b.intakeBuffersMu.Lock()
|
|
defer b.intakeBuffersMu.Unlock()
|
|
assert.True(t, b.intakeBuffers[902].allEmojiOnly)
|
|
}
|
|
|
|
func TestCancelIntake_DiscardsPendingBatch(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, _ := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = int(bufferOnly / time.Millisecond)
|
|
|
|
bufferText(b, 903, false)
|
|
bufferText(b, 903, false)
|
|
require.True(t, b.hasPendingIntake(903))
|
|
|
|
assert.Equal(t, 2, b.cancelIntake(903), "cancel reports how many messages it discarded")
|
|
assert.False(t, b.hasPendingIntake(903))
|
|
assert.Equal(t, 0, b.cancelIntake(903), "cancelling an empty chat is a no-op")
|
|
}
|
|
|
|
// A cancelled buffer must never dispatch afterwards. This is the openclaw#51046
|
|
// shape: the timer had already been armed when the cancel landed.
|
|
func TestCancelIntake_TimerDoesNotFireAfterCancel(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, _ := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = 20
|
|
|
|
bufferText(b, 904, false)
|
|
b.cancelIntake(904)
|
|
|
|
time.Sleep(80 * time.Millisecond)
|
|
assert.False(t, b.hasPendingIntake(904), "cancelled buffer must not resurrect")
|
|
}
|
|
|
|
// A stale timer from a cancelled batch must not flush a newer batch early.
|
|
func TestFlushIntake_IgnoresSupersededSequence(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, _ := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = int(bufferOnly / time.Millisecond)
|
|
|
|
bufferText(b, 905, false)
|
|
b.intakeBuffersMu.Lock()
|
|
staleSeq := b.intakeBuffers[905].seq
|
|
b.intakeBuffersMu.Unlock()
|
|
|
|
b.cancelIntake(905)
|
|
bufferText(b, 905, false) // new batch, new sequence
|
|
|
|
// The old timer firing late must not consume the new batch.
|
|
b.flushIntake(context.Background(), 905, staleSeq)
|
|
assert.True(t, b.hasPendingIntake(905), "superseded flush must leave the newer batch armed")
|
|
}
|
|
|
|
func TestClearChatHistory_CancelsPendingIntake(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
b, mockTg := setupBotForTest(t, 123)
|
|
b.config.DebounceMs = int(bufferOnly / time.Millisecond)
|
|
mockTg.SendMessageFunc = func(_ context.Context, _ *bot.SendMessageParams) (*models.Message, error) {
|
|
return &models.Message{}, nil
|
|
}
|
|
|
|
const chatID int64 = 906
|
|
bufferText(b, chatID, false)
|
|
require.True(t, b.hasPendingIntake(chatID))
|
|
|
|
b.clearChatHistory(context.Background(), chatID, 123, 0, 0, "", false)
|
|
|
|
assert.False(t, b.hasPendingIntake(chatID),
|
|
"clearing history must disarm the buffer, or deleted messages get replayed into memory")
|
|
}
|
|
|
|
func TestDebounceWindow(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
ms int
|
|
want time.Duration
|
|
}{
|
|
{"unset disables debouncing", 0, 0},
|
|
{"negative disables debouncing", -1, 0},
|
|
{"positive converts to duration", 2500, 2500 * time.Millisecond},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c := BotConfig{DebounceMs: tc.ms}
|
|
assert.Equal(t, tc.want, c.DebounceWindow())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCacheHistoryEnabled_DefaultsOn(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
|
|
assert.True(t, (&BotConfig{}).CacheHistoryEnabled(), "cache_history defaults to enabled")
|
|
|
|
off := false
|
|
assert.False(t, (&BotConfig{CacheHistory: &off}).CacheHistoryEnabled())
|
|
|
|
on := true
|
|
assert.True(t, (&BotConfig{CacheHistory: &on}).CacheHistoryEnabled())
|
|
}
|