Files
goclaw/internal/channels/telegram/format_test.go
T
badgerbees 1c4dce0ccf feat(telegram): implement robust message splitting and dynamic HTML retry logic (#236)
* feat(telegram): implement robust message splitting and dynamic HTML retry logic

* fix(telegram): fix sendHTML error chain regression and add split depth limit

- Re-check err.Error() in thread-not-found handler instead of stale errStr,
  restoring the original chained fallback behavior
- Add maxSplitDepth (5) to prevent unbounded recursion when Telegram
  repeatedly rejects split chunks
- Rename misleading test case to reflect actual monolithic fallback behavior

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-17 13:01:20 +07:00

133 lines
3.7 KiB
Go

package telegram
import (
"strings"
"testing"
)
func TestDisplayWidth(t *testing.T) {
tests := []struct {
input string
want int
}{
{"hello", 5},
{"Khởi động", 9}, // Vietnamese diacritics = single-width
{"Hardware tối thiểu", 18}, // Vietnamese diacritics = single-width
{"Ngôn ngữ", 8},
{"đ", 1}, // Vietnamese d-stroke = single-width
{"中文", 4}, // CJK = double-width
{"日本語", 6}, // CJK = double-width
}
for _, tt := range tests {
got := displayWidth(tt.input)
if got != tt.want {
t.Errorf("displayWidth(%q) = %d, want %d", tt.input, got, tt.want)
}
}
}
func TestRenderTableAsCode_Vietnamese(t *testing.T) {
lines := []string{
"| Metric | OpenClaw | ZeroClaw |",
"|--------|----------|----------|",
"| Ngôn ngữ | TypeScript/Node.js | Rust |",
"| Khởi động | > 500s | < 10ms |",
"| Hardware tối thiểu | Mac mini $599 | $10 (bao gồm cả Raspberry Pi) |",
}
result := renderTableAsCode(lines)
// Every non-separator line should have the same number of pipes
resultLines := strings.Split(result, "\n")
if len(resultLines) < 3 {
t.Fatalf("expected at least 3 lines, got %d", len(resultLines))
}
// Check separator line width matches header line width
headerWidth := displayWidth(resultLines[0])
sepWidth := displayWidth(resultLines[1])
if headerWidth != sepWidth {
t.Errorf("header width (%d) != separator width (%d)\nheader: %s\nsep: %s",
headerWidth, sepWidth, resultLines[0], resultLines[1])
}
// Check all data rows match header width
for i := 2; i < len(resultLines); i++ {
rowWidth := displayWidth(resultLines[i])
if rowWidth != headerWidth {
t.Errorf("row %d width (%d) != header width (%d)\nrow: %s\nheader: %s",
i, rowWidth, headerWidth, resultLines[i], resultLines[0])
}
}
}
func TestChunkHTML(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
want []string
}{
{
name: "natural boundary",
input: "hello world",
maxLen: 6,
want: []string{"hello", "world"},
},
{
name: "tag exceeds maxLen uses fallback",
input: "hello <a href='url'>link</a> world",
maxLen: 12,
want: []string{"hello", "<a href='url", "'>link</a>", "world"},
},
{
name: "avoid mid-tag split safe",
input: "hello <a href='url'>link</a> world",
maxLen: 25,
// "hello " (6) -> remaining "<a href='url'>link</a> world" (28)
// maxLen 25. remaining[:25] is "<a href='url'>link</a> wo"
// lastOpen is at "</a" (18). lastClose at "</a>" (21).
// lastOpen < lastClose (18 < 21). No cut change from safety.
// lastSpace is at index 22 (" world"). cutAt=23.
want: []string{"hello", "<a href='url'>link</a>", "world"},
},
{
name: "avoid mid-entity split",
input: "hello &amp; world",
maxLen: 9,
// "hello " (6) -> then "&amp; world"
// At second chunk: remaining="&amp; world", maxLen=9
// remaining[:9] is "&amp; wor"
// lastSpace is at index 5. cutAt=6.
want: []string{"hello", "&amp;", "world"},
},
{
name: "monolithic fallback",
input: "monolithicblock",
maxLen: 5,
want: []string{"monol", "ithic", "block"},
},
{
name: "paragraph preferred",
input: "para1\n\npara2\nline3",
maxLen: 10,
want: []string{"para1", "para2", "line3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := chunkHTML(tt.input, tt.maxLen)
if len(got) != len(tt.want) {
t.Fatalf("chunkHTML() returned %d chunks, want %d\ngot: %q\nwant: %q", len(got), len(tt.want), got, tt.want)
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("chunk[%d] = %q, want %q", i, got[i], tt.want[i])
}
}
})
}
}