docker_mcp_test.go

  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}