package pipeline import ( "encoding/json" "os" "path/filepath" "reflect" "sort" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) func TestMaterializeReferencesResolvesPathsAndDigestsContent(t *testing.T) { configDir := t.TempDir() workingDir := t.TempDir() configReference := filepath.Join(configDir, "config-reference.txt") cliReference := filepath.Join(workingDir, "cli-reference.txt") writeReferenceFile(t, configReference, []byte("config text")) writeReferenceFile(t, cliReference, []byte("cli text")) pipeline := baselineProfile() pipeline.References = ExternalReferenceMap(map[string]string{"roster": "config-reference.txt"}) lane := pipeline.Artifacts["events"] lane.References = ExternalReferenceMap(map[string]string{"glossary": "cli-reference.txt"}) pipeline.Artifacts["events"] = lane catalog := referenceCatalog(t, []contracts.ReferenceSlot{ {Name: "roster"}, {Name: "glossary"}, }) resolved, err := ResolvePipeline(pipeline, ResolveOptions{ ReferenceOverrides: []ReferenceBinding{ {LaneID: "events", SlotName: "glossary", Source: "cli-reference.txt", BindingSource: contracts.ReferenceBindingSourceCLI}, }, }, catalog) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } first, warnings, err := MaterializeReferences(resolved, catalog, ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), WorkingDir: workingDir, }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } if len(warnings) != 0 { t.Fatalf("warnings = %#v, want none", warnings) } second, _, err := MaterializeReferences(resolved, catalog, ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), WorkingDir: workingDir, }) if err != nil { t.Fatalf("MaterializeReferences(second) error = %v, want nil", err) } referenceSet := first.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet roster := referenceSet.Slots["roster"].Items[0] if string(roster.Content) != "config text" { t.Fatalf("roster content = %q, want config text", roster.Content) } if roster.Digest != referenceDigest([]byte("config text")) || roster.Digest != second.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0].Digest { t.Fatalf("roster digest = %q, want stable digest", roster.Digest) } if roster.BindingSource != contracts.ReferenceBindingSourceConfig { t.Fatalf("roster binding source = %q, want config", roster.BindingSource) } if roster.MediaType != referenceMediaType || roster.Origin.Type != referenceOriginFile || roster.SizeBytes != int64(len("config text")) { t.Fatalf("roster metadata = %#v, want text file metadata", roster) } if !strings.Contains(roster.Origin.URI, "config-reference.txt") { t.Fatalf("roster origin URI = %q, want config reference path", roster.Origin.URI) } glossary := referenceSet.Slots["glossary"].Items[0] if string(glossary.Content) != "cli text" { t.Fatalf("glossary content = %q, want cli text", glossary.Content) } if glossary.BindingSource != contracts.ReferenceBindingSourceCLI { t.Fatalf("glossary binding source = %q, want cli", glossary.BindingSource) } if !strings.Contains(glossary.Origin.URI, "cli-reference.txt") { t.Fatalf("glossary origin URI = %q, want cli reference path", glossary.Origin.URI) } provenance := ReferenceProvenance(first) if len(provenance) != 2 { t.Fatalf("ReferenceProvenance() = %#v, want two entries", provenance) } if provenance[0].Stage != string(StageExtract) || provenance[0].LaneID != "events" || provenance[0].SlotName != "glossary" || provenance[0].Digest != glossary.Digest { t.Fatalf("ReferenceProvenance()[0] = %#v, want sorted glossary provenance", provenance[0]) } if provenance[1].Stage != string(StageExtract) || provenance[1].LaneID != "events" || provenance[1].SlotName != "roster" || provenance[1].Digest != roster.Digest { t.Fatalf("ReferenceProvenance()[1] = %#v, want roster provenance", provenance[1]) } encoded, err := json.Marshal(first) if err != nil { t.Fatalf("json.Marshal(materialized) error = %v, want nil", err) } if strings.Contains(string(encoded), "config text") || strings.Contains(string(encoded), "cli text") { t.Fatalf("materialized pipeline JSON contains reference content: %s", encoded) } } func TestMaterializeReferencesStoresSetsAndProvenanceForAllTargets(t *testing.T) { configDir := t.TempDir() writeReferenceFile(t, filepath.Join(configDir, "chunk.txt"), []byte("chunk text")) writeReferenceFile(t, filepath.Join(configDir, "extract.txt"), []byte("extract text")) writeReferenceFile(t, filepath.Join(configDir, "merge.txt"), []byte("merge text")) writeReferenceFile(t, filepath.Join(configDir, "normalize.txt"), []byte("normalize text")) profile := baselineProfile() profile.References = ExternalReferenceMap(map[string]string{ "scene_guide": "chunk.txt", "roster": "extract.txt", "merge_notes": "merge.txt", "normalization_notes": "normalize.txt", }) catalog := referenceCatalogForTargets(t, []contracts.ReferenceSlot{{Name: "scene_guide"}}, []contracts.ReferenceSlot{{Name: "roster"}}, []contracts.ReferenceSlot{{Name: "merge_notes"}}, []contracts.ReferenceSlot{{Name: "normalization_notes"}}, ) resolved, err := ResolvePipeline(profile, ResolveOptions{}, catalog) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } materialized, warnings, err := MaterializeReferences(resolved, catalog, ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } if len(warnings) != 0 { t.Fatalf("warnings = %#v, want none", warnings) } chunkItem := materialized.ChunkReferences.ReferenceSet.Slots["scene_guide"].Items[0] if string(chunkItem.Content) != "chunk text" { t.Fatalf("chunk content = %q, want chunk text", chunkItem.Content) } extractItem := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0] if string(extractItem.Content) != "extract text" { t.Fatalf("extract content = %q, want extract text", extractItem.Content) } mergeItem := materialized.Steps[0].ArtifactLanes[0].MergeReferences.ReferenceSet.Slots["merge_notes"].Items[0] if string(mergeItem.Content) != "merge text" { t.Fatalf("merge content = %q, want merge text", mergeItem.Content) } normalizeItem := materialized.Steps[0].ArtifactLanes[0].NormalizeReferences.ReferenceSet.Slots["normalization_notes"].Items[0] if string(normalizeItem.Content) != "normalize text" { t.Fatalf("normalize content = %q, want normalize text", normalizeItem.Content) } provenance := ReferenceProvenance(materialized) if len(provenance) != 4 { t.Fatalf("ReferenceProvenance() = %#v, want four entries", provenance) } if provenance[0].Stage != string(StageChunk) || provenance[0].LaneID != "" || provenance[0].SlotName != "scene_guide" || provenance[0].Digest != chunkItem.Digest { t.Fatalf("ReferenceProvenance()[0] = %#v, want chunk scene guide provenance", provenance[0]) } if provenance[1].Stage != string(StageExtract) || provenance[1].LaneID != "events" || provenance[1].SlotName != "roster" || provenance[1].Digest != extractItem.Digest { t.Fatalf("ReferenceProvenance()[1] = %#v, want extract roster provenance", provenance[1]) } if provenance[2].Stage != string(StageMerge) || provenance[2].LaneID != "events" || provenance[2].SlotName != "merge_notes" || provenance[2].Digest != mergeItem.Digest { t.Fatalf("ReferenceProvenance()[2] = %#v, want merge notes provenance", provenance[2]) } if provenance[3].Stage != string(StageNormalize) || provenance[3].LaneID != "events" || provenance[3].SlotName != "normalization_notes" || provenance[3].Digest != normalizeItem.Digest { t.Fatalf("ReferenceProvenance()[2] = %#v, want normalize notes provenance", provenance[2]) } } func TestMaterializeReferencesUsesLaneArtifactVariant(t *testing.T) { configDir := t.TempDir() writeReferenceFile(t, filepath.Join(configDir, "merge.txt"), []byte("merge alpha")) writeReferenceFile(t, filepath.Join(configDir, "normalize.txt"), []byte("normalize alpha")) mergers := NewMergerRegistry() normalizers := NewNormalizerRegistry() for _, item := range []struct { kind contracts.ArtifactKind mergeSlot string normalizeSlot string }{ {kind: "test/beta", mergeSlot: "beta_merge", normalizeSlot: "beta_normalize"}, {kind: "test/alpha", mergeSlot: "alpha_merge", normalizeSlot: "alpha_normalize"}, } { if err := RegisterMerger(mergers, ModuleSpec{Key: "shared/merge", Stage: StageMerge, ExecutionClass: contracts.ExecutionClassDeterministic, ArtifactKind: item.kind, ReferenceSlots: []contracts.ReferenceSlot{{Name: item.mergeSlot}}}, func() (contracts.Merger[codecNotes], error) { return typedTestMerger[codecNotes]{key: "shared/merge"}, nil }); err != nil { t.Fatalf("RegisterMerger(%s): %v", item.kind, err) } if err := RegisterNormalizer(normalizers, ModuleSpec{Key: "shared/normalize", Stage: StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, ArtifactKind: item.kind, ReferenceSlots: []contracts.ReferenceSlot{{Name: item.normalizeSlot}}}, func() (contracts.Normalizer[codecNotes], error) { return typedTestNormalizer[codecNotes]{key: "shared/normalize"}, nil }); err != nil { t.Fatalf("RegisterNormalizer(%s): %v", item.kind, err) } } resolved := ResolvedPipeline{ ID: "variants", Steps: []ResolvedPipelineStep{{ ID: "default", ArtifactLanes: []ResolvedArtifactLane{{ ID: "alpha", ArtifactKind: "test/alpha", MergeReferences: referenceTarget(StageMerge, "alpha", "shared/merge", []ReferenceBinding{{ Stage: StageMerge, LaneID: "alpha", SlotName: "alpha_merge", Source: "merge.txt", BindingSource: contracts.ReferenceBindingSourceConfig, }}), NormalizeReferences: referenceTarget(StageNormalize, "alpha", "shared/normalize", []ReferenceBinding{{ Stage: StageNormalize, LaneID: "alpha", SlotName: "alpha_normalize", Source: "normalize.txt", BindingSource: contracts.ReferenceBindingSourceConfig, }}), }}, }}, } materialized, _, err := MaterializeReferences(resolved, ModuleCatalog{Mergers: mergers, Normalizers: normalizers}, ReferenceMaterializationOptions{ConfigPath: filepath.Join(configDir, "notarius.yml")}) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } lane := materialized.Steps[0].ArtifactLanes[0] if got := string(lane.MergeReferences.ReferenceSet.Slots["alpha_merge"].Items[0].Content); got != "merge alpha" { t.Fatalf("merge reference = %q", got) } if got := string(lane.NormalizeReferences.ReferenceSet.Slots["alpha_normalize"].Items[0].Content); got != "normalize alpha" { t.Fatalf("normalize reference = %q", got) } } func TestMaterializeReferencesRejectsNonUTF8Content(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "bad.txt") writeReferenceFile(t, path, []byte{0xff, 0xfe}) resolved := resolvedPipelineWithReference(t, "roster", "bad.txt", contracts.ReferenceBindingSourceConfig, contracts.ReferenceSlot{Name: "roster"}) _, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{{Name: "roster"}}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err == nil || !strings.Contains(err.Error(), "UTF-8") || !strings.Contains(err.Error(), "roster") || !strings.Contains(err.Error(), path) { t.Fatalf("error = %v, want UTF-8 path error", err) } } func TestMaterializeReferencesRejectsNonUTF8ContentForChunkTarget(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "bad.txt") writeReferenceFile(t, path, []byte{0xff, 0xfe}) resolved := resolvedPipelineWithTargetReference(t, StageChunk, "", "scene_guide", "bad.txt", contracts.ReferenceBindingSourceConfig, contracts.ReferenceSlot{Name: "scene_guide"}) _, _, err := MaterializeReferences(resolved, referenceCatalogForTargets(t, []contracts.ReferenceSlot{{Name: "scene_guide"}}, nil, nil, nil), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err == nil || !strings.Contains(err.Error(), "chunk") || !strings.Contains(err.Error(), "UTF-8") || !strings.Contains(err.Error(), "scene_guide") || !strings.Contains(err.Error(), path) { t.Fatalf("error = %v, want chunk UTF-8 path error", err) } } func TestMaterializeReferencesAllowsAnyMediaTypeWhenSlotDoesNotRestrictIt(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "roster.reference") writeReferenceFile(t, path, []byte("plain text")) resolved := resolvedPipelineWithReference(t, "roster", "roster.reference", contracts.ReferenceBindingSourceConfig, contracts.ReferenceSlot{Name: "roster"}) materialized, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{{Name: "roster"}}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0] if item.MediaType != unknownMediaType { t.Fatalf("MediaType = %q, want %q", item.MediaType, unknownMediaType) } } func TestMaterializeReferencesAcceptsDeclaredMarkdownMediaType(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "glossary.md") writeReferenceFile(t, path, []byte("# Terms\n")) slot := contracts.ReferenceSlot{Name: "glossary", AcceptedMediaTypes: []string{"text/markdown"}} resolved := resolvedPipelineWithReference(t, "glossary", "glossary.md", contracts.ReferenceBindingSourceConfig, slot) materialized, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{slot}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["glossary"].Items[0] if item.MediaType != "text/markdown" { t.Fatalf("MediaType = %q, want text/markdown", item.MediaType) } } func TestMaterializeReferencesAcceptsDeclaredJSONMediaType(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "roster.json") writeReferenceFile(t, path, []byte(`{"aria":"cleric"}`)) slot := contracts.ReferenceSlot{Name: "roster", AcceptedMediaTypes: []string{"application/json"}} resolved := resolvedPipelineWithReference(t, "roster", "roster.json", contracts.ReferenceBindingSourceConfig, slot) materialized, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{slot}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0] if item.MediaType != "application/json" { t.Fatalf("MediaType = %q, want application/json", item.MediaType) } } func TestMaterializeReferencesAcceptsDeclaredYAMLMediaType(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "roster.yaml") writeReferenceFile(t, path, []byte("aria: cleric\n")) slot := contracts.ReferenceSlot{Name: "roster", AcceptedMediaTypes: []string{"application/yaml"}} resolved := resolvedPipelineWithReference(t, "roster", "roster.yaml", contracts.ReferenceBindingSourceConfig, slot) materialized, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{slot}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0] if item.MediaType != "application/yaml" { t.Fatalf("MediaType = %q, want application/yaml", item.MediaType) } } func TestMaterializeReferencesRejectsUnacceptedMediaType(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "roster.json") writeReferenceFile(t, path, []byte(`{"aria":"cleric"}`)) slot := contracts.ReferenceSlot{Name: "roster", AcceptedMediaTypes: []string{"text/markdown"}} resolved := resolvedPipelineWithReference(t, "roster", "roster.json", contracts.ReferenceBindingSourceConfig, slot) _, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{slot}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err == nil || !strings.Contains(err.Error(), "media type") || !strings.Contains(err.Error(), "application/json") || !strings.Contains(err.Error(), "roster") { t.Fatalf("error = %v, want media type rejection", err) } } func TestMaterializeReferencesMatchesAcceptedMediaTypesIgnoringParameters(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "roster.txt") writeReferenceFile(t, path, []byte("Aria\n")) slot := contracts.ReferenceSlot{Name: "roster", AcceptedMediaTypes: []string{"text/plain; charset=utf-8"}} resolved := resolvedPipelineWithReference(t, "roster", "roster.txt", contracts.ReferenceBindingSourceConfig, slot) materialized, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{slot}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0] if item.MediaType != referenceMediaType { t.Fatalf("MediaType = %q, want %q", item.MediaType, referenceMediaType) } } func TestMaterializeReferencesRejectsUnacceptedMediaTypeForNormalizeTarget(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "notes.json") writeReferenceFile(t, path, []byte(`{"notes":true}`)) slot := contracts.ReferenceSlot{Name: "normalization_notes", AcceptedMediaTypes: []string{"text/markdown"}} resolved := resolvedPipelineWithTargetReference(t, StageNormalize, "events", "normalization_notes", "notes.json", contracts.ReferenceBindingSourceConfig, slot) _, _, err := MaterializeReferences(resolved, referenceCatalogForTargets(t, nil, nil, nil, []contracts.ReferenceSlot{slot}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err == nil || !strings.Contains(err.Error(), "normalize") || !strings.Contains(err.Error(), "media type") || !strings.Contains(err.Error(), "application/json") || !strings.Contains(err.Error(), "normalization_notes") { t.Fatalf("error = %v, want normalize media type rejection", err) } } func TestMaterializeReferencesWarnsForEmptyFiles(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "empty.txt") writeReferenceFile(t, path, nil) resolved := resolvedPipelineWithReference(t, "roster", "empty.txt", contracts.ReferenceBindingSourceConfig, contracts.ReferenceSlot{Name: "roster"}) materialized, warnings, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{{Name: "roster"}}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } if len(warnings) != 1 || warnings[0].ReasonCode != "empty_reference" { t.Fatalf("warnings = %#v, want empty reference warning", warnings) } item := materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["roster"].Items[0] if item.SizeBytes != 0 || item.Digest != referenceDigest(nil) { t.Fatalf("empty item = %#v, want zero size and empty digest", item) } } func TestMaterializeReferencesWarningScopesIncludeTargetContext(t *testing.T) { configDir := t.TempDir() writeReferenceFile(t, filepath.Join(configDir, "chunk.txt"), nil) writeReferenceFile(t, filepath.Join(configDir, "extract.txt"), nil) writeReferenceFile(t, filepath.Join(configDir, "normalize.txt"), nil) profile := baselineProfile() profile.References = ExternalReferenceMap(map[string]string{ "scene_guide": "chunk.txt", "roster": "extract.txt", "normalization_notes": "normalize.txt", }) catalog := referenceCatalogForTargets(t, []contracts.ReferenceSlot{{Name: "scene_guide"}}, []contracts.ReferenceSlot{{Name: "roster"}}, nil, []contracts.ReferenceSlot{{Name: "normalization_notes"}}, ) resolved, err := ResolvePipeline(profile, ResolveOptions{}, catalog) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } _, warnings, err := MaterializeReferences(resolved, catalog, ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err != nil { t.Fatalf("MaterializeReferences() error = %v, want nil", err) } got := warningScopes(warnings) want := []string{ "pipeline.baseline.chunk.reference.scene_guide", "pipeline.baseline.lane.events.extract.reference.roster", "pipeline.baseline.lane.events.normalize.reference.normalization_notes", } if !reflect.DeepEqual(got, want) { t.Fatalf("warning scopes = %#v, want %#v", got, want) } } func TestMaterializeReferencesEnforcesMaxBytes(t *testing.T) { configDir := t.TempDir() path := filepath.Join(configDir, "large.txt") writeReferenceFile(t, path, []byte("too large")) slot := contracts.ReferenceSlot{Name: "roster", MaxBytes: 3} resolved := resolvedPipelineWithReference(t, "roster", "large.txt", contracts.ReferenceBindingSourceConfig, slot) _, _, err := MaterializeReferences(resolved, referenceCatalog(t, []contracts.ReferenceSlot{slot}), ReferenceMaterializationOptions{ ConfigPath: filepath.Join(configDir, "config.yml"), }) if err == nil || !strings.Contains(err.Error(), "9 bytes") || !strings.Contains(err.Error(), "limit 3") || !strings.Contains(err.Error(), "roster") { t.Fatalf("error = %v, want max bytes error", err) } } func resolvedPipelineWithReference(t *testing.T, slotName, source, bindingSource string, slot contracts.ReferenceSlot) ResolvedPipeline { t.Helper() return resolvedPipelineWithTargetReference(t, StageExtract, "events", slotName, source, bindingSource, slot) } func resolvedPipelineWithTargetReference(t *testing.T, stage ModuleStage, laneID string, slotName, source, bindingSource string, slot contracts.ReferenceSlot) ResolvedPipeline { t.Helper() profile := baselineProfile() switch stage { case StageChunk: profile.Chunk.References = ExternalReferenceMap(map[string]string{slotName: source}) case StageExtract: lane := profile.Artifacts[laneID] lane.References = ExternalReferenceMap(map[string]string{slotName: source}) profile.Artifacts[laneID] = lane case StageMerge: lane := profile.Artifacts[laneID] lane.Merge.References = ExternalReferenceMap(map[string]string{slotName: source}) profile.Artifacts[laneID] = lane case StageNormalize: lane := profile.Artifacts[laneID] lane.Normalize.References = ExternalReferenceMap(map[string]string{slotName: source}) profile.Artifacts[laneID] = lane default: t.Fatalf("unsupported reference target stage %q", stage) } catalog := referenceCatalogForStage(t, stage, []contracts.ReferenceSlot{slot}) resolved, err := ResolvePipeline(profile, ResolveOptions{}, catalog) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } if bindingSource != contracts.ReferenceBindingSourceConfig { switch stage { case StageChunk: resolved.ChunkReferences.Bindings[0].BindingSource = bindingSource case StageExtract: resolved.Steps[0].ArtifactLanes[0].ExtractReferences.Bindings[0].BindingSource = bindingSource case StageMerge: resolved.Steps[0].ArtifactLanes[0].MergeReferences.Bindings[0].BindingSource = bindingSource case StageNormalize: resolved.Steps[0].ArtifactLanes[0].NormalizeReferences.Bindings[0].BindingSource = bindingSource } } return resolved } func referenceCatalog(t *testing.T, slots []contracts.ReferenceSlot) ModuleCatalog { t.Helper() return referenceCatalogForStage(t, StageExtract, slots) } func referenceCatalogForStage(t *testing.T, stage ModuleStage, slots []contracts.ReferenceSlot) ModuleCatalog { t.Helper() switch stage { case StageChunk: return referenceCatalogForTargets(t, slots, nil, nil, nil) case StageExtract: return referenceCatalogForTargets(t, nil, slots, nil, nil) case StageMerge: return referenceCatalogForTargets(t, nil, nil, slots, nil) case StageNormalize: return referenceCatalogForTargets(t, nil, nil, nil, slots) default: t.Fatalf("unsupported reference target stage %q", stage) return ModuleCatalog{} } } func referenceCatalogForTargets(t *testing.T, chunkSlots, extractSlots, mergeSlots, normalizeSlots []contracts.ReferenceSlot) ModuleCatalog { t.Helper() return newProfileCatalogWithOverrides(t, ModuleSpec{ Key: "generic", Stage: StageChunk, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"source"}, Provides: []string{"chunk"}, ReferenceSlots: chunkSlots, }, ModuleSpec{ Key: "event-extractor", Stage: StageExtract, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"chunk"}, Provides: []string{"candidate"}, ReferenceSlots: extractSlots, }, ModuleSpec{ Key: "appendorder", Stage: StageMerge, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"candidate"}, Provides: []string{"merged"}, ReferenceSlots: mergeSlots, }, ModuleSpec{ Key: "noop", Stage: StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"merged"}, Provides: []string{"normalized"}, ReferenceSlots: normalizeSlots, }, ) } func warningScopes(warnings []contracts.Warning) []string { scopes := make([]string, 0, len(warnings)) for _, warning := range warnings { scopes = append(scopes, warning.Scope) } sort.Strings(scopes) return scopes } func writeReferenceFile(t *testing.T, path string, content []byte) { t.Helper() if err := os.WriteFile(path, content, 0o644); err != nil { t.Fatalf("write reference %q: %v", path, err) } }