Add config validation and resolution
This commit is contained in:
334
internal/core/config/validation_test.go
Normal file
334
internal/core/config/validation_test.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
)
|
||||
|
||||
func TestValidateSuccessForValidConfig(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsUnknownLLMProfileReferencedByBinding(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
lane := cfg.Pipelines["example"].Artifacts["events"]
|
||||
lane.Extract.LLMProfile = "missing"
|
||||
cfg.Pipelines["example"].Artifacts["events"] = lane
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), "unknown LLM profile") || !strings.Contains(err.Error(), "events") {
|
||||
t.Fatalf("expected unknown LLM profile error with lane context, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsInvalidProvider(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
profile := cfg.LLMProfiles["default"]
|
||||
profile.Provider = "unsupported"
|
||||
cfg.LLMProfiles["default"] = profile
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), "provider") {
|
||||
t.Fatalf("expected provider error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsInvalidNumericFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(Config) Config
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "total concurrency",
|
||||
mutate: func(cfg Config) Config {
|
||||
cfg.Concurrency.TotalLLM = 0
|
||||
return cfg
|
||||
},
|
||||
want: "total LLM concurrency",
|
||||
},
|
||||
{
|
||||
name: "timeout",
|
||||
mutate: func(cfg Config) Config {
|
||||
profile := cfg.LLMProfiles["default"]
|
||||
profile.TimeoutSeconds = -1
|
||||
cfg.LLMProfiles["default"] = profile
|
||||
return cfg
|
||||
},
|
||||
want: "timeout",
|
||||
},
|
||||
{
|
||||
name: "max retries",
|
||||
mutate: func(cfg Config) Config {
|
||||
profile := cfg.LLMProfiles["default"]
|
||||
profile.MaxRetries = -1
|
||||
cfg.LLMProfiles["default"] = profile
|
||||
return cfg
|
||||
},
|
||||
want: "max retries",
|
||||
},
|
||||
{
|
||||
name: "max concurrency",
|
||||
mutate: func(cfg Config) Config {
|
||||
profile := cfg.LLMProfiles["default"]
|
||||
profile.MaxConcurrency = 0
|
||||
cfg.LLMProfiles["default"] = profile
|
||||
return cfg
|
||||
},
|
||||
want: "max concurrency",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.mutate(validConfig()).Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("expected error containing %q, got %v", tc.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsInvalidDiagnosticsRetention(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Diagnostics.Retention = diagnostics.RetentionMode("sometimes")
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), "retention") {
|
||||
t.Fatalf("expected retention error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsEmptyIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(Config) Config
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "LLM profile",
|
||||
mutate: func(cfg Config) Config {
|
||||
cfg.LLMProfiles[" "] = LLMProfile{}
|
||||
return cfg
|
||||
},
|
||||
want: "LLM profile id",
|
||||
},
|
||||
{
|
||||
name: "pipeline",
|
||||
mutate: func(cfg Config) Config {
|
||||
cfg.Pipelines[" "] = pipeline.PipelineProfile{}
|
||||
return cfg
|
||||
},
|
||||
want: "pipeline id",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.mutate(validConfig()).Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("expected error containing %q, got %v", tc.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsIDsDuplicatedAfterTrimming(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(Config) Config
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "LLM profile",
|
||||
mutate: func(cfg Config) Config {
|
||||
cfg.LLMProfiles[" default "] = cfg.LLMProfiles["default"]
|
||||
return cfg
|
||||
},
|
||||
want: "duplicated",
|
||||
},
|
||||
{
|
||||
name: "pipeline",
|
||||
mutate: func(cfg Config) Config {
|
||||
cfg.Pipelines[" example "] = cfg.Pipelines["example"]
|
||||
return cfg
|
||||
},
|
||||
want: "duplicated",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.mutate(validConfig()).Validate()
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("expected error containing %q, got %v", tc.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUsesTrimmedLLMProfileIDs(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.LLMProfiles[" default "] = cfg.LLMProfiles["default"]
|
||||
delete(cfg.LLMProfiles, "default")
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate: %v", err)
|
||||
}
|
||||
if _, ok := cfg.LLMProfile("default"); !ok {
|
||||
t.Fatalf("expected trimmed LLM profile lookup to succeed")
|
||||
}
|
||||
}
|
||||
|
||||
func validConfig() Config {
|
||||
cfg := Default()
|
||||
profile := cfg.LLMProfiles["default"]
|
||||
profile.BaseURL = "https://example.invalid/v1"
|
||||
profile.Model = "test-model"
|
||||
cfg.LLMProfiles["default"] = profile
|
||||
cfg.Pipelines["example"] = pipeline.PipelineProfile{
|
||||
Input: pipeline.Binding("fake/input"),
|
||||
Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
||||
"events": {
|
||||
Extract: pipeline.Binding("fake/extract"),
|
||||
Validators: []pipeline.ModuleBinding{pipeline.Binding("fake/validator")},
|
||||
},
|
||||
"notes": {
|
||||
Extract: pipeline.Binding("fake/extract"),
|
||||
},
|
||||
},
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func fakeCatalog(t *testing.T, overrides ...pipeline.ModuleSpec) pipeline.ModuleCatalog {
|
||||
t.Helper()
|
||||
specs := map[string]pipeline.ModuleSpec{
|
||||
"fake/input": {
|
||||
Key: "fake/input",
|
||||
Stage: pipeline.StageInput,
|
||||
Provides: []string{"source"},
|
||||
},
|
||||
"generic": {
|
||||
Key: "generic",
|
||||
Stage: pipeline.StageChunk,
|
||||
Requires: []string{"source"},
|
||||
Provides: []string{"chunks"},
|
||||
},
|
||||
"fake/extract": {
|
||||
Key: "fake/extract",
|
||||
Stage: pipeline.StageExtract,
|
||||
Requires: []string{"chunks"},
|
||||
Provides: []string{"artifact"},
|
||||
},
|
||||
"appendorder": {
|
||||
Key: "appendorder",
|
||||
Stage: pipeline.StageMerge,
|
||||
Requires: []string{"artifact"},
|
||||
Provides: []string{"merged"},
|
||||
},
|
||||
"noop": {
|
||||
Key: "noop",
|
||||
Stage: pipeline.StageNormalize,
|
||||
Requires: []string{"merged"},
|
||||
Provides: []string{"normalized"},
|
||||
},
|
||||
"fake/validator": {
|
||||
Key: "fake/validator",
|
||||
Stage: pipeline.StageValidate,
|
||||
Requires: []string{"normalized"},
|
||||
Provides: []string{"validated"},
|
||||
},
|
||||
"json": {
|
||||
Key: "json",
|
||||
Stage: pipeline.StageOutput,
|
||||
Requires: []string{"normalized"},
|
||||
},
|
||||
}
|
||||
for _, override := range overrides {
|
||||
specs[override.Key] = override
|
||||
}
|
||||
|
||||
inputs := pipeline.NewInputAdapterRegistry()
|
||||
chunkers := pipeline.NewChunkerRegistry()
|
||||
extractors := pipeline.NewExtractorRegistry()
|
||||
mergers := pipeline.NewMergerRegistry()
|
||||
normalizers := pipeline.NewNormalizerRegistry()
|
||||
validators := pipeline.NewValidatorRegistry()
|
||||
outputs := pipeline.NewOutputEncoderRegistry()
|
||||
|
||||
mustRegisterInput(t, inputs, specs["fake/input"])
|
||||
mustRegisterChunker(t, chunkers, specs["generic"])
|
||||
mustRegisterExtractor(t, extractors, specs["fake/extract"])
|
||||
mustRegisterMerger(t, mergers, specs["appendorder"])
|
||||
mustRegisterNormalizer(t, normalizers, specs["noop"])
|
||||
mustRegisterValidator(t, validators, specs["fake/validator"])
|
||||
mustRegisterOutput(t, outputs, specs["json"])
|
||||
|
||||
return pipeline.ModuleCatalog{
|
||||
Inputs: inputs,
|
||||
Chunkers: chunkers,
|
||||
Extractors: extractors,
|
||||
Mergers: mergers,
|
||||
Normalizers: normalizers,
|
||||
Validators: validators,
|
||||
Outputs: outputs,
|
||||
}
|
||||
}
|
||||
|
||||
func mustRegisterInput(t *testing.T, registry *pipeline.InputAdapterRegistry, spec pipeline.ModuleSpec) {
|
||||
t.Helper()
|
||||
if err := registry.RegisterWithSpec(spec, func() (contracts.InputAdapter, error) { return nil, nil }); err != nil {
|
||||
t.Fatalf("register input: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRegisterChunker(t *testing.T, registry *pipeline.ChunkerRegistry, spec pipeline.ModuleSpec) {
|
||||
t.Helper()
|
||||
if err := registry.RegisterWithSpec(spec, func() (contracts.Chunker, error) { return nil, nil }); err != nil {
|
||||
t.Fatalf("register chunker: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRegisterExtractor(t *testing.T, registry *pipeline.ExtractorRegistry, spec pipeline.ModuleSpec) {
|
||||
t.Helper()
|
||||
if err := registry.RegisterWithSpec(spec, func() (contracts.Extractor, error) { return nil, nil }); err != nil {
|
||||
t.Fatalf("register extractor: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRegisterMerger(t *testing.T, registry *pipeline.MergerRegistry, spec pipeline.ModuleSpec) {
|
||||
t.Helper()
|
||||
if err := registry.RegisterWithSpec(spec, func() (contracts.Merger, error) { return nil, nil }); err != nil {
|
||||
t.Fatalf("register merger: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRegisterNormalizer(t *testing.T, registry *pipeline.NormalizerRegistry, spec pipeline.ModuleSpec) {
|
||||
t.Helper()
|
||||
if err := registry.RegisterWithSpec(spec, func() (contracts.Normalizer, error) { return nil, nil }); err != nil {
|
||||
t.Fatalf("register normalizer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRegisterValidator(t *testing.T, registry *pipeline.ValidatorRegistry, spec pipeline.ModuleSpec) {
|
||||
t.Helper()
|
||||
if err := registry.RegisterWithSpec(spec, func() (contracts.Validator, error) { return nil, nil }); err != nil {
|
||||
t.Fatalf("register validator: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRegisterOutput(t *testing.T, registry *pipeline.OutputEncoderRegistry, spec pipeline.ModuleSpec) {
|
||||
t.Helper()
|
||||
if err := registry.RegisterWithSpec(spec, func() (contracts.OutputEncoder, error) { return nil, nil }); err != nil {
|
||||
t.Fatalf("register output: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user