package pipeline import ( "context" "errors" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "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.Steps) != 1 || len(prepared.Steps[0].ArtifactLanes) != 1 { t.Fatalf("PreparedPipeline = %#v, want explicit resolved components", prepared) } } func TestPrepareBuildsEveryOrderedStepBeforeExecution(t *testing.T) { var built []string registries, input := constructionRegistries(t, &built, nil) profile := constructionProfile() lane := profile.Artifacts["artifact"] profile.Artifacts = nil profile.Steps = []PipelineStepProfile{ {ID: "first", Artifacts: map[string]ArtifactLaneProfile{"first-artifact": lane}}, {ID: "second", Artifacts: map[string]ArtifactLaneProfile{"second-artifact": lane}}, } resolved, err := ResolvePipeline(profile, 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", "extract", "validator", "merge", "validator", "normalize", "validator", "output", } if !reflect.DeepEqual(built, want) { t.Fatalf("construction order = %#v, want %#v", built, want) } if len(prepared.Steps) != 2 || len(prepared.Steps[0].lanes) != 1 || len(prepared.Steps[1].lanes) != 1 { t.Fatalf("prepared steps = %#v, want two constructed steps", prepared.Steps) } if len(input.requests) != 0 { t.Fatalf("input Parse calls = %d, want zero during preparation", len(input.requests)) } } func TestPrepareRetainsGeneratedSelectorsWithoutReferenceBytes(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) } resolved.Steps[0].ArtifactLanes[0].ExtractReferences.Bindings = []ReferenceBinding{{ Stage: StageExtract, LaneID: "artifact", SlotName: "generated", Artifact: &ArtifactReference{Step: "producer", Lane: "artifact"}, }} prepared, err := Prepare(resolved, registries, ModuleDependencies{}) if err != nil { t.Fatalf("Prepare() error = %v, want nil", err) } if len(prepared.Steps[0].ArtifactLanes[0].Resolved.ExtractReferences.ReferenceSet.Slots) != 0 { t.Fatalf("prepared generated reference set = %#v, want no reference bytes", prepared.Steps[0].ArtifactLanes[0].Resolved.ExtractReferences.ReferenceSet) } if len(built) == 0 { t.Fatal("constructed modules = empty, want preparation to construct the selected components") } } func TestPreparedCheckpointFingerprintsCollectEveryComponentInStableScopeOrder(t *testing.T) { provider := func(value string) checkpointFingerprintTestProvider { return checkpointFingerprintTestProvider{{Name: "identity", Value: value}} } prepared := &PreparedPipeline{ resolved: ResolvedPipeline{ ID: "fingerprints", Input: Binding("input"), Chunk: Binding("chunk"), Output: Binding("output"), }, input: provider("input"), chunker: provider("chunk"), chunkValidators: preparedValidatorChain{validators: []preparedValidator{{ resolved: ResolvedValidator{Binding: Binding("chunk-validator"), Target: ValidatorTargetChunk}, chunk: provider("chunk-validator"), }}}, output: provider("output"), Steps: []PreparedPipelineStep{{ID: "default"}}, } lane := preparedLaneExecutor{ resolved: ResolvedArtifactLane{ ID: "spells", Extract: Binding("extract"), Merge: Binding("merge"), Normalize: Binding("normalize"), }, typed: &preparedTypedLane{ extractor: provider("extract"), merger: provider("merge"), normalizer: provider("normalize"), }, extractValidators: fingerprintTestValidatorChain("extract-validator", provider("extract-validator")), mergeValidators: fingerprintTestValidatorChain("merge-validator", provider("merge-validator")), normalizeValidators: fingerprintTestValidatorChain("normalize-validator", provider("normalize-validator")), } prepared.Steps[0].lanes = []preparedLaneExecutor{lane} first, err := collectPreparedCheckpointFingerprints(prepared) if err != nil { t.Fatalf("collectPreparedCheckpointFingerprints() error = %v, want nil", err) } second, err := collectPreparedCheckpointFingerprints(prepared) if err != nil { t.Fatalf("second collection error = %v, want nil", err) } if !reflect.DeepEqual(first, second) { t.Fatalf("fingerprints are not deterministic: %#v / %#v", first, second) } if len(first) != 10 { t.Fatalf("fingerprints = %#v, want all 10 prepared components", first) } for index := 1; index < len(first); index++ { if first[index-1].Name >= first[index].Name { t.Fatalf("fingerprints are not sorted: %#v", first) } } prepared.checkpointFingerprints = first copy := prepared.CheckpointFingerprints() copy[0].Value = "mutated" if reflect.DeepEqual(copy, prepared.CheckpointFingerprints()) { t.Fatal("CheckpointFingerprints() exposed mutable backing storage") } } func TestPreparedCheckpointFingerprintsValidateProviderValues(t *testing.T) { tests := []struct { name string fingerprints []CheckpointFingerprint want string }{ {name: "empty name", fingerprints: []CheckpointFingerprint{{Value: "value"}}, want: "non-empty name and value"}, {name: "empty value", fingerprints: []CheckpointFingerprint{{Name: "name"}}, want: "non-empty name and value"}, {name: "duplicate after trimming", fingerprints: []CheckpointFingerprint{{Name: "same", Value: "one"}, {Name: " same ", Value: "two"}}, want: "duplicated"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { prepared := &PreparedPipeline{ resolved: ResolvedPipeline{ID: "fingerprints", Input: Binding("input"), Chunk: Binding("chunk"), Output: Binding("output")}, input: checkpointFingerprintTestProvider(test.fingerprints), } _, err := collectPreparedCheckpointFingerprints(prepared) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("error = %v, want substring %q", err, test.want) } }) } } func TestPreparedCheckpointFingerprintsRemainEmptyWithoutProviders(t *testing.T) { registries, _ := constructionRegistries(t, nil, nil) resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog()) if err != nil { t.Fatal(err) } prepared, err := Prepare(resolved, registries, ModuleDependencies{}) if err != nil { t.Fatal(err) } if got := prepared.CheckpointFingerprints(); len(got) != 0 { t.Fatalf("CheckpointFingerprints() = %#v, want empty", got) } } func TestPreparedCheckpointFingerprintsScopeRepeatedValidatorsByPosition(t *testing.T) { provider := checkpointFingerprintTestProvider{{Name: "identity", Value: "same"}} validator := func() preparedValidator { return preparedValidator{resolved: ResolvedValidator{Binding: Binding("configured"), Target: ValidatorTargetChunk}, chunk: provider} } prepared := &PreparedPipeline{ resolved: ResolvedPipeline{ID: "fingerprints", Input: Binding("input"), Chunk: Binding("chunk"), Output: Binding("output")}, chunkValidators: preparedValidatorChain{validators: []preparedValidator{validator(), validator()}}, } got, err := collectPreparedCheckpointFingerprints(prepared) if err != nil { t.Fatalf("collect repeated validators: %v", err) } if len(got) != 2 || got[0].Name == got[1].Name { t.Fatalf("fingerprints = %#v, want independently scoped repeated validators", got) } } type checkpointFingerprintTestProvider []CheckpointFingerprint func (p checkpointFingerprintTestProvider) CheckpointFingerprints() []CheckpointFingerprint { return append([]CheckpointFingerprint(nil), p...) } func (checkpointFingerprintTestProvider) Key() string { return "fingerprint-test" } func (checkpointFingerprintTestProvider) ReferenceSlots() []contracts.ReferenceSlot { return nil } func (checkpointFingerprintTestProvider) Parse(context.Context, contracts.ParseRequest) (*source.SourceDocument, error) { return typedTestDocument(), nil } func (checkpointFingerprintTestProvider) Plan(context.Context, contracts.ChunkRequest) (contracts.ChunkPlanResult, error) { return contracts.ChunkPlanResult{}, nil } func (checkpointFingerprintTestProvider) Encode(context.Context, contracts.OutputRequest) (contracts.OutputResult, error) { return contracts.OutputResult{}, nil } func (checkpointFingerprintTestProvider) Name() string { return "fingerprint-test" } func (checkpointFingerprintTestProvider) ExecutionClass() contracts.ExecutionClass { return contracts.ExecutionClassDeterministic } func (checkpointFingerprintTestProvider) Validate(context.Context, contracts.ChunkValidationRequest) (contracts.ValidationResult, error) { return contracts.ValidationResult{Approved: true}, nil } type checkpointFingerprintOutput struct { contracts.OutputEncoder fingerprints []CheckpointFingerprint } func (o checkpointFingerprintOutput) CheckpointFingerprints() []CheckpointFingerprint { return append([]CheckpointFingerprint(nil), o.fingerprints...) } func fingerprintTestValidatorChain(key string, provider checkpointFingerprintTestProvider) preparedValidatorChain { return preparedValidatorChain{validators: []preparedValidator{{ resolved: ResolvedValidator{Binding: Binding(key), Target: ValidatorTargetTyped}, typed: provider, }}} } func TestPrepareDeliversTargetReferencesAsIndependentBuildInputs(t *testing.T) { var built []string var observations []constructionBuildObservation registries, input := constructionRegistriesWithHooks(t, &built, nil, func(name string, request BuildRequest) { observations = append(observations, constructionBuildObservation{Name: name, Request: request}) }, func(name string, request *BuildRequest) { if name != "extract" { return } request.Options["nested"].(map[string]any)["value"] = "mutated by extractor builder" slot := request.References.Slots["extract"] slot.Items[0].Content = []byte("mutated by extractor builder") request.References.Slots["extract"] = slot }, ) resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog()) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } resolved.ChunkReferences.ReferenceSet = constructionReferenceSet("chunk", "chunk reference") resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet = constructionReferenceSet("extract", "extract reference") resolved.Steps[0].ArtifactLanes[0].MergeReferences.ReferenceSet = constructionReferenceSet("merge", "merge reference") resolved.Steps[0].ArtifactLanes[0].NormalizeReferences.ReferenceSet = constructionReferenceSet("normalize", "normalize reference") resolved.Steps[0].ArtifactLanes[0].Extract.Options = constructionBuildRequest().Options prepared, err := Prepare(resolved, registries, ModuleDependencies{}) if err != nil { t.Fatalf("Prepare() error = %v, want nil", err) } wantNames := []string{"input", "chunk", "validator", "extract", "validator", "merge", "validator", "normalize", "validator", "output"} if !reflect.DeepEqual(built, wantNames) { t.Fatalf("construction order = %#v, want %#v", built, wantNames) } wantContents := []string{"", "chunk reference", "chunk reference", "extract reference", "extract reference", "merge reference", "merge reference", "normalize reference", "normalize reference", ""} if len(observations) != len(wantContents) { t.Fatalf("observed %d build requests, want %d", len(observations), len(wantContents)) } for i, want := range wantContents { if got := constructionReferenceContent(observations[i].Request.References); got != want { t.Errorf("build request %d (%s) reference content = %q, want %q", i, observations[i].Name, got, want) } } if got := constructionReferenceContent(resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet); got != "extract reference" { t.Fatalf("resolved extract references = %q, want original content", got) } if got := constructionReferenceContent(prepared.resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet); got != "extract reference" { t.Fatalf("prepared extract references = %q, want original content", got) } if got := resolved.Steps[0].ArtifactLanes[0].Extract.Options["nested"].(map[string]any)["value"]; got != "original" { t.Fatalf("resolved extract options = %#v, want original nested value", got) } if got := prepared.resolved.Steps[0].ArtifactLanes[0].Extract.Options["nested"].(map[string]any)["value"]; got != "original" { t.Fatalf("prepared extract options = %#v, want original nested value", got) } _, err = prepared.Steps[0].lanes[0].typed.extract(context.Background(), prepared.Steps[0].lanes[0].typed.extractor, contracts.TypedExtractionRequest{ References: CloneReferenceSet(resolved.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet), }) if err != nil { t.Fatalf("prepared extractor operation error = %v, want nil", err) } if len(input.extractRequests) != 1 { t.Fatalf("runtime extraction requests = %d, want one", len(input.extractRequests)) } if got := constructionReferenceContent(input.extractRequests[0].References); got != "extract reference" { t.Fatalf("runtime extraction references = %q, want original content", got) } } func TestRegisteredBuildersReceiveIndependentBuildRequests(t *testing.T) { request := constructionBuildRequest() probe := buildRequestMutationProbe{t: t, want: cloneBuildRequest(request)} inputs := NewInputAdapterRegistry() if err := inputs.RegisterBuilderWithSpec(testModuleSpec("input", StageInput), rejectUnconfiguredOptions, func(request BuildRequest) (contracts.InputAdapter, error) { probe.observe(request) return &constructionInput{key: "input"}, nil }); err != nil { t.Fatal(err) } chunkers := NewChunkerRegistry() if err := chunkers.RegisterBuilderWithSpec(testModuleSpec("chunk", StageChunk), rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Chunker, error) { probe.observe(request) return &typedTestChunker{key: "chunk"}, nil }); err != nil { t.Fatal(err) } outputs := NewOutputEncoderRegistry() if err := outputs.RegisterBuilderWithSpec(testModuleSpec("output", StageOutput), rejectUnconfiguredOptions, func(request BuildRequest) (contracts.OutputEncoder, error) { probe.observe(request) return &typedTestOutput{key: "output"}, nil }); err != nil { t.Fatal(err) } extractors := NewExtractorRegistry() extractSpec := testModuleSpec("extract", StageExtract) extractSpec.ArtifactKind = "test/notes" if err := RegisterExtractorBuilder(extractors, extractSpec, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Extractor[codecNotes], error) { probe.observe(request) return typedTestExtractor[codecNotes]{key: "extract"}, nil }); err != nil { t.Fatal(err) } mergers := NewMergerRegistry() mergeSpec := testModuleSpec("merge", StageMerge) mergeSpec.ArtifactKind = "test/notes" if err := RegisterMergerBuilder(mergers, mergeSpec, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Merger[codecNotes], error) { probe.observe(request) return typedTestMerger[codecNotes]{key: "merge"}, nil }); err != nil { t.Fatal(err) } normalizers := NewNormalizerRegistry() normalizeSpec := testModuleSpec("normalize", StageNormalize) normalizeSpec.ArtifactKind = "test/notes" if err := RegisterNormalizerBuilder(normalizers, normalizeSpec, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.Normalizer[codecNotes], error) { probe.observe(request) return typedTestNormalizer[codecNotes]{key: "normalize"}, nil }); err != nil { t.Fatal(err) } validators := NewValidatorRegistry() if err := RegisterChunkValidatorBuilder(validators, ValidatorSpec{Key: "chunk-validator", ExecutionClass: contracts.ExecutionClassDeterministic}, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.ChunkValidator, error) { probe.observe(request) return typedTestChunkValidator{key: "chunk-validator"}, nil }); err != nil { t.Fatal(err) } if err := RegisterTypedValidatorBuilder(validators, "test/notes", ValidatorSpec{Key: "typed-validator", ExecutionClass: contracts.ExecutionClassDeterministic}, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.TypedValidator[codecNotes], error) { probe.observe(request) return typedTestValidator[codecNotes]{key: "typed-validator"}, nil }); err != nil { t.Fatal(err) } if err := RegisterSerializedValidatorBuilder(validators, SerializedValidatorSpec{ValidatorSpec: ValidatorSpec{Key: "serialized-validator", ExecutionClass: contracts.ExecutionClassDeterministic}, SupportsArtifacts: true}, rejectUnconfiguredOptions, func(request BuildRequest) (contracts.SerializedValidator, error) { probe.observe(request) return typedTestSerializedValidator{key: "serialized-validator"}, nil }); err != nil { t.Fatal(err) } builders := []struct { name string call func() error }{ {name: "input", call: func() error { _, err := inputs.BuildWithRequest("input", request); return err }}, {name: "chunk", call: func() error { _, err := chunkers.BuildWithRequest("chunk", request); return err }}, {name: "output", call: func() error { _, err := outputs.BuildWithRequest("output", request); return err }}, {name: "extract", call: func() error { entry, _ := extractors.typedEntry("extract") _, err := entry.builder(request) return err }}, {name: "merge", call: func() error { entry, _ := mergers.typedEntry("merge", "test/notes") _, err := entry.builder(request) return err }}, {name: "normalize", call: func() error { entry, _ := normalizers.typedEntry("normalize", "test/notes") _, err := entry.builder(request) return err }}, {name: "chunk validator", call: func() error { entry, _ := validators.chunkEntry("chunk-validator") _, err := entry.builder(request) return err }}, {name: "typed validator", call: func() error { entry, _ := validators.typedEntry("typed-validator", "test/notes") _, err := entry.builder(request) return err }}, {name: "serialized validator", call: func() error { entry, _ := validators.serializedEntry("serialized-validator") _, err := entry.builder(request) return err }}, } for _, builder := range builders { t.Run(builder.name, func(t *testing.T) { if err := builder.call(); err != nil { t.Fatal(err) } }) } if !reflect.DeepEqual(request, probe.want) { t.Fatalf("build request = %#v, want unchanged %#v", request, probe.want) } } type buildRequestMutationProbe struct { t *testing.T want BuildRequest } func (probe buildRequestMutationProbe) observe(request BuildRequest) { probe.t.Helper() if !reflect.DeepEqual(request, probe.want) { probe.t.Fatalf("builder request = %#v, want independently owned %#v", request, probe.want) } options := request.Options["nested"].(map[string]any) options["value"] = "mutated" request.Options["items"].([]any)[0].(map[string]any)["value"] = "mutated" request.Options["bytes"].([]byte)[0] = 'x' slot := request.References.Slots["reference"] slot.Items[0].Content[0] = 'x' slot.Items = append(slot.Items, contracts.ReferenceItem{SlotName: "reference", Content: []byte("extra")}) request.References.Slots["reference"] = slot delete(request.References.Slots, "unused") } func constructionBuildRequest() BuildRequest { return BuildRequest{ Options: map[string]any{ "nested": map[string]any{"value": "original"}, "items": []any{map[string]any{"value": "original"}}, "bytes": []byte("original"), }, References: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ "reference": {Slot: contracts.ReferenceSlot{Name: "reference"}, Items: []contracts.ReferenceItem{{SlotName: "reference", Content: []byte("original")}}}, "unused": {Slot: contracts.ReferenceSlot{Name: "unused"}}, }}, } } 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) } }) } } func TestPrepareRejectsInvalidComponentCheckpointFingerprint(t *testing.T) { failure := &constructionFailure{outputFingerprints: []CheckpointFingerprint{{Name: "identity"}}} registries, input := constructionRegistries(t, nil, failure) resolved, err := ResolvePipeline(constructionProfile(), ResolveOptions{}, registries.catalog()) if err != nil { t.Fatal(err) } _, err = Prepare(resolved, registries, ModuleDependencies{}) if err == nil || !strings.Contains(err.Error(), "checkpoint fingerprint") || !strings.Contains(err.Error(), "output:output") { t.Fatalf("Prepare() error = %v, want scoped checkpoint fingerprint error", err) } if len(input.requests) != 0 { t.Fatalf("input Parse calls = %d, want zero", len(input.requests)) } } type constructionFailure struct { requireExtractorLLM bool output error outputFingerprints []CheckpointFingerprint } 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, *constructionInput) { return constructionRegistriesWithHooks(t, built, failure, nil, nil) } type constructionBuildObservation struct { Name string Request BuildRequest } func constructionRegistriesWithHooks(t *testing.T, built *[]string, failure *constructionFailure, observe func(string, BuildRequest), mutate func(string, *BuildRequest)) (Registries, *constructionInput) { t.Helper() if built == nil { built = &[]string{} } if failure == nil { failure = &constructionFailure{} } record := func(name string, request *BuildRequest) { *built = append(*built, name) if observe != nil { observe(name, cloneBuildRequest(*request)) } if mutate != nil { mutate(name, request) } } strict := func(options map[string]any) error { return RejectUnknownOptions(options, "known") } input := &constructionInput{key: "input"} registries := Registries{ Inputs: NewInputAdapterRegistry(), Chunkers: NewChunkerRegistry(), ArtifactCodecs: NewArtifactCodecRegistry(), Extractors: NewExtractorRegistry(), Mergers: NewMergerRegistry(), Normalizers: NewNormalizerRegistry(), Validators: NewValidatorRegistry(), ValidatorChains: NewValidatorChainRegistry(), Outputs: NewOutputEncoderRegistry(), } if err := RegisterArtifactCodec(registries.ArtifactCodecs, notesCodec()); err != nil { t.Fatal(err) } if err := registries.Inputs.RegisterBuilderWithSpec(testModuleSpec("input", StageInput), strict, func(request BuildRequest) (contracts.InputAdapter, error) { record("input", &request) return input, nil }); err != nil { t.Fatal(err) } if err := registries.Chunkers.RegisterBuilderWithSpec(testModuleSpec("chunk", StageChunk), strict, func(request BuildRequest) (contracts.Chunker, error) { record("chunk", &request) return &typedTestChunker{key: "chunk"}, nil }); err != nil { t.Fatal(err) } extractSpec := testModuleSpec("extract", StageExtract) extractSpec.ArtifactKind = "test/notes" if err := RegisterExtractorBuilder(registries.Extractors, extractSpec, strict, func(request BuildRequest) (contracts.Extractor[codecNotes], error) { record("extract", &request) if failure.requireExtractorLLM && request.Dependencies.LLM == nil { return nil, errors.New("structured LLM client is required") } return &constructionExtractor{key: "extract", requests: &input.extractRequests}, nil }); err != nil { t.Fatal(err) } mergeSpec := testModuleSpec("merge", StageMerge) mergeSpec.ArtifactKind = "test/notes" if err := RegisterMergerBuilder(registries.Mergers, mergeSpec, strict, func(request BuildRequest) (contracts.Merger[codecNotes], error) { record("merge", &request) return typedTestMerger[codecNotes]{key: "merge"}, nil }); err != nil { t.Fatal(err) } normalizeSpec := testModuleSpec("normalize", StageNormalize) normalizeSpec.ArtifactKind = "test/notes" if err := RegisterNormalizerBuilder(registries.Normalizers, normalizeSpec, strict, func(request BuildRequest) (contracts.Normalizer[codecNotes], error) { record("normalize", &request) return typedTestNormalizer[codecNotes]{key: "normalize"}, nil }); err != nil { t.Fatal(err) } validatorSpec := ValidatorSpec{Key: "configured", ExecutionClass: contracts.ExecutionClassDeterministic} if err := RegisterChunkValidatorBuilder(registries.Validators, validatorSpec, strict, func(request BuildRequest) (contracts.ChunkValidator, error) { record("validator", &request) return typedTestChunkValidator{key: "configured"}, nil }); err != nil { t.Fatal(err) } if err := RegisterTypedValidatorBuilder(registries.Validators, "test/notes", validatorSpec, strict, func(request BuildRequest) (contracts.TypedValidator[codecNotes], error) { record("validator", &request) return typedTestValidator[codecNotes]{key: "configured"}, nil }); err != nil { t.Fatal(err) } if err := registries.Outputs.RegisterBuilderWithSpec(testModuleSpec("output", StageOutput), strict, func(request BuildRequest) (contracts.OutputEncoder, error) { record("output", &request) if failure.output != nil { return nil, failure.output } output := contracts.OutputEncoder(&typedTestOutput{key: "output"}) if failure.outputFingerprints != nil { output = checkpointFingerprintOutput{OutputEncoder: output, fingerprints: failure.outputFingerprints} } return output, nil }); err != nil { t.Fatal(err) } return registries, input } type constructionInput struct { key string requests []contracts.ParseRequest extractRequests []contracts.TypedExtractionRequest } type constructionExtractor struct { key string requests *[]contracts.TypedExtractionRequest } func (extractor *constructionExtractor) Key() string { return extractor.key } func (*constructionExtractor) ReferenceSlots() []contracts.ReferenceSlot { return nil } func (extractor *constructionExtractor) Extract(_ context.Context, request contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[codecNotes], error) { if extractor.requests != nil { *extractor.requests = append(*extractor.requests, request) } return contracts.TypedExtractionResult[codecNotes]{}, nil } func constructionReferenceSet(slotName, content string) contracts.ReferenceSet { return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ slotName: { Slot: contracts.ReferenceSlot{Name: slotName}, Items: []contracts.ReferenceItem{{SlotName: slotName, Content: []byte(content)}}, }, }} } func constructionReferenceContent(references contracts.ReferenceSet) string { for _, slot := range references.Slots { if len(slot.Items) > 0 { return string(slot.Items[0].Content) } } return "" } func (input *constructionInput) Key() string { return input.key } func (input *constructionInput) Parse(_ context.Context, request contracts.ParseRequest) (*source.SourceDocument, error) { input.requests = append(input.requests, request) return typedTestDocument(), nil }