fix(telegram): address copilot feedback, filter empty chunks and add word-boundary regression test

This commit is contained in:
Badgerbees 2026-03-16 23:28:27 +07:00
parent 758a82e219
commit bb5aa18f37
2 changed files with 65 additions and 5 deletions

View file

@ -236,6 +236,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
splitIdx = smallerLen splitIdx = smallerLen
} }
// Ensure we split at a valid index to guarantee forward progress.
if splitIdx <= 0 { if splitIdx <= 0 {
splitIdx = 1 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. // Attempt to split using the determined index, ensuring code block integrity is maintained.
subChunks := channels.SplitMessage(chunk, splitIdx) 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 { if len(subChunks) == 1 && subChunks[0] == chunk {
part1 := string(runeChunk[:splitIdx]) part1 := string(runeChunk[:splitIdx])
subChunks = []string{part1}
nextStart := splitIdx nextStart := splitIdx
if foundBreak && nextStart < len(runeChunk) { if foundBreak && nextStart < len(runeChunk) {
nextStart++ nextStart++
} }
part2 := string(runeChunk[nextStart:]) if nextStart < len(runeChunk) {
subChunks = []string{part1, part2} 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 // Push sub-chunks back to the front of the queue
queue = append(subChunks, queue...) queue = append(nonEmpty, queue...)
continue continue
} }

View file

@ -39,7 +39,11 @@ func (s *stubCaller) Call(ctx context.Context, url string, data *ta.RequestData)
type stubConstructor struct{} type stubConstructor struct{}
func (s *stubConstructor) JSONRequest(parameters any) (*ta.RequestData, error) { 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( 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, &params)
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) { func TestSend_NotRunning(t *testing.T) {
caller := &stubCaller{ caller := &stubCaller{
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) { callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {