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) } } func TestClassifyError_DocumentLedgerFKIsLedgerError(t *testing.T) { err := errors.New(`persist document ledger: ERROR: insert or update on table "document_records" violates foreign key constraint "document_records_reviewer_id_fkey" (SQLSTATE 23503)`) info := ClassifyError(err, "", "run_1", "sess-p6417-u9", "") if info.Code != "DOC_LEDGER_ERROR" { t.Fatalf("code=%s", info.Code) } if info.Class != ErrorClassFatal { t.Fatalf("class=%s", info.Class) } if len(info.Actions) != 1 || info.Actions[0] != "restart" { t.Fatalf("actions=%v", info.Actions) } if !strings.Contains(info.UserMessage, "文档职责账本") { t.Fatalf("user_message=%q", info.UserMessage) } // SQL detail must not leak into user-facing fields (Upstream may be stripped by leak filter). if strings.Contains(info.UserMessage, "SQLSTATE") || strings.Contains(info.UserMessage, "foreign key") { t.Fatalf("user message leaked SQL: %q", info.UserMessage) } }