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
+217
View File
@@ -0,0 +1,217 @@
package main
import (
"context"
"errors"
"strings"
"sync"
"testing"
"time"
"github.com/go-telegram/bot"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type draftRecorder struct {
mu sync.Mutex
calls []bot.SendMessageDraftParams
err error
delay time.Duration
}
func (r *draftRecorder) send(_ context.Context, p *bot.SendMessageDraftParams) (bool, error) {
time.Sleep(r.delay)
r.mu.Lock()
defer r.mu.Unlock()
r.calls = append(r.calls, *p)
return r.err == nil, r.err
}
func (r *draftRecorder) texts() []string {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]string, len(r.calls))
for i, c := range r.calls {
out[i] = c.Text
}
return out
}
func (r *draftRecorder) count() int { return len(r.texts()) }
const testDraftInterval = 40 * time.Millisecond
func newTestDraft(t *testing.T, rec *draftRecorder, onFirst func()) *draftStream {
t.Helper()
b, mockTg := setupBotForTest(t, 123)
mockTg.SendMessageDraftFunc = rec.send
d := b.newDraftStream(context.Background(), 123, "7", onFirst)
d.interval = testDraftInterval
d.keepalive = time.Hour
d.start()
t.Cleanup(d.close)
return d
}
func eventuallyCalls(t *testing.T, rec *draftRecorder, n int) {
t.Helper()
require.Eventually(t, func() bool { return rec.count() == n }, time.Second, 2*time.Millisecond)
}
func TestDraftStream_FirstUpdateSendsPromptlyThenThrottles(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{}
d := newTestDraft(t, rec, nil)
d.update("Hel")
eventuallyCalls(t, rec, 1)
assert.Equal(t, "Hel", rec.texts()[0], "the first text goes out without waiting for the window")
d.update("Hello")
d.update("Hello, wor")
time.Sleep(testDraftInterval / 4)
assert.Equal(t, 1, rec.count(), "inside the window nothing is sent")
eventuallyCalls(t, rec, 2)
assert.Equal(t, "Hello, wor", rec.texts()[1], "only the newest text is sent when the window elapses")
}
func TestDraftStream_SendsAreSequential(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{delay: 3 * testDraftInterval}
d := newTestDraft(t, rec, nil)
d.update("a")
time.Sleep(testDraftInterval)
d.update("ab")
time.Sleep(testDraftInterval)
d.update("abc")
eventuallyCalls(t, rec, 2)
time.Sleep(2 * testDraftInterval)
assert.Equal(t, []string{"a", "abc"}, rec.texts(), "a slow send never overlaps the next; the newest text wins")
}
func TestDraftStream_ParamsCarryStopButton(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{}
d := newTestDraft(t, rec, nil)
d.update("x")
eventuallyCalls(t, rec, 1)
p := rec.calls[0]
assert.Equal(t, "7", p.DraftID)
assert.Equal(t, int64(123), p.ChatID)
assert.True(t, p.CanStop)
assert.True(t, p.KeepOnStop)
}
func TestDraftStream_OnFirstFiresOnce(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{}
var mu sync.Mutex
fired := 0
d := newTestDraft(t, rec, func() { mu.Lock(); fired++; mu.Unlock() })
d.update("a")
eventuallyCalls(t, rec, 1)
d.update("ab")
eventuallyCalls(t, rec, 2)
mu.Lock()
defer mu.Unlock()
assert.Equal(t, 1, fired)
}
func TestDraftStream_FailureStopsFurtherSends(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{err: errors.New("TEXTDRAFT_PEER_INVALID")}
fired := false
d := newTestDraft(t, rec, func() { fired = true })
d.update("a")
eventuallyCalls(t, rec, 1)
d.update("ab")
time.Sleep(3 * testDraftInterval)
assert.Equal(t, 1, rec.count(), "after a failure the draft goes quiet")
assert.False(t, fired, "a rejected draft never commits the turn")
assert.Equal(t, "", d.current(), "nothing was shown to the user")
}
func TestDraftStream_ClampsToTelegramLimit(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{}
d := newTestDraft(t, rec, nil)
d.update(strings.Repeat("é", telegramMessageLimit+500))
eventuallyCalls(t, rec, 1)
assert.Equal(t, telegramMessageLimit, len([]rune(rec.texts()[0])), "a draft over the cap is cut, not rejected")
assert.Eventually(t, func() bool { return d.current() != "" }, time.Second, 2*time.Millisecond)
}
func TestDraftStream_KeepaliveResendsDuringSilence(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{}
d := newTestDraft(t, rec, nil)
d.keepalive = 2 * testDraftInterval
d.update("thinking")
eventuallyCalls(t, rec, 1)
require.Eventually(t, func() bool { return rec.count() >= 2 }, time.Second, 2*time.Millisecond)
assert.Equal(t, "thinking", rec.texts()[1], "silence re-sends the same text to keep the draft alive")
}
func TestDraftStream_CloseWaitsAndSilences(t *testing.T) { //NOSONAR go:S100 -- underscore separation is idiomatic in Go test names
rec := &draftRecorder{delay: 2 * testDraftInterval}
d := newTestDraft(t, rec, nil)
d.update("a")
time.Sleep(testDraftInterval / 4) // the send is now in flight
d.close()
n := rec.count()
assert.LessOrEqual(t, n, 1)
d.update("after close")
time.Sleep(3 * testDraftInterval)
assert.Equal(t, n, rec.count(), "nothing is sent after close returns")
}
func TestDraftEligible(t *testing.T) {
b, _ := setupBotForTest(t, 123)
assert.False(t, b.draftEligible(123, ""), "off by default")
b.config.StreamDrafts = true
assert.True(t, b.draftEligible(123, ""))
assert.False(t, b.draftEligible(123, "biz-1"), "business connections keep segments")
assert.False(t, b.draftEligible(-100123, ""), "groups keep segments")
}
func TestSplitTelegramMessage(t *testing.T) {
assert.Nil(t, splitTelegramMessage(" ", 10))
assert.Equal(t, []string{"short"}, splitTelegramMessage("short", 10))
parts := splitTelegramMessage("para one\n\npara two\n\npara three", 20)
assert.Equal(t, []string{"para one\n\npara two", "para three"}, parts, "prefers paragraph breaks")
words := splitTelegramMessage("one two three four", 9)
assert.Equal(t, []string{"one two", "three", "four"}, words, "falls back to spaces")
for _, p := range splitTelegramMessage(strings.Repeat("a", 25), 10) {
assert.LessOrEqual(t, len([]rune(p)), 10, "hard cut when there is no separator")
}
multibyte := splitTelegramMessage(strings.Repeat("é", 12), 5)
assert.Len(t, multibyte, 3, "limits count runes, not bytes")
}
func TestClampRunes(t *testing.T) {
assert.Equal(t, "abc", clampRunes("abc", 5))
assert.Equal(t, "abc", clampRunes("abcde", 3))
assert.Equal(t, "éé", clampRunes("ééé", 2))
assert.Equal(t, "", clampRunes("x", 0))
}
func TestJoinNonEmpty(t *testing.T) {
assert.Equal(t, "", joinNonEmpty("", ""))
assert.Equal(t, "a", joinNonEmpty("a", ""))
assert.Equal(t, "b", joinNonEmpty("", "b"))
assert.Equal(t, "a\n\nb", joinNonEmpty("a", "b"))
}