package errors import ( "bytes" "encoding/json" stderrors "errors" "fmt" "strings" "testing" ) func TestError_Unwrap_NilCause(t *testing.T) { t.Parallel() e := &Error{Category: CategoryInternal, Message: "test"} if e.Unwrap() != nil { t.Fatal("expected nil cause") } } func TestError_Unwrap_WithCause(t *testing.T) { t.Parallel() cause := fmt.Errorf("root cause") e := &Error{Category: CategoryInternal, Message: "wrapper", Cause: cause} if e.Unwrap() != cause { t.Fatalf("expected cause, got %v", e.Unwrap()) } } func TestWithCause_Option(t *testing.T) { t.Parallel() cause := fmt.Errorf("underlying") e := NewAPI("api error", WithCause(cause)) var appErr *Error if !stderrors.As(e, &appErr) { t.Fatal("expected *Error type") } if appErr.Cause != cause { t.Fatal("missing cause") } } func TestErrorsIs_ChainTraversal(t *testing.T) { t.Parallel() sentinel := fmt.Errorf("sentinel") e := &Error{Category: CategoryAuth, Message: "auth failed", Cause: sentinel} if !stderrors.Is(e, sentinel) { t.Fatal("errors.Is should find sentinel through Unwrap") } } func TestPrintJSON_WithCause(t *testing.T) { t.Parallel() cause := fmt.Errorf("db connection failed") e := &Error{Category: CategoryInternal, Message: "save failed", Cause: cause} var buf bytes.Buffer PrintJSON(&buf, e) if !strings.Contains(buf.String(), "db connection failed") { t.Fatalf("expected cause in JSON output: %s", buf.String()) } } func TestPrintJSON_WithoutCause(t *testing.T) { t.Parallel() e := &Error{Category: CategoryAuth, Message: "unauthorized"} var buf bytes.Buffer PrintJSON(&buf, e) if strings.Contains(buf.String(), `"cause"`) { t.Fatalf("should not contain cause key: %s", buf.String()) } } func TestPrintJSON_AllFields(t *testing.T) { t.Parallel() rpcData := json.RawMessage(`{"detail":"oops"}`) e := NewAPI("full error", WithOperation("tools/call"), WithReason("timeout"), WithServerKey("doc"), WithHint("retry later"), WithRetryable(true), WithActions("action1", "action2"), WithSnapshot("/tmp/snap.json"), WithRPCCode(-32600), WithRPCData(rpcData), WithCause(fmt.Errorf("root")), ) var buf bytes.Buffer PrintJSON(&buf, e) out := buf.String() for _, expected := range []string{"timeout", "tools/call", "doc", "retry later", "action1", "snap.json", "-32600", "oops", "root"} { if !strings.Contains(out, expected) { t.Fatalf("missing %q in output: %s", expected, out) } } } func TestPrintHuman_WithCause_Verbose(t *testing.T) { t.Parallel() cause := fmt.Errorf("timeout") e := &Error{Category: CategoryDiscovery, Message: "discovery failed", Cause: cause} var buf bytes.Buffer PrintHumanAt(&buf, e, VerbosityVerbose) if !strings.Contains(buf.String(), "timeout") { t.Fatalf("expected cause in verbose human output: %s", buf.String()) } } func TestPrintHuman_WithCause_NormalHidesCause(t *testing.T) { t.Parallel() cause := fmt.Errorf("timeout") e := &Error{Category: CategoryDiscovery, Message: "discovery failed", Cause: cause} var buf bytes.Buffer PrintHuman(&buf, e) if strings.Contains(buf.String(), "Cause:") { t.Fatalf("normal mode should not show Cause: %s", buf.String()) } } func TestPrintHuman_AllFields_Debug(t *testing.T) { t.Parallel() e := NewAPI("api error", WithOperation("initialize"), WithReason("connection_refused"), WithServerKey("doc"), WithHint("check network"), WithRetryable(true), WithActions("run again"), WithSnapshot("/tmp/snap"), WithRPCCode(-32601), WithRPCData(json.RawMessage(`{"x":1}`)), WithCause(fmt.Errorf("network")), ) var buf bytes.Buffer PrintHumanAt(&buf, e, VerbosityDebug) out := buf.String() for _, expected := range []string{"API", "initialize", "connection_refused", "doc", "check network", "run again", "snap", "-32601", "network", "Retryable"} { if !strings.Contains(out, expected) { t.Fatalf("missing %q in debug human output: %s", expected, out) } } } func TestPrintHuman_NilError(t *testing.T) { t.Parallel() var buf bytes.Buffer if err := PrintHuman(&buf, nil); err != nil { t.Fatalf("unexpected error: %v", err) } if buf.Len() != 0 { t.Fatalf("expected empty output, got: %s", buf.String()) } } func TestPrintHuman_PlainError(t *testing.T) { t.Parallel() var buf bytes.Buffer PrintHuman(&buf, fmt.Errorf("plain error")) if !strings.Contains(buf.String(), "plain error") { t.Fatalf("expected plain error: %s", buf.String()) } } func TestPrintJSON_PlainError(t *testing.T) { t.Parallel() var buf bytes.Buffer PrintJSON(&buf, fmt.Errorf("plain error")) if !strings.Contains(buf.String(), "plain error") { t.Fatalf("expected plain error: %s", buf.String()) } } func TestExitCode_AllCategories(t *testing.T) { t.Parallel() tests := []struct { cat Category want int }{ {CategoryAPI, 1}, {CategoryAuth, 2}, {CategoryValidation, 3}, {CategoryDiscovery, 6}, {CategoryInternal, 5}, } for _, tt := range tests { e := &Error{Category: tt.cat} if got := e.ExitCode(); got != tt.want { t.Errorf("ExitCode(%s) = %d, want %d", tt.cat, got, tt.want) } } } func TestExitCode_NonTyped(t *testing.T) { t.Parallel() if got := ExitCode(fmt.Errorf("plain")); got != 5 { t.Fatalf("expected 5, got %d", got) } } func TestWithRPCData_Empty(t *testing.T) { t.Parallel() opt := WithRPCData(json.RawMessage("")) if opt == nil { t.Fatal("expected non-nil no-op option for empty RPC data") } // Should not panic when applied e := &Error{} opt(e) if len(e.RPCData) != 0 { t.Fatal("empty RPC data option should be a no-op") } } func TestWithActions_Empty(t *testing.T) { t.Parallel() e := NewAPI("test", WithActions("", "", "")) var appErr *Error stderrors.As(e, &appErr) if len(appErr.Actions) != 0 { t.Fatalf("expected no actions, got %v", appErr.Actions) } } func TestSafePath_Valid(t *testing.T) { t.Parallel() if err := SafePath("/tmp/test.json"); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestSafePath_Traversal(t *testing.T) { t.Parallel() if err := SafePath("../../etc/passwd"); err == nil { t.Fatal("expected error for path traversal") } } func TestSafePath_Empty(t *testing.T) { t.Parallel() if err := SafePath(""); err == nil { t.Fatal("expected error for empty path") } } func TestCrossPlatformCoverageIsMCPToolNotFound(t *testing.T) { t.Parallel() cases := []struct { name string err error want bool }{ {name: "nil", err: nil, want: false}, {name: "plain message", err: fmt.Errorf("unknown tool get_cell_doc"), want: true}, {name: "structured server code", err: NewAPI("call failed", WithServerDiag(ServerDiagnostics{ServerErrorCode: "TOOL_NOT_FOUND"})), want: true}, {name: "ordinary failure", err: NewAPI("permission denied", WithReason("forbidden")), want: false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := IsMCPToolNotFound(tc.err); got != tc.want { t.Fatalf("IsMCPToolNotFound(%v) = %v, want %v", tc.err, got, tc.want) } }) } }