447 lines
12 KiB
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 = 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)
|
|
}
|
|
}
|