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") } }