Apply collection policies during sibling inheritance
This commit is contained in:
281
internal/engine/inheritance_collection_test.go
Normal file
281
internal/engine/inheritance_collection_test.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user