Files
Agent-System/internal/stream/classify_test.go
weichenandCursor f6813cc272 feat(api): add additive HTTP/SSE error envelopes
Public errors now emit code, safe message, and request_id while keeping the legacy error field. Raw SQL, paths, tokens, and XSS payloads are stripped; the frontend branches on code instead of substring matching.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 14:40:56 +00:00

102 lines
3.2 KiB
Go

package stream
import (
"errors"
"net"
"strings"
"testing"
)
func TestClassifyError_TimeoutIsTransient(t *testing.T) {
info := ClassifyError(errors.New("context deadline exceeded"), "dialogue_generating", "req_1", "sess_1", "")
if info.Class != ErrorClassTransient {
t.Fatalf("class=%s", info.Class)
}
if info.Code != "CHAT_TIMEOUT" && info.Code != "TRANSIENT_ERROR" {
// Accept either stable code chosen in implementation; prefer CHAT_TIMEOUT when message contains timeout/deadline
t.Fatalf("code=%s", info.Code)
}
if len(info.Actions) != 1 || info.Actions[0] != "retry" {
t.Fatalf("actions=%v", info.Actions)
}
if info.RequestID != "req_1" || info.PhaseID != "dialogue_generating" {
t.Fatalf("meta=%+v", info)
}
}
func TestClassifyError_DoesNotLeakSQL(t *testing.T) {
info := ClassifyError(errors.New("pq: password authentication failed /home/weichen/x"), "dialogue_generating", "req_x", "sess_1", "")
if strings.Contains(info.Upstream, "pq:") || strings.Contains(info.Upstream, "/home/") || strings.Contains(info.UserMessage, "pq:") {
t.Fatalf("%+v", info)
}
if info.Message != info.UserMessage {
t.Fatal("message alias")
}
}
func TestClassifyError_GenericIsFatal(t *testing.T) {
info := ClassifyError(errors.New("invalid project config"), "intent_recognizing", "req_2", "sess_1", "")
if info.Class != ErrorClassFatal {
t.Fatalf("class=%s", info.Class)
}
hasRestart := false
for _, a := range info.Actions {
if a == "restart" {
hasRestart = true
}
}
if !hasRestart {
t.Fatalf("expected restart action, got %v", info.Actions)
}
}
func TestClassifyError_WorkspaceHintIsResumable(t *testing.T) {
info := ClassifyError(errors.New("step failed mid-run"), "step_executing", "req_3", "sess_1", "research_123")
if info.Class != ErrorClassResumable {
t.Fatalf("class=%s", info.Class)
}
hasContinue := false
hasRestart := false
for _, a := range info.Actions {
if a == "continue" {
hasContinue = true
}
if a == "restart" {
hasRestart = true
}
}
if !hasContinue {
t.Fatalf("expected continue, got %v", info.Actions)
}
if !hasRestart {
t.Fatalf("expected restart, got %v", info.Actions)
}
}
func TestClassifyError_DialogueNotNetwork(t *testing.T) {
info := ClassifyError(errors.New("dialogue generation failed"), "dialogue_generating", "req_5", "sess_1", "")
if info.Class == ErrorClassTransient {
t.Fatalf("dialogue substring must not classify as transient/network, class=%s code=%s", info.Class, info.Code)
}
if info.Class != ErrorClassFatal {
t.Fatalf("class=%s", info.Class)
}
}
func TestClassifyError_NetworkIsTransient(t *testing.T) {
info := ClassifyError(&net.OpError{Op: "dial", Err: errors.New("connection refused")}, "dialogue_generating", "req_4", "sess_1", "")
if info.Class != ErrorClassTransient {
t.Fatalf("class=%s", info.Class)
}
}
func TestClassifyError_DocumentBusyIsVisibleStateWithoutRetry(t *testing.T) {
info := ClassifyError(errors.New("文档正在被 squad(run_1) 编辑中,禁止并行修改"), "", "req_busy", "sess_1", "")
if info.Code != "DOC_BUSY" {
t.Fatalf("code=%s", info.Code)
}
if len(info.Actions) != 0 {
t.Fatalf("busy state must not encourage duplicate retry, actions=%v", info.Actions)
}
}