Make typed module spec lookup artifact-aware

This commit is contained in:
2026-07-17 13:53:55 +00:00
parent fbc3d9add6
commit 3d3cc0c08e
9 changed files with 419 additions and 40 deletions

View File

@@ -1268,6 +1268,111 @@ func TestRunPipelineReferenceFlagBindsUnambiguousSlot(t *testing.T) {
}
}
func TestCLIReferenceDiscoveryUsesLaneArtifactVariant(t *testing.T) {
catalog := referenceVariantCatalog(t)
cfg := config.Config{Pipelines: map[string]pipeline.PipelineProfile{
"variants": {
ID: "variants",
Chunk: pipeline.Binding("generic"),
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"alpha": {
Extract: pipeline.Binding("extract/alpha"),
Merge: pipeline.Binding("shared/merge"),
Normalize: pipeline.Binding("shared/normalize"),
},
"beta": {
Extract: pipeline.Binding("extract/beta"),
Merge: pipeline.Binding("shared/merge"),
Normalize: pipeline.Binding("shared/normalize"),
},
},
},
}}
targets, err := selectedReferenceTargets(cfg, "variants", nil, catalog)
if err != nil {
t.Fatalf("selectedReferenceTargets() error = %v, want nil", err)
}
tests := []struct {
name string
selector cliReferenceSelector
laneID string
stage pipeline.ModuleStage
}{
{
name: "qualified merger",
selector: cliReferenceSelector{LaneID: "alpha", Stage: pipeline.StageMerge, SlotName: "alpha_merge"},
laneID: "alpha",
stage: pipeline.StageMerge,
},
{
name: "unqualified merger",
selector: cliReferenceSelector{Stage: pipeline.StageMerge, SlotName: "beta_merge"},
laneID: "beta",
stage: pipeline.StageMerge,
},
{
name: "flat normalizer",
selector: cliReferenceSelector{SlotName: "alpha_normalize"},
laneID: "alpha",
stage: pipeline.StageNormalize,
},
{
name: "lane normalizer",
selector: cliReferenceSelector{LaneID: "beta", SlotName: "beta_normalize"},
laneID: "beta",
stage: pipeline.StageNormalize,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
target, err := resolveCLIReferenceTarget(targets, tc.selector)
if err != nil {
t.Fatalf("resolveCLIReferenceTarget() error = %v, want nil", err)
}
if target.laneID != tc.laneID || target.stage != tc.stage {
t.Fatalf("target = %#v, want lane %q stage %q", target, tc.laneID, tc.stage)
}
})
}
_, err = resolveCLIReferenceTarget(targets, cliReferenceSelector{LaneID: "beta", Stage: pipeline.StageMerge, SlotName: "alpha_merge"})
if err == nil || !strings.Contains(err.Error(), "not declared") {
t.Fatalf("beta alpha-variant reference error = %v, want slot rejection", err)
}
}
func TestCLIReferenceDiscoveryReportsMissingArtifactVariant(t *testing.T) {
catalog := referenceVariantCatalog(t)
if err := pipeline.RegisterExtractor(catalog.Extractors, pipeline.ModuleSpec{Key: "extract/missing", Stage: pipeline.StageExtract, ArtifactKind: "test/missing"}, func() (contracts.Extractor[fakeRunArtifact], error) {
return referenceVariantExtractor{key: "extract/missing"}, nil
}); err != nil {
t.Fatalf("register missing-kind extractor: %v", err)
}
cfg := config.Config{Pipelines: map[string]pipeline.PipelineProfile{
"variants": {
ID: "variants",
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"missing": {
Extract: pipeline.Binding("extract/missing"),
Merge: pipeline.Binding("shared/merge"),
Normalize: pipeline.Binding("shared/normalize"),
},
},
},
}}
_, err := selectedReferenceTargets(cfg, "variants", nil, catalog)
want := []string{"pipeline \"variants\"", "lane \"missing\"", "merge module \"shared/merge\"", "artifact kind \"test/missing\"", "registered kinds: test/alpha, test/beta"}
if err == nil {
t.Fatal("selectedReferenceTargets() error = nil, want missing variant error")
}
for _, value := range want {
if !strings.Contains(err.Error(), value) {
t.Fatalf("selectedReferenceTargets() error = %q, want %q", err, value)
}
}
}
func TestRunPipelineReferenceFlagBindsLaneQualifiedSlot(t *testing.T) {
configPath := writeTestConfig(t, testConfigYAML("example", "events", "notes"))
inputPath := filepath.Join(t.TempDir(), "missing.json")
@@ -3652,6 +3757,30 @@ const fakeRunArtifactKind contracts.ArtifactKind = "test/fake"
type fakeRunArtifact struct {
Value bool `json:"value"`
}
type referenceVariantExtractor struct{ key string }
func (e referenceVariantExtractor) Key() string { return e.key }
func (referenceVariantExtractor) ReferenceSlots() []contracts.ReferenceSlot { return nil }
func (referenceVariantExtractor) Extract(context.Context, contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[fakeRunArtifact], error) {
return contracts.TypedExtractionResult[fakeRunArtifact]{}, nil
}
type referenceVariantMerger struct{ key string }
func (m referenceVariantMerger) Key() string { return m.key }
func (referenceVariantMerger) Merge(context.Context, contracts.TypedMergeRequest[fakeRunArtifact]) (contracts.TypedMergeResult[fakeRunArtifact], error) {
return contracts.TypedMergeResult[fakeRunArtifact]{}, nil
}
type referenceVariantNormalizer struct{ key string }
func (n referenceVariantNormalizer) Key() string { return n.key }
func (referenceVariantNormalizer) ReferenceSlots() []contracts.ReferenceSlot { return nil }
func (referenceVariantNormalizer) Normalize(context.Context, contracts.TypedNormalizeRequest[fakeRunArtifact]) (contracts.TypedNormalizeResult[fakeRunArtifact], error) {
return contracts.TypedNormalizeResult[fakeRunArtifact]{}, nil
}
type fakeRunCodec struct{}
func (fakeRunCodec) Kind() contracts.ArtifactKind { return fakeRunArtifactKind }
@@ -3994,6 +4123,52 @@ func fakeCatalog(t *testing.T, overrides ...pipeline.ModuleSpec) pipeline.Module
}
}
func referenceVariantCatalog(t *testing.T) pipeline.ModuleCatalog {
t.Helper()
chunkers := pipeline.NewChunkerRegistry()
extractors := pipeline.NewExtractorRegistry()
mergers := pipeline.NewMergerRegistry()
normalizers := pipeline.NewNormalizerRegistry()
mustRegisterChunker(t, chunkers, pipeline.ModuleSpec{Key: "generic", Stage: pipeline.StageChunk})
for _, item := range []struct {
key string
kind contracts.ArtifactKind
}{
{key: "extract/alpha", kind: "test/alpha"},
{key: "extract/beta", kind: "test/beta"},
} {
item := item
if err := pipeline.RegisterExtractor(extractors, pipeline.ModuleSpec{Key: item.key, Stage: pipeline.StageExtract, ArtifactKind: item.kind}, func() (contracts.Extractor[fakeRunArtifact], error) {
return referenceVariantExtractor{key: item.key}, nil
}); err != nil {
t.Fatalf("register extractor %q: %v", item.key, err)
}
}
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"},
} {
mergeSpec := pipeline.ModuleSpec{Key: "shared/merge", Stage: pipeline.StageMerge, ArtifactKind: item.kind, ReferenceSlots: []contracts.ReferenceSlot{{Name: item.mergeSlot}}}
if err := pipeline.RegisterMerger(mergers, mergeSpec, func() (contracts.Merger[fakeRunArtifact], error) {
return referenceVariantMerger{key: "shared/merge"}, nil
}); err != nil {
t.Fatalf("register merger %q: %v", item.kind, err)
}
normalizeSpec := pipeline.ModuleSpec{Key: "shared/normalize", Stage: pipeline.StageNormalize, ArtifactKind: item.kind, ReferenceSlots: []contracts.ReferenceSlot{{Name: item.normalizeSlot}}}
if err := pipeline.RegisterNormalizer(normalizers, normalizeSpec, func() (contracts.Normalizer[fakeRunArtifact], error) {
return referenceVariantNormalizer{key: "shared/normalize"}, nil
}); err != nil {
t.Fatalf("register normalizer %q: %v", item.kind, err)
}
}
return pipeline.ModuleCatalog{Chunkers: chunkers, Extractors: extractors, Mergers: mergers, Normalizers: normalizers}
}
func mustRegisterInput(t *testing.T, registry *pipeline.InputAdapterRegistry, spec pipeline.ModuleSpec) {
t.Helper()
if err := registry.RegisterWithSpec(spec, func() (contracts.InputAdapter, error) { return fakeRunInputAdapter{}, nil }); err != nil {