package config import ( "os" "path/filepath" "reflect" "strings" "testing" "gopkg.in/yaml.v3" ) func TestParseCompositionDocumentRetainsPresenceOwnershipAndDeclarationOrder(t *testing.T) { document := mustParseComposition(t, "root.yml", `zeta: false zero: 0 empty_map: {} empty_list: [] nested: value: "" `) wantOrder := []string{"zeta", "zero", "empty_map", "empty_list", "nested"} gotOrder := make([]string, 0, len(document.root.fields)) for _, field := range document.root.fields { gotOrder = append(gotOrder, field.key) } if !reflect.DeepEqual(gotOrder, wantOrder) { t.Fatalf("declaration order = %#v, want %#v", gotOrder, wantOrder) } tests := []struct { path string kind yaml.Kind tag string value string }{ {path: "zeta", kind: yaml.ScalarNode, tag: "!!bool", value: "false"}, {path: "zero", kind: yaml.ScalarNode, tag: "!!int", value: "0"}, {path: "empty_map", kind: yaml.MappingNode, tag: "!!map"}, {path: "empty_list", kind: yaml.SequenceNode, tag: "!!seq"}, {path: "nested.value", kind: yaml.ScalarNode, tag: "!!str", value: ""}, } for _, tt := range tests { node := compositionNodeAtPath(t, document.root, tt.path) if node.kind != tt.kind || node.tag != tt.tag || node.value != tt.value || !reflect.DeepEqual(node.sources, []string{"root.yml"}) { t.Fatalf("node %s = kind=%v tag=%q value=%q sources=%#v", tt.path, node.kind, node.tag, node.value, node.sources) } } } func TestParseCompositionDocumentRejectsAmbiguousOrMalformedYAML(t *testing.T) { tests := []struct { name string yaml string want string }{ {name: "duplicate top-level key", yaml: "value: 1\nvalue: 2\n", want: "duplicate YAML key"}, {name: "duplicate nested key", yaml: "outer:\n value: 1\n value: 2\n", want: "outer.value"}, {name: "alias", yaml: "base: &base\n value: 1\ncopy: *base\n", want: "aliases are not supported"}, {name: "trailing document", yaml: "value: 1\n---\nvalue: 2\n", want: "exactly one YAML document"}, {name: "top-level sequence", yaml: "- value\n", want: "top-level document must be a mapping"}, {name: "non-string key", yaml: "1: value\n", want: "mapping keys must be strings"}, {name: "malformed", yaml: "outer: [\n", want: "decode YAML"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := parseCompositionDocument("broken.yml", strings.NewReader(tt.yaml)) if err == nil { t.Fatal("parseCompositionDocument() error = nil") } if !strings.Contains(err.Error(), "broken.yml") || !strings.Contains(err.Error(), tt.want) { t.Fatalf("error = %q, want source and %q", err, tt.want) } }) } } func TestMergeAdditiveCompositionJoinsOnlyDisjointMappings(t *testing.T) { root := mustParseComposition(t, "pipeline.yml", `scriptorium: artifacts: recap: enabled: true zero: 0 `) imports := mustParseComposition(t, "conf.d/artifacts.yml", `scriptorium: artifacts: handout: enabled: false empty_list: [] `) merged, err := mergeAdditiveComposition(root, imports) if err != nil { t.Fatalf("mergeAdditiveComposition() error = %v", err) } for _, path := range []string{ "empty_list", "scriptorium.artifacts.handout.enabled", "scriptorium.artifacts.recap.enabled", "zero", } { _ = compositionNodeAtPath(t, merged.root, path) } if got := compositionNodeAtPath(t, merged.root, "scriptorium.artifacts.handout.enabled").sources; !reflect.DeepEqual(got, []string{"conf.d/artifacts.yml"}) { t.Fatalf("handout sources = %#v", got) } if got := compositionNodeAtPath(t, merged.root, "scriptorium.artifacts.recap.enabled").sources; !reflect.DeepEqual(got, []string{"pipeline.yml"}) { t.Fatalf("recap sources = %#v", got) } // Merge operations return a new document and retain the declared order in // each input for source-aware diagnostics. if len(root.root.fields) != 2 || len(imports.root.fields) != 2 { t.Fatalf("merge mutated inputs: root=%d import=%d", len(root.root.fields), len(imports.root.fields)) } } func TestMergeAdditiveCompositionRejectsEveryDuplicateClass(t *testing.T) { tests := []struct { name string baseYAML string nextYAML string path string }{ {name: "equal scalar", baseYAML: "value: true\n", nextYAML: "value: true\n", path: "value"}, {name: "different scalar", baseYAML: "value: true\n", nextYAML: "value: false\n", path: "value"}, {name: "atomic list", baseYAML: "values: [one]\n", nextYAML: "values: [two]\n", path: "values"}, {name: "keyed entry", baseYAML: "items:\n shared:\n left: 1\n", nextYAML: "items:\n shared:\n left: 2\n", path: "items.shared.left"}, {name: "kind conflict", baseYAML: "value:\n nested: true\n", nextYAML: "value: scalar\n", path: "value"}, {name: "duplicate empty map", baseYAML: "value: {}\n", nextYAML: "value: {}\n", path: "value"}, {name: "empty map then populated map", baseYAML: "value: {}\n", nextYAML: "value: {nested: true}\n", path: "value"}, {name: "populated map then empty map", baseYAML: "value: {nested: true}\n", nextYAML: "value: {}\n", path: "value"}, {name: "duplicate empty list", baseYAML: "value: []\n", nextYAML: "value: []\n", path: "value"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { base := mustParseComposition(t, "base.yml", tt.baseYAML) next := mustParseComposition(t, "next.yml", tt.nextYAML) _, err := mergeAdditiveComposition(base, next) if err == nil { t.Fatal("mergeAdditiveComposition() error = nil") } for _, want := range []string{tt.path, "base.yml", "next.yml"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error = %q, want %q", err, want) } } }) } } func TestMergeAdditiveCompositionReportsAllClaimingSources(t *testing.T) { left := mustParseComposition(t, "left.yml", "group:\n left: 1\n") right := mustParseComposition(t, "right.yml", "group:\n right: 2\n") base, err := mergeAdditiveComposition(left, right) if err != nil { t.Fatal(err) } overlap := mustParseComposition(t, "overlap.yml", "group: scalar\n") _, err = mergeAdditiveComposition(base, overlap) if err == nil { t.Fatal("mergeAdditiveComposition() error = nil") } for _, want := range []string{"group", "left.yml", "right.yml", "overlap.yml"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error = %q, want %q", err, want) } } } func TestMergeOverlayCompositionRecursesMapsAndReplacesAtomicValues(t *testing.T) { base := mustParseComposition(t, "base.yml", `feature: enabled: true retries: 3 values: [one, two] inherited: kept artifacts: recap: enabled: true `) overlay := mustParseComposition(t, "testing.yml", `feature: enabled: false retries: 0 values: [] added: present artifacts: handout: enabled: false `) merged, err := mergeOverlayComposition(base, overlay) if err != nil { t.Fatalf("mergeOverlayComposition() error = %v", err) } assertCompositionScalar(t, merged.root, "feature.enabled", "!!bool", "false", "testing.yml") assertCompositionScalar(t, merged.root, "feature.retries", "!!int", "0", "testing.yml") assertCompositionScalar(t, merged.root, "feature.inherited", "!!str", "kept", "base.yml") assertCompositionScalar(t, merged.root, "feature.added", "!!str", "present", "testing.yml") if values := compositionNodeAtPath(t, merged.root, "feature.values"); values.kind != yaml.SequenceNode || len(values.items) != 0 || !reflect.DeepEqual(values.sources, []string{"testing.yml"}) { t.Fatalf("replaced list = %#v", values) } _ = compositionNodeAtPath(t, merged.root, "artifacts.recap.enabled") _ = compositionNodeAtPath(t, merged.root, "artifacts.handout.enabled") } func TestMergeOverlayCompositionRejectsKindChangesAndNullDeletion(t *testing.T) { kindTests := []struct { name string baseYAML string overlay string }{ {name: "map to scalar", baseYAML: "value: {nested: true}\n", overlay: "value: replacement\n"}, {name: "scalar to map", baseYAML: "value: original\n", overlay: "value: {nested: true}\n"}, {name: "list to scalar", baseYAML: "value: [one]\n", overlay: "value: replacement\n"}, {name: "scalar to list", baseYAML: "value: original\n", overlay: "value: [one]\n"}, } for _, tt := range kindTests { t.Run(tt.name, func(t *testing.T) { _, err := mergeOverlayComposition( mustParseComposition(t, "base.yml", tt.baseYAML), mustParseComposition(t, "overlay.yml", tt.overlay), ) if err == nil || !strings.Contains(err.Error(), "value") || !strings.Contains(err.Error(), "kind change") { t.Fatalf("error = %v, want value kind change", err) } }) } for _, overlayYAML := range []string{"value: null\n", "value: ~\n", "nested:\n value:\n"} { _, err := mergeOverlayComposition( mustParseComposition(t, "base.yml", "value: original\nnested:\n value: original\n"), mustParseComposition(t, "overlay.yml", overlayYAML), ) if err == nil || !strings.Contains(err.Error(), "null cannot delete") || !strings.Contains(err.Error(), "overlay.yml") { t.Fatalf("error = %v, want source-qualified null rejection", err) } } } func TestCompositionCanonicalOutputsAreDeterministic(t *testing.T) { first := mustParseComposition(t, "first.yml", `zeta: 01 alpha: list: [true, false] empty: {} `) second := mustParseComposition(t, "second.yml", `alpha: empty: {} list: - true - false zeta: 1 `) firstYAML, err := first.canonicalYAML() if err != nil { t.Fatal(err) } secondYAML, err := second.canonicalYAML() if err != nil { t.Fatal(err) } if !reflect.DeepEqual(firstYAML, secondYAML) { t.Fatalf("canonical YAML differs:\n%s\n---\n%s", firstYAML, secondYAML) } firstDigest, err := first.canonicalDigestInput() if err != nil { t.Fatal(err) } secondDigest, err := second.canonicalDigestInput() if err != nil { t.Fatal(err) } if !reflect.DeepEqual(firstDigest, secondDigest) { t.Fatalf("digest input differs:\n%s\n---\n%s", firstDigest, secondDigest) } left := mustParseComposition(t, "left.yml", "zeta: 1\n") right := mustParseComposition(t, "right.yml", "alpha: 2\n") leftRight, err := mergeAdditiveComposition(left, right) if err != nil { t.Fatal(err) } rightLeft, err := mergeAdditiveComposition(right, left) if err != nil { t.Fatal(err) } want, _ := leftRight.canonicalDigestInput() got, _ := rightLeft.canonicalDigestInput() if !reflect.DeepEqual(got, want) { t.Fatalf("source traversal changed semantic digest input: got %s want %s", got, want) } wantYAML, _ := leftRight.canonicalYAML() gotYAML, _ := rightLeft.canonicalYAML() if !reflect.DeepEqual(gotYAML, wantYAML) { t.Fatalf("source traversal changed canonical YAML: got %s want %s", gotYAML, wantYAML) } records, err := first.semanticRecords() if err != nil { t.Fatal(err) } paths := make([]string, 0, len(records)) for _, record := range records { paths = append(paths, record.Path) } if wantPaths := []string{"alpha.empty", "alpha.list", "zeta"}; !reflect.DeepEqual(paths, wantPaths) { t.Fatalf("semantic record paths = %#v, want %#v", paths, wantPaths) } } func TestPipelineCompositionEnvelopeIsNotPublicYet(t *testing.T) { path := filepath.Join(t.TempDir(), "pipeline.yml") if err := os.WriteFile(path, []byte(`composition: imports: [] whisperx: transcribe_url: https://transcription.example.com/transcribe `), 0o644); err != nil { t.Fatal(err) } _, err := LoadPipeline(path) if err == nil || !strings.Contains(err.Error(), "field composition not found") { t.Fatalf("LoadPipeline() error = %v, want strict public-schema rejection", err) } } func mustParseComposition(t *testing.T, source, input string) *compositionDocument { t.Helper() document, err := parseCompositionBytes(source, []byte(input)) if err != nil { t.Fatalf("parseCompositionBytes(%q) error = %v", source, err) } return document } func compositionNodeAtPath(t *testing.T, root *compositionNode, path string) *compositionNode { t.Helper() node := root for _, segment := range strings.Split(path, ".") { if node == nil || node.kind != yaml.MappingNode { t.Fatalf("path %q reached non-mapping at %q", path, segment) } index := compositionFieldIndex(node.fields, segment) if index < 0 { t.Fatalf("path %q missing segment %q", path, segment) } node = node.fields[index].value } return node } func assertCompositionScalar(t *testing.T, root *compositionNode, path, tag, value, source string) { t.Helper() node := compositionNodeAtPath(t, root, path) if node.kind != yaml.ScalarNode || node.tag != tag || node.value != value || !reflect.DeepEqual(node.sources, []string{source}) { t.Fatalf("%s = kind=%s tag=%q value=%q sources=%#v", path, yamlKindName(node.kind), node.tag, node.value, node.sources) } }