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 = -1 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 TestValidateAllowsPartialLLMProfileNumericConfig(t *testing.T) { cfg := validConfig() cfg.LLMProfiles["retry-only"] = LLMProfile{MaxRetries: 3} if err := cfg.Validate(); err != nil { t.Fatalf("Validate: %v", 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) } }