package config import ( "errors" "os" "path/filepath" "testing" renderformat "gitea.maximumdirect.net/eric/scriptorium/internal/format" ) func TestLoadConfigMissingImplicitPathUsesBuiltInDefaults(t *testing.T) { tmp := t.TempDir() missing := filepath.Join(tmp, "missing.yml") got, err := LoadConfig(missing, false) if err != nil { t.Fatalf("expected no error, got %v", err) } want := BuiltInDefaults() if got != want { t.Fatalf("unexpected settings: got=%+v want=%+v", got, want) } } func TestLoadConfigMissingExplicitPathReturnsError(t *testing.T) { tmp := t.TempDir() missing := filepath.Join(tmp, "missing.yml") _, err := LoadConfig(missing, true) if err == nil { t.Fatal("expected error for missing explicit config") } if !errors.Is(err, ErrConfigNotFound) { t.Fatalf("expected ErrConfigNotFound, got %v", err) } } func TestLoadConfigInvalidYAMLReturnsError(t *testing.T) { path := writeConfigFile(t, "config.yml", "prompt_dir: [") _, err := LoadConfig(path, true) if err == nil { t.Fatal("expected invalid YAML error") } if !errors.Is(err, ErrInvalidConfigYAML) { t.Fatalf("expected ErrInvalidConfigYAML, got %v", err) } } func TestLoadConfigInvalidYAMLImplicitPathReturnsError(t *testing.T) { path := writeConfigFile(t, "config.yml", "prompt_dir: [") _, err := LoadConfig(path, false) if err == nil { t.Fatal("expected invalid YAML error") } if !errors.Is(err, ErrInvalidConfigYAML) { t.Fatalf("expected ErrInvalidConfigYAML, got %v", err) } } func TestLoadConfigUnknownFieldReturnsError(t *testing.T) { path := writeConfigFile(t, "config.yml", "unknown_field: true\n") _, err := LoadConfig(path, true) if err == nil { t.Fatal("expected unknown field error") } if !errors.Is(err, ErrInvalidConfigYAML) { t.Fatalf("expected ErrInvalidConfigYAML, got %v", err) } } func TestLoadConfigAPIKeyFieldIsRejectedAsUnknown(t *testing.T) { path := writeConfigFile(t, "config.yml", "api_key: secret\n") _, err := LoadConfig(path, true) if err == nil { t.Fatal("expected unknown field error for api_key") } if !errors.Is(err, ErrInvalidConfigYAML) { t.Fatalf("expected ErrInvalidConfigYAML, got %v", err) } } func TestLoadConfigValidConfigSetsDirectoriesAndServerAddr(t *testing.T) { path := writeConfigFile(t, "config.yml", ` prompt_dir: ./prompts profile_dir: ./profiles schema_dir: ./schemas server: addr: 127.0.0.1:9090 defaults: render_format: json `) got, err := LoadConfig(path, true) if err != nil { t.Fatalf("expected no error, got %v", err) } if got.PromptDir != filepath.Clean("./prompts") { t.Fatalf("unexpected prompt_dir: %q", got.PromptDir) } if got.ProfileDir != filepath.Clean("./profiles") { t.Fatalf("unexpected profile_dir: %q", got.ProfileDir) } if got.SchemaDir != filepath.Clean("./schemas") { t.Fatalf("unexpected schema_dir: %q", got.SchemaDir) } if got.ServerAddr != "127.0.0.1:9090" { t.Fatalf("unexpected server.addr: %q", got.ServerAddr) } if got.DefaultRenderFormat != renderformat.PreparedRunFormatJSON { t.Fatalf("unexpected defaults.render_format: %q", got.DefaultRenderFormat) } } func TestLoadConfigEmptyFileResolvesToBuiltInDefaults(t *testing.T) { path := writeConfigFile(t, "config.yml", "") got, err := LoadConfig(path, true) if err != nil { t.Fatalf("expected no error, got %v", err) } want := BuiltInDefaults() if got != want { t.Fatalf("unexpected settings: got=%+v want=%+v", got, want) } } func TestLoadConfigImplicitSearchPrefersUsrLocalEtcOverEtc(t *testing.T) { tmp := t.TempDir() localPath := filepath.Join(tmp, "usr-local.yml") etcPath := filepath.Join(tmp, "etc.yml") if err := os.WriteFile(localPath, []byte("prompt_dir: ./from-usr-local\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(etcPath, []byte("prompt_dir: ./from-etc\n"), 0o644); err != nil { t.Fatal(err) } orig := defaultConfigSearchPaths defaultConfigSearchPaths = []string{localPath, etcPath} t.Cleanup(func() { defaultConfigSearchPaths = orig }) got, err := LoadConfig("", false) if err != nil { t.Fatalf("expected no error, got %v", err) } if got.PromptDir != filepath.Clean("./from-usr-local") { t.Fatalf("expected usr-local config to win, got prompt_dir=%q", got.PromptDir) } } func TestLoadConfigImplicitSearchFallsBackToEtcWhenUsrLocalMissing(t *testing.T) { tmp := t.TempDir() missingLocal := filepath.Join(tmp, "missing-local.yml") etcPath := filepath.Join(tmp, "etc.yml") if err := os.WriteFile(etcPath, []byte("prompt_dir: ./from-etc\n"), 0o644); err != nil { t.Fatal(err) } orig := defaultConfigSearchPaths defaultConfigSearchPaths = []string{missingLocal, etcPath} t.Cleanup(func() { defaultConfigSearchPaths = orig }) got, err := LoadConfig("", false) if err != nil { t.Fatalf("expected no error, got %v", err) } if got.PromptDir != filepath.Clean("./from-etc") { t.Fatalf("expected etc fallback config, got prompt_dir=%q", got.PromptDir) } } func TestApplyCLIOverridesAppliesPrecedence(t *testing.T) { base := AppSettings{ PromptDir: "/from/config/prompts", ProfileDir: "/from/config/profiles", SchemaDir: "/from/config/schemas", ServerAddr: ":1234", DefaultRenderFormat: renderformat.PreparedRunFormatJSON, } got, err := ApplyCLIOverrides(base, CLIOverrides{ PromptDir: "./prompts-cli", ProfileDir: "./profiles-cli", SchemaDir: "./schemas-cli", ServerAddr: ":8081", RenderFormat: "text", }) if err != nil { t.Fatalf("expected no error, got %v", err) } if got.PromptDir != filepath.Clean("./prompts-cli") { t.Fatalf("unexpected prompt dir: %q", got.PromptDir) } if got.ProfileDir != filepath.Clean("./profiles-cli") { t.Fatalf("unexpected profile dir: %q", got.ProfileDir) } if got.SchemaDir != filepath.Clean("./schemas-cli") { t.Fatalf("unexpected schema dir: %q", got.SchemaDir) } if got.ServerAddr != ":8081" { t.Fatalf("unexpected server addr: %q", got.ServerAddr) } if got.DefaultRenderFormat != renderformat.PreparedRunFormatText { t.Fatalf("unexpected render format: %q", got.DefaultRenderFormat) } } func TestApplyCLIOverridesInvalidRenderFormatReturnsError(t *testing.T) { _, err := ApplyCLIOverrides(BuiltInDefaults(), CLIOverrides{RenderFormat: "yaml"}) if err == nil { t.Fatal("expected invalid render format error") } if !errors.Is(err, ErrInvalidConfig) { t.Fatalf("expected ErrInvalidConfig, got %v", err) } } func writeConfigFile(t *testing.T, name, content string) string { t.Helper() path := filepath.Join(t.TempDir(), name) if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("failed to write config file: %v", err) } return path }