Clarify pipeline composition envelope handling

This commit is contained in:
2026-09-05 00:31:32 +00:00
parent abd405015b
commit 979d8049b1
5 changed files with 118 additions and 40 deletions

View File

@@ -9,6 +9,82 @@ import (
"testing"
)
func TestSplitPipelineCompositionEnvelopePreservesInputAndDeclarationOrder(t *testing.T) {
document, err := parseCompositionBytes("pipeline.yml", []byte(`composition:
imports: [first.yml, second.yml]
default_profile: production
profiles:
production:
overlay: production.yml
testing:
overlay: testing.yml
workspace:
root: /srv/narratio
`))
if err != nil {
t.Fatal(err)
}
originalRoot := cloneCompositionNode(document.root)
originalSources := append([]string(nil), document.sources...)
body, envelope, err := splitPipelineCompositionEnvelope(document)
if err != nil {
t.Fatalf("splitPipelineCompositionEnvelope() error = %v", err)
}
if !reflect.DeepEqual(document.root, originalRoot) || !reflect.DeepEqual(document.sources, originalSources) {
t.Fatalf("splitPipelineCompositionEnvelope() mutated its input")
}
if compositionFieldIndex(body.root.fields, "composition") >= 0 {
t.Fatalf("pipeline body still contains composition envelope")
}
if compositionFieldIndex(body.root.fields, "workspace") < 0 {
t.Fatalf("pipeline body lost ordinary root fields")
}
if want := []string{"first.yml", "second.yml"}; !reflect.DeepEqual(envelope.imports, want) {
t.Fatalf("imports = %#v, want %#v", envelope.imports, want)
}
if len(envelope.profiles) != 2 || envelope.profiles[0].name != "production" || envelope.profiles[1].name != "testing" {
t.Fatalf("profiles = %#v, want declaration order", envelope.profiles)
}
}
func TestSplitPipelineCompositionEnvelopeRejectsMalformedShapesWithoutMutation(t *testing.T) {
tests := []struct {
name string
yaml string
want string
}{
{name: "envelope", yaml: "composition: []\n", want: "expected a mapping"},
{name: "imports mapping", yaml: "composition:\n imports: {}\n", want: "expected a list"},
{name: "import value", yaml: "composition:\n imports: [1]\n", want: "expected a string path"},
{name: "default profile", yaml: "composition:\n default_profile: {}\n", want: "expected a string profile name"},
{name: "profiles sequence", yaml: "composition:\n profiles: []\n", want: "expected a mapping"},
{name: "empty profiles", yaml: "composition:\n profiles: {}\n", want: "at least one named profile"},
{name: "profile declaration", yaml: "composition:\n profiles:\n production: production.yml\n", want: "expected a mapping"},
{name: "invalid profile name", yaml: "composition:\n profiles:\n ' production ':\n overlay: production.yml\n", want: "surrounding whitespace"},
{name: "unknown field", yaml: "composition:\n unexpected: true\n", want: "unknown composition field"},
{name: "unknown default", yaml: "composition:\n default_profile: testing\n profiles:\n production:\n overlay: production.yml\n", want: "does not name a declared profile"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
document, err := parseCompositionBytes("pipeline.yml", []byte(test.yaml))
if err != nil {
t.Fatal(err)
}
originalRoot := cloneCompositionNode(document.root)
_, _, err = splitPipelineCompositionEnvelope(document)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("splitPipelineCompositionEnvelope() error = %v, want containing %q", err, test.want)
}
if !reflect.DeepEqual(document.root, originalRoot) {
t.Fatalf("splitPipelineCompositionEnvelope() mutated invalid input")
}
})
}
}
func TestLoadPipelineCompositionImportsDisjointFieldsAndTracksOwnership(t *testing.T) {
dir := t.TempDir()
rootPath := writePipelineSource(t, dir, "pipeline.yml", `composition: