1package config
2
3import (
4 "bytes"
5 "encoding/json"
6 "io"
7 "maps"
8 "slices"
9 "testing"
10
11 "github.com/stretchr/testify/require"
12)
13
14// TestConfigMerging defines the rules on how configuration merging works.
15// Generally, things are either appended to or replaced by the later configuration.
16// Whether one or the other happen depends on effects its effects.
17func TestConfigMerging(t *testing.T) {
18 t.Run("empty", func(t *testing.T) {
19 c := exerciseMerge(t, Config{}, Config{})
20 require.NotNil(t, c)
21 })
22
23 t.Run("mcps", func(t *testing.T) {
24 c := exerciseMerge(t, Config{
25 MCP: MCPs{
26 "foo": {
27 Command: "foo-mcp",
28 Args: []string{"serve"},
29 Type: MCPSSE,
30 Timeout: 10,
31 },
32 "zaz": {
33 Disabled: true,
34 Env: map[string]string{"FOO": "bar"},
35 Headers: map[string]string{"api-key": "exposed"},
36 URL: "nope",
37 },
38 },
39 }, Config{
40 MCP: MCPs{
41 "foo": {
42 Args: []string{"serve", "--stdio"},
43 Type: MCPStdio,
44 Timeout: 7,
45 },
46 "bar": {
47 Command: "bar",
48 },
49 "zaz": {
50 Env: map[string]string{"FOO": "foo", "BAR": "bar"},
51 Headers: map[string]string{"api-key": "$API"},
52 URL: "http://bar",
53 },
54 },
55 })
56 require.NotNil(t, c)
57 require.Len(t, slices.Collect(maps.Keys(c.MCP)), 3)
58 require.Equal(t, MCPConfig{
59 Command: "foo-mcp",
60 Args: []string{"serve", "--stdio"},
61 Type: MCPStdio,
62 Timeout: 10,
63 }, c.MCP["foo"])
64 require.Equal(t, MCPConfig{
65 Command: "bar",
66 }, c.MCP["bar"])
67 require.Equal(t, MCPConfig{
68 Disabled: true,
69 URL: "http://bar",
70 Env: map[string]string{"FOO": "foo", "BAR": "bar"},
71 Headers: map[string]string{"api-key": "$API"},
72 }, c.MCP["zaz"])
73 })
74
75 t.Run("lsps", func(t *testing.T) {
76 result := exerciseMerge(t, Config{
77 LSP: LSPs{
78 "gopls": LSPConfig{
79 Env: map[string]string{"FOO": "bar"},
80 RootMarkers: []string{"go.sum"},
81 FileTypes: []string{"go"},
82 },
83 },
84 }, Config{
85 LSP: LSPs{
86 "gopls": LSPConfig{
87 Command: "gopls",
88 InitOptions: map[string]any{"a": 10},
89 RootMarkers: []string{"go.sum"},
90 },
91 },
92 }, Config{
93 LSP: LSPs{
94 "gopls": LSPConfig{
95 Args: []string{"serve", "--stdio"},
96 InitOptions: map[string]any{"a": 12, "b": 18},
97 RootMarkers: []string{"go.sum", "go.mod"},
98 FileTypes: []string{"go"},
99 Disabled: true,
100 },
101 },
102 },
103 Config{
104 LSP: LSPs{
105 "gopls": LSPConfig{
106 Options: map[string]any{"opt1": "10"},
107 RootMarkers: []string{"go.work"},
108 },
109 },
110 },
111 )
112 require.NotNil(t, result)
113 require.Equal(t, LSPConfig{
114 Disabled: true,
115 Command: "gopls",
116 Args: []string{"serve", "--stdio"},
117 Env: map[string]string{"FOO": "bar"},
118 FileTypes: []string{"go"},
119 RootMarkers: []string{"go.mod", "go.sum", "go.work"},
120 InitOptions: map[string]any{"a": 12.0, "b": 18.0},
121 Options: map[string]any{"opt1": "10"},
122 }, result.LSP["gopls"])
123 })
124}
125
126func exerciseMerge(tb testing.TB, confs ...Config) *Config {
127 tb.Helper()
128 readers := make([]io.Reader, 0, len(confs))
129 for _, c := range confs {
130 bts, err := json.Marshal(c)
131 require.NoError(tb, err)
132 readers = append(readers, bytes.NewReader(bts))
133 }
134 result, err := loadFromReaders(readers)
135 require.NoError(tb, err)
136 return result
137}