package config import ( "bytes" "errors" "os" "path/filepath" "strings" "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) } } func TestInheritedPresenceOverridesRemainExplicit(t *testing.T) { t.Parallel() loaded, err := loadConfigSource(configkit.Bytes("presence.yml", []byte(`version: 5 pipelines: parent: input: {module: input} structured_output_repair_attempts: 3 chunk: module: generic validators: [] artifacts: score: extract: module: extract validators: [{module: configured-a}] child: extends: parent structured_output_repair_attempts: 0 artifacts: score: extract: validators: [] `)), LoadOptions{}) if err != nil { var loadError *configkit.LoadError if errors.As(err, &loadError) { t.Fatalf("loadConfigSource() diagnostics = %#v", loadError.Diagnostics()) } t.Fatalf("loadConfigSource() error = %v", err) } parent := loaded.Config.Pipelines["parent"] child := loaded.Config.Pipelines["child"] if parent.Chunk.Module != "generic" || !parent.Chunk.Validators.Set || parent.Chunk.Validators.Validators == nil || len(parent.Chunk.Validators.Validators) != 0 { t.Fatalf("parent empty chunk validators = %#v", parent.Chunk) } if !parent.Artifacts["score"].Extract.Validators.Set || len(parent.Artifacts["score"].Extract.Validators.Validators) != 1 || parent.Artifacts["score"].Extract.Validators.Validators[0].Module != "configured-a" { t.Fatalf("parent extraction validators = %#v", parent.Artifacts["score"].Extract.Validators) } if parent.StructuredOutputRepairAttempts == nil || *parent.StructuredOutputRepairAttempts != 3 { t.Fatalf("parent repair budget = %#v", parent.StructuredOutputRepairAttempts) } if child.StructuredOutputRepairAttempts == nil || *child.StructuredOutputRepairAttempts != 0 { t.Fatalf("child zero repair budget = %#v", child.StructuredOutputRepairAttempts) } extract := child.Artifacts["score"].Extract if !extract.Validators.Set || extract.Validators.Validators == nil || len(extract.Validators.Validators) != 0 { t.Fatalf("child empty extraction validators = %#v", extract.Validators) } if !child.Chunk.Validators.Set || child.Chunk.Validators.Validators == nil || len(child.Chunk.Validators.Validators) != 0 { t.Fatalf("inherited empty chunk validators = %#v", child.Chunk.Validators) } resolved, err := loaded.Config.Resolve(ResolveInput{PipelineID: "child", Catalog: effectiveCatalog(t)}) if err != nil { t.Fatalf("Resolve(child) error = %v", err) } lanes := resolved.ResolvedPipeline.AllArtifactLanes() if len(lanes) != 1 || lanes[0].Extract.StructuredOutputRepairAttempts == nil || *lanes[0].Extract.StructuredOutputRepairAttempts != 0 { t.Fatalf("resolved child repair budget = %#v", lanes) } if !resolved.ResolvedPipeline.Chunk.Validators.Set || len(resolved.ResolvedPipeline.Chunk.Validators.Validators) != 0 || !lanes[0].Extract.Validators.Set || len(lanes[0].Extract.Validators.Validators) != 0 { t.Fatalf("resolved empty validator overrides = chunk %#v, extraction %#v", resolved.ResolvedPipeline.Chunk.Validators, lanes[0].Extract.Validators) } } func TestInheritedFalseOptionRetainsExplicitMetadata(t *testing.T) { t.Parallel() loaded, err := loadConfigSource(configkit.Bytes("false-option.yml", []byte(`version: 5 pipelines: parent: input: {module: input} output: module: json options: evidence_context: {enabled: false} child: extends: parent `)), LoadOptions{}) if err != nil { t.Fatalf("loadConfigSource() error = %v", err) } options := loaded.Config.Pipelines["child"].Output.Options["evidence_context"].(map[string]any) if enabled, ok := options["enabled"].(bool); !ok || enabled { t.Fatalf("inherited evidence_context.enabled = %#v", options["enabled"]) } path := configkit.Keys("pipelines", "child", "output", "options", "evidence_context", "enabled") info, ok := loaded.result.Metadata().Lookup(path) if !ok || !info.Explicit || info.Null || len(info.Origin.Inheritance) == 0 { t.Fatalf("inherited false metadata = %#v, found=%v", info, ok) } } func TestInheritedAndDirectNullRepairBudgetsRemainInvalid(t *testing.T) { t.Parallel() for _, test := range []struct { name string source string path string }{ { name: "child null", source: `version: 5 pipelines: parent: {input: {module: input}, structured_output_repair_attempts: 2} child: extends: parent structured_output_repair_attempts: null `, path: "/pipelines/child/structured_output_repair_attempts", }, { name: "inherited null", source: `version: 5 pipelines: parent: {input: {module: input}, structured_output_repair_attempts: null} child: {extends: parent} `, path: "/pipelines/parent/structured_output_repair_attempts", }, } { t.Run(test.name, func(t *testing.T) { _, err := loadConfigSource(configkit.Bytes("null.yml", []byte(test.source)), LoadOptions{}) assertDocumentDiagnosticAt(t, err, test.path) }) } } func TestOverlayAndOverridesComposeBeforeInheritance(t *testing.T) { t.Parallel() root := t.TempDir() overlays := filepath.Join(root, "overlays") if err := os.MkdirAll(overlays, 0o700); err != nil { t.Fatal(err) } main := filepath.Join(root, "config.yml") if err := os.WriteFile(main, []byte(`version: 5 pipelines: parent: input: {module: input} structured_output_repair_attempts: 1 inherited: extends: parent overridden: extends: parent structured_output_repair_attempts: 0 cancelled: extends: parent input: {module: input} `), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(overlays, "development.yml"), []byte(`pipelines: parent: structured_output_repair_attempts: 2 cancelled: extends: null `), 0o600); err != nil { t.Fatal(err) } loaded, err := Load(main, LoadOptions{Overlay: "development"}) if err != nil { t.Fatalf("Load() error = %v", err) } if got := *loaded.Config.Pipelines["inherited"].StructuredOutputRepairAttempts; got != 2 { t.Fatalf("overlay-inherited repair budget = %d, want 2", got) } if got := *loaded.Config.Pipelines["overridden"].StructuredOutputRepairAttempts; got != 0 { t.Fatalf("child replacement repair budget = %d, want 0", got) } if loaded.Config.Pipelines["cancelled"].StructuredOutputRepairAttempts != nil { t.Fatalf("cancelled child inherited parent settings: %#v", loaded.Config.Pipelines["cancelled"]) } withOverride, err := Load(main, LoadOptions{ Overlay: "development", Overrides: []configkit.Override{{ Path: configkit.Keys("pipelines", "parent", "structured_output_repair_attempts"), Value: 3, Flag: "--repair-budget", }}, }) if err != nil { t.Fatalf("Load() with explicit override error = %v", err) } if got := *withOverride.Config.Pipelines["inherited"].StructuredOutputRepairAttempts; got != 3 { t.Fatalf("override-inherited repair budget = %d, want 3", got) } } func TestInheritedReferencePathsKeepDeclaringSource(t *testing.T) { t.Parallel() root := t.TempDir() parentDirectory := filepath.Join(root, "parents") childDirectory := filepath.Join(root, "children") overlayDirectory := filepath.Join(root, "overlays") fixtureDirectory := filepath.Join(root, "fixtures") for _, directory := range []string{parentDirectory, childDirectory, overlayDirectory, fixtureDirectory} { if err := os.MkdirAll(directory, 0o700); err != nil { t.Fatal(err) } } for _, fixture := range []struct{ directory, name string }{ {parentDirectory, "shared.txt"}, {parentDirectory, "parent.txt"}, {parentDirectory, "step.txt"}, {childDirectory, "child.txt"}, {overlayDirectory, "overlay.txt"}, {fixtureDirectory, "outside.txt"}, } { if err := os.WriteFile(filepath.Join(fixture.directory, fixture.name), []byte("fixture\n"), 0o600); err != nil { t.Fatal(err) } } parent := filepath.Join(parentDirectory, "pipelines.yml") if err := os.WriteFile(parent, []byte(`pipelines: " parent ": input: {module: input} references: " shared ": {path: ./shared.txt} nested: extends: " shared " source: {path: ./parent.txt} outside: {path: ../fixtures/outside.txt} steps: - id: first references: step-source: {path: ./step.txt} artifacts: summary: extract: {module: extract} `), 0o600); err != nil { t.Fatal(err) } child := filepath.Join(childDirectory, "pipelines.yml") if err := os.WriteFile(child, []byte(`pipelines: child: extends: " parent " references: source: {path: ./child.txt} `), 0o600); err != nil { t.Fatal(err) } main := filepath.Join(root, "notarius.yml") if err := os.WriteFile(main, []byte(`version: 5 configkit: imports: - path: parents/pipelines.yml - path: children/pipelines.yml `), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(overlayDirectory, "development.yml"), []byte(`pipelines: " parent ": references: " shared ": {path: ./overlay.txt} `), 0o600); err != nil { t.Fatal(err) } loaded, err := loadConfig(main, LoadOptions{}) if err != nil { t.Fatalf("loadConfig() error = %v", err) } assertReferencePath := func(pipelineID, referenceID, want string) { t.Helper() if got := loaded.Config.Pipelines[pipelineID].References[referenceID].Path; got != want { t.Errorf("%s reference %s path = %q, want %q", pipelineID, referenceID, got, want) } } assertReferencePath("parent", "shared", filepath.Join(parentDirectory, "shared.txt")) assertReferencePath("child", "shared", filepath.Join(parentDirectory, "shared.txt")) assertReferencePath("child", "nested", filepath.Join(parentDirectory, "shared.txt")) assertReferencePath("parent", "source", filepath.Join(parentDirectory, "parent.txt")) assertReferencePath("child", "source", filepath.Join(childDirectory, "child.txt")) // Keep ../ coverage separate from paths that distinguish the declaring file. assertReferencePath("child", "outside", filepath.Join(fixtureDirectory, "outside.txt")) if got := loaded.Config.Pipelines["child"].Steps[0].References["step-source"].Path; got != filepath.Join(parentDirectory, "step.txt") { t.Errorf("inherited step reference path = %q", got) } overlaid, err := loadConfig(main, LoadOptions{Overlay: "development"}) if err != nil { t.Fatalf("loadConfig() with overlay error = %v", err) } for _, pipelineID := range []string{"parent", "child"} { for _, referenceID := range []string{"shared", "nested"} { if got, want := overlaid.Config.Pipelines[pipelineID].References[referenceID].Path, filepath.Join(overlayDirectory, "overlay.txt"); got != want { t.Errorf("overlaid %s reference %s path = %q, want %q", pipelineID, referenceID, got, want) } } } if got, want := overlaid.Config.Pipelines["child"].References["source"].Path, filepath.Join(childDirectory, "child.txt"); got != want { t.Errorf("child replacement path = %q, want %q", got, want) } if got, want := overlaid.Config.Pipelines["child"].Steps[0].References["step-source"].Path, filepath.Join(parentDirectory, "step.txt"); got != want { t.Errorf("overlaid inherited step reference path = %q, want %q", got, want) } } func TestInheritedInspectionRetainsOriginsRedactionAndOwnership(t *testing.T) { t.Parallel() root := t.TempDir() parentDirectory := filepath.Join(root, "parents") childDirectory := filepath.Join(root, "children") overlayDirectory := filepath.Join(root, "overlays") for _, directory := range []string{parentDirectory, childDirectory, overlayDirectory} { if err := os.MkdirAll(directory, 0o700); err != nil { t.Fatal(err) } } parentFile := filepath.Join(parentDirectory, "pipelines.yml") if err := os.WriteFile(parentFile, []byte(`pipelines: parent: input: {module: input} structured_output_repair_attempts: 2 references: source: {path: ./shared.txt} artifacts: summary: extract: module: extract options: nested: {value: original} credentials: {api_key: fake-parent-secret} `), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(parentDirectory, "shared.txt"), []byte("source\n"), 0o600); err != nil { t.Fatal(err) } childFile := filepath.Join(childDirectory, "pipelines.yml") if err := os.WriteFile(childFile, []byte(`pipelines: child: extends: parent structured_output_repair_attempts: 0 `), 0o600); err != nil { t.Fatal(err) } main := filepath.Join(root, "notarius.yml") if err := os.WriteFile(main, []byte(`version: 5 configkit: imports: - path: parents/pipelines.yml - path: children/pipelines.yml `), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(overlayDirectory, "development.yml"), []byte(`pipelines: parent: artifacts: summary: extract: options: credentials: {api_key: fake-overlay-secret} `), 0o600); err != nil { t.Fatal(err) } loaded, err := Load(main, LoadOptions{Overlay: "development"}) if err != nil { t.Fatalf("Load() error = %v", err) } child := loaded.Config.Pipelines["child"] wantPath := filepath.Join(parentDirectory, "shared.txt") if got := child.References["source"].Path; got != wantPath { t.Fatalf("runtime inherited path = %q, want %q", got, wantPath) } if child.StructuredOutputRepairAttempts == nil || *child.StructuredOutputRepairAttempts != 0 { t.Fatalf("runtime inherited repair setting = %#v", child.StructuredOutputRepairAttempts) } var effective bytes.Buffer if err := loaded.WriteEffective(&effective); err != nil { t.Fatalf("WriteEffective() error = %v", err) } if !strings.Contains(effective.String(), wantPath) || strings.Contains(effective.String(), "extends:") || !strings.Contains(effective.String(), "structured_output_repair_attempts: 0") { t.Fatalf("effective configuration is inconsistent with runtime conversion: %s", effective.String()) } for _, secret := range []string{"fake-parent-secret", "fake-overlay-secret"} { if strings.Contains(effective.String(), secret) { t.Fatalf("effective configuration exposed %q", secret) } } for _, test := range []struct { name string path configkit.Path }{ {name: "repair setting", path: configkit.Keys("pipelines", "child", "structured_output_repair_attempts")}, {name: "normalized reference", path: configkit.Keys("pipelines", "child", "references", "source", "path")}, } { t.Run(test.name, func(t *testing.T) { explanation, ok := loaded.result.Explain(test.path) if !ok || len(explanation.History) == 0 { t.Fatalf("inherited explanation = %#v, found=%v", explanation, ok) } inheritanceFromParent := false for _, change := range explanation.History { if len(change.Origin.Inheritance) == 0 { continue } for _, location := range change.Origin.Locations { for _, source := range loaded.result.Sources() { if source.ID == location.SourceID && source.Path == parentFile { inheritanceFromParent = true } } } } if !inheritanceFromParent { t.Fatalf("inheritance history lacks parent source evidence: %#v", explanation.History) } var rendered bytes.Buffer if err := loaded.WriteExplanation(&rendered, test.path.String()); err != nil { t.Fatalf("WriteExplanation() error = %v", err) } if !strings.Contains(rendered.String(), filepath.ToSlash(filepath.Join("parents", "pipelines.yml"))) { t.Fatalf("explanation lacks parent source evidence: %s", rendered.String()) } if test.name == "normalized reference" { foundNormalization := false for _, change := range explanation.History { if change.Origin.Operation == configkit.ChangeNormalization { foundNormalization = true } } if !foundNormalization || !strings.Contains(rendered.String(), wantPath) { t.Fatalf("normalized path evidence = %#v; rendered %s", explanation.History, rendered.String()) } } }) } secretPath := configkit.Keys("pipelines", "child", "artifacts", "summary", "extract", "options", "credentials", "api_key") secretExplanation, ok := loaded.result.Explain(secretPath) if !ok || !secretExplanation.Effective.Redacted { t.Fatalf("inherited secret explanation = %#v, found=%v", secretExplanation, ok) } redactedHistory := false for _, change := range secretExplanation.History { redactedHistory = redactedHistory || change.Before.Redacted || change.After.Redacted } if !redactedHistory { t.Fatalf("secret history was not redacted: %#v", secretExplanation.History) } var secretOutput bytes.Buffer if err := loaded.WriteExplanation(&secretOutput, secretPath.String()); err != nil { t.Fatalf("WriteExplanation(secret) error = %v", err) } if strings.Contains(secretOutput.String(), "fake-parent-secret") || strings.Contains(secretOutput.String(), "fake-overlay-secret") || !strings.Contains(secretOutput.String(), "[REDACTED]") { t.Fatalf("secret explanation did not redact values and history: %s", secretOutput.String()) } childOptions := loaded.Config.Pipelines["child"].Artifacts["summary"].Extract.Options childOptions["nested"].(map[string]any)["value"] = "changed" parentValue := loaded.Config.Pipelines["parent"].Artifacts["summary"].Extract.Options["nested"].(map[string]any)["value"] if parentValue != "original" { t.Fatalf("child runtime mutation changed parent value: %#v", parentValue) } var frozen bytes.Buffer if err := loaded.WriteEffective(&frozen); err != nil { t.Fatalf("WriteEffective() after runtime mutation error = %v", err) } if !strings.Contains(frozen.String(), "original") || strings.Contains(frozen.String(), "changed") { t.Fatalf("runtime mutation changed frozen inspection: %s", frozen.String()) } }