Files
notarius/internal/core/config/validation_contract_test.go

447 lines
12 KiB
Go

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 TestValidateScriptoriumSourcesAreMutuallyExclusive(t *testing.T) {
cfg := Default()
cfg.Scriptorium = ScriptoriumConfig{ProfileDir: "./profiles", ProfileFile: "./profile.yml"}
assertValidationContains(t, cfg, "scriptorium profile_dir and profile_file are mutually exclusive")
}
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 = 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 = 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 = map[string]string{"slot": "source.txt"}
},
want: "input references are not supported",
},
{
name: "output references",
set: func(profile *pipeline.PipelineProfile) {
profile.Output.References = 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: 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)
}
}