package engine import ( "reflect" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestRecursiveMappingMergePreservesValuesPathsAndInputs(t *testing.T) { type expectedNode struct { path Path kind document.Kind text string entries int } tests := []struct { name, base, incoming string want []expectedNode events []string }{ { name: "nested mappings", base: "shared: {kept: base, changed: old}\n", incoming: "shared: {changed: new, added: child}\n", want: []expectedNode{ {path: Keys("shared"), kind: document.Mapping, entries: 3}, {path: Keys("shared", "kept"), kind: document.String, text: "base"}, {path: Keys("shared", "changed"), kind: document.String, text: "new"}, {path: Keys("shared", "added"), kind: document.String, text: "child"}, }, events: []string{"combine:/shared", "replace:/shared/changed", "add:/shared/added"}, }, { name: "sequence replacement", base: "value: [old]\n", incoming: "value: []\n", want: []expectedNode{{path: Keys("value"), kind: document.Sequence}}, events: []string{"replace:/value"}, }, { name: "scalar replacement", base: "value: old\n", incoming: "value: new\n", want: []expectedNode{{path: Keys("value"), kind: document.String, text: "new"}}, events: []string{"replace:/value"}, }, { name: "null replacement", base: "value: old\n", incoming: "value: null\n", want: []expectedNode{{path: Keys("value"), kind: document.Null}}, events: []string{"replace:/value"}, }, { name: "empty mapping retains children", base: "shared: {kept: base}\n", incoming: "shared: {}\n", want: []expectedNode{ {path: Keys("shared"), kind: document.Mapping, entries: 1}, {path: Keys("shared", "kept"), kind: document.String, text: "base"}, }, events: []string{"combine:/shared"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { base := parseCompositionDocument(t, "source-1", test.base) incoming := parseCompositionDocument(t, "source-2", test.incoming) baseBefore, incomingBefore := document.Clone(base), document.Clone(incoming) owned := document.Clone(base) var events []string mergeRecursiveMappings(owned, incoming, Path{}, new(mappingIndex), recursiveMergeHooks{ added: func(path Path, _ *document.Node) { events = append(events, "add:"+path.String()) }, combined: func(path Path, _ *document.Node) { events = append(events, "combine:"+path.String()) }, replaced: func(path Path, _, _ *document.Node) { events = append(events, "replace:"+path.String()) }, }) if !reflect.DeepEqual(events, test.events) { t.Fatalf("merge events = %#v, want %#v", events, test.events) } for _, expected := range test.want { node, found := documentAtPath(owned, expected.path) if !found || node.Kind != expected.kind || node.Text != expected.text || len(node.Mapping) != expected.entries { t.Errorf("merged %s = %#v, found %v", expected.path.String(), node, found) } } if !reflect.DeepEqual(base, baseBefore) || !reflect.DeepEqual(incoming, incomingBefore) { t.Fatal("merge changed an input") } owned.Mapping[0].Value.Location.SourceID = "changed" if !reflect.DeepEqual(base, baseBefore) || !reflect.DeepEqual(incoming, incomingBefore) { t.Fatal("merged result aliases an input") } }) } }