1package main
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "io"
8 "log"
9 "strings"
10 "testing"
11)
12
13func testBridge() *bridge {
14 return &bridge{
15 servers: map[string]*managedServer{},
16 mapping: map[string]toolMapping{},
17 logger: log.New(io.Discard, "", 0),
18 }
19}
20
21func TestHandleToolCallUnknownToolIsFriendlyError(t *testing.T) {
22 b := testBridge()
23 res := b.handleToolCall("mcp__ghost__tool", nil)
24 if !res.IsError {
25 t.Fatal("expected error result")
26 }
27}
28
29func TestHandleToolCallStartFailureIsFriendlyError(t *testing.T) {
30 b := testBridge()
31 b.servers["bogus"] = newManagedServer("bogus",
32 ServerConfig{Transport: "no-such-transport", ConnectTimeout: 1, RequestTimeout: 1},
33 log.New(io.Discard, "", 0))
34 b.mapping["mcp__bogus__t"] = toolMapping{serverName: "bogus", mcpTool: "t"}
35
36 res := b.handleToolCall("mcp__bogus__t", nil)
37 if !res.IsError {
38 t.Fatal("expected error result for failed server start")
39 }
40}
41
42func TestClassifyTimeoutUsesErrorsIs(t *testing.T) {
43 wrapped := errors.Join(errors.New("call failed"), context.DeadlineExceeded)
44 if !errors.Is(wrapped, context.DeadlineExceeded) {
45 t.Fatal("sanity: errors.Is must see through the wrap")
46 }
47 // The user-facing message for a deadline error must mention the
48 // configured timeout even when the error text never says "timeout".
49 msg := timeoutErrorMessage("slow", 42)
50 if !strings.Contains(msg, "42") || !strings.Contains(msg, "slow") {
51 t.Fatalf("timeout message missing details: %q", msg)
52 }
53}
54
55func TestSanitizeName(t *testing.T) {
56 cases := []struct{ in, want string }{
57 {"filesystem", "filesystem"},
58 {"my-tool", "my_tool"},
59 {"my.tool", "my_tool"},
60 {"3d-render", "t_3d_render"},
61 {"", "unnamed"},
62 }
63 for _, c := range cases {
64 if got := sanitizeName(c.in); got != c.want {
65 t.Errorf("sanitizeName(%q) = %q, want %q", c.in, got, c.want)
66 }
67 }
68}
69
70func TestToolName(t *testing.T) {
71 if got, want := toolName("fs-server", "read.file"), "mcp__fs_server__read_file"; got != want {
72 t.Fatalf("toolName = %q, want %q", got, want)
73 }
74}
75
76func TestRegisterCachedToolLogsCollision(t *testing.T) {
77 var buf bytes.Buffer
78 b := testBridge()
79 b.logger = log.New(&buf, "", 0)
80 // nil Extension: registerCachedTool must not reach b.e.Tool for
81 // the colliding (skipped) registration, and the first registration
82 // needs a real Extension — so pre-seed the mapping instead.
83 b.mapping["mcp__srv__my_tool"] = toolMapping{serverName: "srv", mcpTool: "my-tool"}
84
85 b.registerCachedTool("srv", cachedTool{Name: "my_tool", Schema: []byte(`{}`)})
86
87 if !strings.Contains(buf.String(), "collision") {
88 t.Fatalf("expected collision log, got: %q", buf.String())
89 }
90}