package config import ( "testing" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) func TestPipelineInheritancePreservesCompositionAndReplacesSteps(t *testing.T) { t.Parallel() tests := []struct { name string childSteps string wantStepIDs []string }{ { name: "inherits ordered steps", wantStepIDs: []string{"prepare", "summarize"}, }, { name: "replaces ordered steps", childSteps: ` steps: - id: replacement artifacts: summary: extract: {module: extract}`, wantStepIDs: []string{"replacement"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { source := `version: 5 pipelines: parent: input: {module: input} structured_output_repair_attempts: 2 steps: - id: prepare artifacts: score: extract: {module: extract, retries: 2} - id: summarize artifacts: notes: extract: {module: extract} child: extends: parent llm_profile: local structured_output_repair_attempts: 0` + test.childSteps + "\n" loaded, err := loadConfigSource(configkit.Bytes("inheritance.yml", []byte(source)), LoadOptions{}) if err != nil { t.Fatalf("loadConfigSource() error = %v", err) } parent := loaded.Config.Pipelines["parent"] child := loaded.Config.Pipelines["child"] if parent.Input.Module != "input" || child.Input.Module != "input" { t.Fatalf("parent/child inputs = %#v / %#v", parent.Input, child.Input) } if child.LLMProfile != "local" || child.StructuredOutputRepairAttempts == nil || *child.StructuredOutputRepairAttempts != 0 { t.Fatalf("child overrides = %#v", child) } if parent.StructuredOutputRepairAttempts == nil || *parent.StructuredOutputRepairAttempts != 2 { t.Fatalf("parent settings changed: %#v", parent) } if len(child.Steps) != len(test.wantStepIDs) { t.Fatalf("child steps = %#v, want %v", child.Steps, test.wantStepIDs) } for i, want := range test.wantStepIDs { if child.Steps[i].ID != want { t.Fatalf("child step %d = %q, want %q", i, child.Steps[i].ID, want) } } if test.childSteps == "" && child.Steps[0].Artifacts["score"].Extract.Module != "extract" { t.Fatalf("inherited first artifact = %#v", child.Steps[0].Artifacts["score"]) } for _, id := range []string{"parent", "child"} { resolved, err := loaded.Config.Resolve(ResolveInput{PipelineID: id, Catalog: effectiveCatalog(t)}) if err != nil { t.Fatalf("Resolve(%q) error = %v", id, err) } if resolved.ResolvedPipeline.ID != id { t.Fatalf("resolved pipeline ID = %q, want %q", resolved.ResolvedPipeline.ID, id) } wantResolvedSteps := len(test.wantStepIDs) if id == "parent" { wantResolvedSteps = 2 } if len(resolved.ResolvedPipeline.Steps) != wantResolvedSteps { t.Fatalf("resolved %q steps = %#v", id, resolved.ResolvedPipeline.Steps) } if id == "child" && resolved.ResolvedPipeline.Steps[0].ArtifactLanes[0].Extract.LLMProfile != "local" { t.Fatalf("resolved child profile = %q", resolved.ResolvedPipeline.Steps[0].ArtifactLanes[0].Extract.LLMProfile) } } }) } } func TestArtifactLaneInheritanceUsesSiblingMappingComposition(t *testing.T) { t.Parallel() loaded, err := loadConfigSource(configkit.Bytes("lanes.yml", []byte(`version: 5 pipelines: session: input: {module: input} artifacts: base: extract: {module: extract, retries: 2} derived: extends: base extract: {retries: 4} `)), LoadOptions{}) if err != nil { t.Fatalf("loadConfigSource() error = %v", err) } lanes := loaded.Config.Pipelines["session"].Artifacts if lanes["base"].Extract.Module != "extract" || lanes["base"].Extract.Retries != 2 { t.Fatalf("base lane = %#v", lanes["base"]) } if lanes["derived"].Extract.Module != "extract" || lanes["derived"].Extract.Retries != 4 { t.Fatalf("derived lane = %#v", lanes["derived"]) } if _, err := loaded.Config.Resolve(ResolveInput{PipelineID: "session", Catalog: effectiveCatalog(t)}); err != nil { t.Fatalf("Resolve() error = %v", err) } }