From bb5aa18f370099b6418a9dd51c2fe9213b2ac6b4 Mon Sep 17 00:00:00 2001 From: Badgerbees Date: Mon, 16 Mar 2026 23:28:27 +0700 Subject: [PATCH] fix(telegram): address copilot feedback, filter empty chunks and add word-boundary regression test --- pkg/channels/telegram/telegram.go | 21 ++++++++--- pkg/channels/telegram/telegram_test.go | 49 +++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index ea8699f4d..df3935127 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -236,6 +236,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err splitIdx = smallerLen } + // Ensure we split at a valid index to guarantee forward progress. if splitIdx <= 0 { splitIdx = 1 } @@ -243,19 +244,31 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err // Attempt to split using the determined index, ensuring code block integrity is maintained. subChunks := channels.SplitMessage(chunk, splitIdx) - // Force a manual split if the message remains monolithic after the attempt. + // Safety fallback: If SplitMessage failed to shorten the chunk, force a manual split. if len(subChunks) == 1 && subChunks[0] == chunk { part1 := string(runeChunk[:splitIdx]) + subChunks = []string{part1} + nextStart := splitIdx if foundBreak && nextStart < len(runeChunk) { nextStart++ } - part2 := string(runeChunk[nextStart:]) - subChunks = []string{part1, part2} + if nextStart < len(runeChunk) { + part2 := string(runeChunk[nextStart:]) + subChunks = append(subChunks, part2) + } + } + + // Filter out empty chunks to avoid sending empty messages to Telegram. + nonEmpty := make([]string, 0, len(subChunks)) + for _, s := range subChunks { + if s != "" { + nonEmpty = append(nonEmpty, s) + } } // Push sub-chunks back to the front of the queue - queue = append(subChunks, queue...) + queue = append(nonEmpty, queue...) continue } diff --git a/pkg/channels/telegram/telegram_test.go b/pkg/channels/telegram/telegram_test.go index c2186d0a3..1f838ce92 100644 --- a/pkg/channels/telegram/telegram_test.go +++ b/pkg/channels/telegram/telegram_test.go @@ -39,7 +39,11 @@ func (s *stubCaller) Call(ctx context.Context, url string, data *ta.RequestData) type stubConstructor struct{} func (s *stubConstructor) JSONRequest(parameters any) (*ta.RequestData, error) { - return &ta.RequestData{}, nil + b, _ := json.Marshal(parameters) + return &ta.RequestData{ + ContentType: "application/json", + BodyRaw: b, + }, nil } func (s *stubConstructor) MultipartRequest( @@ -235,6 +239,49 @@ func TestSend_MarkdownShortButHTMLLong_MultipleCalls(t *testing.T) { ) } +func TestSend_HTMLOverflow_WordBoundary(t *testing.T) { + caller := &stubCaller{ + callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) { + return successResponse(t), nil + }, + } + ch := newTestChannel(t, caller) + + // We want to force a split near index ~2600. + // Prefix of 430 bold units (6 chars each) = 2580 chars. + // Expansion per unit is 3 chars. 2580 + 430*3 = 3870. + prefix := strings.Repeat("**a** ", 430) + targetWord := "TARGETWORDTHATSTAYSTOGETHER" + suffix := strings.Repeat(" **b**", 250) + content := prefix + targetWord + suffix + + err := ch.Send(context.Background(), bus.OutboundMessage{ + ChatID: "123456", + Content: content, + }) + + assert.NoError(t, err) + + foundFullWord := false + for i, call := range caller.calls { + var params map[string]interface{} + _ = json.Unmarshal(call.Data.BodyRaw, ¶ms) + text, _ := params["text"].(string) + + // The word might be wrapped in tags if we were unlucky, + // but since it's plain text in our 'content', it should stay plain. + hasWord := strings.Contains(text, targetWord) + t.Logf("Chunk %d length: %d, contains target word: %v", i, len(text), hasWord) + + if hasWord { + foundFullWord = true + break + } + } + + assert.True(t, foundFullWord, "The target word should not be split between chunks") +} + func TestSend_NotRunning(t *testing.T) { caller := &stubCaller{ callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {