Prepare pipelines before source execution
This commit is contained in:
211
internal/framework/pipeline/preparation_test.go
Normal file
211
internal/framework/pipeline/preparation_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestResolvePipelineValidatesModuleAndValidatorOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*PipelineProfile)
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "module",
|
||||
mutate: func(profile *PipelineProfile) {
|
||||
profile.Input.Options = map[string]any{"surprise": true}
|
||||
},
|
||||
want: []string{`pipeline "construction" input module "input" options`, `unknown option "surprise"`},
|
||||
},
|
||||
{
|
||||
name: "validator",
|
||||
mutate: func(profile *PipelineProfile) {
|
||||
profile.Chunk.Validators = ValidatorOverride{Set: true, Validators: []ModuleBinding{{Module: "configured", Options: map[string]any{"surprise": true}}}}
|
||||
},
|
||||
want: []string{`pipeline "construction" chunk module "chunk" validator "configured" options`, `unknown option "surprise"`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
registries, _ := constructionRegistries(t, nil, nil)
|
||||
profile := constructionProfile()
|
||||
test.mutate(&profile)
|
||||
_, err := ResolvePipeline(profile, ResolveOptions{}, registries.catalog())
|
||||
if err == nil {
|
||||
t.Fatal("ResolvePipeline() error = nil, want option validation error")
|
||||
}
|
||||
for _, want := range test.want {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("ResolvePipeline() error = %q, want substring %q", err, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareConstructsEverythingInStableOrder(t *testing.T) {
|
||||
var built []string
|
||||
registries, _ := constructionRegistries(t, &built, nil)
|
||||
resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog())
|
||||
if err != nil {
|
||||
t.Fatalf("ResolvePipeline() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
prepared, err := Prepare(resolved, registries, ModuleDependencies{})
|
||||
if err != nil {
|
||||
t.Fatalf("Prepare() error = %v, want nil", err)
|
||||
}
|
||||
want := []string{"input", "chunk", "validator", "extract", "validator", "merge", "validator", "normalize", "validator", "output"}
|
||||
if !reflect.DeepEqual(built, want) {
|
||||
t.Fatalf("construction order = %#v, want %#v", built, want)
|
||||
}
|
||||
if prepared.Input.Module != "input" || prepared.Chunk.Module != "chunk" || prepared.Output.Module != "output" || len(prepared.ArtifactLanes) != 1 {
|
||||
t.Fatalf("PreparedPipeline = %#v, want explicit resolved components", prepared)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareFailuresOccurBeforeInputParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
deps ModuleDependencies
|
||||
configure func(*constructionFailure)
|
||||
want string
|
||||
wantBuilt []string
|
||||
}{
|
||||
{
|
||||
name: "missing required llm dependency",
|
||||
configure: func(failure *constructionFailure) {
|
||||
failure.requireExtractorLLM = true
|
||||
},
|
||||
want: `lane "artifact" extract module "extract"`,
|
||||
wantBuilt: []string{"input", "chunk", "validator", "extract"},
|
||||
},
|
||||
{
|
||||
name: "late output construction",
|
||||
configure: func(failure *constructionFailure) {
|
||||
failure.output = errors.New("output unavailable")
|
||||
},
|
||||
want: `output module "output"`,
|
||||
wantBuilt: []string{"input", "chunk", "validator", "extract", "validator", "merge", "validator", "normalize", "validator", "output"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
failure := &constructionFailure{}
|
||||
test.configure(failure)
|
||||
var built []string
|
||||
registries, input := constructionRegistries(t, &built, failure)
|
||||
resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog())
|
||||
if err != nil {
|
||||
t.Fatalf("ResolvePipeline() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
_, err = Prepare(resolved, registries, test.deps)
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("Prepare() error = %v, want substring %q", err, test.want)
|
||||
}
|
||||
if len(input.requests) != 0 {
|
||||
t.Fatalf("input Parse calls = %d, want zero", len(input.requests))
|
||||
}
|
||||
if !reflect.DeepEqual(built, test.wantBuilt) {
|
||||
t.Fatalf("construction order = %#v, want %#v", built, test.wantBuilt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type constructionFailure struct {
|
||||
requireExtractorLLM bool
|
||||
output error
|
||||
}
|
||||
|
||||
func constructionProfile() PipelineProfile {
|
||||
validators := ValidatorOverride{Set: true, Validators: []ModuleBinding{{Module: "configured"}}}
|
||||
return PipelineProfile{
|
||||
ID: "construction",
|
||||
Input: Binding("input"),
|
||||
Chunk: ModuleBinding{Module: "chunk", Validators: validators},
|
||||
Artifacts: map[string]ArtifactLaneProfile{
|
||||
"artifact": {
|
||||
Extract: ModuleBinding{Module: "extract", Validators: validators},
|
||||
Merge: ModuleBinding{Module: "merge", Validators: validators},
|
||||
Normalize: ModuleBinding{Module: "normalize", Validators: validators},
|
||||
},
|
||||
},
|
||||
Output: Binding("output"),
|
||||
}
|
||||
}
|
||||
|
||||
func constructionRegistries(t *testing.T, built *[]string, failure *constructionFailure) (Registries, *runnerInputAdapter) {
|
||||
t.Helper()
|
||||
if built == nil {
|
||||
built = &[]string{}
|
||||
}
|
||||
if failure == nil {
|
||||
failure = &constructionFailure{}
|
||||
}
|
||||
record := func(name string) { *built = append(*built, name) }
|
||||
strict := func(options map[string]any) error { return RejectUnknownOptions(options, "known") }
|
||||
modules := defaultRunnerModules()
|
||||
registries := Registries{
|
||||
Inputs: NewInputAdapterRegistry(), Chunkers: NewChunkerRegistry(), ArtifactCodecs: NewArtifactCodecRegistry(),
|
||||
Extractors: NewExtractorRegistry(), Mergers: NewMergerRegistry(), Normalizers: NewNormalizerRegistry(),
|
||||
Validators: NewValidatorRegistry(), ValidatorChains: NewValidatorChainRegistry(), Outputs: NewOutputEncoderRegistry(),
|
||||
}
|
||||
if err := registries.Inputs.RegisterBuilderWithSpec(defaultModuleSpec("input", StageInput), strict, func(BuildRequest) (contracts.InputAdapter, error) {
|
||||
record("input")
|
||||
return modules.input, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := registries.Chunkers.RegisterBuilderWithSpec(defaultModuleSpec("chunk", StageChunk), strict, func(BuildRequest) (contracts.Chunker, error) {
|
||||
record("chunk")
|
||||
return modules.chunker, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := registries.Extractors.RegisterLegacyRawBuilderWithSpec(defaultModuleSpec("extract", StageExtract), strict, func(request BuildRequest) (contracts.LegacyRawExtractor, error) {
|
||||
record("extract")
|
||||
if failure.requireExtractorLLM && request.Dependencies.LLM == nil {
|
||||
return nil, errors.New("structured LLM client is required")
|
||||
}
|
||||
return &runnerExtractor{key: "extract"}, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := registries.Mergers.RegisterLegacyRawBuilderWithSpec(defaultModuleSpec("merge", StageMerge), strict, func(BuildRequest) (contracts.LegacyRawMerger, error) {
|
||||
record("merge")
|
||||
return modules.mergers["merge"], nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := registries.Normalizers.RegisterLegacyRawBuilderWithSpec(defaultModuleSpec("normalize", StageNormalize), strict, func(BuildRequest) (contracts.LegacyRawNormalizer, error) {
|
||||
record("normalize")
|
||||
return modules.normalizers["normalize"], nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := registries.Validators.RegisterLegacyRawBuilderWithSpec(ValidatorSpec{Key: "configured", ExecutionClass: contracts.ExecutionClassDeterministic}, strict, func(BuildRequest) (contracts.LegacyRawValidator, error) {
|
||||
record("validator")
|
||||
return modules.validators["configured"], nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := registries.Outputs.RegisterBuilderWithSpec(defaultModuleSpec("output", StageOutput), strict, func(BuildRequest) (contracts.OutputEncoder, error) {
|
||||
record("output")
|
||||
if failure.output != nil {
|
||||
return nil, failure.output
|
||||
}
|
||||
return modules.output, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return registries, modules.input
|
||||
}
|
||||
Reference in New Issue
Block a user