mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
d72c9c1ee6
All 12 channel Send methods now return proper sentinel errors (ErrNotRunning, ErrTemporary, ErrRateLimit, ErrSendFailed) instead of plain fmt.Errorf strings, enabling Manager's sendWithRetry classification logic to actually work. - Add ClassifySendError/ClassifyNetError helpers in errutil.go for HTTP-based channels - LINE/WeCom Bot/WeCom App: use ClassifySendError for HTTP status-based classification - SDK channels (Telegram/Discord/Slack/QQ/DingTalk/Feishu): wrap errors as ErrTemporary - WebSocket channels (OneBot/WhatsApp/MaixCam): wrap write errors as ErrTemporary - WhatsApp: add missing IsRunning() check in Send - WhatsApp/OneBot/MaixCam: add ctx.Done() check before entering write path - Telegram Stop: clean up placeholders sync.Map to prevent state leaks
98 lines
2.6 KiB
Go
98 lines
2.6 KiB
Go
package channels
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
func TestClassifySendError(t *testing.T) {
|
|
raw := fmt.Errorf("some API error")
|
|
|
|
tests := []struct {
|
|
name string
|
|
statusCode int
|
|
wantIs error
|
|
wantNil bool
|
|
}{
|
|
{"429 -> ErrRateLimit", 429, ErrRateLimit, false},
|
|
{"500 -> ErrTemporary", 500, ErrTemporary, false},
|
|
{"502 -> ErrTemporary", 502, ErrTemporary, false},
|
|
{"503 -> ErrTemporary", 503, ErrTemporary, false},
|
|
{"400 -> ErrSendFailed", 400, ErrSendFailed, false},
|
|
{"403 -> ErrSendFailed", 403, ErrSendFailed, false},
|
|
{"404 -> ErrSendFailed", 404, ErrSendFailed, false},
|
|
{"200 -> raw error", 200, nil, false},
|
|
{"201 -> raw error", 201, nil, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ClassifySendError(tt.statusCode, raw)
|
|
if err == nil {
|
|
t.Fatal("expected non-nil error")
|
|
}
|
|
if tt.wantIs != nil {
|
|
if !errors.Is(err, tt.wantIs) {
|
|
t.Errorf("errors.Is(err, %v) = false, want true; err = %v", tt.wantIs, err)
|
|
}
|
|
} else {
|
|
// Should return the raw error unchanged
|
|
if err != raw {
|
|
t.Errorf("expected raw error to be returned unchanged for status %d, got %v", tt.statusCode, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifySendErrorNoFalsePositive(t *testing.T) {
|
|
raw := fmt.Errorf("some error")
|
|
|
|
// 429 should NOT match ErrTemporary or ErrSendFailed
|
|
err := ClassifySendError(429, raw)
|
|
if errors.Is(err, ErrTemporary) {
|
|
t.Error("429 should not match ErrTemporary")
|
|
}
|
|
if errors.Is(err, ErrSendFailed) {
|
|
t.Error("429 should not match ErrSendFailed")
|
|
}
|
|
|
|
// 500 should NOT match ErrRateLimit or ErrSendFailed
|
|
err = ClassifySendError(500, raw)
|
|
if errors.Is(err, ErrRateLimit) {
|
|
t.Error("500 should not match ErrRateLimit")
|
|
}
|
|
if errors.Is(err, ErrSendFailed) {
|
|
t.Error("500 should not match ErrSendFailed")
|
|
}
|
|
|
|
// 400 should NOT match ErrRateLimit or ErrTemporary
|
|
err = ClassifySendError(400, raw)
|
|
if errors.Is(err, ErrRateLimit) {
|
|
t.Error("400 should not match ErrRateLimit")
|
|
}
|
|
if errors.Is(err, ErrTemporary) {
|
|
t.Error("400 should not match ErrTemporary")
|
|
}
|
|
}
|
|
|
|
func TestClassifyNetError(t *testing.T) {
|
|
t.Run("nil error returns nil", func(t *testing.T) {
|
|
if err := ClassifyNetError(nil); err != nil {
|
|
t.Errorf("expected nil, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("non-nil error wraps as ErrTemporary", func(t *testing.T) {
|
|
raw := fmt.Errorf("connection refused")
|
|
err := ClassifyNetError(raw)
|
|
if err == nil {
|
|
t.Fatal("expected non-nil error")
|
|
}
|
|
if !errors.Is(err, ErrTemporary) {
|
|
t.Errorf("errors.Is(err, ErrTemporary) = false, want true; err = %v", err)
|
|
}
|
|
})
|
|
}
|