package engine import ( "errors" "testing" ) func TestSourceReplacementRetiresIdentityForEveryValueKind(t *testing.T) { t.Parallel() for _, value := range []string{"null", "42", "{}", "[{name: new}]"} { t.Run(value, func(t *testing.T) { lower := "configkit:\n sequences: [{destination: [items], key: id}]\nitems: [{id: old}]\n" main := "configkit:\n sequences: [{destination: [items], key: name, mode: replace}]\nitems: " + value + "\n" loaded, err := testGroupedLoad(loadRequest[map[string]any]{ main: loadInput{source: Bytes("main", []byte(main))}, underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte(lower))}}}}, }) if err != nil || loaded == nil { t.Fatalf("replacement retained obsolete identity: %v", err) } }) } } func TestAncestorRemovalCanEstablishPendingReplacementIdentity(t *testing.T) { t.Parallel() for _, inheritance := range []bool{false, true} { name := "tiers" if inheritance { name = "inheritance" } t.Run(name, func(t *testing.T) { var loaded *testResult[map[string]any] var err error if inheritance { loaded, err = testLoad[map[string]any](Bytes("source", []byte(`configkit: sequences: - {destination: [base, group, items], key: id} - {destination: [child, group, items], key: name} base: {group: {items: [{id: old}]}} child: extends: base group: null grandchild: extends: child group: {items: [{name: new}]} `))) } else { loaded, err = testGroupedLoad(loadRequest[map[string]any]{ main: loadInput{source: Bytes("main", []byte("group: {items: [{name: new}]}\n"))}, underlays: []loadGroup{ {inputs: []loadInput{{source: Bytes("base", []byte("configkit:\n sequences: [{destination: [group, items], key: id}]\ngroup: {items: [{id: old}]}\n"))}}}, {inputs: []loadInput{{source: Bytes("remove", []byte("configkit:\n sequences: [{destination: [group, items], key: name}]\ngroup: null\n"))}}}, }, }) } if err != nil { t.Fatal(err) } value := loaded.Config if inheritance { value = value["grandchild"].(map[string]any) } items := value["group"].(map[string]any)["items"].([]any) if len(items) != 1 || items[0].(map[string]any)["name"] != "new" { t.Fatalf("reintroduced collection = %#v", items) } }) } } func TestInheritanceAncestorRetirementDoesNotResurrectOldIdentity(t *testing.T) { t.Parallel() _, err := testLoad[map[string]any](Bytes("source", []byte(`configkit: sequences: [{destination: [base, group, items], key: id}] base: {group: {items: [{id: old}]}} child: extends: base group: null grandchild: extends: child group: {items: [{name: new}]} `))) if err != nil { t.Fatal(err) } } func TestReplacementRetirementPreservesApplicationConstraintsAndRetainedKeys(t *testing.T) { t.Parallel() for _, withValue := range []bool{false, true} { main := "configkit:\n sequences: [{destination: [items], key: name, mode: replace}]\n" if withValue { main += "items: null\n" } definition := Definition[map[string]any]{} if withValue { definition.sequenceIdentityRegistrations = []sequenceIdentityRegistration{{ selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}, }} } _, err := testGroupedLoad(loadRequest[map[string]any]{ main: loadInput{source: Bytes("main", []byte(main))}, definition: definition, underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("configkit:\n sequences: [{destination: [items], key: id}]\nitems: [{id: old}]\n"))}}}}, }) var loadErr *LoadError if !errors.As(err, &loadErr) || len(loadErr.Diagnostics()) == 0 || loadErr.Diagnostics()[0].Code != CodeSequenceConflict { t.Fatalf("retirement bypassed identity agreement: %v", err) } } }