test: more tests

Carlos Alexandro Becker created

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Change summary

crush.json                    |  11 
internal/config/load_test.go  |  16 -
internal/config/merge_test.go | 453 +++++++++++++++++++++++++++++++++++++
3 files changed, 454 insertions(+), 26 deletions(-)

Detailed changes

crush.json 🔗

@@ -1,15 +1,6 @@
 {
   "$schema": "https://charm.land/crush.json",
-  "gopls": {
-    "args": [
-      "mcp"
-    ]
-  },
   "lsp": {
-    "gopls": {
-      "args": [
-        "server"
-      ]
-    }
+    "gopls": {}
   }
 }

internal/config/load_test.go 🔗

@@ -5,7 +5,6 @@ import (
 	"log/slog"
 	"os"
 	"path/filepath"
-	"strings"
 	"testing"
 
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
@@ -22,21 +21,6 @@ func TestMain(m *testing.M) {
 	os.Exit(exitVal)
 }
 
-func TestConfig_LoadFromReaders(t *testing.T) {
-	data1 := strings.NewReader(`{"providers": {"openai": {"api_key": "key1", "base_url": "https://api.openai.com/v1"}}}`)
-	data2 := strings.NewReader(`{"providers": {"openai": {"api_key": "key2", "base_url": "https://api.openai.com/v2"}}}`)
-	data3 := strings.NewReader(`{"providers": {"openai": {}}}`)
-
-	loadedConfig, err := loadFromReaders([]io.Reader{data1, data2, data3})
-
-	require.NoError(t, err)
-	require.NotNil(t, loadedConfig)
-	require.Equal(t, 1, loadedConfig.Providers.Len())
-	pc, _ := loadedConfig.Providers.Get("openai")
-	require.Equal(t, "key2", pc.APIKey)
-	require.Equal(t, "https://api.openai.com/v2", pc.BaseURL)
-}
-
 func TestConfig_setDefaults(t *testing.T) {
 	cfg := &Config{}
 

internal/config/merge_test.go 🔗

@@ -121,6 +121,459 @@ func TestConfigMerging(t *testing.T) {
 			Options:     map[string]any{"opt1": "10"},
 		}, result.LSP["gopls"])
 	})
+
+	t.Run("tui_options", func(t *testing.T) {
+		maxDepth := 5
+		maxItems := 100
+		newMaxDepth := 10
+		newMaxItems := 200
+
+		c := exerciseMerge(t, Config{
+			Options: &Options{
+				TUI: &TUIOptions{
+					CompactMode: false,
+					DiffMode:    "unified",
+					Completions: Completions{
+						MaxDepth: &maxDepth,
+						MaxItems: &maxItems,
+					},
+				},
+			},
+		}, Config{
+			Options: &Options{
+				TUI: &TUIOptions{
+					CompactMode: true,
+					DiffMode:    "split",
+					Completions: Completions{
+						MaxDepth: &newMaxDepth,
+						MaxItems: &newMaxItems,
+					},
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.True(t, c.Options.TUI.CompactMode)
+		require.Equal(t, "split", c.Options.TUI.DiffMode)
+		require.Equal(t, newMaxDepth, *c.Options.TUI.Completions.MaxDepth)
+	})
+
+	t.Run("options", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Options: &Options{
+				ContextPaths:              []string{"CRUSH.md"},
+				Debug:                     false,
+				DebugLSP:                  false,
+				DisableProviderAutoUpdate: false,
+				DisableMetrics:            false,
+				DataDirectory:             ".crush",
+				DisabledTools:             []string{"bash"},
+				Attribution: &Attribution{
+					CoAuthoredBy:  false,
+					GeneratedWith: false,
+				},
+				TUI: &TUIOptions{},
+			},
+		}, Config{
+			Options: &Options{
+				ContextPaths:              []string{".cursorrules"},
+				Debug:                     true,
+				DebugLSP:                  true,
+				DisableProviderAutoUpdate: true,
+				DisableMetrics:            true,
+				DataDirectory:             ".custom",
+				DisabledTools:             []string{"edit"},
+				Attribution: &Attribution{
+					CoAuthoredBy:  true,
+					GeneratedWith: true,
+				},
+				TUI: &TUIOptions{},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, []string{"CRUSH.md", ".cursorrules"}, c.Options.ContextPaths)
+		require.True(t, c.Options.Debug)
+		require.True(t, c.Options.DebugLSP)
+		require.True(t, c.Options.DisableProviderAutoUpdate)
+		require.True(t, c.Options.DisableMetrics)
+		require.Equal(t, ".custom", c.Options.DataDirectory)
+		require.Equal(t, []string{"bash", "edit"}, c.Options.DisabledTools)
+		require.True(t, c.Options.Attribution.CoAuthoredBy)
+		require.True(t, c.Options.Attribution.GeneratedWith)
+	})
+
+	t.Run("tools", func(t *testing.T) {
+		maxDepth := 5
+		maxItems := 100
+		newMaxDepth := 10
+		newMaxItems := 200
+
+		c := exerciseMerge(t, Config{
+			Tools: Tools{
+				Ls: ToolLs{
+					MaxDepth: &maxDepth,
+					MaxItems: &maxItems,
+				},
+			},
+		}, Config{
+			Tools: Tools{
+				Ls: ToolLs{
+					MaxDepth: &newMaxDepth,
+					MaxItems: &newMaxItems,
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, newMaxDepth, *c.Tools.Ls.MaxDepth)
+	})
+
+	t.Run("models", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Models: map[SelectedModelType]SelectedModel{
+				"large": {
+					Model:    "gpt-4",
+					Provider: "openai",
+				},
+			},
+		}, Config{
+			Models: map[SelectedModelType]SelectedModel{
+				"large": {
+					Model:    "gpt-4o",
+					Provider: "openai",
+				},
+				"small": {
+					Model:    "gpt-3.5-turbo",
+					Provider: "openai",
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Len(t, c.Models, 2)
+		require.Equal(t, "gpt-4o", c.Models["large"].Model)
+		require.Equal(t, "gpt-3.5-turbo", c.Models["small"].Model)
+	})
+
+	t.Run("schema", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Schema: "https://example.com/schema.json",
+		}, Config{
+			Schema: "https://example.com/new-schema.json",
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, "https://example.com/schema.json", c.Schema)
+	})
+
+	t.Run("schema_empty_first", func(t *testing.T) {
+		c := exerciseMerge(t, Config{}, Config{
+			Schema: "https://example.com/schema.json",
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, "https://example.com/schema.json", c.Schema)
+	})
+
+	t.Run("permissions", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Permissions: &Permissions{
+				AllowedTools: []string{"bash", "view"},
+			},
+		}, Config{
+			Permissions: &Permissions{
+				AllowedTools: []string{"edit", "write"},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, []string{"bash", "view", "edit", "write"}, c.Permissions.AllowedTools)
+	})
+
+	t.Run("mcp_timeout_max", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			MCP: MCPs{
+				"test": {
+					Timeout: 10,
+				},
+			},
+		}, Config{
+			MCP: MCPs{
+				"test": {
+					Timeout: 5,
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, 10, c.MCP["test"].Timeout)
+	})
+
+	t.Run("mcp_disabled_true_if_any", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			MCP: MCPs{
+				"test": {
+					Disabled: false,
+				},
+			},
+		}, Config{
+			MCP: MCPs{
+				"test": {
+					Disabled: true,
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.True(t, c.MCP["test"].Disabled)
+	})
+
+	t.Run("lsp_disabled_true_if_any", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			LSP: LSPs{
+				"test": {
+					Disabled: false,
+				},
+			},
+		}, Config{
+			LSP: LSPs{
+				"test": {
+					Disabled: true,
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.True(t, c.LSP["test"].Disabled)
+	})
+
+	t.Run("lsp_args_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			LSP: LSPs{
+				"test": {
+					Args: []string{"old", "args"},
+				},
+			},
+		}, Config{
+			LSP: LSPs{
+				"test": {
+					Args: []string{"new", "args"},
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, []string{"new", "args"}, c.LSP["test"].Args)
+	})
+
+	t.Run("lsp_filetypes_merged_and_deduplicated", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			LSP: LSPs{
+				"test": {
+					FileTypes: []string{"go", "mod"},
+				},
+			},
+		}, Config{
+			LSP: LSPs{
+				"test": {
+					FileTypes: []string{"go", "sum"},
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, []string{"go", "mod", "sum"}, c.LSP["test"].FileTypes)
+	})
+
+	t.Run("lsp_rootmarkers_merged_and_deduplicated", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			LSP: LSPs{
+				"test": {
+					RootMarkers: []string{"go.mod", "go.sum"},
+				},
+			},
+		}, Config{
+			LSP: LSPs{
+				"test": {
+					RootMarkers: []string{"go.sum", "go.work"},
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, []string{"go.mod", "go.sum", "go.work"}, c.LSP["test"].RootMarkers)
+	})
+
+	t.Run("options_attribution_nil", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Options: &Options{
+				Attribution: &Attribution{
+					CoAuthoredBy:  true,
+					GeneratedWith: true,
+				},
+				TUI: &TUIOptions{},
+			},
+		}, Config{
+			Options: &Options{
+				TUI: &TUIOptions{},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.True(t, c.Options.Attribution.CoAuthoredBy)
+		require.True(t, c.Options.Attribution.GeneratedWith)
+	})
+
+	t.Run("tui_compact_mode_true_if_any", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Options: &Options{
+				TUI: &TUIOptions{
+					CompactMode: false,
+				},
+			},
+		}, Config{
+			Options: &Options{
+				TUI: &TUIOptions{
+					CompactMode: true,
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.True(t, c.Options.TUI.CompactMode)
+	})
+
+	t.Run("tui_diff_mode_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Options: &Options{
+				TUI: &TUIOptions{
+					DiffMode: "unified",
+				},
+			},
+		}, Config{
+			Options: &Options{
+				TUI: &TUIOptions{
+					DiffMode: "split",
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, "split", c.Options.TUI.DiffMode)
+	})
+
+	t.Run("options_data_directory_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			Options: &Options{
+				DataDirectory: ".crush",
+				TUI:           &TUIOptions{},
+			},
+		}, Config{
+			Options: &Options{
+				DataDirectory: ".custom",
+				TUI:           &TUIOptions{},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, ".custom", c.Options.DataDirectory)
+	})
+
+	t.Run("mcp_args_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			MCP: MCPs{
+				"test": {
+					Args: []string{"old"},
+				},
+			},
+		}, Config{
+			MCP: MCPs{
+				"test": {
+					Args: []string{"new"},
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, []string{"new"}, c.MCP["test"].Args)
+	})
+
+	t.Run("mcp_command_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			MCP: MCPs{
+				"test": {
+					Command: "old-command",
+				},
+			},
+		}, Config{
+			MCP: MCPs{
+				"test": {
+					Command: "new-command",
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, "new-command", c.MCP["test"].Command)
+	})
+
+	t.Run("mcp_type_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			MCP: MCPs{
+				"test": {
+					Type: MCPSSE,
+				},
+			},
+		}, Config{
+			MCP: MCPs{
+				"test": {
+					Type: MCPStdio,
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, MCPStdio, c.MCP["test"].Type)
+	})
+
+	t.Run("mcp_url_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			MCP: MCPs{
+				"test": {
+					URL: "http://old",
+				},
+			},
+		}, Config{
+			MCP: MCPs{
+				"test": {
+					URL: "http://new",
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, "http://new", c.MCP["test"].URL)
+	})
+
+	t.Run("lsp_command_replaced", func(t *testing.T) {
+		c := exerciseMerge(t, Config{
+			LSP: LSPs{
+				"test": {
+					Command: "old-command",
+				},
+			},
+		}, Config{
+			LSP: LSPs{
+				"test": {
+					Command: "new-command",
+				},
+			},
+		})
+
+		require.NotNil(t, c)
+		require.Equal(t, "new-command", c.LSP["test"].Command)
+	})
 }
 
 func exerciseMerge(tb testing.TB, confs ...Config) *Config {