@@ -1,465 +0,0 @@
1−package config
2−
3−import (
4− "fmt"
5− "os"
6− "path/filepath"
7− "testing"
8−
9− "github.com/kujtimiihoxha/termai/internal/llm/models"
10− "github.com/spf13/viper"
11− "github.com/stretchr/testify/assert"
12− "github.com/stretchr/testify/require"
13−)
14−
15−func TestLoad(t *testing.T) {
16− setupTest(t)
17−
18− t.Run("loads configuration successfully", func(t *testing.T) {
19− homeDir := t.TempDir()
20− t.Setenv("HOME", homeDir)
21− configPath := filepath.Join(homeDir, ".termai.json")
22−
23− configContent := `{
24− "data": {
25− "directory": "custom-dir"
26− },
27− "log": {
28− "level": "debug"
29− },
30− "mcpServers": {
31− "test-server": {
32− "command": "test-command",
33− "env": ["TEST_ENV=value"],
34− "args": ["--arg1", "--arg2"],
35− "type": "stdio",
36− "url": "",
37− "headers": {}
38− },
39− "sse-server": {
40− "command": "",
41− "env": [],
42− "args": [],
43− "type": "sse",
44− "url": "https://api.example.com/events",
45− "headers": {
46− "Authorization": "Bearer token123",
47− "Content-Type": "application/json"
48− }
49− }
50− },
51− "providers": {
52− "anthropic": {
53− "apiKey": "test-api-key",
54− "enabled": true
55− }
56− },
57− "model": {
58− "coder": "claude-3-haiku",
59− "task": "claude-3-haiku"
60− }
61− }`
62− err := os.WriteFile(configPath, []byte(configContent), 0o644)
63− require.NoError(t, err)
64−
65− cfg = nil
66− viper.Reset()
67−
68− err = Load(false)
69− require.NoError(t, err)
70−
71− config := Get()
72− assert.NotNil(t, config)
73− assert.Equal(t, "custom-dir", config.Data.Directory)
74− assert.Equal(t, "debug", config.Log.Level)
75−
76− assert.Contains(t, config.MCPServers, "test-server")
77− stdioServer := config.MCPServers["test-server"]
78− assert.Equal(t, "test-command", stdioServer.Command)
79− assert.Equal(t, []string{"TEST_ENV=value"}, stdioServer.Env)
80− assert.Equal(t, []string{"--arg1", "--arg2"}, stdioServer.Args)
81− assert.Equal(t, MCPStdio, stdioServer.Type)
82− assert.Equal(t, "", stdioServer.URL)
83− assert.Empty(t, stdioServer.Headers)
84−
85− assert.Contains(t, config.MCPServers, "sse-server")
86− sseServer := config.MCPServers["sse-server"]
87− assert.Equal(t, "", sseServer.Command)
88− assert.Empty(t, sseServer.Env)
89− assert.Empty(t, sseServer.Args)
90− assert.Equal(t, MCPSse, sseServer.Type)
91− assert.Equal(t, "https://api.example.com/events", sseServer.URL)
92− assert.Equal(t, map[string]string{
93− "authorization": "Bearer token123",
94− "content-type": "application/json",
95− }, sseServer.Headers)
96−
97− assert.Contains(t, config.Providers, models.ModelProvider("anthropic"))
98− provider := config.Providers[models.ModelProvider("anthropic")]
99− assert.Equal(t, "test-api-key", provider.APIKey)
100− assert.True(t, provider.Enabled)
101−
102− assert.NotNil(t, config.Model)
103− assert.Equal(t, models.Claude3Haiku, config.Model.Coder)
104− assert.Equal(t, models.Claude3Haiku, config.Model.Task)
105− assert.Equal(t, defaultMaxTokens, config.Model.CoderMaxTokens)
106− })
107−
108− t.Run("loads configuration with environment variables", func(t *testing.T) {
109− homeDir := t.TempDir()
110− t.Setenv("HOME", homeDir)
111− configPath := filepath.Join(homeDir, ".termai.json")
112− err := os.WriteFile(configPath, []byte("{}"), 0o644)
113− require.NoError(t, err)
114−
115− t.Setenv("ANTHROPIC_API_KEY", "env-anthropic-key")
116− t.Setenv("OPENAI_API_KEY", "env-openai-key")
117− t.Setenv("GEMINI_API_KEY", "env-gemini-key")
118−
119− cfg = nil
120− viper.Reset()
121−
122− err = Load(false)
123− require.NoError(t, err)
124−
125− config := Get()
126− assert.NotNil(t, config)
127−
128− assert.Equal(t, defaultDataDirectory, config.Data.Directory)
129− assert.Equal(t, defaultLogLevel, config.Log.Level)
130−
131− assert.Contains(t, config.Providers, models.ModelProvider("anthropic"))
132− assert.Equal(t, "env-anthropic-key", config.Providers[models.ModelProvider("anthropic")].APIKey)
133− assert.True(t, config.Providers[models.ModelProvider("anthropic")].Enabled)
134−
135− assert.Contains(t, config.Providers, models.ModelProvider("openai"))
136− assert.Equal(t, "env-openai-key", config.Providers[models.ModelProvider("openai")].APIKey)
137− assert.True(t, config.Providers[models.ModelProvider("openai")].Enabled)
138−
139− assert.Contains(t, config.Providers, models.ModelProvider("gemini"))
140− assert.Equal(t, "env-gemini-key", config.Providers[models.ModelProvider("gemini")].APIKey)
141− assert.True(t, config.Providers[models.ModelProvider("gemini")].Enabled)
142−
143− assert.Equal(t, models.Claude37Sonnet, config.Model.Coder)
144− })
145−
146− t.Run("local config overrides global config", func(t *testing.T) {
147− homeDir := t.TempDir()
148− t.Setenv("HOME", homeDir)
149− globalConfigPath := filepath.Join(homeDir, ".termai.json")
150− globalConfig := `{
151− "data": {
152− "directory": "global-dir"
153− },
154− "log": {
155− "level": "info"
156− }
157− }`
158− err := os.WriteFile(globalConfigPath, []byte(globalConfig), 0o644)
159− require.NoError(t, err)
160−
161− workDir := t.TempDir()
162− origDir, err := os.Getwd()
163− require.NoError(t, err)
164− defer os.Chdir(origDir)
165− err = os.Chdir(workDir)
166− require.NoError(t, err)
167−
168− localConfigPath := filepath.Join(workDir, ".termai.json")
169− localConfig := `{
170− "data": {
171− "directory": "local-dir"
172− },
173− "log": {
174− "level": "debug"
175− }
176− }`
177− err = os.WriteFile(localConfigPath, []byte(localConfig), 0o644)
178− require.NoError(t, err)
179−
180− cfg = nil
181− viper.Reset()
182−
183− err = Load(false)
184− require.NoError(t, err)
185−
186− config := Get()
187− assert.NotNil(t, config)
188−
189− assert.Equal(t, "local-dir", config.Data.Directory)
190− assert.Equal(t, "debug", config.Log.Level)
191− })
192−
193− t.Run("missing config file should not return error", func(t *testing.T) {
194− emptyDir := t.TempDir()
195− t.Setenv("HOME", emptyDir)
196−
197− cfg = nil
198− viper.Reset()
199−
200− err := Load(false)
201− assert.NoError(t, err)
202− })
203−
204− t.Run("model priority and fallbacks", func(t *testing.T) {
205− testCases := []struct {
206− name string
207− anthropicKey string
208− openaiKey string
209− geminiKey string
210− expectedModel models.ModelID
211− explicitModel models.ModelID
212− useExplicitModel bool
213− }{
214− {
215− name: "anthropic has priority",
216− anthropicKey: "test-key",
217− openaiKey: "test-key",
218− geminiKey: "test-key",
219− expectedModel: models.Claude37Sonnet,
220− },
221− {
222− name: "fallback to openai when no anthropic",
223− anthropicKey: "",
224− openaiKey: "test-key",
225− geminiKey: "test-key",
226− expectedModel: models.GPT41,
227− },
228− {
229− name: "fallback to gemini when no others",
230− anthropicKey: "",
231− openaiKey: "",
232− geminiKey: "test-key",
233− expectedModel: models.GRMINI20Flash,
234− },
235− {
236− name: "explicit model overrides defaults",
237− anthropicKey: "test-key",
238− openaiKey: "test-key",
239− geminiKey: "test-key",
240− explicitModel: models.GPT41,
241− useExplicitModel: true,
242− expectedModel: models.GPT41,
243− },
244− }
245−
246− for _, tc := range testCases {
247− t.Run(tc.name, func(t *testing.T) {
248− homeDir := t.TempDir()
249− t.Setenv("HOME", homeDir)
250− configPath := filepath.Join(homeDir, ".termai.json")
251−
252− configContent := "{}"
253− if tc.useExplicitModel {
254− configContent = fmt.Sprintf(`{"model":{"coder":"%s"}}`, tc.explicitModel)
255− }
256−
257− err := os.WriteFile(configPath, []byte(configContent), 0o644)
258− require.NoError(t, err)
259−
260− if tc.anthropicKey != "" {
261− t.Setenv("ANTHROPIC_API_KEY", tc.anthropicKey)
262− } else {
263− t.Setenv("ANTHROPIC_API_KEY", "")
264− }
265−
266− if tc.openaiKey != "" {
267− t.Setenv("OPENAI_API_KEY", tc.openaiKey)
268− } else {
269− t.Setenv("OPENAI_API_KEY", "")
270− }
271−
272− if tc.geminiKey != "" {
273− t.Setenv("GEMINI_API_KEY", tc.geminiKey)
274− } else {
275− t.Setenv("GEMINI_API_KEY", "")
276− }
277−
278− cfg = nil
279− viper.Reset()
280−
281− err = Load(false)
282− require.NoError(t, err)
283−
284− config := Get()
285− assert.NotNil(t, config)
286− assert.Equal(t, tc.expectedModel, config.Model.Coder)
287− })
288− }
289− })
290−}
291−
292−func TestGet(t *testing.T) {
293− t.Run("get returns same config instance", func(t *testing.T) {
294− setupTest(t)
295− homeDir := t.TempDir()
296− t.Setenv("HOME", homeDir)
297− configPath := filepath.Join(homeDir, ".termai.json")
298− err := os.WriteFile(configPath, []byte("{}"), 0o644)
299− require.NoError(t, err)
300−
301− cfg = nil
302− viper.Reset()
303−
304− config1 := Get()
305− require.NotNil(t, config1)
306−
307− config2 := Get()
308− require.NotNil(t, config2)
309−
310− assert.Same(t, config1, config2)
311− })
312−
313− t.Run("get loads config if not loaded", func(t *testing.T) {
314− setupTest(t)
315− homeDir := t.TempDir()
316− t.Setenv("HOME", homeDir)
317− configPath := filepath.Join(homeDir, ".termai.json")
318− configContent := `{"data":{"directory":"test-dir"}}`
319− err := os.WriteFile(configPath, []byte(configContent), 0o644)
320− require.NoError(t, err)
321−
322− cfg = nil
323− viper.Reset()
324−
325− config := Get()
326− require.NotNil(t, config)
327− assert.Equal(t, "test-dir", config.Data.Directory)
328− })
329−}
330−
331−func TestWorkingDirectory(t *testing.T) {
332− t.Run("returns current working directory", func(t *testing.T) {
333− setupTest(t)
334− homeDir := t.TempDir()
335− t.Setenv("HOME", homeDir)
336− configPath := filepath.Join(homeDir, ".termai.json")
337− err := os.WriteFile(configPath, []byte("{}"), 0o644)
338− require.NoError(t, err)
339−
340− cfg = nil
341− viper.Reset()
342−
343− err = Load(false)
344− require.NoError(t, err)
345−
346− wd := WorkingDirectory()
347− expectedWd, err := os.Getwd()
348− require.NoError(t, err)
349− assert.Equal(t, expectedWd, wd)
350− })
351−}
352−
353−func TestWrite(t *testing.T) {
354− t.Run("writes config to file", func(t *testing.T) {
355− setupTest(t)
356− homeDir := t.TempDir()
357− t.Setenv("HOME", homeDir)
358− configPath := filepath.Join(homeDir, ".termai.json")
359− err := os.WriteFile(configPath, []byte("{}"), 0o644)
360− require.NoError(t, err)
361−
362− cfg = nil
363− viper.Reset()
364−
365− err = Load(false)
366− require.NoError(t, err)
367−
368− viper.Set("data.directory", "modified-dir")
369−
370− err = Write()
371− require.NoError(t, err)
372−
373− content, err := os.ReadFile(configPath)
374− require.NoError(t, err)
375− assert.Contains(t, string(content), "modified-dir")
376− })
377−}
378−
379−func TestMCPType(t *testing.T) {
380− t.Run("MCPType constants", func(t *testing.T) {
381− assert.Equal(t, MCPType("stdio"), MCPStdio)
382− assert.Equal(t, MCPType("sse"), MCPSse)
383− })
384−
385− t.Run("MCPType JSON unmarshaling", func(t *testing.T) {
386− homeDir := t.TempDir()
387− t.Setenv("HOME", homeDir)
388− configPath := filepath.Join(homeDir, ".termai.json")
389−
390− configContent := `{
391− "mcpServers": {
392− "stdio-server": {
393− "type": "stdio"
394− },
395− "sse-server": {
396− "type": "sse"
397− },
398− "invalid-server": {
399− "type": "invalid"
400− }
401− }
402− }`
403− err := os.WriteFile(configPath, []byte(configContent), 0o644)
404− require.NoError(t, err)
405−
406− cfg = nil
407− viper.Reset()
408−
409− err = Load(false)
410− require.NoError(t, err)
411−
412− config := Get()
413− assert.NotNil(t, config)
414−
415− assert.Equal(t, MCPStdio, config.MCPServers["stdio-server"].Type)
416− assert.Equal(t, MCPSse, config.MCPServers["sse-server"].Type)
417− assert.Equal(t, MCPType("invalid"), config.MCPServers["invalid-server"].Type)
418− })
419−
420− t.Run("default MCPType", func(t *testing.T) {
421− homeDir := t.TempDir()
422− t.Setenv("HOME", homeDir)
423− configPath := filepath.Join(homeDir, ".termai.json")
424−
425− configContent := `{
426− "mcpServers": {
427− "test-server": {
428− "command": "test-command"
429− }
430− }
431− }`
432− err := os.WriteFile(configPath, []byte(configContent), 0o644)
433− require.NoError(t, err)
434−
435− cfg = nil
436− viper.Reset()
437−
438− err = Load(false)
439− require.NoError(t, err)
440−
441− config := Get()
442− assert.NotNil(t, config)
443−
444− assert.Equal(t, MCPType(""), config.MCPServers["test-server"].Type)
445− })
446−}
447−
448−func setupTest(t *testing.T) {
449− origHome := os.Getenv("HOME")
450− origXdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
451− origAnthropicKey := os.Getenv("ANTHROPIC_API_KEY")
452− origOpenAIKey := os.Getenv("OPENAI_API_KEY")
453− origGeminiKey := os.Getenv("GEMINI_API_KEY")
454−
455− t.Cleanup(func() {
456− t.Setenv("HOME", origHome)
457− t.Setenv("XDG_CONFIG_HOME", origXdgConfigHome)
458− t.Setenv("ANTHROPIC_API_KEY", origAnthropicKey)
459− t.Setenv("OPENAI_API_KEY", origOpenAIKey)
460− t.Setenv("GEMINI_API_KEY", origGeminiKey)
461−
462− cfg = nil
463− viper.Reset()
464− })
465−}