package config import ( "bytes" "strconv" "strings" "testing" configkit "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" units "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/chunk/units" jsonoutput "gitea.maximumdirect.net/eric/notarius/internal/modules/generic/output/json" ) func TestLoadedConfigConvertsDocumentsAndResolvesRuntimePipeline(t *testing.T) { t.Parallel() loaded, err := loadConfigSource(configkit.Bytes("document.yml", []byte(`version: 5 pipelines: session: llm_profile: local structured_output_repair_attempts: 0 input: module: input chunk: module: generic validators: [] artifacts: score: extract: module: extract validators: - module: configured-a options: nested: level: 1 `)), LoadOptions{}) if err != nil { t.Fatalf("loadConfigSource() error = %v", err) } profile := loaded.Config.Pipelines["session"] if profile.LLMProfile != "local" || profile.StructuredOutputRepairAttempts == nil || *profile.StructuredOutputRepairAttempts != 0 { t.Fatalf("pipeline profile = %#v", profile) } if !profile.Chunk.Validators.Set || profile.Chunk.Validators.Validators == nil || len(profile.Chunk.Validators.Validators) != 0 { t.Fatalf("empty validator chain was not preserved: %#v", profile.Chunk.Validators) } validator := profile.Artifacts["score"].Extract.Validators.Validators[0] if validator.Module != "configured-a" || validator.Options["nested"].(map[string]any)["level"] == nil { t.Fatalf("validator conversion = %#v", validator) } effective, err := loaded.Config.Resolve(ResolveInput{PipelineID: "session", Catalog: effectiveCatalog(t)}) if err != nil { t.Fatalf("Resolve() error = %v", err) } if effective.ResolvedPipeline.ID != "session" || effective.ResolvedPipeline.Digest == "" { t.Fatalf("resolved pipeline = %#v", effective.ResolvedPipeline) } } func TestLoadedConfigResolvesPromptKitTransientRetriesAndRetainsInspection(t *testing.T) { t.Parallel() for _, test := range []struct { name string document string want int }{ {name: "omitted", document: "version: 5\n", want: defaultTransientRetries}, {name: "disabled", document: "version: 5\npromptkit:\n transient_retries: 0\n", want: 0}, {name: "configured", document: "version: 5\npromptkit:\n transient_retries: 5\n", want: 5}, } { t.Run(test.name, func(t *testing.T) { loaded, err := loadConfigSource(configkit.Bytes("document.yml", []byte(test.document)), LoadOptions{}) if err != nil { t.Fatal(err) } if got := loaded.Config.PromptKit.TransientRetries; got != test.want { t.Fatalf("transient retries = %d, want %d", got, test.want) } var effective bytes.Buffer if err := loaded.WriteEffective(&effective); err != nil { t.Fatal(err) } if !strings.Contains(effective.String(), "transient_retries: "+strconv.Itoa(test.want)) { t.Fatalf("effective configuration = %q", effective.String()) } var explanation bytes.Buffer if err := loaded.WriteExplanation(&explanation, "/promptkit/transient_retries"); err != nil { t.Fatal(err) } if !strings.Contains(explanation.String(), strconv.Itoa(test.want)) { t.Fatalf("explanation = %q", explanation.String()) } }) } } func TestLoadedConfigOwnsRuntimeValuesAndKeepsFrozenInspection(t *testing.T) { t.Parallel() loaded, err := loadConfigSource(configkit.Bytes("document.yml", []byte(`version: 5 pipelines: session: input: {module: input} artifacts: score: extract: module: extract options: nested: {value: original} `)), LoadOptions{}) if err != nil { t.Fatalf("loadConfigSource() error = %v", err) } first := loaded.RuntimeConfig() first.Pipelines["session"] = pipelineProfileWithChangedOption(first.Pipelines["session"]) second := loaded.RuntimeConfig() if got := second.Pipelines["session"].Artifacts["score"].Extract.Options["nested"].(map[string]any)["value"]; got != "original" { t.Fatalf("runtime configuration aliases caller mutation: %#v", got) } loaded.Config.Pipelines["session"] = pipelineProfileWithChangedOption(loaded.Config.Pipelines["session"]) var effective bytes.Buffer if err := loaded.WriteEffective(&effective); err != nil { t.Fatalf("WriteEffective() error = %v", err) } if !strings.Contains(effective.String(), "original") || strings.Contains(effective.String(), "changed") { t.Fatalf("effective document changed with runtime mutation: %q", effective.String()) } } func TestLoadedConfigPreservesConfigKitIntegerOptionsForModules(t *testing.T) { t.Parallel() loaded, err := loadConfigSource(configkit.Bytes("document.yml", []byte(`version: 5 pipelines: session: input: {module: input} chunk: module: generic options: max_units: 3 overlap_units: 1 output: module: json options: evidence_context: enabled: true lanes: [score] window_units: 2 `)), LoadOptions{}) if err != nil { t.Fatalf("loadConfigSource() error = %v", err) } profile := loaded.Config.Pipelines["session"] chunkOptions, err := units.DecodeOptions(profile.Chunk.Options) if err != nil || chunkOptions.MaxUnits != 3 || chunkOptions.OverlapUnits != 1 { t.Fatalf("chunk options = %#v, %v", chunkOptions, err) } outputOptions, err := jsonoutput.DecodeOptions(profile.Output.Options) if err != nil || !outputOptions.EvidenceContext.Enabled || outputOptions.EvidenceContext.WindowUnits != 2 { t.Fatalf("output options = %#v, %v", outputOptions, err) } } func TestLoadedConfigRedactsSensitiveNestedOptions(t *testing.T) { t.Parallel() loaded, err := loadConfigSource(configkit.Bytes("document.yml", []byte(`version: 5 pipelines: session: input: module: input options: nested: api_key: input-secret artifacts: score: extract: module: extract validators: - module: configured-a options: credentials: token: validator-secret `)), LoadOptions{}) if err != nil { t.Fatalf("loadConfigSource() error = %v", err) } var effective bytes.Buffer if err := loaded.WriteEffective(&effective); err != nil { t.Fatalf("WriteEffective() error = %v", err) } for _, secret := range []string{"input-secret", "validator-secret"} { if strings.Contains(effective.String(), secret) { t.Fatalf("effective configuration exposes %q: %q", secret, effective.String()) } } if !strings.Contains(effective.String(), "[REDACTED]") { t.Fatalf("effective configuration did not mark sensitive values: %q", effective.String()) } var explanation bytes.Buffer if err := loaded.WriteExplanation(&explanation, "/pipelines/session/artifacts/score/extract/validators/0/options/credentials/token"); err != nil { t.Fatalf("WriteExplanation() error = %v", err) } if strings.Contains(explanation.String(), "validator-secret") || !strings.Contains(explanation.String(), "[REDACTED]") { t.Fatalf("explanation exposes sensitive value: %q", explanation.String()) } } func pipelineProfileWithChangedOption(profile pipeline.PipelineProfile) pipeline.PipelineProfile { profile = clonePipelineProfile(profile) nested := profile.Artifacts["score"].Extract.Options["nested"].(map[string]any) nested["value"] = "changed" lane := profile.Artifacts["score"] lane.Extract.Options["nested"] = nested profile.Artifacts["score"] = lane return profile }