Files

265 lines
6.8 KiB
Go

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)
}
})
}
}