1package config
2
3import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "testing"
9
10 "github.com/charmbracelet/crush/internal/env"
11 "github.com/stretchr/testify/require"
12)
13
14var errDockerUnavailable = errors.New("docker unavailable")
15
16func setDockerMCPVersionRunner(t *testing.T, runner func(context.Context) error) {
17 t.Helper()
18 orig := dockerMCPVersionRunner
19 dockerMCPVersionRunner = runner
20 t.Cleanup(func() {
21 dockerMCPVersionRunner = orig
22 })
23}
24
25func TestIsDockerMCPEnabled(t *testing.T) {
26 t.Parallel()
27
28 t.Run("returns false when MCP is nil", func(t *testing.T) {
29 t.Parallel()
30 cfg := &Config{
31 MCP: nil,
32 }
33 require.False(t, cfg.IsDockerMCPEnabled())
34 })
35
36 t.Run("returns false when docker mcp not configured", func(t *testing.T) {
37 t.Parallel()
38 cfg := &Config{
39 MCP: make(map[string]MCPConfig),
40 }
41 require.False(t, cfg.IsDockerMCPEnabled())
42 })
43
44 t.Run("returns true when docker mcp is configured", func(t *testing.T) {
45 t.Parallel()
46 cfg := &Config{
47 MCP: map[string]MCPConfig{
48 DockerMCPName: {
49 Type: MCPStdio,
50 Command: "docker",
51 },
52 },
53 }
54 require.True(t, cfg.IsDockerMCPEnabled())
55 })
56}
57
58func TestEnableDockerMCP(t *testing.T) {
59 t.Run("adds docker mcp to config", func(t *testing.T) {
60 setDockerMCPVersionRunner(t, func(context.Context) error { return nil })
61
62 // Create a temporary directory for config.
63 tmpDir := t.TempDir()
64 configPath := filepath.Join(tmpDir, "crush.json")
65
66 cfg := &Config{
67 MCP: make(map[string]MCPConfig),
68 }
69 store := &ConfigStore{
70 config: cfg,
71 globalDataPath: configPath,
72 resolver: NewShellVariableResolver(env.New()),
73 }
74
75 err := store.EnableDockerMCP()
76 require.NoError(t, err)
77
78 // Check in-memory config.
79 require.True(t, cfg.IsDockerMCPEnabled())
80 mcpConfig, exists := cfg.MCP[DockerMCPName]
81 require.True(t, exists)
82 require.Equal(t, MCPStdio, mcpConfig.Type)
83 require.Equal(t, "docker", mcpConfig.Command)
84 require.Equal(t, []string{"mcp", "gateway", "run"}, mcpConfig.Args)
85 require.False(t, mcpConfig.Disabled)
86
87 // Check persisted config.
88 data, err := os.ReadFile(configPath)
89 require.NoError(t, err)
90 require.Contains(t, string(data), "docker")
91 require.Contains(t, string(data), "gateway")
92 })
93
94 t.Run("fails when docker mcp not available", func(t *testing.T) {
95 setDockerMCPVersionRunner(t, func(context.Context) error { return errDockerUnavailable })
96
97 // Create a temporary directory for config.
98 tmpDir := t.TempDir()
99 configPath := filepath.Join(tmpDir, "crush.json")
100
101 cfg := &Config{
102 MCP: make(map[string]MCPConfig),
103 }
104 store := &ConfigStore{
105 config: cfg,
106 globalDataPath: configPath,
107 resolver: NewShellVariableResolver(env.New()),
108 }
109
110 err := store.EnableDockerMCP()
111 require.Error(t, err)
112 require.Contains(t, err.Error(), "docker mcp is not available")
113 })
114}
115
116func TestDisableDockerMCP(t *testing.T) {
117 t.Parallel()
118
119 t.Run("removes docker mcp from config", func(t *testing.T) {
120 t.Parallel()
121
122 // Create a temporary directory for config.
123 tmpDir := t.TempDir()
124 configPath := filepath.Join(tmpDir, "crush.json")
125
126 cfg := &Config{
127 MCP: map[string]MCPConfig{
128 DockerMCPName: {
129 Type: MCPStdio,
130 Command: "docker",
131 Args: []string{"mcp", "gateway", "run"},
132 Disabled: false,
133 },
134 },
135 }
136 store := &ConfigStore{
137 config: cfg,
138 globalDataPath: configPath,
139 resolver: NewShellVariableResolver(env.New()),
140 }
141
142 // Verify it's enabled first.
143 require.True(t, cfg.IsDockerMCPEnabled())
144
145 err := store.DisableDockerMCP()
146 require.NoError(t, err)
147
148 // Check in-memory config.
149 require.False(t, cfg.IsDockerMCPEnabled())
150 _, exists := cfg.MCP[DockerMCPName]
151 require.False(t, exists)
152 })
153
154 t.Run("does nothing when MCP is nil", func(t *testing.T) {
155 t.Parallel()
156
157 cfg := &Config{
158 MCP: nil,
159 }
160 store := &ConfigStore{
161 config: cfg,
162 globalDataPath: filepath.Join(t.TempDir(), "crush.json"),
163 resolver: NewShellVariableResolver(env.New()),
164 }
165
166 err := store.DisableDockerMCP()
167 require.NoError(t, err)
168 })
169}
170
171func TestEnableDockerMCPWithRealDockerWhenAvailable(t *testing.T) {
172 t.Parallel()
173
174 if !IsDockerMCPAvailable() {
175 t.Skip("docker mcp not available on this machine")
176 }
177
178 tmpDir := t.TempDir()
179 configPath := filepath.Join(tmpDir, "crush.json")
180
181 cfg := &Config{
182 MCP: make(map[string]MCPConfig),
183 }
184 store := &ConfigStore{
185 config: cfg,
186 globalDataPath: configPath,
187 resolver: NewShellVariableResolver(env.New()),
188 }
189
190 err := store.EnableDockerMCP()
191 require.NoError(t, err)
192 require.True(t, cfg.IsDockerMCPEnabled())
193}