package pipeline import ( "context" "encoding/json" "errors" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) func TestResolvePipelineWithExplicitModules(t *testing.T) { catalog := newProfileCatalog(t) registerProfileSpecs(t, catalog, ModuleSpec{Key: "window", Stage: StageChunk, Requires: []string{"source"}, Provides: []string{"chunk"}}, ModuleSpec{Key: "record-extractor", Stage: StageExtract, Requires: []string{"chunk"}, Provides: []string{"candidate"}}, ModuleSpec{Key: "dedupe", Stage: StageMerge, Requires: []string{"candidate"}, Provides: []string{"merged"}}, ModuleSpec{Key: "canonical", Stage: StageNormalize, Requires: []string{"merged"}, Provides: []string{"normalized"}}, ModuleSpec{Key: "schema-check", Stage: StageValidate, Requires: []string{"normalized"}, Provides: []string{"validated"}}, ModuleSpec{Key: "ndjson", Stage: StageOutput, Requires: []string{"validated"}, Provides: []string{"encoded"}}, ) resolved, err := ResolvePipeline(PipelineProfile{ ID: " campaign ", Input: ModuleBinding{Module: " text ", LLMProfile: " fast "}, Chunk: ModuleBinding{Module: " window ", Options: map[string]any{ "size": 10, }}, Artifacts: map[string]ArtifactLaneProfile{ " records ": { Extract: ModuleBinding{Module: " record-extractor ", LLMProfile: " careful "}, Merge: Binding(" dedupe "), Normalize: Binding(" canonical "), Validators: []ModuleBinding{Binding(" schema-check ")}, }, }, Output: Binding(" ndjson "), }, ResolveOptions{}, catalog) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } if resolved.ID != "campaign" { t.Fatalf("ID = %q, want campaign", resolved.ID) } if !reflect.DeepEqual(resolved.Input, ModuleBinding{Module: "text", LLMProfile: "fast"}) { t.Fatalf("Input = %#v, want trimmed explicit input", resolved.Input) } if resolved.Chunk.Module != "window" || resolved.Chunk.LLMProfile != DefaultLLMProfile { t.Fatalf("Chunk = %#v, want explicit module and default LLM profile", resolved.Chunk) } if resolved.Chunk.Options["size"] != 10 { t.Fatalf("Chunk.Options = %#v, want size option", resolved.Chunk.Options) } if len(resolved.ArtifactLanes) != 1 { t.Fatalf("len(ArtifactLanes) = %d, want 1", len(resolved.ArtifactLanes)) } lane := resolved.ArtifactLanes[0] if lane.ID != "records" { t.Fatalf("lane.ID = %q, want records", lane.ID) } if !reflect.DeepEqual(lane.Extract, ModuleBinding{Module: "record-extractor", LLMProfile: "careful"}) { t.Fatalf("lane.Extract = %#v, want explicit extractor", lane.Extract) } if lane.Merge.Module != "dedupe" || lane.Normalize.Module != "canonical" { t.Fatalf("lane merge/normalize = %#v/%#v, want explicit modules", lane.Merge, lane.Normalize) } if len(lane.Validators) != 1 || lane.Validators[0].Module != "schema-check" { t.Fatalf("lane.Validators = %#v, want schema-check", lane.Validators) } if resolved.Output.Module != "ndjson" { t.Fatalf("Output.Module = %q, want ndjson", resolved.Output.Module) } if !strings.HasPrefix(resolved.Digest, "sha256:") { t.Fatalf("Digest = %q, want sha256 digest", resolved.Digest) } } func TestResolvePipelineAppliesDefaults(t *testing.T) { resolved, err := ResolvePipeline(PipelineProfile{ ID: "defaulted", Input: Binding("text"), Artifacts: map[string]ArtifactLaneProfile{ "events": {Extract: Binding("event-extractor")}, }, }, ResolveOptions{}, newProfileCatalog(t)) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } if resolved.Input.LLMProfile != DefaultLLMProfile { t.Fatalf("Input.LLMProfile = %q, want %q", resolved.Input.LLMProfile, DefaultLLMProfile) } if !reflect.DeepEqual(resolved.Chunk, ModuleBinding{Module: DefaultChunkModule, LLMProfile: DefaultLLMProfile}) { t.Fatalf("Chunk = %#v, want default chunk binding", resolved.Chunk) } if !reflect.DeepEqual(resolved.Output, ModuleBinding{Module: DefaultOutputModule, LLMProfile: DefaultLLMProfile}) { t.Fatalf("Output = %#v, want default output binding", resolved.Output) } lane := resolved.ArtifactLanes[0] if !reflect.DeepEqual(lane.Merge, ModuleBinding{Module: DefaultMergeModule, LLMProfile: DefaultLLMProfile}) { t.Fatalf("Merge = %#v, want default merge binding", lane.Merge) } if !reflect.DeepEqual(lane.Normalize, ModuleBinding{Module: DefaultNormalizeModule, LLMProfile: DefaultLLMProfile}) { t.Fatalf("Normalize = %#v, want default normalize binding", lane.Normalize) } if lane.Extract.LLMProfile != DefaultLLMProfile { t.Fatalf("Extract.LLMProfile = %q, want %q", lane.Extract.LLMProfile, DefaultLLMProfile) } } func TestResolvePipelineSelectsOnlyRequestedLanes(t *testing.T) { profile := multiLaneProfile() resolved, err := ResolvePipeline(profile, ResolveOptions{Only: []string{" summaries ", "events", "summaries"}}, newProfileCatalog(t)) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } got := laneIDs(resolved.ArtifactLanes) want := []string{"events", "summaries"} if !reflect.DeepEqual(got, want) { t.Fatalf("lane IDs = %#v, want %#v", got, want) } } func TestResolvePipelineAppliesReferenceBindings(t *testing.T) { profile := multiLaneProfile() profile.References = map[string]string{ " roster ": " ./shared-roster.yml ", "unclaimed": "./ignored.yml", } lane := profile.Artifacts["events"] lane.References = map[string]string{ "roster": "./lane-roster.yml", " lore ": " ./lore.md ", } profile.Artifacts["events"] = lane catalog := newProfileCatalogWithOverride(t, ModuleSpec{ Key: "event-extractor", Stage: StageExtract, Requires: []string{"chunk"}, Provides: []string{"candidate"}, ReferenceSlots: []contracts.ReferenceSlot{ {Name: "roster", Required: true}, {Name: "lore"}, }, }) resolved, err := ResolvePipeline(profile, ResolveOptions{Only: []string{"events", "summaries"}}, catalog) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } events := resolvedLane(t, resolved.ArtifactLanes, "events") want := []ReferenceBinding{ {LaneID: "events", SlotName: "lore", Source: "./lore.md", BindingSource: contracts.ReferenceBindingSourceConfig}, {LaneID: "events", SlotName: "roster", Source: "./lane-roster.yml", BindingSource: contracts.ReferenceBindingSourceConfig}, } if !reflect.DeepEqual(events.References, want) { t.Fatalf("events references = %#v, want %#v", events.References, want) } summaries := resolvedLane(t, resolved.ArtifactLanes, "summaries") if len(summaries.References) != 0 { t.Fatalf("summaries references = %#v, want none", summaries.References) } } func TestResolvePipelineRejectsUndeclaredReferenceSlot(t *testing.T) { profile := baselineProfile() lane := profile.Artifacts["events"] lane.References = map[string]string{"missing": "./missing.yml"} profile.Artifacts["events"] = lane _, err := ResolvePipeline(profile, ResolveOptions{}, newProfileCatalog(t)) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, "events", "missing", "not declared") } func TestResolvePipelineRequiresBoundReferenceSlotsForSelectedLanes(t *testing.T) { catalog := newProfileCatalogWithOverride(t, ModuleSpec{ Key: "event-extractor", Stage: StageExtract, Requires: []string{"chunk"}, Provides: []string{"candidate"}, ReferenceSlots: []contracts.ReferenceSlot{ {Name: "roster", Required: true}, }, }) if _, err := ResolvePipeline(multiLaneProfile(), ResolveOptions{Only: []string{"notes"}}, catalog); err != nil { t.Fatalf("ResolvePipeline(unselected required slot) error = %v, want nil", err) } _, err := ResolvePipeline(multiLaneProfile(), ResolveOptions{Only: []string{"events"}}, catalog) if err == nil { t.Fatal("ResolvePipeline(selected required slot) error = nil, want error") } assertErrorContains(t, err, "events", "required", "roster", "not bound") } func TestResolvePipelineReferenceUnbindCanLeaveRequiredSlotMissing(t *testing.T) { profile := baselineProfile() profile.References = map[string]string{"roster": "./roster.yml"} catalog := newProfileCatalogWithOverride(t, ModuleSpec{ Key: "event-extractor", Stage: StageExtract, Requires: []string{"chunk"}, Provides: []string{"candidate"}, ReferenceSlots: []contracts.ReferenceSlot{ {Name: "roster", Required: true}, }, }) _, err := ResolvePipeline(profile, ResolveOptions{ ReferenceUnbinds: []ReferenceUnbind{{LaneID: "events", SlotName: "roster"}}, }, catalog) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, "events", "required", "roster", "not bound") } func TestResolvePipelineUsesReferenceSlotsFromSpecWithoutConstructingExtractor(t *testing.T) { profile := baselineProfile() profile.References = map[string]string{"roster": "./roster.yml"} catalog := emptyProfileCatalog() for _, spec := range defaultProfileSpecs() { if spec.Key != "event-extractor" { registerProfileSpecs(t, catalog, spec) } } if err := catalog.Extractors.RegisterWithSpec(ModuleSpec{ Key: "event-extractor", Stage: StageExtract, Requires: []string{"chunk"}, Provides: []string{"candidate"}, ReferenceSlots: []contracts.ReferenceSlot{ {Name: "roster", Required: true}, }, }, func() (contracts.Extractor, error) { return nil, errors.New("constructor should not run") }); err != nil { t.Fatalf("RegisterWithSpec() error = %v, want nil", err) } resolved, err := ResolvePipeline(profile, ResolveOptions{}, catalog) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } if got := resolved.ArtifactLanes[0].References[0].Source; got != "./roster.yml" { t.Fatalf("reference source = %q, want ./roster.yml", got) } } func TestResolvePipelineRejectsUnknownOnlyLane(t *testing.T) { _, err := ResolvePipeline(multiLaneProfile(), ResolveOptions{Only: []string{"missing"}}, newProfileCatalog(t)) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, "pipeline", "missing", "not declared") } func TestResolvePipelineRejectsEmptyOnlyLane(t *testing.T) { _, err := ResolvePipeline(multiLaneProfile(), ResolveOptions{Only: []string{" \t"}}, newProfileCatalog(t)) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, "pipeline", "artifact lane", "empty") } func TestResolvePipelineRejectsEmptyArtifactSet(t *testing.T) { _, err := ResolvePipeline(PipelineProfile{ ID: "empty", Input: Binding("text"), Artifacts: map[string]ArtifactLaneProfile{}, }, ResolveOptions{}, newProfileCatalog(t)) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, "empty", "artifact lane") } func TestResolvePipelineRejectsEmptyPipelineID(t *testing.T) { _, err := ResolvePipeline(PipelineProfile{ ID: " ", Input: Binding("text"), Artifacts: map[string]ArtifactLaneProfile{ "events": {Extract: Binding("event-extractor")}, }, }, ResolveOptions{}, newProfileCatalog(t)) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, "pipeline id", "empty") } func TestResolvePipelineRejectsMissingInput(t *testing.T) { _, err := ResolvePipeline(PipelineProfile{ ID: "missing-input", Artifacts: map[string]ArtifactLaneProfile{ "events": {Extract: Binding("event-extractor")}, }, }, ResolveOptions{}, newProfileCatalog(t)) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, "missing-input", "input", "empty") } func TestResolvePipelineRejectsUnknownModuleKeys(t *testing.T) { tests := []struct { name string profile PipelineProfile want []string }{ { name: "input", profile: PipelineProfile{ ID: "unknown-input", Input: Binding("missing-input"), Artifacts: map[string]ArtifactLaneProfile{ "events": {Extract: Binding("event-extractor")}, }, }, want: []string{"unknown-input", "input", "missing-input"}, }, { name: "chunk", profile: withProfileChange(func(profile PipelineProfile) PipelineProfile { profile.Chunk = Binding("missing-chunk") return profile }), want: []string{"baseline", "chunk", "missing-chunk"}, }, { name: "extract", profile: withProfileChange(func(profile PipelineProfile) PipelineProfile { lane := profile.Artifacts["events"] lane.Extract = Binding("missing-extractor") profile.Artifacts["events"] = lane return profile }), want: []string{"baseline", "events", "extract", "missing-extractor"}, }, { name: "merge", profile: withProfileChange(func(profile PipelineProfile) PipelineProfile { lane := profile.Artifacts["events"] lane.Merge = Binding("missing-merge") profile.Artifacts["events"] = lane return profile }), want: []string{"baseline", "events", "merge", "missing-merge"}, }, { name: "normalize", profile: withProfileChange(func(profile PipelineProfile) PipelineProfile { lane := profile.Artifacts["events"] lane.Normalize = Binding("missing-normalize") profile.Artifacts["events"] = lane return profile }), want: []string{"baseline", "events", "normalize", "missing-normalize"}, }, { name: "validate", profile: withProfileChange(func(profile PipelineProfile) PipelineProfile { lane := profile.Artifacts["events"] lane.Validators = []ModuleBinding{Binding("missing-validator")} profile.Artifacts["events"] = lane return profile }), want: []string{"baseline", "events", "validate", "missing-validator"}, }, { name: "output", profile: withProfileChange(func(profile PipelineProfile) PipelineProfile { profile.Output = Binding("missing-output") return profile }), want: []string{"baseline", "output", "missing-output"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := ResolvePipeline(test.profile, ResolveOptions{}, newProfileCatalog(t)) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, test.want...) }) } } func TestResolvePipelineRejectsMissingCapabilities(t *testing.T) { tests := []struct { name string spec ModuleSpec want []string }{ { name: "input", spec: ModuleSpec{Key: "text", Stage: StageInput, Requires: []string{"raw"}}, want: []string{"baseline", "input", "text", "raw"}, }, { name: "chunk", spec: ModuleSpec{Key: "generic", Stage: StageChunk, Requires: []string{"missing"}}, want: []string{"baseline", "chunk", "generic", "missing"}, }, { name: "extract", spec: ModuleSpec{Key: "event-extractor", Stage: StageExtract, Requires: []string{"missing"}}, want: []string{"baseline", "events", "extract", "event-extractor", "missing"}, }, { name: "merge", spec: ModuleSpec{Key: "appendorder", Stage: StageMerge, Requires: []string{"missing"}}, want: []string{"baseline", "events", "merge", "appendorder", "missing"}, }, { name: "normalize", spec: ModuleSpec{Key: "noop", Stage: StageNormalize, Requires: []string{"missing"}}, want: []string{"baseline", "events", "normalize", "noop", "missing"}, }, { name: "validate", spec: ModuleSpec{Key: "grounded", Stage: StageValidate, Requires: []string{"missing"}}, want: []string{"baseline", "events", "validate", "grounded", "missing"}, }, { name: "output", spec: ModuleSpec{Key: "json", Stage: StageOutput, Requires: []string{"missing"}}, want: []string{"baseline", "output", "json", "missing"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { catalog := newProfileCatalogWithOverride(t, test.spec) profile := baselineProfile() lane := profile.Artifacts["events"] lane.Validators = []ModuleBinding{Binding("grounded")} profile.Artifacts["events"] = lane _, err := ResolvePipeline(profile, ResolveOptions{}, catalog) if err == nil { t.Fatal("ResolvePipeline() error = nil, want error") } assertErrorContains(t, err, test.want...) }) } } func TestResolvePipelineOrdersLanesDeterministically(t *testing.T) { resolved, err := ResolvePipeline(multiLaneProfile(), ResolveOptions{}, newProfileCatalog(t)) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } got := laneIDs(resolved.ArtifactLanes) want := []string{"events", "notes", "summaries"} if !reflect.DeepEqual(got, want) { t.Fatalf("lane IDs = %#v, want %#v", got, want) } } func TestResolvePipelineDigestIsDeterministicForEquivalentMaps(t *testing.T) { left := PipelineProfile{ ID: "digest", Input: Binding("text"), Output: Binding("json"), Chunk: ModuleBinding{Module: "generic", Options: map[string]any{"b": 2, "a": 1}}, Artifacts: map[string]ArtifactLaneProfile{ "events": {Extract: Binding("event-extractor")}, "notes": {Extract: Binding("note-extractor")}, }, } right := PipelineProfile{ ID: "digest", Input: Binding("text"), Output: Binding("json"), Chunk: ModuleBinding{Module: "generic", Options: map[string]any{"a": 1, "b": 2}}, Artifacts: map[string]ArtifactLaneProfile{ "notes": {Extract: Binding("note-extractor")}, "events": {Extract: Binding("event-extractor")}, }, } leftResolved, err := ResolvePipeline(left, ResolveOptions{}, newProfileCatalog(t)) if err != nil { t.Fatalf("ResolvePipeline(left) error = %v, want nil", err) } rightResolved, err := ResolvePipeline(right, ResolveOptions{}, newProfileCatalog(t)) if err != nil { t.Fatalf("ResolvePipeline(right) error = %v, want nil", err) } if leftResolved.Digest != rightResolved.Digest { t.Fatalf("digests differ for equivalent profiles: %q != %q", leftResolved.Digest, rightResolved.Digest) } } func TestResolvePipelineDigestChangesWhenBindingChanges(t *testing.T) { left := baselineProfile() right := baselineProfile() right.Chunk = Binding("window") catalog := newProfileCatalog(t) registerProfileSpecs(t, catalog, ModuleSpec{Key: "window", Stage: StageChunk, Requires: []string{"source"}, Provides: []string{"chunk"}}) leftResolved, err := ResolvePipeline(left, ResolveOptions{}, catalog) if err != nil { t.Fatalf("ResolvePipeline(left) error = %v, want nil", err) } rightResolved, err := ResolvePipeline(right, ResolveOptions{}, catalog) if err != nil { t.Fatalf("ResolvePipeline(right) error = %v, want nil", err) } if leftResolved.Digest == rightResolved.Digest { t.Fatalf("digest = %q for both profiles, want changed digest", leftResolved.Digest) } } func TestBindingTrimsModuleAndLeavesResolutionFieldsEmpty(t *testing.T) { binding := Binding(" module ") if binding.Module != "module" { t.Fatalf("Module = %q, want module", binding.Module) } if binding.LLMProfile != "" { t.Fatalf("LLMProfile = %q, want empty", binding.LLMProfile) } if binding.Options != nil { t.Fatalf("Options = %#v, want nil", binding.Options) } } func TestResolvedPipelineDigestExcludesDigestField(t *testing.T) { resolved, err := ResolvePipeline(baselineProfile(), ResolveOptions{}, newProfileCatalog(t)) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } changed := resolved changed.Digest = "sha256:changed" leftDigest, err := resolvedPipelineDigest(resolved) if err != nil { t.Fatalf("resolvedPipelineDigest(resolved) error = %v, want nil", err) } rightDigest, err := resolvedPipelineDigest(changed) if err != nil { t.Fatalf("resolvedPipelineDigest(changed) error = %v, want nil", err) } if leftDigest != rightDigest { t.Fatalf("digest with changed digest field = %q, want %q", rightDigest, leftDigest) } } func baselineProfile() PipelineProfile { return PipelineProfile{ ID: "baseline", Input: Binding("text"), Artifacts: map[string]ArtifactLaneProfile{ "events": {Extract: Binding("event-extractor")}, }, } } func multiLaneProfile() PipelineProfile { profile := baselineProfile() profile.ID = "multi" profile.Artifacts = map[string]ArtifactLaneProfile{ "summaries": {Extract: Binding("note-extractor")}, "events": {Extract: Binding("event-extractor")}, "notes": {Extract: Binding("note-extractor")}, } return profile } func withProfileChange(change func(PipelineProfile) PipelineProfile) PipelineProfile { return change(baselineProfile()) } func laneIDs(lanes []ResolvedArtifactLane) []string { ids := make([]string, 0, len(lanes)) for _, lane := range lanes { ids = append(ids, lane.ID) } return ids } func resolvedLane(t *testing.T, lanes []ResolvedArtifactLane, laneID string) ResolvedArtifactLane { t.Helper() for _, lane := range lanes { if lane.ID == laneID { return lane } } t.Fatalf("lane %q not found in %#v", laneID, laneIDs(lanes)) return ResolvedArtifactLane{} } func assertErrorContains(t *testing.T, err error, values ...string) { t.Helper() message := err.Error() for _, value := range values { if !strings.Contains(message, value) { t.Fatalf("error = %q, want substring %q", message, value) } } } func newProfileCatalog(t *testing.T) ModuleCatalog { t.Helper() catalog := emptyProfileCatalog() registerProfileSpecs(t, catalog, defaultProfileSpecs()...) return catalog } func newProfileCatalogWithOverride(t *testing.T, override ModuleSpec) ModuleCatalog { t.Helper() specs := defaultProfileSpecs() for index, spec := range specs { if spec.Stage == override.Stage && spec.Key == override.Key { specs[index] = override catalog := emptyProfileCatalog() registerProfileSpecs(t, catalog, specs...) return catalog } } catalog := emptyProfileCatalog() registerProfileSpecs(t, catalog, specs...) registerProfileSpecs(t, catalog, override) return catalog } func emptyProfileCatalog() ModuleCatalog { return ModuleCatalog{ Inputs: NewInputAdapterRegistry(), Chunkers: NewChunkerRegistry(), Extractors: NewExtractorRegistry(), Mergers: NewMergerRegistry(), Normalizers: NewNormalizerRegistry(), Validators: NewValidatorRegistry(), Outputs: NewOutputEncoderRegistry(), } } func defaultProfileSpecs() []ModuleSpec { return []ModuleSpec{ ModuleSpec{Key: "text", Stage: StageInput, Provides: []string{"source"}}, ModuleSpec{Key: "generic", Stage: StageChunk, Requires: []string{"source"}, Provides: []string{"chunk"}}, ModuleSpec{Key: "event-extractor", Stage: StageExtract, Requires: []string{"chunk"}, Provides: []string{"candidate"}}, ModuleSpec{Key: "note-extractor", Stage: StageExtract, Requires: []string{"chunk"}, Provides: []string{"candidate"}}, ModuleSpec{Key: "appendorder", Stage: StageMerge, Requires: []string{"candidate"}, Provides: []string{"merged"}}, ModuleSpec{Key: "noop", Stage: StageNormalize, Requires: []string{"merged"}, Provides: []string{"normalized"}}, ModuleSpec{Key: "grounded", Stage: StageValidate, Requires: []string{"normalized"}, Provides: []string{"validated"}}, ModuleSpec{Key: "json", Stage: StageOutput, Requires: []string{"normalized"}, Provides: []string{"encoded"}}, } } func registerProfileSpecs(t *testing.T, catalog ModuleCatalog, specs ...ModuleSpec) { t.Helper() for _, spec := range specs { switch spec.Stage { case StageInput: if err := catalog.Inputs.RegisterWithSpec(spec, profileInputConstructor(spec.Key)); err != nil { t.Fatalf("register input spec %#v: %v", spec, err) } case StageChunk: if err := catalog.Chunkers.RegisterWithSpec(spec, profileChunkerConstructor(spec.Key)); err != nil { t.Fatalf("register chunk spec %#v: %v", spec, err) } case StageExtract: if err := catalog.Extractors.RegisterWithSpec(spec, profileExtractorConstructor(spec.Key)); err != nil { t.Fatalf("register extractor spec %#v: %v", spec, err) } case StageMerge: if err := catalog.Mergers.RegisterWithSpec(spec, profileMergerConstructor(spec.Key)); err != nil { t.Fatalf("register merger spec %#v: %v", spec, err) } case StageNormalize: if err := catalog.Normalizers.RegisterWithSpec(spec, profileNormalizerConstructor(spec.Key)); err != nil { t.Fatalf("register normalizer spec %#v: %v", spec, err) } case StageValidate: if err := catalog.Validators.RegisterWithSpec(spec, profileValidatorConstructor(spec.Key)); err != nil { t.Fatalf("register validator spec %#v: %v", spec, err) } case StageOutput: if err := catalog.Outputs.RegisterWithSpec(spec, profileOutputConstructor(spec.Key)); err != nil { t.Fatalf("register output spec %#v: %v", spec, err) } default: t.Fatalf("unsupported spec stage %q", spec.Stage) } } } func profileInputConstructor(key string) InputAdapterConstructor { return func() (contracts.InputAdapter, error) { return profileInputAdapter{key: key}, nil } } type profileInputAdapter struct { key string } func (adapter profileInputAdapter) Key() string { return adapter.key } func (adapter profileInputAdapter) Parse(ctx context.Context, req contracts.ParseRequest) (*source.SourceDocument, error) { return &source.SourceDocument{}, nil } func profileChunkerConstructor(key string) ChunkerConstructor { return func() (contracts.Chunker, error) { return registryChunker{key: key}, nil } } func profileExtractorConstructor(key string) ExtractorConstructor { return func() (contracts.Extractor, error) { return registryFakeExtractor{key: key}, nil } } func profileMergerConstructor(key string) MergerConstructor { return func() (contracts.Merger, error) { return registryMerger{key: key}, nil } } func profileNormalizerConstructor(key string) NormalizerConstructor { return func() (contracts.Normalizer, error) { return registryNormalizer{key: key}, nil } } func profileValidatorConstructor(key string) ValidatorConstructor { return func() (contracts.Validator, error) { return registryValidator{name: key}, nil } } func profileOutputConstructor(key string) OutputEncoderConstructor { return func() (contracts.OutputEncoder, error) { return registryOutputEncoder{key: key}, nil } } func TestResolvedPipelineCanMarshalToCanonicalJSON(t *testing.T) { resolved, err := ResolvePipeline(baselineProfile(), ResolveOptions{}, newProfileCatalog(t)) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } if _, err := json.Marshal(resolved); err != nil { t.Fatalf("json.Marshal(resolved) error = %v, want nil", err) } }