From bfa68892ac3aa871de76329b46036398f02173ce Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Wed, 9 Sep 2026 16:09:39 +0800 Subject: [PATCH] fix(stream): avoid dial substring false-positive in ClassifyError Remove "dial" from networkHints to stop "dialogue" misclassification; extend workspace/resumable and EmitErrorInfo test coverage. Co-authored-by: Cursor --- internal/stream/classify.go | 1 - internal/stream/classify_test.go | 17 +++++++++++++++++ internal/stream/stream_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/internal/stream/classify.go b/internal/stream/classify.go index 3d6f05c4..60bc1098 100644 --- a/internal/stream/classify.go +++ b/internal/stream/classify.go @@ -76,7 +76,6 @@ func isNetworkError(err error, msg string) bool { } networkHints := []string{ - "dial", "connection refused", "eof", "broken pipe", diff --git a/internal/stream/classify_test.go b/internal/stream/classify_test.go index 4274d36a..dcc3eef1 100644 --- a/internal/stream/classify_test.go +++ b/internal/stream/classify_test.go @@ -45,14 +45,31 @@ func TestClassifyError_WorkspaceHintIsResumable(t *testing.T) { 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) { diff --git a/internal/stream/stream_test.go b/internal/stream/stream_test.go index 6dfacc2c..f3c8fd1c 100644 --- a/internal/stream/stream_test.go +++ b/internal/stream/stream_test.go @@ -51,6 +51,33 @@ func TestStreamer_BasicEvents(t *testing.T) { } } +func TestStreamer_EmitError_PopulatesErrorInfo(t *testing.T) { + rawErr := errors.New("invalid project config") + var received StreamEvent + + streamer := NewStreamer(true, func(ev StreamEvent) { + received = ev + }) + + streamer.EmitError(rawErr) + + if received.Type != EventError { + t.Fatalf("type=%s", received.Type) + } + if received.ErrorInfo == nil { + t.Fatal("ErrorInfo is nil") + } + if received.Error != received.ErrorInfo.UserMessage { + t.Fatalf("Error=%q UserMessage=%q", received.Error, received.ErrorInfo.UserMessage) + } + if received.ErrorInfo.Upstream != rawErr.Error() { + t.Fatalf("Upstream=%q want %q", received.ErrorInfo.Upstream, rawErr.Error()) + } + if received.ErrorInfo.Class == "" { + t.Fatal("Class is empty") + } +} + func TestStreamer_FilterDetails(t *testing.T) { var received []StreamEvent