185 lines
6.1 KiB
Go
185 lines
6.1 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"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 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
|
|
}
|