Files
go-telegram-bot/intake_buffer_test.go
2026-07-24 23:41:28 +02:00

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())
}