package engine import ( "reflect" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestInheritanceReplacementResetsOnlyReplacedYamlIdentity(t *testing.T) { t.Parallel() for _, test := range []struct { name, key, value, applicationKey string mode sequenceMode wantFields []string conflict bool }{ {name: "changed key", key: "name", value: "[{name: child}]", mode: sequenceModeReplace, wantFields: []string{"name"}}, {name: "unkeyed replacement", value: "[{name: child}]", mode: sequenceModeReplace}, {name: "non-sequence replacement", value: "42", mode: sequenceModeReplace}, {name: "agreeing application", key: "name", value: "[{name: child}]", applicationKey: "name", mode: sequenceModeReplace, wantFields: []string{"name"}}, {name: "conflicting application", key: "name", value: "[{name: child}]", applicationKey: "id", mode: sequenceModeReplace, conflict: true}, {name: "application still validates replacement", value: "[{name: child}]", applicationKey: "id", mode: sequenceModeReplace, conflict: true}, {name: "mode-only key change", key: "name", mode: sequenceModeReplace, conflict: true}, {name: "mode-only keeps identity", mode: sequenceModeReplace, wantFields: []string{"id"}}, {name: "append key change", key: "name", value: "[{name: child}]", mode: sequenceModeAppend, conflict: true}, {name: "upsert key change", key: "name", value: "[{name: child}]", mode: sequenceModeUpsert, conflict: true}, } { t.Run(test.name, func(t *testing.T) { input := "base: {items: [{id: parent}]}\nchild:\n extends: base\n" if test.value != "" { input += " items: " + test.value + "\n" } root := parseItemPolicyDocument(t, "replacement", input) policies := collectionPolicyScope{} policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) higher := collectionPolicy{explicitMode: test.mode, explicitModePresent: true} if test.key != "" { higher.identityFields, higher.identityPresent = []string{test.key}, true } policies.set(Keys("child", "items"), higher) application := sequencePolicy{} if test.applicationKey != "" { application = mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{ selector: sequenceIdentitySelector{literalSequenceSelector("child"), literalSequenceSelector("items")}, identityFields: []string{test.applicationKey}, }}) } expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, application, defaultProjectionLimits()) if test.conflict { wantCode := CodeSequenceConflict if test.applicationKey != "" && test.key == "" { wantCode = CodeSequenceInvalid } if len(diagnostics) == 0 || diagnostics[0].Code != wantCode || diagnostics[0].Path.String() != "/child/items" && diagnostics[0].Path.String() != "/child/items/0/id" { t.Fatalf("missing identity/constraint failure: %#v", diagnostics) } return } if len(diagnostics) != 0 { t.Fatal(diagnostics) } policy, found := expanded.collectionPolicies.at(Keys("child", "items")) if !found || !reflect.DeepEqual(policy.identityFields, test.wantFields) || policy.identityPresent != (test.wantFields != nil) { t.Fatalf("effective YAML identity = %#v, want %v", policy, test.wantFields) } if test.value != "" { actual, _ := documentAtPath(expanded.root, Keys("child", "items")) want, _ := documentAtPath(root, Keys("child", "items")) if !configurationValuesEqual(actual, want) { t.Fatal("explicit replacement retained parent payload") } } }) } } func TestLoadInheritanceReplacementUsesNewYamlKey(t *testing.T) { t.Parallel() loaded, err := testLoad[map[string]any](Bytes("replacement", []byte(`configkit: sequences: - {destination: [base, items], key: id} - {destination: [child, items], key: name, mode: replace} base: {items: [{id: parent}]} child: extends: base items: [{name: child}] `))) if err != nil { t.Fatal(err) } value, found := loaded.Explain(Keys("child", "items")) if !found || value.Effective.YAML != "- name: child\n" { t.Fatalf("replacement payload = %#v", value) } } func TestInheritanceCombinesIdentifiedSequencesAtDestination(t *testing.T) { root := parseItemPolicyDocument(t, "inheritance-values", `base: items: - id: shared value: parent child: extends: base items: - id: shared value: child `) modeLocation := Location{SourceID: "base-policy", Line: 4, Column: 5} policy := collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, modeLocation: &modeLocation, } policies := collectionPolicyScope{} policies.set(Keys("base", "items"), policy) expanded, diagnostics := expandInheritanceWithCollectionRules( root, nil, policies, sequencePolicy{}, defaultProjectionLimits(), ) if len(diagnostics) != 0 { t.Fatalf("expand identified inheritance: %#v", diagnostics) } items, found := documentAtPath(expanded.root, Keys("child", "items")) if !found || items.Kind != document.Sequence || len(items.Sequence) != 1 { t.Fatalf("child items = %#v, found %t; parent mode should not force an append", items, found) } if value, exists := documentAtPath(items.Sequence[0], Keys("value")); !exists || value.Text != "child" { t.Fatalf("matched record = %#v, want complete child replacement", items.Sequence[0]) } childPolicy, found := expanded.collectionPolicies.at(Keys("child", "items")) if !found || !childPolicy.identityPresent || childPolicy.identityFields[0] != "id" { t.Fatalf("inherited destination identity = %#v, found %t; scope=%#v", childPolicy, found, expanded.collectionPolicies.collections) } if childPolicy.explicitModePresent { t.Fatalf("parent explicit mode leaked to a different destination: %#v", childPolicy) } var transfer *inheritanceOperation for index := range expanded.operations { if expanded.operations[index].transfer != nil && expanded.operations[index].transfer.to.String() == "/child" { transfer = &expanded.operations[index] break } } if transfer == nil || len(transfer.sequences) != 1 || transfer.sequences[0].path.String() != "/child/items" || len(transfer.sequences[0].replacements) != 1 { t.Fatalf("inheritance sequence projection = %#v, want one mapped replacement", transfer) } if origin, exists := expanded.origins["/child/items/0/value"]; !exists || len(origin.Locations) == 0 || origin.Locations[0].SourceID != "inheritance-values" { t.Fatalf("replacement origin lost the child's source context: %#v", origin) } } func TestLoadAppliesYamlCollectionIdentityDuringSiblingInheritance(t *testing.T) { input := `configkit: sequences: - destination: [base, items] key: id mode: append base: items: - id: shared value: parent child: extends: base items: - id: shared value: child ` loaded, err := Load[map[string]any](Bytes("inheritance-policy", []byte(input)), testSecretBinding()) if err != nil { if loadError, ok := err.(*LoadError); ok { t.Fatalf("load inherited collection policy: %v; diagnostics=%#v", err, loadError.Diagnostics()) } t.Fatal(err) } value, found := loaded.Inspection.Explain(Keys("child", "items").Index(0).Key("value")) if !found || value.Effective.YAML != "child\n" { t.Fatalf("inherited sequence value = %#v, found %t", value, found) } if len(value.History) == 0 { t.Fatal("inherited sequence replacement has no inspection history") } parentValueVisible, childValueVisible := false, false for _, change := range value.History { parentValueVisible = parentValueVisible || change.Before.YAML == "parent\n" || change.After.YAML == "parent\n" childValueVisible = childValueVisible || change.After.YAML == "child\n" } if !parentValueVisible || !childValueVisible { t.Fatalf("sequence replacement history omitted its parent or child value: %#v", value.History) } } func TestInheritanceUsesDestinationExplicitModeAndApplicationConstraint(t *testing.T) { t.Run("destination append", func(t *testing.T) { root := parseItemPolicyDocument(t, "append", `base: items: - id: base child: extends: base items: - id: child `) policies := collectionPolicyScope{} policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) childLocation := Location{SourceID: "child-policy", Line: 4, Column: 5} policies.set(Keys("child", "items"), collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, modeLocation: &childLocation, }) expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatalf("expand with destination append: %#v", diagnostics) } items, _ := documentAtPath(expanded.root, Keys("child", "items")) if len(items.Sequence) != 2 || items.Sequence[0].Mapping[0].Value.Text != "base" || items.Sequence[1].Mapping[0].Value.Text != "child" { t.Fatalf("appended items = %#v", items) } }) t.Run("new matching namespace", func(t *testing.T) { root := parseItemPolicyDocument(t, "dynamic", `base: enabled: true child: extends: base extensions: - id: duplicate - id: duplicate `) applicationPolicy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{ selector: sequenceIdentitySelector{ {kind: sequenceSelectorLiteral, key: "child"}, {kind: sequenceSelectorLiteral, key: "extensions"}, }, identityFields: []string{"id"}, }}) if len(diagnostics) != 0 { t.Fatalf("compile identity constraint: %#v", diagnostics) } _, diagnostics = expandInheritanceWithCollectionRules(root, nil, collectionPolicyScope{}, applicationPolicy, defaultProjectionLimits()) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict || diagnostics[0].Path.String() != "/child/extensions" { t.Fatalf("dynamic application constraint diagnostics = %#v", diagnostics) } }) } func TestInheritanceReplacementModesAndCancellation(t *testing.T) { for _, test := range []struct { name string value string kind document.Kind }{ {name: "empty sequence", value: "[]", kind: document.Sequence}, {name: "null", value: "null", kind: document.Null}, } { t.Run(test.name, func(t *testing.T) { root := parseItemPolicyDocument(t, "replacement", "base:\n items:\n - id: prior\nchild:\n extends: base\n items: "+test.value+"\n") policies := collectionPolicyScope{} policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) policies.set(Keys("child", "items"), collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeReplace, explicitModePresent: true, }) expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatalf("expand replacement: %#v", diagnostics) } items, found := documentAtPath(expanded.root, Keys("child", "items")) if !found || items.Kind != test.kind || (items.Kind == document.Sequence && len(items.Sequence) != 0) { t.Fatalf("replacement value = %#v, found %t", items, found) } }) } for _, value := range []string{"null", "{}"} { t.Run("append rejects "+value, func(t *testing.T) { root := parseItemPolicyDocument(t, "append-kind", "base:\n items: [{id: prior}]\nchild:\n extends: base\n items: "+value+"\n") policies := collectionPolicyScope{} policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) policies.set(Keys("child", "items"), collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, }) _, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits()) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/child/items" { t.Fatalf("append kind diagnostics = %#v", diagnostics) } }) } t.Run("incompatible inherited identity", func(t *testing.T) { root := parseItemPolicyDocument(t, "identity", `base: items: [{id: one}] child: extends: base items: [{id: two}] `) policies := collectionPolicyScope{} policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) policies.set(Keys("child", "items"), collectionPolicy{identityFields: []string{"slug"}, identityPresent: true}) _, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits()) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict || diagnostics[0].Path.String() != "/child/items" { t.Fatalf("identity conflict diagnostics = %#v", diagnostics) } }) t.Run("cancelled relationship", func(t *testing.T) { root := parseItemPolicyDocument(t, "cancel", `base: items: [{id: parent}] child: extends: null items: [{id: local}] `) policies := collectionPolicyScope{} policies.set(Keys("base", "items"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatalf("expand cancelled relationship: %#v", diagnostics) } if _, found := expanded.collectionPolicies.at(Keys("child", "items")); found { t.Fatal("cancelled relationship transferred the parent's collection policy") } }) } func TestInheritanceReplacementRetiresRecordLocalCollectionPolicy(t *testing.T) { root := parseItemPolicyDocument(t, "record-scope", `base: items: - id: shared children: - key: old child: extends: base items: - id: shared children: - key: new `) nested := collectionPolicy{identityFields: []string{"key"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true} recordScope := collectionPolicyScope{} recordScope.set(Keys("children"), nested) outer := collectionPolicy{identityFields: []string{"id"}, identityPresent: true} outer.setRecordPolicyAt(0, newCollectionRecordPolicy()) owner := outer.recordOrder[0] outer.recordPolicies[owner] = recordScope policies := collectionPolicyScope{} policies.set(Keys("base", "items"), outer) expanded, diagnostics := expandInheritanceWithCollectionRules(root, nil, policies, sequencePolicy{}, defaultProjectionLimits()) if len(diagnostics) != 0 { t.Fatalf("expand replacement record: %#v", diagnostics) } children, found := documentAtPath(expanded.root, Keys("child", "items").Index(0).Key("children")) if !found || children.Kind != document.Sequence || len(children.Sequence) != 1 { t.Fatalf("replacement children = %#v, found %t", children, found) } if key, exists := documentAtPath(children.Sequence[0], Keys("key")); !exists || key.Text != "new" { t.Fatalf("replacement child = %#v, want only new record", children.Sequence[0]) } childPolicy, exists := expanded.collectionPolicies.at(Keys("child", "items")) if !exists { t.Fatal("inherited outer collection policy is missing") } record, found := childPolicy.recordPolicyAt(0) if !found { t.Fatal("replacement item has no independent record owner") } if nestedPolicy, exists := record.scope.at(Keys("children")); exists && nestedPolicy.explicitModePresent { t.Fatalf("replaced record retained its former nested operation: %#v", nestedPolicy) } }