Add shared ConfigKit loading and runtime settings conversion
This commit is contained in:
176
internal/config/load_test.go
Normal file
176
internal/config/load_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
func useConfigSearchPaths(t *testing.T, paths ...string) {
|
||||
t.Helper()
|
||||
previous := defaultConfigSearchPaths
|
||||
defaultConfigSearchPaths = paths
|
||||
t.Cleanup(func() { defaultConfigSearchPaths = previous })
|
||||
}
|
||||
|
||||
func writeApplicationFile(t *testing.T, path, body string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSelectsOrderedSourceAndKeepsItsSnapshot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
first := filepath.Join(dir, "first.yml")
|
||||
second := filepath.Join(dir, "second.yml")
|
||||
writeApplicationFile(t, first, "prompt_dir: ./first\n")
|
||||
writeApplicationFile(t, second, "prompt_dir: ./second\n")
|
||||
useConfigSearchPaths(t, first, second)
|
||||
|
||||
result, err := Load(LoadOptions{Path: filepath.Join(dir, "ignored.yml")})
|
||||
if err != nil {
|
||||
t.Fatalf("load first implicit source: %v", err)
|
||||
}
|
||||
if result.Config.PromptDir != "first" || len(result.Sources()) != 1 || result.Sources()[0].Path != first {
|
||||
t.Fatalf("unexpected implicit source: config=%+v sources=%+v", result.Config, result.Sources())
|
||||
}
|
||||
writeApplicationFile(t, first, "prompt_dir: ./changed\n")
|
||||
if result.Config.PromptDir != "first" {
|
||||
t.Fatalf("result did not retain resolved snapshot: %+v", result.Config)
|
||||
}
|
||||
|
||||
if err := os.Remove(first); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err = Load(LoadOptions{})
|
||||
if err != nil || result.Config.PromptDir != "second" || result.Sources()[0].Path != second {
|
||||
t.Fatalf("second implicit source not selected: result=%+v err=%v", result, err)
|
||||
}
|
||||
|
||||
result, err = Load(LoadOptions{Path: first, Explicit: true})
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("missing explicit source was accepted: result=%+v err=%v", result, err)
|
||||
}
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("typed load error was lost: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsExplicitEmptyAndDistinguishesEmptyFile(t *testing.T) {
|
||||
useConfigSearchPaths(t, filepath.Join(t.TempDir(), "absent.yml"))
|
||||
if _, err := Load(LoadOptions{Explicit: true, Path: ""}); err == nil {
|
||||
t.Fatal("explicit empty path was accepted")
|
||||
} else {
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("expected upstream source diagnostic, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
empty := filepath.Join(dir, "empty.yml")
|
||||
writeApplicationFile(t, empty, "")
|
||||
if _, err := Load(LoadOptions{Path: empty, Explicit: true}); err == nil {
|
||||
t.Fatal("empty file was treated as defaults-only")
|
||||
}
|
||||
writeApplicationFile(t, empty, "{}\n")
|
||||
result, err := Load(LoadOptions{Path: empty, Explicit: true})
|
||||
if err != nil || !reflect.DeepEqual(result.Config, documentDefaults()) {
|
||||
t.Fatalf("empty mapping did not load defaults: result=%+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaultsOnlyAndOverridesBeforeValidation(t *testing.T) {
|
||||
useConfigSearchPaths(t, filepath.Join(t.TempDir(), "absent.yml"))
|
||||
result, err := Load(LoadOptions{})
|
||||
if err != nil || !reflect.DeepEqual(result.Config, documentDefaults()) || len(result.Sources()) != 0 {
|
||||
t.Fatalf("defaults-only load: result=%+v err=%v", result, err)
|
||||
}
|
||||
|
||||
result, err = Load(LoadOptions{Overrides: []configkit.Override{{
|
||||
Path: configkit.Keys("server", "max_request_bytes"), Value: int64(0), Flag: "--max-request-bytes",
|
||||
}}})
|
||||
if err != nil || result.Config.Server.MaxRequestBytes != 0 || len(result.Sources()) != 1 || result.Sources()[0].Layer != configkit.LayerCLI {
|
||||
t.Fatalf("defaults-only override lost: result=%+v err=%v", result, err)
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "invalid.yml")
|
||||
writeApplicationFile(t, path, "defaults:\n render_format: invalid-choice\n")
|
||||
result, err = Load(LoadOptions{Path: path, Explicit: true, Overrides: []configkit.Override{{
|
||||
Path: configkit.Keys("defaults", "render_format"), Value: "json", Flag: "--render-format",
|
||||
}}})
|
||||
if err != nil || result.Config.Defaults.RenderFormat != "json" {
|
||||
t.Fatalf("override did not precede validation: result=%+v err=%v", result, err)
|
||||
}
|
||||
if len(result.Sources()) != 2 || result.Sources()[0].Layer != configkit.LayerBase || result.Sources()[1].Layer != configkit.LayerCLI {
|
||||
t.Fatalf("source order changed: %+v", result.Sources())
|
||||
}
|
||||
writeApplicationFile(t, path, "defaults: [\n")
|
||||
if _, err := Load(LoadOptions{Path: path, Explicit: true, Overrides: []configkit.Override{{
|
||||
Path: configkit.Keys("defaults", "render_format"), Value: "json", Flag: "--render-format",
|
||||
}}}); err == nil {
|
||||
t.Fatal("override bypassed malformed lower-level YAML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsCopiesResolvedValuesAndSortsBackends(t *testing.T) {
|
||||
result, err := loadDocumentBytes(`schema_dir: ""
|
||||
transient_retries: 0
|
||||
server:
|
||||
addr: ""
|
||||
artifact_root: ""
|
||||
max_request_bytes: 0
|
||||
defaults:
|
||||
render_format: json
|
||||
backends:
|
||||
zed:
|
||||
endpoint: http://zed.invalid/v1
|
||||
extra_params:
|
||||
count: 4
|
||||
alpha:
|
||||
queue_capacity: 0
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings := Settings(result.Config)
|
||||
if settings.SchemaDir != "" || settings.ServerAddr != "" || settings.MaxRequestBytes != 0 || settings.DefaultRenderFormat != "json" {
|
||||
t.Fatalf("resolved scalar values changed: %+v", settings)
|
||||
}
|
||||
if settings.TransientRetries == nil || *settings.TransientRetries != 0 {
|
||||
t.Fatalf("retry presence lost: %+v", settings.TransientRetries)
|
||||
}
|
||||
if len(settings.Backends) != 2 || settings.Backends[0].ID != "alpha" || settings.Backends[1].ID != "zed" {
|
||||
t.Fatalf("backend order changed: %+v", settings.Backends)
|
||||
}
|
||||
if settings.Backends[0].QueueCapacity == nil || *settings.Backends[0].QueueCapacity != 0 || settings.Backends[1].QueueCapacity != nil {
|
||||
t.Fatalf("queue presence changed: %+v", settings.Backends)
|
||||
}
|
||||
if value := settings.Backends[1].ExtraParams["count"]; value != int64(4) {
|
||||
t.Fatalf("dynamic number changed shape: %T %v", value, value)
|
||||
}
|
||||
|
||||
if got := Settings(documentDefaults()); got.TransientRetries != nil || got.Backends != nil {
|
||||
t.Fatalf("default nullable values changed: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDiscoveryErrorQuotesPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
loop := filepath.Join(dir, "loop-\n.yml")
|
||||
if err := os.Symlink(loop, loop); err != nil {
|
||||
t.Skipf("symlink unavailable: %v", err)
|
||||
}
|
||||
useConfigSearchPaths(t, loop)
|
||||
_, err := Load(LoadOptions{})
|
||||
if err == nil || !strings.Contains(err.Error(), `\n`) || strings.Contains(err.Error(), "loop-\n.yml") {
|
||||
t.Fatalf("discovery error did not quote path safely: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user