200 lines
6.6 KiB
Go
200 lines
6.6 KiB
Go
package config
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
ck "go.maximumdirect.net/configkit"
|
|
)
|
|
|
|
func TestDefaultsAndVersion(t *testing.T) {
|
|
result, err := Load(ck.Input{Source: ck.NoSource()}, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Config.Version != 3 || len(result.Config.Pipeline.Stages) != 5 {
|
|
t.Fatalf("defaults: %+v", result.Config)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "config.yml")
|
|
for _, text := range []string{"version: 2\n", "version: null\n", "version: invalid\n"} {
|
|
if err := os.WriteFile(path, []byte(text), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Load(ck.Input{Source: ck.File(path)}, ""); err == nil {
|
|
t.Fatalf("accepted %q", text)
|
|
}
|
|
}
|
|
if err := os.WriteFile(path, []byte("pipeline:\n stages: []\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
empty, err := Load(ck.Input{Source: ck.File(path)}, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(empty.Config.Pipeline.Stages) != 0 {
|
|
t.Fatal("empty pipeline was not preserved")
|
|
}
|
|
|
|
}
|
|
|
|
func TestMainContextImportsAndOverlay(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(dir, "overlays"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, contents := range map[string]string{
|
|
"main.yml": "version: 3\nconfig:\n imports:\n - path: fragment.yml\noutput:\n schema: bare-segments\n",
|
|
"fragment.yml": "context:\n description: imported\n",
|
|
"overlays/selected.yml": "context:\n description: overlay\n",
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(contents), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
result, err := Load(ck.Input{Source: ck.File(filepath.Join(dir, "main.yml"))}, "selected", ck.Override{Path: ck.Keys("context", "description"), Value: "override", Flag: "--description"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info, ok := result.Metadata().Lookup(ck.Keys("context", "description")); !ok || info.Origin.Layer != ck.LayerCLI {
|
|
t.Fatalf("override origin = %+v", info)
|
|
}
|
|
if result.Config.Context.Description != "override" {
|
|
t.Fatalf("description = %q", result.Config.Context.Description)
|
|
}
|
|
found := false
|
|
for _, source := range result.Sources() {
|
|
if source.Layer == ck.LayerMain && source.ImportedFrom == nil {
|
|
context, ok := result.SourceContext(source.ID)
|
|
if !ok {
|
|
t.Fatal("main context missing")
|
|
}
|
|
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, "fragment.yml"))
|
|
if err != nil || len(data) == 0 {
|
|
t.Fatalf("main context cannot access companion: %v", err)
|
|
}
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("main source missing")
|
|
}
|
|
if _, err := Load(ck.Input{Source: ck.File(filepath.Join(dir, "main.yml"))}, "missing"); err == nil {
|
|
t.Fatal("missing overlay accepted")
|
|
}
|
|
if _, err := Load(ck.Input{Source: ck.NoSource()}, "selected"); err == nil {
|
|
t.Fatal("overlay without context accepted")
|
|
}
|
|
}
|
|
|
|
func TestDiscoveryAndSelectedFailure(t *testing.T) {
|
|
first, second := t.TempDir(), t.TempDir()
|
|
search := ck.Searches(ck.SearchDirectory(first), ck.SearchDirectory(second))
|
|
load := func() (*ck.Result[Config], error) {
|
|
return Load(ck.Input{Source: ck.SearchConfig("audita", search)}, "")
|
|
}
|
|
result, err := load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, source := range result.Sources() {
|
|
if source.Layer == ck.LayerMain {
|
|
t.Fatalf("unexpected main source: %+v", source)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(second, "config.yml"), []byte("context:\n description: selected\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err = load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Config.Context.Description != "selected" {
|
|
t.Fatalf("description = %q", result.Config.Context.Description)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(first, "config.yml"), []byte("version: 2\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := load(); err == nil {
|
|
t.Fatal("invalid selected file fell through")
|
|
}
|
|
}
|
|
|
|
func TestOverlayUsesMainDirectoryWithinWiderRoot(t *testing.T) {
|
|
root := t.TempDir()
|
|
dir := filepath.Join(root, "project")
|
|
if err := os.MkdirAll(filepath.Join(dir, "overlays"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "config.yml"), []byte("version: 3\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "overlays", "local.yml"), []byte("context:\n description: project\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := Load(ck.Input{Source: ck.File(filepath.Join(dir, "config.yml")), Filesystem: &ck.FilesystemContext{Root: root}}, "local")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Config.Context.Description != "project" {
|
|
t.Fatalf("description = %q", result.Config.Context.Description)
|
|
}
|
|
}
|
|
|
|
func TestSchemaConstraintsAndExplicitValues(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "config.yml")
|
|
for _, tc := range []struct {
|
|
name, text string
|
|
valid bool
|
|
}{
|
|
{"nullable selections", "glossaries: null\nprotected_terms: []\nllm:\n validation_profile: null\n", true},
|
|
{"repeated modules", "pipeline:\n stages:\n - module: grammar\n - module: grammar\n", true},
|
|
{"removed backends", "backends: {}\n", false},
|
|
{"removed debug", "diagnostics:\n debug: true\n", false},
|
|
{"unknown module", "pipeline:\n stages:\n - module: unknown\n", false},
|
|
{"invalid chunk limit", "chunking:\n target_sections: 0\n", false},
|
|
{"invalid threshold", "thresholds:\n grammar: 1.1\n", false},
|
|
{"null group", "llm: null\n", false},
|
|
{"zero budgets", "llm:\n transient_retries: 0\n repair_attempts: 0\n", true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if err := os.WriteFile(path, []byte(tc.text), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := Load(ck.Input{Source: ck.File(path)}, "")
|
|
if (err == nil) != tc.valid {
|
|
t.Fatalf("load error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOverridesFollowInheritance(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.yml")
|
|
data := `glossary_collections:
|
|
common:
|
|
entries:
|
|
- id: a
|
|
name: First
|
|
category: character
|
|
derived:
|
|
extends: common
|
|
`
|
|
if err := os.WriteFile(path, []byte(data), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := Load(ck.Input{Source: ck.File(path)}, "", ck.Override{Path: ck.Keys("glossary_collections", "common", "entries"), Value: []any{map[string]any{"id": "a", "name": "Changed", "category": "character"}}, Flag: "--set"})
|
|
if err != nil {
|
|
t.Fatalf("%v: %+v", err, err.(*ck.LoadError).Diagnostics())
|
|
}
|
|
if got := result.Config.GlossaryCollections["derived"].Entries[0].Name; got != "First" {
|
|
t.Fatalf("inherited name = %q", got)
|
|
}
|
|
if got := result.Config.GlossaryCollections["common"].Entries[0].Name; got != "Changed" {
|
|
t.Fatalf("overridden name = %q", got)
|
|
}
|
|
}
|