package config import ( "errors" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) func TestPrecedenceFileValuesOverrideBuiltInDefaults(t *testing.T) { cfg := applyFileConfig(t, `version: 4 concurrency: total_llm: 4 stage_workers: extract: 2 output: directory: ./file-output cache: chunk_plans: directory: ./file-plans mode: refresh checkpoints: directory: ./file-checkpoints debug: directory: ./file-debug `) if cfg.Concurrency.TotalLLM != 4 || cfg.Concurrency.StageWorkers["extract"] != 2 || cfg.Output.Directory != "./file-output" || cfg.Cache.ChunkPlans.Directory != "file-plans" || cfg.Cache.ChunkPlans.Mode != pipeline.ChunkCacheRefresh || cfg.Cache.Checkpoints.Directory != "file-checkpoints" || cfg.Debug.Directory != "./file-debug" { t.Fatalf("file values did not override defaults: %#v", cfg) } } func TestPrecedenceOperationalEnvironmentOverridesFileValues(t *testing.T) { cfg := applyFileConfig(t, `version: 4 concurrency: total_llm: 2 stage_workers: extract: 1 output: directory: ./file-output cache: chunk_plans: directory: ./file-plans mode: refresh checkpoints: directory: ./file-checkpoints debug: directory: ./file-debug `) env := map[string]string{ "NOTARIUS_TOTAL_LLM_CONCURRENCY": "8", "NOTARIUS_STAGE_WORKERS_EXTRACT": "6", "NOTARIUS_OUTPUT_DIR": "/env/output", "NOTARIUS_CACHE_CHUNK_PLANS_MODE": "bypass", "NOTARIUS_CACHE_CHUNK_PLANS_DIR": "/env/plans", "NOTARIUS_CACHE_CHECKPOINTS_DIR": "/env/checkpoints", "NOTARIUS_DEBUG_DIR": "/env/debug", } if err := cfg.ApplyEnvOverridesWithLookup(lookupValues(env)); err != nil { t.Fatal(err) } if cfg.Concurrency.TotalLLM != 8 || cfg.Concurrency.StageWorkers["extract"] != 6 || cfg.Output.Directory != "/env/output" || cfg.Cache.ChunkPlans.Directory != "/env/plans" || cfg.Cache.ChunkPlans.Mode != pipeline.ChunkCacheBypass || cfg.Cache.Checkpoints.Directory != "/env/checkpoints" || cfg.Debug.Directory != "/env/debug" { t.Fatalf("environment values did not override file values: %#v", cfg) } } func TestPrecedenceExtractWorkersFollowEffectiveConcurrencyUnlessExplicit(t *testing.T) { tests := []struct { name string file string env map[string]string wantTotal int wantWorker int }{ { name: "default follows environment total", file: "version: 4\n", env: map[string]string{"NOTARIUS_TOTAL_LLM_CONCURRENCY": "5"}, wantTotal: 5, wantWorker: 5, }, { name: "file worker is retained", file: "version: 4\nconcurrency:\n total_llm: 3\n stage_workers:\n extract: 2\n", env: map[string]string{"NOTARIUS_TOTAL_LLM_CONCURRENCY": "6"}, wantTotal: 6, wantWorker: 2, }, { name: "environment worker is retained", file: "version: 4\nconcurrency:\n total_llm: 2\n", env: map[string]string{ "NOTARIUS_TOTAL_LLM_CONCURRENCY": "6", "NOTARIUS_STAGE_WORKERS_EXTRACT": "4", }, wantTotal: 6, wantWorker: 4, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := applyFileConfig(t, tt.file) if err := cfg.ApplyEnvOverridesWithLookup(lookupValues(tt.env)); err != nil { t.Fatal(err) } if cfg.Concurrency.TotalLLM != tt.wantTotal || cfg.Concurrency.StageWorkers["extract"] != tt.wantWorker { t.Fatalf("concurrency = %#v, want total %d and extract %d", cfg.Concurrency, tt.wantTotal, tt.wantWorker) } }) } } func TestPrecedenceEmptyFileCacheDirectoriesDeferPerUserResolution(t *testing.T) { cfg := applyFileConfig(t, `version: 4 cache: chunk_plans: directory: "" checkpoints: directory: "" `) if err := cfg.Validate(); err != nil { t.Fatalf("empty file cache directories should be valid: %v", err) } if cfg.Cache.ChunkPlans.Directory != "" || cfg.Cache.Checkpoints.Directory != "" { t.Fatalf("empty cache directories were not preserved for deferred resolution: %#v", cfg.Cache) } resolver := func() (string, error) { return "/user/cache", nil } chunkPlans, err := DefaultChunkPlanRoot(resolver) if err != nil { t.Fatal(err) } checkpoints, err := DefaultCheckpointRoot(resolver) if err != nil { t.Fatal(err) } if chunkPlans != "/user/cache/notarius/chunk-plans" || checkpoints != "/user/cache/notarius/checkpoints" { t.Fatalf("deferred cache roots = %q, %q", chunkPlans, checkpoints) } } func TestDefaultCacheRootsRejectInvalidUserCacheResolvers(t *testing.T) { tests := []struct { name string resolver func() (string, error) want string }{ {name: "nil resolver", want: "must not be nil"}, { name: "resolver failure", resolver: func() (string, error) { return "", errors.New("cache home unavailable") }, want: "resolve user cache directory", }, {name: "empty directory", resolver: func() (string, error) { return " ", nil }, want: "must not be empty"}, } families := []struct { name string root func(func() (string, error)) (string, error) }{ {name: "chunk plans", root: DefaultChunkPlanRoot}, {name: "checkpoints", root: DefaultCheckpointRoot}, } for _, family := range families { for _, tt := range tests { t.Run(family.name+"/"+tt.name, func(t *testing.T) { _, err := family.root(tt.resolver) if err == nil || !strings.Contains(err.Error(), tt.want) { t.Fatalf("error = %v, want substring %q", err, tt.want) } }) } } } func TestEnvEmptyDirectoryOverridesAreErrors(t *testing.T) { tests := []string{ "NOTARIUS_OUTPUT_DIR", "NOTARIUS_CACHE_CHUNK_PLANS_DIR", "NOTARIUS_CACHE_CHECKPOINTS_DIR", "NOTARIUS_DEBUG_DIR", } for _, name := range tests { t.Run(name, func(t *testing.T) { cfg := Default() err := cfg.ApplyEnvOverridesWithLookup(lookupValues(map[string]string{name: " \t"})) if err == nil || !strings.Contains(err.Error(), name) { t.Fatalf("error = %v, want responsible environment variable", err) } }) } } func TestEnvInvalidIntegersAndChunkCacheModesReportTheirNames(t *testing.T) { tests := map[string]string{ "NOTARIUS_TOTAL_LLM_CONCURRENCY": "not-an-integer", "NOTARIUS_STAGE_WORKERS_EXTRACT": "not-an-integer", "NOTARIUS_CACHE_CHUNK_PLANS_MODE": "not-a-cache-mode", } for name, value := range tests { t.Run(name, func(t *testing.T) { cfg := Default() err := cfg.ApplyEnvOverridesWithLookup(lookupValues(map[string]string{name: value})) if err == nil || !strings.Contains(err.Error(), name) { t.Fatalf("error = %v, want responsible environment variable", err) } }) } } func TestEnvRemovedProviderVariablesAreIgnored(t *testing.T) { before := Default() cfg := Default() removed := map[string]string{ "NOTARIUS_LLM_DEFAULT_ENDPOINT": "ignored-provider-setting", "NOTARIUS_LLM_DEFAULT_MODEL": "ignored-provider-setting", } if err := cfg.ApplyEnvOverridesWithLookup(lookupValues(removed)); err != nil { t.Fatal(err) } if !reflect.DeepEqual(cfg, before) { t.Fatalf("removed provider variables changed configuration: %#v", cfg) } } func lookupValues(values map[string]string) func(string) (string, bool) { return func(name string) (string, bool) { value, ok := values[name] return value, ok } }