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