351 lines
15 KiB
Go
351 lines
15 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssemblyPolicyRetirementUsesSemanticPathSegments(t *testing.T) {
|
|
t.Parallel()
|
|
var policies collectionPolicyScope
|
|
slashPath := Keys("branch/name", "items")
|
|
separateKeysPath := Keys("branch", "name", "items")
|
|
numericKeyPath := Keys("items", "0", "named")
|
|
sequenceIndexPath := Keys("items").Index(0).Key("indexed")
|
|
policies.set(slashPath, collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
|
policies.set(separateKeysPath, collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
|
policies.set(numericKeyPath, collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
|
policies.set(sequenceIndexPath, collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
|
|
|
assembly := newAssemblyAccumulator(&composedDocument{collectionPolicies: policies})
|
|
assembly.retireCollectionPoliciesAtOrBelow(Keys("branch/name"))
|
|
assertRetirement := func(path Path, identityPresent bool) {
|
|
t.Helper()
|
|
policy, found := assembly.current.collectionPolicies.at(path)
|
|
if !found || policy.identityPresent != identityPresent {
|
|
t.Fatalf("policy at %s = %#v (found %t), want identity present %t", path, policy, found, identityPresent)
|
|
}
|
|
}
|
|
assertRetirement(slashPath, false)
|
|
assertRetirement(separateKeysPath, true)
|
|
|
|
assembly.retireCollectionPoliciesAtOrBelow(Keys("items", "0"))
|
|
assertRetirement(numericKeyPath, false)
|
|
assertRetirement(sequenceIndexPath, true)
|
|
assembly.retireCollectionPoliciesAtOrBelow(Keys("items").Index(0))
|
|
assertRetirement(sequenceIndexPath, false)
|
|
}
|
|
|
|
func TestAssemblyPolicyRetirementPrunesAndReindexesAroundModeOnlyEntries(t *testing.T) {
|
|
t.Parallel()
|
|
identityPath := Keys("catalog", "items")
|
|
modePath := Keys("catalog", "settings")
|
|
var policies collectionPolicyScope
|
|
policies.set(identityPath, collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
|
policies.set(modePath, collectionPolicy{explicitMode: sequenceModeAppend, explicitModePresent: true})
|
|
assembly := newAssemblyAccumulator(&composedDocument{collectionPolicies: policies})
|
|
|
|
assembly.retireCollectionPoliciesAtOrBelow(Keys("catalog"))
|
|
retired, found := assembly.current.collectionPolicies.at(identityPath)
|
|
if !found || retired.identityPresent || !assembly.collectionPolicyRetirement.empty() {
|
|
t.Fatalf("first retirement left identity or stale index: %#v, index %#v", retired, assembly.collectionPolicyRetirement)
|
|
}
|
|
mode, found := assembly.current.collectionPolicies.at(modePath)
|
|
if !found || mode.explicitMode != sequenceModeAppend || !mode.explicitModePresent {
|
|
t.Fatalf("mode-only policy changed during retirement: %#v, found %t", mode, found)
|
|
}
|
|
|
|
assembly.retireCollectionPoliciesAtOrBelow(Keys("catalog"))
|
|
newPath := Keys("catalog", "items", "nested")
|
|
assembly.setCollectionPolicy(newPath, collectionPolicy{identityFields: []string{"name"}, identityPresent: true})
|
|
if assembly.collectionPolicyRetirement.empty() {
|
|
t.Fatal("new identity-bearing policy was not reindexed")
|
|
}
|
|
assembly.retireCollectionPoliciesAtOrBelow(Keys("catalog"))
|
|
newPolicy, found := assembly.current.collectionPolicies.at(newPath)
|
|
if !found || newPolicy.identityPresent || !assembly.collectionPolicyRetirement.empty() {
|
|
t.Fatalf("repeated ancestor retirement left new identity or stale index: %#v, index %#v", newPolicy, assembly.collectionPolicyRetirement)
|
|
}
|
|
mode, found = assembly.current.collectionPolicies.at(modePath)
|
|
if !found || mode.explicitMode != sequenceModeAppend || !mode.explicitModePresent {
|
|
t.Fatalf("repeated retirement changed retained mode: %#v, found %t", mode, found)
|
|
}
|
|
}
|
|
|
|
func TestDisjointScalarReplacementsRetainIndexedCollectionPolicies(t *testing.T) {
|
|
t.Parallel()
|
|
const policyCount = 128
|
|
const replacementCount = 32
|
|
|
|
var policies collectionPolicyScope
|
|
collectionEntries := make([]document.Entry, 0, policyCount)
|
|
for index := 0; index < policyCount; index++ {
|
|
key := fmt.Sprintf("collection-%03d", index)
|
|
path := Keys("collections", key)
|
|
policies.set(path, collectionPolicy{identityFields: []string{"id"}, identityPresent: true})
|
|
collectionEntries = append(collectionEntries, document.Entry{
|
|
Key: key, Value: &document.Node{Kind: document.Sequence},
|
|
})
|
|
}
|
|
|
|
baseScalars := make([]document.Entry, 0, replacementCount)
|
|
incomingScalars := make([]document.Entry, 0, replacementCount)
|
|
for index := 0; index < replacementCount; index++ {
|
|
key := fmt.Sprintf("scalar-%03d", index)
|
|
baseScalars = append(baseScalars, document.Entry{
|
|
Key: key, Value: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{
|
|
Key: "previous", Value: &document.Node{Kind: document.Boolean, Text: "true"},
|
|
}}},
|
|
})
|
|
incomingScalars = append(incomingScalars, document.Entry{
|
|
Key: key, Value: &document.Node{Kind: document.String, Text: "replaced"},
|
|
})
|
|
}
|
|
base := &composedDocument{
|
|
root: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{
|
|
{Key: "collections", Value: &document.Node{Kind: document.Mapping, Mapping: collectionEntries}},
|
|
{Key: "scalars", Value: &document.Node{Kind: document.Mapping, Mapping: baseScalars}},
|
|
}},
|
|
collectionPolicies: policies,
|
|
origins: make(map[string]Origin),
|
|
}
|
|
incoming := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{
|
|
Key: "scalars", Value: &document.Node{Kind: document.Mapping, Mapping: incomingScalars},
|
|
}}}
|
|
assembly := newAssemblyAccumulator(base)
|
|
if _, diagnostics := assembly.applyOverlayWithCollectionProjection(
|
|
incoming, nil, collectionPolicyScope{}, nil, LayerOverlay, sequencePolicy{}, nil,
|
|
); len(diagnostics) != 0 {
|
|
t.Fatalf("apply disjoint scalar replacements: %#v", diagnostics)
|
|
}
|
|
for index := 0; index < policyCount; index++ {
|
|
path := Keys("collections", fmt.Sprintf("collection-%03d", index))
|
|
policy, found := assembly.current.collectionPolicies.at(path)
|
|
if !found || !policy.identityPresent || len(policy.identityFields) != 1 || policy.identityFields[0] != "id" {
|
|
t.Fatalf("disjoint replacement changed policy %s: %#v, found %t", path, policy, found)
|
|
}
|
|
}
|
|
for index := 0; index < replacementCount; index++ {
|
|
path := Keys("scalars", fmt.Sprintf("scalar-%03d", index))
|
|
value, found := documentAtPath(assembly.current.root, path)
|
|
if !found || value.Kind != document.String || value.Text != "replaced" {
|
|
t.Fatalf("scalar replacement at %s = %#v, found %t", path, value, found)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDisjointTierOverlaysRetainNestedCollectionPolicies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nestedOwner := newCollectionRecordPolicy()
|
|
nestedOwner.scope.set(Keys("prompts"), collectionPolicy{
|
|
identityFields: []string{"name"}, identityPresent: true,
|
|
explicitMode: sequenceModeAppend, explicitModePresent: true,
|
|
})
|
|
itemsPolicy := collectionPolicyWithRecords(collectionPolicy{
|
|
identityFields: []string{"id"}, identityPresent: true,
|
|
explicitMode: sequenceModeAppend, explicitModePresent: true,
|
|
}, nestedOwner)
|
|
var policies collectionPolicyScope
|
|
policies.set(Keys("items"), itemsPolicy)
|
|
policies.set(Keys("keyOnly"), collectionPolicy{
|
|
identityFields: []string{"id"}, identityPresent: true,
|
|
})
|
|
policies.set(Keys("modeOnly"), collectionPolicy{
|
|
explicitMode: sequenceModeAppend, explicitModePresent: true,
|
|
})
|
|
|
|
base := &composedDocument{
|
|
root: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{
|
|
{Key: "items", Value: &document.Node{Kind: document.Sequence, Sequence: []*document.Node{{
|
|
Kind: document.Mapping,
|
|
Mapping: []document.Entry{
|
|
{Key: "id", Value: &document.Node{Kind: document.String, Text: "one"}},
|
|
{Key: "prompts", Value: &document.Node{Kind: document.Sequence}},
|
|
},
|
|
}}}},
|
|
{Key: "keyOnly", Value: &document.Node{Kind: document.Sequence}},
|
|
{Key: "modeOnly", Value: &document.Node{Kind: document.Sequence}},
|
|
}},
|
|
collectionPolicies: policies,
|
|
origins: make(map[string]Origin),
|
|
}
|
|
assembly := newAssemblyAccumulator(base)
|
|
initialItems, found := assembly.current.collectionPolicies.at(Keys("items"))
|
|
if !found {
|
|
t.Fatal("initial item policy is missing")
|
|
}
|
|
initialRecord, found := initialItems.recordPolicyAt(0)
|
|
if !found {
|
|
t.Fatal("initial nested record owner is missing")
|
|
}
|
|
|
|
for index, key := range []string{"firstTier", "secondTier", "thirdTier"} {
|
|
overlay := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{
|
|
Key: key, Value: &document.Node{Kind: document.Boolean, Text: "true"},
|
|
}}}
|
|
if _, diagnostics := assembly.applyOverlayWithCollectionProjection(
|
|
overlay, nil, collectionPolicyScope{}, nil, LayerUnderlay, sequencePolicy{}, nil,
|
|
); len(diagnostics) != 0 {
|
|
t.Fatalf("apply disjoint tier %d: %#v", index, diagnostics)
|
|
}
|
|
|
|
retainedItems, found := assembly.current.collectionPolicies.at(Keys("items"))
|
|
if !found || !retainedItems.identityPresent || len(retainedItems.identityFields) != 1 || retainedItems.identityFields[0] != "id" ||
|
|
!retainedItems.explicitModePresent || retainedItems.explicitMode != sequenceModeAppend {
|
|
t.Fatalf("tier %d changed retained item policy: %#v, found %t", index, retainedItems, found)
|
|
}
|
|
retainedRecord, found := retainedItems.recordPolicyAt(0)
|
|
if !found || retainedRecord.owner != initialRecord.owner {
|
|
t.Fatalf("tier %d replaced the nested record owner: %#v, found %t", index, retainedRecord, found)
|
|
}
|
|
nested, found := retainedRecord.scope.at(Keys("prompts"))
|
|
if !found || !nested.identityPresent || len(nested.identityFields) != 1 || nested.identityFields[0] != "name" ||
|
|
!nested.explicitModePresent || nested.explicitMode != sequenceModeAppend {
|
|
t.Fatalf("tier %d changed nested collection policy: %#v, found %t", index, nested, found)
|
|
}
|
|
keyOnly, found := assembly.current.collectionPolicies.at(Keys("keyOnly"))
|
|
if !found || !keyOnly.identityPresent || keyOnly.explicitModePresent {
|
|
t.Fatalf("tier %d changed key-only policy: %#v, found %t", index, keyOnly, found)
|
|
}
|
|
modeOnly, found := assembly.current.collectionPolicies.at(Keys("modeOnly"))
|
|
if !found || modeOnly.identityPresent || !modeOnly.explicitModePresent || modeOnly.explicitMode != sequenceModeAppend {
|
|
t.Fatalf("tier %d changed mode-only policy: %#v, found %t", index, modeOnly, found)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOverlayNonSequenceClaimAtRetainedCollectionStillFails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var policies collectionPolicyScope
|
|
policies.set(Keys("items"), collectionPolicy{
|
|
identityFields: []string{"id"}, identityPresent: true,
|
|
explicitMode: sequenceModeAppend, explicitModePresent: true,
|
|
})
|
|
base := &composedDocument{
|
|
root: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{
|
|
Key: "items", Value: &document.Node{Kind: document.Sequence},
|
|
}}},
|
|
collectionPolicies: policies,
|
|
origins: make(map[string]Origin),
|
|
}
|
|
assembly := newAssemblyAccumulator(base)
|
|
overlay := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{
|
|
Key: "items", Value: &document.Node{Kind: document.String, Text: "not a sequence"},
|
|
}}}
|
|
if _, diagnostics := assembly.applyOverlayWithCollectionProjection(
|
|
overlay, nil, collectionPolicyScope{}, nil, LayerUnderlay, sequencePolicy{}, nil,
|
|
); len(diagnostics) == 0 {
|
|
t.Fatal("non-sequence overlay at an append collection unexpectedly succeeded")
|
|
}
|
|
}
|