-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathintegration_test.go
More file actions
271 lines (230 loc) · 7.91 KB
/
Copy pathintegration_test.go
File metadata and controls
271 lines (230 loc) · 7.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
package main
import (
"context"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/standardbeagle/mcp-tui/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// buildTestBinary compiles mcp-tui into the test's temp directory and returns an
// absolute path to it. An absolute path matters: a bare relative name like
// "mcp-tui-test" is looked up on PATH by os/exec rather than in the working
// directory, and on Windows the binary needs an .exe suffix.
func buildTestBinary(t *testing.T) string {
t.Helper()
bin := filepath.Join(t.TempDir(), testutil.ExeName("mcp-tui-test"))
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
buildCmd := exec.CommandContext(ctx, "go", "build", "-o", bin, ".")
buildCmd.Dir = "."
out, err := buildCmd.CombinedOutput()
require.NoError(t, err, "Failed to build mcp-tui binary: %s", out)
return bin
}
func TestCLIIntegration(t *testing.T) {
// Skip integration tests in short mode
if testing.Short() {
t.Skip("Skipping integration tests in short mode")
}
bin := buildTestBinary(t)
tests := []struct {
name string
args []string
wantErr bool
contains []string
}{
{
name: "help command",
args: []string{"--help"},
wantErr: false,
contains: []string{"MCP", "client"},
},
{
name: "version command",
args: []string{"--version"},
wantErr: false,
contains: []string{"mcp-tui", "version"},
},
{
name: "tool help",
args: []string{"tool", "--help"},
wantErr: false,
contains: []string{"list", "call", "describe"},
},
{
name: "tool list without connection",
args: []string{"tool", "list"},
wantErr: true,
contains: []string{"no MCP server connection specified"},
},
{
name: "tool list with invalid command",
args: []string{"tool", "list", "--cmd", "nonexistent-command-xyz"},
wantErr: true,
contains: []string{"executable file not found"},
},
{
name: "server help",
args: []string{"server", "--help"},
wantErr: false,
contains: []string{"Show information about"},
},
{
name: "resource help",
args: []string{"resource", "--help"},
wantErr: false,
contains: []string{"resources"},
},
{
name: "prompt help",
args: []string{"prompt", "--help"},
wantErr: false,
contains: []string{"prompts"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, bin, tt.args...)
output, err := cmd.CombinedOutput()
outputStr := string(output)
if tt.wantErr {
assert.Error(t, err, "Expected command to fail")
} else {
assert.NoError(t, err, "Expected command to succeed. Output: %s", outputStr)
}
for _, contains := range tt.contains {
assert.Contains(t, outputStr, contains, "Output should contain '%s'", contains)
}
})
}
}
func TestCommandValidation(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration tests in short mode")
}
// Build the binary
bin := buildTestBinary(t)
// An absolute path to a real executable, on every platform.
scriptPath := testutil.ScriptPath(t, "absolute-path-server", "exit 0\n")
testutil.RequirePwsh(t)
pwshPath, err := testutil.LookPwsh()
require.NoError(t, err)
tests := []struct {
name string
args []string
contains []string
}{
{
name: "dangerous command rejected",
args: []string{"tool", "list", "--cmd", "ls;rm", "--args", "test"},
contains: []string{"dangerous pattern", "ls;rm"}, // validator catches ';' before exec
},
{
// An absolute command path passes validation and is executed; the MCP
// handshake then fails because the script does not speak the protocol.
name: "absolute path works but fails at MCP level",
args: append([]string{"tool", "list"}, testutil.ServerFlags(t, pwshPath, []string{"-NoProfile", "-NonInteractive", "-File", scriptPath})...),
contains: []string{"Starting process", pwshPath},
},
{
name: "empty command rejected",
args: []string{"tool", "list", "--cmd", ""},
contains: []string{"no MCP server connection specified"}, // Empty cmd means no connection
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, bin, tt.args...)
output, err := cmd.CombinedOutput()
outputStr := string(output)
// Should fail with validation error
assert.Error(t, err, "Expected command validation to fail")
for _, contains := range tt.contains {
assert.Contains(t, outputStr, contains, "Output should contain '%s'", contains)
}
})
}
}
func TestStdioServerIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration tests in short mode")
}
// Test with a stand-in server command that we know will start
// This tests the stdio transport validation without requiring an actual MCP server
// Build the binary
bin := buildTestBinary(t)
t.Run("stdio transport with a stand-in server", func(t *testing.T) {
// Spawning the CLI, which spawns a server process and fails the MCP
// handshake, takes ~2.5s unloaded. A 5s budget leaves no headroom: on a
// loaded machine the command is killed mid-run and the assertions below
// see empty output rather than the error they are checking for.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// A stand-in server that passes validation but never speaks MCP.
serverCmd, serverArgs := testutil.ServerExitsImmediately(t)
args := append([]string{"tool", "list"}, testutil.ServerFlags(t, serverCmd, serverArgs)...)
cmd := exec.CommandContext(ctx, bin, args...)
output, err := cmd.CombinedOutput()
outputStr := string(output)
// Should fail at initialization, not validation
assert.Error(t, err, "Expected MCP initialization to fail")
// Should not contain validation errors
assert.NotContains(t, outputStr, "command validation failed")
// Should contain MCP-related error
assert.Contains(t, outputStr, "MCP initialization failed")
})
}
func TestDebugMode(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration tests in short mode")
}
// Build the binary
bin := buildTestBinary(t)
t.Run("debug flag", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
serverCmd, serverArgs := testutil.ServerExitsImmediately(t)
args := append([]string{"tool", "list", "--log-level", "debug"}, testutil.ServerFlags(t, serverCmd, serverArgs)...)
cmd := exec.CommandContext(ctx, bin, args...)
output, err := cmd.CombinedOutput()
outputStr := string(output)
// Should fail at MCP initialization (expected)
assert.Error(t, err)
// Debug output should be present
assert.Contains(t, outputStr, "Creating MCP service")
})
}
func TestOutputFormats(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration tests in short mode")
}
// Build the binary
bin := buildTestBinary(t)
t.Run("verbose output", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
serverCmd, serverArgs := testutil.ServerExitsImmediately(t)
args := append([]string{"tool", "list"}, testutil.ServerFlags(t, serverCmd, serverArgs)...)
cmd := exec.CommandContext(ctx, bin, args...)
output, _ := cmd.CombinedOutput()
outputStr := string(output)
// Should have stderr output with emojis for user-friendly messages
lines := strings.Split(outputStr, "\n")
hasEmojiOutput := false
for _, line := range lines {
if strings.Contains(line, "🔄") || strings.Contains(line, "🚀") || strings.Contains(line, "❌") {
hasEmojiOutput = true
break
}
}
assert.True(t, hasEmojiOutput, "Should have user-friendly output with emojis")
})
}