Debounce and caching

This commit is contained in:
HugeFrog24
2026-09-09 18:18:18 +02:00
parent 82fff17e66
commit ffb3a31db8
46 changed files with 8764 additions and 2 deletions
+181
View File
@@ -0,0 +1,181 @@
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"
)
// storeUserMessage puts a user message with a Telegram id into DB and memory,
// the way screenIncomingMessage does, without going through handleUpdate.
func storeUserMessage(t *testing.T, b *Bot, chatID int64, telegramID int, text string) Message {
t.Helper()
msg := b.createMessage(chatID, chatID, "owner", "user", text, true)
msg.TelegramMessageID = telegramID
require.NoError(t, b.storeMessage(&msg))
b.addMessageToChatMemory(b.getOrCreateChatMemory(chatID), msg)
return msg
}
func editUpdate(chatID int64, telegramID int, text string) *models.Update {
return &models.Update{EditedMessage: &models.Message{
ID: telegramID, Chat: models.Chat{ID: chatID}, From: &models.User{ID: chatID, Username: "owner"}, Text: text,
}}
}
func TestHandleUpdate_EditedMessageUpdatesTextAndRearmsWindow(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)
const chatID int64 = 123
from := &models.User{ID: chatID, Username: "owner"}
b.handleUpdate(context.Background(), nil, &models.Update{Message: &models.Message{
ID: 77, Chat: models.Chat{ID: chatID}, From: from, Text: "How to use libtik",
}})
require.True(t, b.hasPendingIntake(chatID))
b.intakeBuffersMu.Lock()
seqBefore := b.intakeBuffers[chatID].seq
timerBefore := b.intakeBuffers[chatID].timer
b.intakeBuffersMu.Unlock()
b.handleUpdate(context.Background(), nil, editUpdate(chatID, 77, "How to use libtibik"))
var stored Message
require.NoError(t, b.db.Where("chat_id = ? AND telegram_message_id = ?", chatID, 77).First(&stored).Error)
assert.Equal(t, "How to use libtibik", stored.Text, "database copy follows the edit")
mem := b.getOrCreateChatMemory(chatID)
assert.Equal(t, "How to use libtibik", mem.Messages[len(mem.Messages)-1].Text, "memory copy follows the edit")
require.True(t, b.hasPendingIntake(chatID), "an edit must not flush or drop the batch")
b.intakeBuffersMu.Lock()
defer b.intakeBuffersMu.Unlock()
assert.Equal(t, seqBefore, b.intakeBuffers[chatID].seq, "same batch")
assert.NotSame(t, timerBefore, b.intakeBuffers[chatID].timer, "the window restarted")
assert.Equal(t, 1, b.intakeBuffers[chatID].count, "an edit is not a new message")
}
func TestHandleUpdate_EditFromEmojiToTextClearsEmojiFlag(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)
const chatID int64 = 123
b.handleUpdate(context.Background(), nil, &models.Update{Message: &models.Message{
ID: 78, Chat: models.Chat{ID: chatID}, From: &models.User{ID: chatID}, Text: "👍",
}})
b.intakeBuffersMu.Lock()
require.True(t, b.intakeBuffers[chatID].allEmojiOnly)
b.intakeBuffersMu.Unlock()
b.handleUpdate(context.Background(), nil, editUpdate(chatID, 78, "can you explain that again?"))
b.intakeBuffersMu.Lock()
defer b.intakeBuffersMu.Unlock()
assert.False(t, b.intakeBuffers[chatID].allEmojiOnly, "a real question must not get an emoji-only reply")
}
func TestHandleUpdate_CaptionEditApplies(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
b, _ := setupBotForTest(t, 123)
const chatID int64 = 123
storeUserMessage(t, b, chatID, 79, "old caption")
b.handleUpdate(context.Background(), nil, &models.Update{EditedMessage: &models.Message{
ID: 79, Chat: models.Chat{ID: chatID}, From: &models.User{ID: chatID}, Caption: "new caption",
}})
var stored Message
require.NoError(t, b.db.Where("chat_id = ? AND telegram_message_id = ?", chatID, 79).First(&stored).Error)
assert.Equal(t, "new caption", stored.Text)
}
func TestHandleUpdate_EditOfAnsweredMessageOnlyCorrectsRecord(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)
const chatID int64 = 123
storeUserMessage(t, b, chatID, 80, "first question")
_, err := b.screenOutgoingMessage(chatID, "first answer")
require.NoError(t, err)
// A newer, unrelated message is waiting in its window.
b.handleUpdate(context.Background(), nil, &models.Update{Message: &models.Message{
ID: 81, Chat: models.Chat{ID: chatID}, From: &models.User{ID: chatID}, Text: "second question",
}})
b.intakeBuffersMu.Lock()
timerBefore := b.intakeBuffers[chatID].timer
b.intakeBuffersMu.Unlock()
b.handleUpdate(context.Background(), nil, editUpdate(chatID, 80, "first question, corrected"))
var stored Message
require.NoError(t, b.db.Where("telegram_message_id = ?", 80).First(&stored).Error)
assert.Equal(t, "first question, corrected", stored.Text, "the record is corrected")
b.intakeBuffersMu.Lock()
defer b.intakeBuffersMu.Unlock()
assert.Same(t, timerBefore, b.intakeBuffers[chatID].timer, "an answered message's edit must not touch another batch's window")
}
func TestHandleUpdate_EditRestartsUncommittedTurn(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
b, _ := setupBotForTest(t, 123)
const chatID int64 = 123
msg := storeUserMessage(t, b, chatID, 82, "how do I use libtik")
turn, ctx, ok := b.beginTextTurn(textReq(chatID), msg.ID)
require.True(t, ok)
b.handleUpdate(context.Background(), nil, editUpdate(chatID, 82, "how do I use libtibik"))
assert.ErrorIs(t, ctx.Err(), context.Canceled, "the model was reading the typo; rerun with the fix")
assert.Equal(t, cancelRestart, b.turnReason(turn))
next := b.finishTurn(turn)
require.NotNil(t, next)
assert.Equal(t, chatID, next.chatID)
}
func TestHandleUpdate_EditOfUnknownMessageIsIgnored(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
b, _ := setupBotForTest(t, 123)
b.handleUpdate(context.Background(), nil, editUpdate(123, 999, "never stored"))
assert.False(t, b.hasPendingIntake(123))
var count int64
b.db.Model(&Message{}).Where("chat_id = ?", 123).Count(&count)
assert.Zero(t, count, "an edit never creates a message")
}
func TestHandleUpdate_StopGenerationCancelsMatchingTurn(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
b, _ := setupBotForTest(t, 123)
turn, ctx, ok := b.beginTextTurn(textReq(123), 1)
require.True(t, ok)
b.setTurnDraft(turn, turn.draftIDString())
b.handleUpdate(context.Background(), nil, &models.Update{
StoppedMessageGeneration: &models.MessageGenerationStopped{
Chat: models.Chat{ID: 123}, DraftID: int(turn.seq),
},
})
assert.ErrorIs(t, ctx.Err(), context.Canceled)
assert.Equal(t, cancelStop, b.turnReason(turn))
b.finishTurn(turn)
}
func TestClearChatHistory_DiscardsCollectedFlush(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
b, mockTg := setupBotForTest(t, 123)
mockTg.SendMessageFunc = func(_ context.Context, _ *bot.SendMessageParams) (*models.Message, error) {
return &models.Message{}, nil
}
const chatID int64 = 123
turn, _, ok := b.beginTextTurn(textReq(chatID), 5)
require.True(t, ok)
b.markCommitted(turn)
_, _, ok = b.beginTextTurn(textReq(chatID), 7)
require.False(t, ok, "collected behind the running turn")
b.clearChatHistory(context.Background(), chatID, chatID, 0, 0, "", false)
assert.Nil(t, b.finishTurn(turn), "the collected flush must not rerun against the emptied chat")
}