package engine import ( "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestCombineSequenceOperationsAndItemEvidence(t *testing.T) { tests := []struct { name string lower string higher string operation sequenceMode want []string wantLower []sequenceItemProjection wantHigher []sequenceItemProjection wantReplacements []sequenceItemReplacement wantAppended []int }{ { name: "replace uses only higher records", lower: "items: [{id: a}, {id: b}]\n", higher: "items: [{id: b, text: revised}]\n", operation: sequenceModeReplace, want: []string{"b"}, wantHigher: []sequenceItemProjection{{inputIndex: 0, outputIndex: 0}}, }, { name: "append retains then appends", lower: "items: [{id: a}, {id: b}]\n", higher: "items: [{id: c}]\n", operation: sequenceModeAppend, want: []string{"a", "b", "c"}, wantLower: []sequenceItemProjection{{inputIndex: 0, outputIndex: 0}, {inputIndex: 1, outputIndex: 1}}, wantHigher: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}}, wantAppended: []int{0}, }, { name: "upsert replaces whole records in place and appends new records", lower: "items: [{id: a, text: old}, {id: b}, {id: c, remove: true}]\n", higher: "items: [{id: c, text: revised}, {id: a, text: revised}, {id: d}]\n", operation: sequenceModeUpsert, want: []string{"a", "b", "c", "d"}, wantLower: []sequenceItemProjection{{inputIndex: 1, outputIndex: 1}}, wantHigher: []sequenceItemProjection{{inputIndex: 0, outputIndex: 2}, {inputIndex: 1, outputIndex: 0}, {inputIndex: 2, outputIndex: 3}}, wantReplacements: []sequenceItemReplacement{{lowerIndex: 0, higherIndex: 1, outputIndex: 0}, {lowerIndex: 2, higherIndex: 0, outputIndex: 2}}, wantAppended: []int{2}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { lower := sequenceFromYAML(t, test.lower) higher := sequenceFromYAML(t, test.higher) policy := sequenceCombinationPolicy(test.operation, "id") result, diagnostics := combineSequence(lower, higher, policy, sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 { t.Fatalf("combination diagnostics = %#v", diagnostics) } if got := sequenceIdentityValues(result.value, "id"); !reflect.DeepEqual(got, test.want) { t.Fatalf("combined IDs = %#v, want %#v", got, test.want) } if !reflect.DeepEqual(result.lowerItems, test.wantLower) || !reflect.DeepEqual(result.higherItems, test.wantHigher) || !reflect.DeepEqual(result.replacements, test.wantReplacements) || !reflect.DeepEqual(result.appendedHigher, test.wantAppended) { t.Fatalf("item evidence = %#v", result) } }) } } func TestCombineSequenceDistinguishesAbsenceFromNullAndKindMismatch(t *testing.T) { policy := sequenceCombinationPolicy(sequenceModeAppend, "id") retained := sequenceFromYAML(t, "items: [{id: lower}]\n") unchanged, diagnostics := combineSequence(retained, nil, policy, sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || !reflect.DeepEqual(sequenceIdentityValues(unchanged.value, "id"), []string{"lower"}) || len(unchanged.lowerItems) != 1 { t.Fatalf("absent higher value changed lower sequence: %#v, %#v", unchanged, diagnostics) } tests := []struct { name string lower string higher string }{ {name: "null higher value", lower: "items: [{id: lower}]\n", higher: "items: null\n"}, {name: "mapping higher value", lower: "items: [{id: lower}]\n", higher: "items: {id: higher}\n"}, {name: "null lower value", lower: "items: null\n", higher: "items: []\n"}, {name: "scalar lower value", lower: "items: blocked\n", higher: "items: []\n"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { result, issues := combineSequence(sequenceFromYAML(t, test.lower), sequenceFromYAML(t, test.higher), policy, sequenceCombinationContext{destination: Keys("items")}) if result.value != nil || len(issues) != 1 || issues[0].Code != CodeSequenceInvalid || issues[0].Path.String() != "/items" { t.Fatalf("kind mismatch result = %#v, %#v", result, issues) } }) } for _, operation := range []sequenceMode{sequenceModeAppend, sequenceModeUpsert} { result, issues := combineSequence(nil, sequenceFromYAML(t, "items: null\n"), sequenceCombinationPolicy(operation, "id"), sequenceCombinationContext{destination: Keys("items")}) if result.value != nil || len(issues) != 1 || issues[0].Code != CodeSequenceInvalid { t.Fatalf("null incoming value under operation %d = %#v, %#v", operation, result, issues) } } } func TestReplaceSequenceAllowsNullAndNonSequenceValues(t *testing.T) { for _, test := range []struct { name string higher string wantKind document.Kind }{ {name: "null", higher: "items: null\n", wantKind: document.Null}, {name: "mapping", higher: "items: {value: replacement}\n", wantKind: document.Mapping}, } { t.Run(test.name, func(t *testing.T) { lower := sequenceFromYAML(t, "items: [{id: lower}]\n") higher := sequenceFromYAML(t, test.higher) result, diagnostics := combineSequence(lower, higher, sequenceCombinationPolicy(sequenceModeReplace), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || result.value == nil || result.value.Kind != test.wantKind { t.Fatalf("replacement result = %#v, %#v", result, diagnostics) } }) } result, diagnostics := combineSequence(sequenceFromYAML(t, "items: null\n"), sequenceFromYAML(t, "items: [{id: new}]\n"), sequenceCombinationPolicy(sequenceModeReplace), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || !reflect.DeepEqual(sequenceIdentityValues(result.value, "id"), []string{"new"}) { t.Fatalf("replacement over explicit lower null = %#v, %#v", result, diagnostics) } } func TestCombineSequenceRequiresIdentityForAppendAndUpsertEvenWhenEmpty(t *testing.T) { for _, operation := range []sequenceMode{sequenceModeAppend, sequenceModeUpsert} { result, diagnostics := combineSequence(nil, sequenceFromYAML(t, "items: []\n"), sequenceCombinationPolicy(operation), sequenceCombinationContext{destination: Keys("items")}) if result.value != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid { t.Fatalf("empty input bypassed identity requirement for operation %d: %#v, %#v", operation, result, diagnostics) } } for _, operation := range []sequenceMode{sequenceModeAppend, sequenceModeUpsert} { result, diagnostics := combineSequence(nil, sequenceFromYAML(t, "items: [{name: no-id}]\n"), sequenceCombinationPolicy(operation, "id"), sequenceCombinationContext{destination: Keys("items")}) if result.value != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/items/0/id" { t.Fatalf("missing identity for operation %d = %#v, %#v", operation, result, diagnostics) } } } func TestCombineSequenceValidatesDuplicatesWithinEachContributionInEveryMode(t *testing.T) { duplicate := "items: [{id: same}, {id: same}]\n" for _, operation := range []sequenceMode{sequenceModeReplace, sequenceModeAppend, sequenceModeUpsert} { _, diagnostics := combineSequence(nil, sequenceFromYAML(t, duplicate), sequenceCombinationPolicy(operation, "id"), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict { t.Fatalf("higher duplicate under operation %d = %#v", operation, diagnostics) } } for _, operation := range []sequenceMode{sequenceModeAppend, sequenceModeUpsert} { _, diagnostics := combineSequence(sequenceFromYAML(t, duplicate), sequenceFromYAML(t, "items: [{id: other}]\n"), sequenceCombinationPolicy(operation, "id"), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict { t.Fatalf("lower duplicate under operation %d = %#v", operation, diagnostics) } } } func TestAppendSequenceRejectsDuplicatesAcrossContributions(t *testing.T) { const identityCanary = "identity-canary-64" lower := sequenceFromYAML(t, "items: [{id: "+identityCanary+"}]\n") higher := sequenceFromYAML(t, "items: [{id: "+identityCanary+"}]\n") _, diagnostics := combineSequence(lower, higher, sequenceCombinationPolicy(sequenceModeAppend, "id"), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict || diagnostics[0].Path.String() != "/items" || diagnostics[0].Primary == nil || len(diagnostics[0].Related) != 1 { t.Fatalf("cross-contribution duplicate = %#v", diagnostics) } if strings.Contains(diagnostics[0].Message, identityCanary) { t.Fatalf("identity value leaked in diagnostic: %#v", diagnostics[0]) } } func TestCombineSequenceReplacementResetsLowerIdentity(t *testing.T) { lower := sequenceFromYAML(t, "items: [{id: old}]\n") higher := sequenceFromYAML(t, "items: [{name: new}]\n") policy := sequenceCombinationPolicy(sequenceModeReplace, "name") result, diagnostics := combineSequence(lower, higher, policy, sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || !reflect.DeepEqual(sequenceIdentityValues(result.value, "name"), []string{"new"}) { t.Fatalf("replacement applied obsolete identity: %#v, %#v", result, diagnostics) } } func TestCombineSequenceDifferentIdentityDeclarationsConflict(t *testing.T) { declarations := extractSequenceDeclarationsYAML(t, `configkit: sequences: - destination: [items] key: id - destination: [items] key: name `, rootDirectiveContextSource).sequences if _, diagnostics := resolveCollectionPolicy(Keys("items"), declarations, nil, nil); len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict { t.Fatalf("different collection key declarations = %#v", diagnostics) } } func TestCombineSequenceEmptyInputsAndStableOrder(t *testing.T) { for _, operation := range []sequenceMode{sequenceModeAppend, sequenceModeUpsert} { lower := sequenceFromYAML(t, "items: [{id: a}, {id: b}]\n") empty := sequenceFromYAML(t, "items: []\n") result, diagnostics := combineSequence(lower, empty, sequenceCombinationPolicy(operation, "id"), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || !reflect.DeepEqual(sequenceIdentityValues(result.value, "id"), []string{"a", "b"}) { t.Fatalf("empty higher sequence under operation %d = %#v, %#v", operation, result, diagnostics) } } result, diagnostics := combineSequence(nil, sequenceFromYAML(t, "items: []\n"), sequenceCombinationPolicy(sequenceModeReplace, "id"), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || result.value == nil || result.value.Kind != document.Sequence || len(result.value.Sequence) != 0 { t.Fatalf("explicit replacement with empty input = %#v, %#v", result, diagnostics) } } func TestCombineSequenceInitializesAbsentLowerCollection(t *testing.T) { higher := sequenceFromYAML(t, "items: [{id: first}, {id: second}]\n") for _, operation := range []sequenceMode{sequenceModeReplace, sequenceModeAppend, sequenceModeUpsert} { result, diagnostics := combineSequence(nil, higher, sequenceCombinationPolicy(operation, "id"), sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || !reflect.DeepEqual(sequenceIdentityValues(result.value, "id"), []string{"first", "second"}) { t.Fatalf("absent lower collection under operation %d = %#v, %#v", operation, result, diagnostics) } } } func TestCombineSequenceClonesInputsAndDoesNotMutatePolicy(t *testing.T) { for _, test := range []struct { name string operation sequenceMode }{ {name: "replace", operation: sequenceModeReplace}, {name: "append", operation: sequenceModeAppend}, {name: "upsert", operation: sequenceModeUpsert}, } { t.Run(test.name, func(t *testing.T) { lower := sequenceFromYAML(t, "items: [{id: a, text: original}]\n") higherID := "b" if test.operation == sequenceModeUpsert { higherID = "a" } higher := sequenceFromYAML(t, "items: [{id: "+higherID+", text: incoming}]\n") policy := sequenceCombinationPolicy(test.operation, "id") policyCopy := append([]string(nil), policy.effectiveIdentityFields...) result, diagnostics := combineSequence(lower, higher, policy, sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 { t.Fatal(diagnostics) } for _, item := range result.value.Sequence { for entryIndex := range item.Mapping { if item.Mapping[entryIndex].Key == "text" { item.Mapping[entryIndex].Value.Text = "changed" } } } if lower.Sequence[0].Mapping[1].Value.Text != "original" || higher.Sequence[0].Mapping[1].Value.Text != "incoming" { t.Fatal("combined result aliases a mutable input node") } if !reflect.DeepEqual(policy.effectiveIdentityFields, policyCopy) { t.Fatal("combination mutated the resolved policy") } }) } } func TestCombineSequenceEquivalentIdentityFieldSets(t *testing.T) { lower := sequenceFromYAML(t, "items: [{id: a, namespace: one}]\n") higher := sequenceFromYAML(t, "items: [{namespace: one, id: b}]\n") policy, diagnostics := resolveCollectionPolicy(Keys("items"), extractSequenceDeclarationsYAML(t, `configkit: sequences: - destination: [items] key: [namespace, id, id] `, rootDirectiveContextSource).sequences, nil, nil) if len(diagnostics) != 0 { t.Fatal(diagnostics) } result, diagnostics := combineSequence(lower, higher, policy, sequenceCombinationContext{destination: Keys("items")}) if len(diagnostics) != 0 || !reflect.DeepEqual(sequenceIdentityValues(result.value, "id"), []string{"a", "b"}) { t.Fatalf("equivalent identity policy failed: %#v, %#v", result, diagnostics) } } func sequenceCombinationPolicy(operation sequenceMode, fields ...string) collectionPolicyResolution { normalized, _ := normalizeSequenceIdentityFields(fields) return collectionPolicyResolution{operation: operation, effectiveIdentityFields: normalized} } func sequenceFromYAML(t *testing.T, input string) *document.Node { t.Helper() parsed := parsePolicyNode(t, input) if len(parsed.Mapping) != 1 || parsed.Mapping[0].Key != "items" { t.Fatalf("sequence fixture root = %#v", parsed.Mapping) } return parsed.Mapping[0].Value } func sequenceIdentityValues(sequence *document.Node, field string) []string { if sequence == nil || sequence.Kind != document.Sequence { return nil } values := make([]string, len(sequence.Sequence)) for index, item := range sequence.Sequence { for _, entry := range item.Mapping { if entry.Key == field { values[index] = entry.Value.Text break } } } return values }