diff options
Diffstat (limited to 'internal/appconfig/config_test.go')
| -rw-r--r-- | internal/appconfig/config_test.go | 578 |
1 files changed, 0 insertions, 578 deletions
diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go index c98d904..d4e7739 100644 --- a/internal/appconfig/config_test.go +++ b/internal/appconfig/config_test.go @@ -839,581 +839,3 @@ func TestProjectConfigPath(t *testing.T) { t.Fatalf("ProjectConfigPath() = %q, want empty", path) } } - -func TestIgnoreConfig_Defaults(t *testing.T) { - clearHexaiEnv(t) - cfg := Load(nil) - if cfg.IgnoreGitignore == nil || !*cfg.IgnoreGitignore { - t.Error("expected IgnoreGitignore default true") - } - if cfg.IgnoreLSPNotify == nil || !*cfg.IgnoreLSPNotify { - t.Error("expected IgnoreLSPNotify default true") - } - if len(cfg.IgnoreExtraPatterns) != 0 { - t.Errorf("expected empty IgnoreExtraPatterns, got %v", cfg.IgnoreExtraPatterns) - } -} - -func TestIgnoreConfig_FromFile(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[ignore] -gitignore = false -extra_patterns = ["*.min.js", "dist/**"] -lsp_notify_ignored = false -`) - cfg := LoadWithOptions(newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: dir}) - if cfg.IgnoreGitignore == nil || *cfg.IgnoreGitignore { - t.Error("expected IgnoreGitignore false from file") - } - if cfg.IgnoreLSPNotify == nil || *cfg.IgnoreLSPNotify { - t.Error("expected IgnoreLSPNotify false from file") - } - want := []string{"*.min.js", "dist/**"} - if !reflect.DeepEqual(cfg.IgnoreExtraPatterns, want) { - t.Errorf("IgnoreExtraPatterns = %v, want %v", cfg.IgnoreExtraPatterns, want) - } -} - -func TestIgnoreConfig_EnvOverrides(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[ignore] -gitignore = true -lsp_notify_ignored = true -`) - withEnv(t, "HEXAI_IGNORE_GITIGNORE", "false") - withEnv(t, "HEXAI_IGNORE_LSP_NOTIFY", "0") - withEnv(t, "HEXAI_IGNORE_EXTRA_PATTERNS", "*.bak,*.tmp") - cfg := LoadWithOptions(newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: dir}) - if cfg.IgnoreGitignore == nil || *cfg.IgnoreGitignore { - t.Error("expected IgnoreGitignore false from env override") - } - if cfg.IgnoreLSPNotify == nil || *cfg.IgnoreLSPNotify { - t.Error("expected IgnoreLSPNotify false from env override") - } - want := []string{"*.bak", "*.tmp"} - if !reflect.DeepEqual(cfg.IgnoreExtraPatterns, want) { - t.Errorf("IgnoreExtraPatterns = %v, want %v", cfg.IgnoreExtraPatterns, want) - } -} - -func TestIgnoreConfig_ProjectOverride(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[ignore] -gitignore = true -`) - // Set up a fake git repo with project override - projectDir := t.TempDir() - if err := os.Mkdir(filepath.Join(projectDir, ".git"), 0o755); err != nil { - t.Fatalf("mkdir .git: %v", err) - } - projectCfg := filepath.Join(projectDir, ProjectConfigFilename) - writeFile(t, projectCfg, ` -[ignore] -gitignore = false -extra_patterns = ["build/**"] -`) - cfg := LoadWithOptions(newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: projectDir}) - if cfg.IgnoreGitignore == nil || *cfg.IgnoreGitignore { - t.Error("expected project override to set IgnoreGitignore false") - } - want := []string{"build/**"} - if !reflect.DeepEqual(cfg.IgnoreExtraPatterns, want) { - t.Errorf("IgnoreExtraPatterns = %v, want %v", cfg.IgnoreExtraPatterns, want) - } -} - -func TestIgnoreConfig_DisableGitignore(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[ignore] -gitignore = false -`) - cfg := LoadWithOptions(newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: dir}) - if cfg.IgnoreGitignore == nil || *cfg.IgnoreGitignore { - t.Error("expected IgnoreGitignore false") - } - // LSP notify should still be true (default, not overridden) - if cfg.IgnoreLSPNotify == nil || !*cfg.IgnoreLSPNotify { - t.Error("expected IgnoreLSPNotify to remain true (default)") - } -} - -func TestTmuxEditConfig_FromFile(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[tmux_edit] -popup_width = "90%" -popup_height = "85%" -default_agent = "claude" - -[[tmux_edit.agents]] -name = "claude" -display_name = "Claude Code" -detect_pattern = "(?i)(claude|anthropic)" -prompt_pattern = '(?s)>\s*(.+?)$' -clear_first = true -clear_keys = "C-u" -newline_keys = "S-Enter" -submit_keys = "Enter" - -[[tmux_edit.agents]] -name = "cursor" -display_name = "Cursor" -detect_pattern = "(?i)cursor" -prompt_pattern = '(?s)│\s*(.+?)$' -strip_patterns = ["INSERT", "Add a follow-up"] -clear_first = true -clear_keys = "C-u" -newline_keys = "S-Enter" -submit_keys = "Enter" -`) - cfg := LoadWithOptions(newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: dir}) - if cfg.TmuxEditPopupWidth != "90%" { - t.Errorf("PopupWidth = %q, want 90%%", cfg.TmuxEditPopupWidth) - } - if cfg.TmuxEditPopupHeight != "85%" { - t.Errorf("PopupHeight = %q, want 85%%", cfg.TmuxEditPopupHeight) - } - if cfg.TmuxEditDefaultAgent != "claude" { - t.Errorf("DefaultAgent = %q, want claude", cfg.TmuxEditDefaultAgent) - } - if len(cfg.TmuxEditAgents) != 2 { - t.Fatalf("got %d agents, want 2", len(cfg.TmuxEditAgents)) - } - a := cfg.TmuxEditAgents[0] - if a.Name != "claude" || a.DisplayName != "Claude Code" { - t.Errorf("agent[0] = %q/%q, want claude/Claude Code", a.Name, a.DisplayName) - } - if a.ClearFirst == nil || !*a.ClearFirst { - t.Error("expected ClearFirst = true for claude agent") - } - b := cfg.TmuxEditAgents[1] - if b.Name != "cursor" { - t.Errorf("agent[1].Name = %q, want cursor", b.Name) - } - if len(b.StripPatterns) != 2 { - t.Errorf("agent[1].StripPatterns = %v, want 2 entries", b.StripPatterns) - } -} - -func TestTmuxEditConfig_Merge(t *testing.T) { - clearHexaiEnv(t) - a := newDefaultConfig() - b := App{ - TmuxEditPopupWidth: "70%", - TmuxEditDefaultAgent: "amp", - TmuxEditAgents: []TmuxEditAgentCfg{ - {Name: "amp", DisplayName: "Amp"}, - }, - } - a.mergeWith(&b) - if a.TmuxEditPopupWidth != "70%" { - t.Errorf("PopupWidth = %q, want 70%%", a.TmuxEditPopupWidth) - } - if a.TmuxEditDefaultAgent != "amp" { - t.Errorf("DefaultAgent = %q, want amp", a.TmuxEditDefaultAgent) - } - if len(a.TmuxEditAgents) != 1 || a.TmuxEditAgents[0].Name != "amp" { - t.Errorf("Agents = %v, want single amp", a.TmuxEditAgents) - } -} - -func TestTmuxEditConfig_SkipsEmptyName(t *testing.T) { - clearHexaiEnv(t) - dir := t.TempDir() - cfgPath := filepath.Join(dir, "config.toml") - writeFile(t, cfgPath, ` -[tmux_edit] -[[tmux_edit.agents]] -name = "" -display_name = "Empty" -`) - cfg := LoadWithOptions(newLogger(), LoadOptions{ConfigPath: cfgPath, ProjectRoot: dir}) - if len(cfg.TmuxEditAgents) != 0 { - t.Errorf("got %d agents, want 0 (empty name should be skipped)", len(cfg.TmuxEditAgents)) - } -} - -// --- Phase 1: Config Parsing Tests --- - -func TestParseTemperatureValue(t *testing.T) { - tests := []struct { - name string - input any - wantValue *float64 - wantOK bool - }{ - {"float64 zero", float64(0.0), floatPtr(0.0), true}, - {"float64 half", float64(0.5), floatPtr(0.5), true}, - {"float64 one", float64(1.0), floatPtr(1.0), true}, - {"float64 two", float64(2.0), floatPtr(2.0), true}, - {"int64 zero", int64(0), floatPtr(0.0), true}, - {"int64 one", int64(1), floatPtr(1.0), true}, - {"int64 two", int64(2), floatPtr(2.0), true}, - {"string zero", "0", floatPtr(0.0), true}, - {"string one", "1", floatPtr(1.0), true}, - {"string two", "2", floatPtr(2.0), true}, - {"string float", "0.75", floatPtr(0.75), true}, - {"string empty", "", nil, true}, - {"string whitespace", " ", nil, true}, - {"string invalid", "invalid", nil, false}, - {"string negative", "-0.5", floatPtr(-0.5), true}, - {"string very small", "0.0001", floatPtr(0.0001), true}, - {"string high precision", "1.123456789", floatPtr(1.123456789), true}, - {"nil value", nil, nil, false}, - {"bool value", true, nil, false}, - {"map value", map[string]any{}, nil, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, ok := parseTemperatureValue(tt.input, "test", newLogger()) - if ok != tt.wantOK { - t.Errorf("parseTemperatureValue() ok = %v, want %v", ok, tt.wantOK) - } - if !ok { - return - } - if (got == nil) != (tt.wantValue == nil) { - t.Errorf("parseTemperatureValue() = %v, want %v", got, tt.wantValue) - return - } - if got != nil && tt.wantValue != nil && *got != *tt.wantValue { - t.Errorf("parseTemperatureValue() = %v, want %v", *got, *tt.wantValue) - } - }) - } -} - -func TestDecodeModelEntry(t *testing.T) { - tests := []struct { - name string - input any - wantCfg *SurfaceConfig - wantOK bool - }{ - { - name: "simple string model", - input: "gpt-4", - wantCfg: &SurfaceConfig{Model: "gpt-4"}, - wantOK: true, - }, - { - name: "empty string", - input: "", - wantCfg: nil, - wantOK: false, - }, - { - name: "whitespace string", - input: " ", - wantCfg: nil, - wantOK: false, - }, - { - name: "object with all fields", - input: map[string]any{ - "model": "claude-3", - "provider": "anthropic", - "temperature": float64(0.7), - }, - wantCfg: &SurfaceConfig{ - Model: "claude-3", - Provider: "anthropic", - Temperature: floatPtr(0.7), - }, - wantOK: true, - }, - { - name: "object with model only", - input: map[string]any{ - "model": "gpt-4o", - }, - wantCfg: &SurfaceConfig{Model: "gpt-4o"}, - wantOK: true, - }, - { - name: "object with provider only", - input: map[string]any{ - "provider": "openai", - }, - wantCfg: &SurfaceConfig{Provider: "openai"}, - wantOK: true, - }, - { - name: "object with temperature only", - input: map[string]any{ - "temperature": float64(0.5), - }, - wantCfg: &SurfaceConfig{Temperature: floatPtr(0.5)}, - wantOK: true, - }, - { - name: "object with empty fields", - input: map[string]any{ - "model": "", - "provider": "", - }, - wantCfg: nil, - wantOK: false, - }, - { - name: "object with invalid model type", - input: map[string]any{ - "model": 123, - }, - wantCfg: nil, - wantOK: false, - }, - { - name: "object with invalid provider type", - input: map[string]any{ - "provider": 456, - }, - wantCfg: nil, - wantOK: false, - }, - { - name: "object with invalid temperature", - input: map[string]any{ - "model": "gpt-4", - "temperature": "not a number", - }, - wantCfg: nil, - wantOK: false, - }, - { - name: "nil input", - input: nil, - wantCfg: nil, - wantOK: false, - }, - { - name: "invalid type (int)", - input: 123, - wantCfg: nil, - wantOK: false, - }, - { - name: "invalid type (slice)", - input: []string{"gpt-4"}, - wantCfg: nil, - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, ok := decodeModelEntry(tt.input, "test", newLogger()) - if ok != tt.wantOK { - t.Errorf("decodeModelEntry() ok = %v, want %v", ok, tt.wantOK) - } - if !ok { - return - } - if (got == nil) != (tt.wantCfg == nil) { - t.Errorf("decodeModelEntry() = %v, want %v", got, tt.wantCfg) - return - } - if got == nil { - return - } - if got.Model != tt.wantCfg.Model { - t.Errorf("Model = %q, want %q", got.Model, tt.wantCfg.Model) - } - if got.Provider != tt.wantCfg.Provider { - t.Errorf("Provider = %q, want %q", got.Provider, tt.wantCfg.Provider) - } - if (got.Temperature == nil) != (tt.wantCfg.Temperature == nil) { - t.Errorf("Temperature nil mismatch: got %v, want %v", got.Temperature, tt.wantCfg.Temperature) - } else if got.Temperature != nil && *got.Temperature != *tt.wantCfg.Temperature { - t.Errorf("Temperature = %v, want %v", *got.Temperature, *tt.wantCfg.Temperature) - } - }) - } -} - -func TestResolvedModel(t *testing.T) { - tests := []struct { - name string - section sectionOpenAI - want string - }{ - { - name: "explicit model no presets", - section: sectionOpenAI{Model: "gpt-4"}, - want: "gpt-4", - }, - { - name: "empty model", - section: sectionOpenAI{Model: ""}, - want: "", - }, - { - name: "whitespace model", - section: sectionOpenAI{Model: " "}, - want: "", - }, - { - name: "preset match exact case", - section: sectionOpenAI{ - Model: "fast", - Presets: map[string]string{"fast": "gpt-3.5-turbo"}, - }, - want: "gpt-3.5-turbo", - }, - { - name: "preset match case insensitive", - section: sectionOpenAI{ - Model: "FAST", - Presets: map[string]string{"fast": "gpt-3.5-turbo"}, - }, - want: "gpt-3.5-turbo", - }, - { - name: "no preset match returns original", - section: sectionOpenAI{ - Model: "custom-model", - Presets: map[string]string{"fast": "gpt-3.5-turbo"}, - }, - want: "custom-model", - }, - { - name: "preset empty value returns original", - section: sectionOpenAI{ - Model: "fast", - Presets: map[string]string{"fast": ""}, - }, - want: "fast", - }, - { - name: "preset whitespace value returns original", - section: sectionOpenAI{ - Model: "fast", - Presets: map[string]string{"fast": " "}, - }, - want: "fast", - }, - { - name: "multiple presets uses correct one", - section: sectionOpenAI{ - Model: "smart", - Presets: map[string]string{ - "fast": "gpt-3.5-turbo", - "smart": "gpt-4", - "mini": "gpt-3.5-mini", - }, - }, - want: "gpt-4", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.section.resolvedModel() - if got != tt.want { - t.Errorf("resolvedModel() = %q, want %q", got, tt.want) - } - }) - } -} - -func TestParseSurfaceEntries(t *testing.T) { - tests := []struct { - name string - input any - wantLen int - wantOK bool - }{ - { - name: "nil input", - input: nil, - wantLen: 0, - wantOK: false, - }, - { - name: "single string", - input: "gpt-4", - wantLen: 1, - wantOK: true, - }, - { - name: "single map", - input: map[string]any{ - "model": "claude-3", - "provider": "anthropic", - }, - wantLen: 1, - wantOK: true, - }, - { - name: "array of strings", - input: []any{ - "gpt-4", - "claude-3", - }, - wantLen: 2, - wantOK: true, - }, - { - name: "array of maps", - input: []any{ - map[string]any{"model": "gpt-4", "provider": "openai"}, - map[string]any{"model": "claude-3", "provider": "anthropic"}, - }, - wantLen: 2, - wantOK: true, - }, - { - name: "array with invalid entries", - input: []any{ - "gpt-4", - 123, - "claude-3", - }, - wantLen: 2, - wantOK: true, - }, - { - name: "array with all invalid entries", - input: []any{ - 123, - true, - nil, - }, - wantLen: 0, - wantOK: false, - }, - { - name: "empty array", - input: []any{}, - wantLen: 0, - wantOK: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, ok := parseSurfaceEntries(tt.input, "test", newLogger()) - if ok != tt.wantOK { - t.Errorf("parseSurfaceEntries() ok = %v, want %v", ok, tt.wantOK) - } - if len(got) != tt.wantLen { - t.Errorf("parseSurfaceEntries() len = %d, want %d", len(got), tt.wantLen) - } - }) - } -} |
