package config import ( "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) func TestValidateConcurrencyRules(t *testing.T) { tests := []struct { name string setup func(*Config) want string }{ { name: "non-positive total", setup: func(cfg *Config) { cfg.Concurrency.TotalLLM = 0 }, want: "total LLM concurrency must be greater than zero", }, { name: "worker below one", setup: func(cfg *Config) { cfg.Concurrency.TotalLLM = 3 cfg.Concurrency.StageWorkers = map[string]int{"extract": 0} cfg.Concurrency.extractWorkersConfigured = true }, want: "stage_workers.extract must be between 1", }, { name: "worker above total", setup: func(cfg *Config) { cfg.Concurrency.TotalLLM = 3 cfg.Concurrency.StageWorkers = map[string]int{"extract": 4} cfg.Concurrency.extractWorkersConfigured = true }, want: "stage_workers.extract must be between 1", }, { name: "worker lower boundary", setup: func(cfg *Config) { cfg.Concurrency.TotalLLM = 3 cfg.Concurrency.StageWorkers = map[string]int{"extract": 1} cfg.Concurrency.extractWorkersConfigured = true }, }, { name: "worker upper boundary", setup: func(cfg *Config) { cfg.Concurrency.TotalLLM = 3 cfg.Concurrency.StageWorkers = map[string]int{"extract": 3} cfg.Concurrency.extractWorkersConfigured = true }, }, { name: "unknown worker key", setup: func(cfg *Config) { cfg.Concurrency.StageWorkers = map[string]int{"worker": 1} }, want: "stage_workers key \"worker\" is not supported", }, { name: "blank worker key", setup: func(cfg *Config) { cfg.Concurrency.StageWorkers = map[string]int{" ": 1} }, want: "stage_workers key must not be empty", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.setup(&cfg) err := cfg.Validate() if tt.want == "" { if err != nil { t.Fatalf("Validate() error = %v", err) } return } if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("Validate() error = %v, want context %q", err, tt.want) } }) } } func TestValidatePromptKitSourcesAreMutuallyExclusive(t *testing.T) { cfg := Default() cfg.PromptKit = PromptKitConfig{ProfileDir: "./profiles", ProfileFile: "./profile.yml"} assertValidationContains(t, cfg, "promptkit profile_dir and profile_file are mutually exclusive") } func TestValidatePromptKitLocalBackendEndpoints(t *testing.T) { tests := []struct { name string endpoint string profileSource PromptKitConfig }{ { name: "HTTP endpoint with path and profile directory", endpoint: "http://localhost:8000/v1", profileSource: PromptKitConfig{ProfileDir: "./profiles"}, }, { name: "case-insensitive HTTPS endpoint and profile file", endpoint: "HTTPS://inference.example.test/api", profileSource: PromptKitConfig{ProfileFile: "./profiles.yml"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.PromptKit = tt.profileSource cfg.PromptKit.LocalBackend = &PromptKitLocalBackendConfig{ Endpoint: tt.endpoint, ConcurrencyLimit: 2, } if err := cfg.Validate(); err != nil { t.Fatalf("Validate() error = %v", err) } }) } } func TestValidatePromptKitLocalBackendRejectsInvalidValues(t *testing.T) { tests := []struct { name string endpoint string concurrencyLimit int want string }{ {name: "blank endpoint", endpoint: " ", want: "promptkit.local_backend.endpoint"}, {name: "relative URL", endpoint: "localhost:8000/v1", want: "promptkit.local_backend.endpoint"}, {name: "unsupported scheme", endpoint: "ftp://localhost/model", want: "promptkit.local_backend.endpoint"}, {name: "missing host", endpoint: "http:///v1", want: "promptkit.local_backend.endpoint"}, {name: "user information", endpoint: "http://user:secret@localhost/v1", want: "promptkit.local_backend.endpoint"}, {name: "query", endpoint: "http://localhost/v1?model=example", want: "promptkit.local_backend.endpoint"}, {name: "empty query", endpoint: "http://localhost/v1?", want: "promptkit.local_backend.endpoint"}, {name: "fragment", endpoint: "http://localhost/v1#model", want: "promptkit.local_backend.endpoint"}, {name: "empty fragment", endpoint: "http://localhost/v1#", want: "promptkit.local_backend.endpoint"}, { name: "negative concurrency", endpoint: "http://localhost:8000/v1", concurrencyLimit: -1, want: "promptkit.local_backend.concurrency_limit", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.PromptKit.LocalBackend = &PromptKitLocalBackendConfig{ Endpoint: tt.endpoint, ConcurrencyLimit: tt.concurrencyLimit, } assertValidationContains(t, cfg, tt.want) }) } } func TestValidateStateSurfaceRules(t *testing.T) { tests := []struct { name string setup func(*Config) want string }{ { name: "blank output root", setup: func(cfg *Config) { cfg.Output.Directory = " " }, want: "output.directory must not be empty", }, { name: "blank debug root", setup: func(cfg *Config) { cfg.Debug.Directory = " " }, want: "debug.directory must not be empty", }, { name: "NUL in output root", setup: func(cfg *Config) { cfg.Output.Directory = "./out\x00put" }, want: "output.directory must not contain NUL", }, { name: "NUL in chunk plan root", setup: func(cfg *Config) { cfg.Cache.ChunkPlans.Directory = "./plans\x00" }, want: "cache.chunk_plans.directory must not contain NUL", }, { name: "NUL in checkpoint root", setup: func(cfg *Config) { cfg.Cache.Checkpoints.Directory = "./checkpoints\x00" }, want: "cache.checkpoints.directory must not contain NUL", }, { name: "NUL in debug root", setup: func(cfg *Config) { cfg.Debug.Directory = "./debug\x00" }, want: "debug.directory must not contain NUL", }, { name: "invalid chunk plan mode", setup: func(cfg *Config) { cfg.Cache.ChunkPlans.Mode = pipeline.ChunkCacheMode("invalid") }, want: "cache.chunk_plans.mode:", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.setup(&cfg) assertValidationContains(t, cfg, tt.want) }) } } func TestValidateIdentifiersAfterTrimming(t *testing.T) { tests := []struct { name string setup func(*Config) want string }{ { name: "empty pipeline id", setup: func(cfg *Config) { cfg.Pipelines = map[string]pipeline.PipelineProfile{" ": {}} }, want: "pipeline id must not be empty", }, { name: "duplicate pipeline ids", setup: func(cfg *Config) { cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": {}, " main ": {}} }, want: "pipeline id \"main\" is duplicated after trimming", }, { name: "empty lane id", setup: func(cfg *Config) { profile := validationProfile() profile.Artifacts = map[string]pipeline.ArtifactLaneProfile{" ": {}} cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} }, want: "artifact lane id must not be empty", }, { name: "duplicate lane ids", setup: func(cfg *Config) { profile := validationProfile() profile.Artifacts = map[string]pipeline.ArtifactLaneProfile{"spells": {}, " spells ": {}} cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} }, want: "artifact lane id \"spells\" is duplicated after trimming", }, { name: "empty reference slot", setup: func(cfg *Config) { profile := validationProfile() profile.References = pipeline.ExternalReferenceMap(map[string]string{" ": "source.txt"}) cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} }, want: "reference slot name must not be empty", }, { name: "duplicate reference slots", setup: func(cfg *Config) { profile := validationProfile() profile.References = pipeline.ExternalReferenceMap(map[string]string{"slot": "one.txt", " slot ": "two.txt"}) cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} }, want: "reference slot \"slot\" is duplicated after trimming", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.setup(&cfg) assertValidationContains(t, cfg, tt.want) }) } } func TestValidateBindingRetriesAndProfiles(t *testing.T) { tests := []struct { name string setup func(*Config) want string }{ { name: "negative retries", setup: func(cfg *Config) { profile := validationProfile() profile.Input.Retries = -1 cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} }, want: "input retries must be greater than or equal to zero", }, { name: "whitespace-only input profile", setup: func(cfg *Config) { profile := validationProfile() profile.Input.LLMProfile = " " cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} }, want: "input llm_profile must not be empty when set", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.setup(&cfg) assertValidationContains(t, cfg, tt.want) }) } } func TestValidateReferencesAreUnsupportedOnInputAndOutput(t *testing.T) { tests := []struct { name string set func(*pipeline.PipelineProfile) want string }{ { name: "input references", set: func(profile *pipeline.PipelineProfile) { profile.Input.References = pipeline.ExternalReferenceMap(map[string]string{"slot": "source.txt"}) }, want: "input references are not supported", }, { name: "output references", set: func(profile *pipeline.PipelineProfile) { profile.Output.References = pipeline.ExternalReferenceMap(map[string]string{"slot": "source.txt"}) }, want: "output references are not supported", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { profile := validationProfile() tt.set(&profile) cfg := Default() cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} assertValidationContains(t, cfg, tt.want) }) } } func TestValidateValidatorBindingRules(t *testing.T) { tests := []struct { name string setup func(*pipeline.PipelineProfile) want string }{ { name: "empty validator module", setup: func(profile *pipeline.PipelineProfile) { profile.Chunk.Validators = pipeline.ValidatorOverride{ Set: true, Validators: []pipeline.ModuleBinding{{}}, } }, want: "chunk validators[0] module must not be empty", }, { name: "validator retries", setup: func(profile *pipeline.PipelineProfile) { profile.Chunk.Validators = pipeline.ValidatorOverride{ Set: true, Validators: []pipeline.ModuleBinding{{ Module: "validator", Retries: 1, }}, } }, want: "chunk validators[0] retries are not supported", }, { name: "validator references", setup: func(profile *pipeline.PipelineProfile) { profile.Chunk.Validators = pipeline.ValidatorOverride{ Set: true, Validators: []pipeline.ModuleBinding{{ Module: "validator", References: pipeline.ExternalReferenceMap(map[string]string{"slot": "source.txt"}), }}, } }, want: "chunk validators[0] references are not supported", }, { name: "nested validators", setup: func(profile *pipeline.PipelineProfile) { profile.Chunk.Validators = pipeline.ValidatorOverride{ Set: true, Validators: []pipeline.ModuleBinding{{ Module: "validator", Validators: pipeline.ValidatorOverride{Set: true}, }}, } }, want: "chunk validators[0] nested validators are not supported", }, { name: "input validator chain", setup: func(profile *pipeline.PipelineProfile) { profile.Input.Validators = pipeline.ValidatorOverride{Set: true} }, want: "input validators are not supported", }, { name: "output validator chain", setup: func(profile *pipeline.PipelineProfile) { profile.Output.Validators = pipeline.ValidatorOverride{Set: true} }, want: "output validators are not supported", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { profile := validationProfile() tt.setup(&profile) cfg := Default() cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} assertValidationContains(t, cfg, tt.want) }) } } func TestValidateLaneValidatorCompatibility(t *testing.T) { tests := []struct { name string lane func(*pipeline.ArtifactLaneProfile) want string }{ { name: "deprecated non-empty lane validators", lane: func(lane *pipeline.ArtifactLaneProfile) { lane.Validators = []pipeline.ModuleBinding{{Module: "old-validator"}} }, want: "validators are not supported at artifact lane level", }, { name: "stage validators omitted", lane: func(lane *pipeline.ArtifactLaneProfile) { }, }, { name: "stage validators explicitly empty", lane: func(lane *pipeline.ArtifactLaneProfile) { lane.Extract.Validators = pipeline.ValidatorOverride{Set: true} }, }, { name: "stage validators configured", lane: func(lane *pipeline.ArtifactLaneProfile) { lane.Extract.Validators = pipeline.ValidatorOverride{ Set: true, Validators: []pipeline.ModuleBinding{{Module: "validator"}}, } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { profile := validationProfile() lane := profile.Artifacts["lane"] tt.lane(&lane) profile.Artifacts["lane"] = lane cfg := Default() cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile} err := cfg.Validate() if tt.want == "" { if err != nil { t.Fatalf("Validate() error = %v", err) } return } if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("Validate() error = %v, want context %q", err, tt.want) } }) } } func validationProfile() pipeline.PipelineProfile { return pipeline.PipelineProfile{ ID: "main", Input: pipeline.Binding("input"), Artifacts: map[string]pipeline.ArtifactLaneProfile{ "lane": {}, }, } } func assertValidationContains(t *testing.T, cfg Config, want string) { t.Helper() err := cfg.Validate() if err == nil || !strings.Contains(err.Error(), want) { t.Fatalf("Validate() error = %v, want context %q", err, want) } }