// Package config provides configuration management for claude-mnemonic. package config import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) // ConfigSuite is a test suite for config operations. type ConfigSuite struct { suite.Suite tempDir string origHomeDir string } func (s *ConfigSuite) SetupTest() { var err error s.tempDir, err = os.MkdirTemp("", "config-test-*") s.Require().NoError(err) // Save and override HOME s.origHomeDir = os.Getenv("HOME") os.Setenv("HOME", s.tempDir) } func (s *ConfigSuite) TearDownTest() { os.Setenv("HOME", s.origHomeDir) os.RemoveAll(s.tempDir) } func TestConfigSuite(t *testing.T) { suite.Run(t, new(ConfigSuite)) } // TestDefault tests default configuration values. func (s *ConfigSuite) TestDefault() { cfg := Default() s.Equal(DefaultWorkerPort, cfg.WorkerPort) s.Equal(DefaultModel, cfg.Model) s.Equal(4, cfg.MaxConns) s.Equal(100, cfg.ContextObservations) s.Equal(25, cfg.ContextFullCount) s.Equal(10, cfg.ContextSessionCount) s.True(cfg.ContextShowReadTokens) s.True(cfg.ContextShowWorkTokens) s.Equal("narrative", cfg.ContextFullField) s.True(cfg.ContextShowLastSummary) s.Equal(DefaultObservationTypes, cfg.ContextObsTypes) s.Equal(DefaultObservationConcepts, cfg.ContextObsConcepts) } // TestDataDir tests data directory path. func (s *ConfigSuite) TestDataDir() { dir := DataDir() s.Contains(dir, ".claude-mnemonic") } // TestDBPath tests database path. func (s *ConfigSuite) TestDBPath() { path := DBPath() s.Contains(path, "claude-mnemonic.db") } // TestSettingsPath tests settings file path. func (s *ConfigSuite) TestSettingsPath() { path := SettingsPath() s.Contains(path, "settings.json") } // TestEnsureDataDir tests data directory creation. func (s *ConfigSuite) TestEnsureDataDir() { err := EnsureDataDir() s.NoError(err) dir := DataDir() info, err := os.Stat(dir) s.NoError(err) s.True(info.IsDir()) } // TestEnsureSettings tests settings file creation. func (s *ConfigSuite) TestEnsureSettings() { // First ensure data dir exists err := EnsureDataDir() s.NoError(err) // Ensure settings creates default file err = EnsureSettings() s.NoError(err) path := SettingsPath() info, err := os.Stat(path) s.NoError(err) s.False(info.IsDir()) // Second call should not error (file exists) err = EnsureSettings() s.NoError(err) } // TestEnsureAll tests full initialization. func (s *ConfigSuite) TestEnsureAll() { err := EnsureAll() s.NoError(err) // Verify dir and settings exist _, err = os.Stat(DataDir()) s.NoError(err) _, err = os.Stat(SettingsPath()) s.NoError(err) } // TestLoad_TableDriven tests configuration loading with various scenarios. func (s *ConfigSuite) TestLoad_TableDriven() { tests := []struct { name string settingsJSON string expectedModel string expectedPort int expectedObsObs int }{ { name: "no settings file", settingsJSON: "", expectedPort: DefaultWorkerPort, expectedModel: DefaultModel, expectedObsObs: 100, }, { name: "custom port", settingsJSON: `{"CLAUDE_MNEMONIC_WORKER_PORT": 38888}`, expectedPort: 38888, expectedModel: DefaultModel, expectedObsObs: 100, }, { name: "custom model", settingsJSON: `{"CLAUDE_MNEMONIC_MODEL": "sonnet"}`, expectedPort: DefaultWorkerPort, expectedModel: "sonnet", expectedObsObs: 100, }, { name: "custom observations", settingsJSON: `{"CLAUDE_MNEMONIC_CONTEXT_OBSERVATIONS": 200}`, expectedPort: DefaultWorkerPort, expectedModel: DefaultModel, expectedObsObs: 200, }, { name: "multiple settings", settingsJSON: `{"CLAUDE_MNEMONIC_WORKER_PORT": 39999, "CLAUDE_MNEMONIC_MODEL": "opus", "CLAUDE_MNEMONIC_CONTEXT_OBSERVATIONS": 50}`, expectedPort: 39999, expectedModel: "opus", expectedObsObs: 50, }, { name: "invalid JSON returns defaults", settingsJSON: `{invalid}`, expectedPort: DefaultWorkerPort, expectedModel: DefaultModel, expectedObsObs: 100, }, } for _, tt := range tests { s.Run(tt.name, func() { // Create fresh temp dir tempDir, err := os.MkdirTemp("", "config-test-*") s.Require().NoError(err) defer os.RemoveAll(tempDir) os.Setenv("HOME", tempDir) // Create data dir err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750) s.Require().NoError(err) if tt.settingsJSON != "" { writeErr := os.WriteFile( filepath.Join(tempDir, ".claude-mnemonic", "settings.json"), []byte(tt.settingsJSON), 0600, ) s.Require().NoError(writeErr) } cfg, err := Load() s.NoError(err) s.NotNil(cfg) s.Equal(tt.expectedPort, cfg.WorkerPort) s.Equal(tt.expectedModel, cfg.Model) s.Equal(tt.expectedObsObs, cfg.ContextObservations) }) } } // TestGetWorkerPort_TableDriven tests worker port retrieval with various scenarios. func TestGetWorkerPort_TableDriven(t *testing.T) { tests := []struct { name string envValue string wantPort int setEnv bool }{ { name: "no env, use default", envValue: "", wantPort: DefaultWorkerPort, setEnv: false, }, { name: "env set to valid port", envValue: "38888", wantPort: 38888, setEnv: true, }, { name: "env set to invalid value", envValue: "invalid", wantPort: DefaultWorkerPort, setEnv: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Save original env origEnv := os.Getenv("CLAUDE_MNEMONIC_WORKER_PORT") defer os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", origEnv) if tt.setEnv { os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", tt.envValue) } else { os.Unsetenv("CLAUDE_MNEMONIC_WORKER_PORT") } // We can't easily test GetWorkerPort since it uses Get() which caches // So we test the env parsing logic directly if tt.setEnv && tt.envValue != "" { if tt.wantPort != DefaultWorkerPort { assert.Equal(t, tt.envValue, os.Getenv("CLAUDE_MNEMONIC_WORKER_PORT")) } } }) } } // TestSplitTrim tests the splitTrim helper function. func TestSplitTrim(t *testing.T) { tests := []struct { name string input string expected []string }{ { name: "empty string", input: "", expected: []string{}, }, { name: "single value", input: "bugfix", expected: []string{"bugfix"}, }, { name: "multiple values", input: "bugfix,feature,refactor", expected: []string{"bugfix", "feature", "refactor"}, }, { name: "values with spaces", input: " bugfix , feature , refactor ", expected: []string{"bugfix", "feature", "refactor"}, }, { name: "empty values filtered", input: "bugfix,,feature,,", expected: []string{"bugfix", "feature"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := splitTrim(tt.input) assert.Equal(t, tt.expected, result) }) } } // TestDefaultObservationTypes tests default observation types. func TestDefaultObservationTypes(t *testing.T) { expected := []string{ "bugfix", "feature", "refactor", "change", "discovery", "decision", } assert.Equal(t, expected, DefaultObservationTypes) } // TestDefaultObservationConcepts tests default observation concepts. func TestDefaultObservationConcepts(t *testing.T) { expected := []string{ "how-it-works", "why-it-exists", "what-changed", "problem-solution", "gotcha", "pattern", "trade-off", } assert.Equal(t, expected, DefaultObservationConcepts) } // TestCriticalConcepts tests critical concepts list. func TestCriticalConcepts(t *testing.T) { expected := []string{ "gotcha", "pattern", "problem-solution", "trade-off", } assert.Equal(t, expected, CriticalConcepts) } // TestLoad_ClaudeCodePath tests claude code path loading. func TestLoad_ClaudeCodePath(t *testing.T) { // Create temp dir tempDir, err := os.MkdirTemp("", "config-test-*") require.NoError(t, err) defer os.RemoveAll(tempDir) origHome := os.Getenv("HOME") os.Setenv("HOME", tempDir) defer os.Setenv("HOME", origHome) // Create data dir and settings err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750) require.NoError(t, err) settingsJSON := `{"CLAUDE_CODE_PATH": "/usr/local/bin/claude"}` err = os.WriteFile( filepath.Join(tempDir, ".claude-mnemonic", "settings.json"), []byte(settingsJSON), 0600, ) require.NoError(t, err) cfg, err := Load() require.NoError(t, err) assert.Equal(t, "/usr/local/bin/claude", cfg.ClaudeCodePath) } // TestGet tests the global config getter. func TestGet(t *testing.T) { // Save and restore HOME origHome := os.Getenv("HOME") tempDir, err := os.MkdirTemp("", "config-get-test-*") require.NoError(t, err) defer func() { os.Setenv("HOME", origHome) os.RemoveAll(tempDir) }() os.Setenv("HOME", tempDir) // Create data dir err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750) require.NoError(t, err) // Get() should return a valid config cfg := Get() require.NotNil(t, cfg) assert.Greater(t, cfg.WorkerPort, 0) assert.NotEmpty(t, cfg.Model) } // TestGetWorkerPort_WithEnv tests GetWorkerPort with environment variable. func TestGetWorkerPort_WithEnv(t *testing.T) { // Save original env origEnv := os.Getenv("CLAUDE_MNEMONIC_WORKER_PORT") defer os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", origEnv) // Test with valid port in env os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", "45678") port := GetWorkerPort() assert.Equal(t, 45678, port) // Test with invalid port (should fall back to config) os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", "not-a-number") port = GetWorkerPort() // Should return from Get().WorkerPort, which is default assert.Greater(t, port, 0) // Test with zero port (should fall back to config) os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", "0") port = GetWorkerPort() // Zero is invalid, so should use default assert.Greater(t, port, 0) // Test with no env (should use config) os.Unsetenv("CLAUDE_MNEMONIC_WORKER_PORT") port = GetWorkerPort() assert.Greater(t, port, 0) } // TestLoad_ContextSettings tests context-related settings loading. func TestLoad_ContextSettings(t *testing.T) { // Create temp dir tempDir, err := os.MkdirTemp("", "config-test-*") require.NoError(t, err) defer os.RemoveAll(tempDir) origHome := os.Getenv("HOME") os.Setenv("HOME", tempDir) defer os.Setenv("HOME", origHome) // Create data dir and settings err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750) require.NoError(t, err) settingsJSON := `{ "CLAUDE_MNEMONIC_CONTEXT_FULL_COUNT": 50, "CLAUDE_MNEMONIC_CONTEXT_SESSION_COUNT": 20, "CLAUDE_MNEMONIC_CONTEXT_OBS_TYPES": "bugfix,feature", "CLAUDE_MNEMONIC_CONTEXT_OBS_CONCEPTS": "security,performance" }` err = os.WriteFile( filepath.Join(tempDir, ".claude-mnemonic", "settings.json"), []byte(settingsJSON), 0600, ) require.NoError(t, err) cfg, err := Load() require.NoError(t, err) assert.Equal(t, 50, cfg.ContextFullCount) assert.Equal(t, 20, cfg.ContextSessionCount) assert.Equal(t, []string{"bugfix", "feature"}, cfg.ContextObsTypes) assert.Equal(t, []string{"security", "performance"}, cfg.ContextObsConcepts) }