785 lines
34 KiB
Go
785 lines
34 KiB
Go
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.maximumdirect.net/configkit/internal/document"
|
|
)
|
|
|
|
func TestSequenceHistoryReplaysAppendAtAssignedIndexes(t *testing.T) {
|
|
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{
|
|
{selector: sequenceIdentitySelector{
|
|
literalSequenceSelector("collections"), anyMappingSequenceSelector(), literalSequenceSelector("records"),
|
|
}, identityFields: []string{"id"}},
|
|
})
|
|
first := parseCompositionDocument(t, "source-1", "collections: {north: {records: [{id: a, value: first}]}, south: {records: [{id: x}]}}\n")
|
|
second := parseCompositionDocument(t, "source-2", "collections: {north: {records: [{id: b, value: second}]}, south: {records: [{id: y}]}}\n")
|
|
composed, diagnostics := composeDocumentsWithPolicy(policy, first, second)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("compose diagnostics = %#v", diagnostics)
|
|
}
|
|
for applicationIndex, application := range composed.applications {
|
|
for path, want := range map[string]int{"/collections/north/records": 1, "/collections/south/records": 1} {
|
|
sequence, present := documentAtPath(application.value, mustParsePath(t, path))
|
|
if !present || sequence.Kind != document.Sequence || len(sequence.Sequence) != want {
|
|
t.Errorf("application %d retained %s = %#v, %v; want one source fragment item", applicationIndex, path, sequence, present)
|
|
}
|
|
}
|
|
}
|
|
|
|
working := parseCompositionDocument(t, "defaults", "collections: {north: {records: [{id: default}]}, south: {records: [{id: default}]}}\n")
|
|
index := new(mappingIndex)
|
|
for _, application := range composed.applications {
|
|
applyHistoryApplication(&working, application, index)
|
|
}
|
|
for path, want := range map[string][]string{
|
|
"/collections/north/records": {"a", "b"},
|
|
"/collections/south/records": {"x", "y"},
|
|
} {
|
|
sequence, present := documentAtPath(working, mustParsePath(t, path))
|
|
if !present || sequence.Kind != document.Sequence || len(sequence.Sequence) != len(want) {
|
|
t.Fatalf("replayed %s = %#v", path, sequence)
|
|
}
|
|
for itemIndex, id := range want {
|
|
field := sequence.Sequence[itemIndex].Mapping[0].Value.Text
|
|
if field != id {
|
|
t.Errorf("replayed %s[%d].id = %q, want %q", path, itemIndex, field, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
application := composed.applications[1]
|
|
cloned := cloneHistoryApplications(composed.applications)
|
|
cloned[1].sequences[0].path.segments[0].key = "mutated"
|
|
if composed.applications[1].sequences[0].path.String() != "/collections/north/records" {
|
|
t.Fatal("cloned sequence history path aliases its source")
|
|
}
|
|
addedPath := Keys("collections", "north", "records").Index(1).Key("value")
|
|
if operation, relevant := applicationOperationPath(application, addedPath, nil); !relevant || operation.String() != addedPath.String() {
|
|
t.Fatalf("appended value operation = %s, %v", operation.String(), relevant)
|
|
}
|
|
if _, relevant := applicationOperationPath(application, Keys("collections", "north", "records").Index(0).Key("value"), nil); relevant {
|
|
t.Fatal("later contribution was reported as changing an earlier record")
|
|
}
|
|
origin := historyApplicationOrigin(application, addedPath, nil)
|
|
if len(origin.Locations) != 1 || origin.Locations[0].SourceID != "source-2" {
|
|
t.Fatalf("appended value origin = %#v", origin)
|
|
}
|
|
overlay := historyApplication{
|
|
layer: LayerOverlay,
|
|
value: parseCompositionDocument(t, "overlay", "collections: {north: {records: []}}\n"),
|
|
}
|
|
applyHistoryApplication(&working, overlay, index)
|
|
replaced, present := documentAtPath(working, Keys("collections", "north", "records"))
|
|
if !present || replaced.Kind != document.Sequence || len(replaced.Sequence) != 0 {
|
|
t.Fatalf("ordinary later list replacement = %#v, %v", replaced, present)
|
|
}
|
|
}
|
|
|
|
func TestTierSequenceHistoryReplaysSelectedOperation(t *testing.T) {
|
|
t.Parallel()
|
|
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{
|
|
selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"},
|
|
}})
|
|
for _, test := range []struct {
|
|
name string
|
|
mode sequenceMode
|
|
}{{"append", sequenceModeAppend}, {"upsert", sequenceModeUpsert}, {"replace", sequenceModeReplace}} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mode := test.mode
|
|
lower, diagnostics := composeDocumentsWithPolicy(policy, parseCompositionDocument(t, "lower", "records: [{id: a, removed: old}, {id: b, value: old, removed: old}]\n"))
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
secondID := "d"
|
|
if mode == sequenceModeUpsert {
|
|
secondID = "b"
|
|
}
|
|
declarations := []sequenceDeclarationSpec{{destination: Keys("records"), modePresent: true, mode: mode}}
|
|
higher, diagnostics := composeDocumentsWithCollectionPolicy(policy, lower.collectionPolicies, declarations,
|
|
parseCompositionDocument(t, "first-peer", "records: [{id: c, value: first}]\n"),
|
|
parseCompositionDocument(t, "second-peer", "records: [{id: "+secondID+", value: second}]\n"),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
assembly := newAssemblyAccumulator(lower)
|
|
if diagnostics := assembly.applyOverlayWithCollectionPolicy(higher.root, higher.origins, higher.collectionPolicies, higher.applications, LayerMain, policy, declarations); len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
if len(assembly.current.applications) != 3 {
|
|
t.Fatalf("source applications = %d, want all three sources", len(assembly.current.applications))
|
|
}
|
|
if mode == sequenceModeUpsert {
|
|
query := Keys("records").Index(1).Key("value")
|
|
if _, relevant := applicationOperationPath(assembly.current.applications[1], query, nil); relevant {
|
|
t.Fatal("first peer was attributed to another peer's upserted record")
|
|
}
|
|
origin := historyApplicationOrigin(assembly.current.applications[2], query, nil)
|
|
if len(origin.Locations) != 1 || origin.Locations[0].SourceID != "second-peer" {
|
|
t.Fatalf("upsert history source = %#v", origin)
|
|
}
|
|
}
|
|
working := parseCompositionDocument(t, "initial", "{}\n")
|
|
index := new(mappingIndex)
|
|
for _, application := range assembly.current.applications {
|
|
applyHistoryApplication(&working, application, index)
|
|
}
|
|
if !configurationValuesEqual(working, assembly.current.root) {
|
|
t.Fatal("tier history differs from structural assembly before decoding")
|
|
}
|
|
overlay := parseCompositionDocument(t, "overlay", "records: [{id: c, value: overlay}]\n")
|
|
declarations[0].mode = sequenceModeUpsert
|
|
if diagnostics := assembly.applyOverlayWithCollectionPolicy(overlay, nil, collectionPolicyScope{}, nil, LayerOverlay, policy, declarations); len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
applyHistoryApplication(&working, assembly.current.applications[3], index)
|
|
if !configurationValuesEqual(working, assembly.current.root) {
|
|
t.Fatal("subsequent overlay history differs from structural assembly")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSequenceHistoryPreservesTierReplacementAndEmptyLists(t *testing.T) {
|
|
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{
|
|
{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}},
|
|
{selector: sequenceIdentitySelector{literalSequenceSelector("independent")}, identityFields: []string{"id"}},
|
|
})
|
|
first, diagnostics := composeDocumentsWithPolicy(policy,
|
|
parseCompositionDocument(t, "underlay-1a", "records: [{id: shared}]\nindependent: [{id: x}]\n"),
|
|
parseCompositionDocument(t, "underlay-1b", "records: [{id: lower}]\n"),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("first tier diagnostics = %#v", diagnostics)
|
|
}
|
|
for index := range first.applications {
|
|
first.applications[index].layer = LayerUnderlay
|
|
}
|
|
second, diagnostics := composeDocumentsWithPolicy(policy,
|
|
parseCompositionDocument(t, "underlay-2", "records: [{id: shared}]\nindependent: []\n"),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("second tier diagnostics = %#v", diagnostics)
|
|
}
|
|
main, diagnostics := composeDocumentsWithPolicy(policy,
|
|
parseCompositionDocument(t, "main", "records: []\n"),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("main diagnostics = %#v", diagnostics)
|
|
}
|
|
|
|
assembly := newAssemblyAccumulator(first)
|
|
if diagnostics := assembly.applyOverlay(second.root, second.origins, second.applications, LayerUnderlay); len(diagnostics) != 0 {
|
|
t.Fatalf("second tier handoff diagnostics = %#v", diagnostics)
|
|
}
|
|
if diagnostics := assembly.applyOverlay(main.root, main.origins, main.applications, LayerMain); len(diagnostics) != 0 {
|
|
t.Fatalf("main handoff diagnostics = %#v", diagnostics)
|
|
}
|
|
if len(assembly.current.applications) != 4 {
|
|
t.Fatalf("retained source applications = %d, want 4", len(assembly.current.applications))
|
|
}
|
|
|
|
working := parseCompositionDocument(t, "defaults", "records: [{id: default}]\nindependent: [{id: default}]\n")
|
|
index := new(mappingIndex)
|
|
for _, application := range assembly.current.applications {
|
|
applyHistoryApplication(&working, application, index)
|
|
}
|
|
for path, want := range map[string][]string{
|
|
"/records": nil,
|
|
"/independent": nil,
|
|
} {
|
|
sequence, present := documentAtPath(working, mustParsePath(t, path))
|
|
if !present || sequence.Kind != document.Sequence || len(sequence.Sequence) != len(want) {
|
|
t.Errorf("replayed replacement %s = %#v, want an explicit empty sequence", path, sequence)
|
|
}
|
|
}
|
|
|
|
// The same identity may appear in separate tiers, while each tier still
|
|
// retains its individual source applications for explanation replay.
|
|
if got := []Layer{assembly.current.applications[0].layer, assembly.current.applications[1].layer,
|
|
assembly.current.applications[2].layer, assembly.current.applications[3].layer}; !reflect.DeepEqual(got, []Layer{LayerUnderlay, LayerUnderlay, LayerUnderlay, LayerMain}) {
|
|
t.Fatalf("application layers = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestHistoryMergeSupportsSemanticIndexedPrefixes(t *testing.T) {
|
|
root := parseCompositionDocument(t, "base", "records: [{id: one, helper: {value: old}}]\n")
|
|
application := historyApplication{
|
|
path: Keys("records").Index(0).Key("helper"),
|
|
value: parseCompositionDocument(t, "helper", "value: new\n"),
|
|
}
|
|
applyHistoryApplication(&root, application, new(mappingIndex))
|
|
value, present := documentAtPath(root, Keys("records").Index(0).Key("helper").Key("value"))
|
|
if !present || value.Text != "new" {
|
|
t.Fatalf("indexed helper replay = %#v, %v", value, present)
|
|
}
|
|
|
|
root = parseCompositionDocument(t, "base", "records: [{id: one, helper: {items: [{id: a}]}}]\n")
|
|
application.value = parseCompositionDocument(t, "helper", "items: [{id: b}]\n")
|
|
application.sequences = []sequenceHistoryContribution{{path: Keys("items"), mode: sequenceHistoryAppend, offset: 1, length: 1}}
|
|
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
|
|
applyHistoryApplication(&root, application, new(mappingIndex))
|
|
items, present := documentAtPath(root, Keys("records").Index(0).Key("helper").Key("items"))
|
|
if !present || len(items.Sequence) != 2 {
|
|
t.Fatalf("indexed helper lost its local sequence operation: %#v", items)
|
|
}
|
|
}
|
|
|
|
func TestLayeredUpsertExplanationFollowsStructuralOrder(t *testing.T) {
|
|
t.Parallel()
|
|
type config struct {
|
|
Records []map[string]any `yaml:"records"`
|
|
}
|
|
loaded, err := testGroupedLoad(loadRequest[config]{
|
|
main: loadInput{source: Bytes("main", []byte("configkit:\n sequences: [{destination: [records], key: id}]\nrecords: [{id: b, value: new-b}, {id: c, value: new-c}, {id: a, value: new-a}]\n"))},
|
|
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("records: [{id: a, value: old-a}, {id: b, value: old-b, removed: old}]\n"))}}}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
explanation, ok := loaded.Explain(Keys("records"))
|
|
if !ok || len(explanation.History) == 0 {
|
|
t.Fatal("missing collection history")
|
|
}
|
|
for _, change := range explanation.History {
|
|
if change.Origin.Operation == ChangeDecoding {
|
|
t.Fatal("structural upsert was misreported as a decoding reconciliation")
|
|
}
|
|
}
|
|
if last := explanation.History[len(explanation.History)-1]; last.After.YAML != explanation.Effective.YAML || last.Origin.Layer != LayerMain {
|
|
t.Fatalf("main contribution did not produce the effective collection: %#v", last)
|
|
}
|
|
removed, ok := loaded.Explain(Keys("records").Index(1).Key("removed"))
|
|
if !ok || len(removed.History) == 0 || removed.Effective.Present {
|
|
t.Fatalf("missing displaced field history: %#v, %v", removed, ok)
|
|
}
|
|
if last := removed.History[len(removed.History)-1]; last.After.Present || last.Origin.Layer != LayerMain || last.Origin.Operation == ChangeDecoding {
|
|
t.Fatalf("upsert removal attribution = %#v", last)
|
|
}
|
|
}
|
|
|
|
func TestProjectedSequenceHistoryAgreesAcrossMetadataAndExplanation(t *testing.T) {
|
|
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}})
|
|
composed, diagnostics := composeDocumentsWithPolicy(policy,
|
|
parseCompositionDocument(t, "first", "records: [{id: first, token: public}]"),
|
|
parseCompositionDocument(t, "second", "records: [{id: second, token: private}]"),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("compose diagnostics = %#v", diagnostics)
|
|
}
|
|
path := Keys("records").Index(1).Key("token")
|
|
baseline := &document.Node{Kind: document.Mapping}
|
|
model := sourceHistoryModelForInspection(baseline, composed.root, composed.origins, composed.applications, composed.overlays)
|
|
metadata := Metadata{historyModel: model}
|
|
finalizeEffectiveMetadataPaths(&metadata, composed.root)
|
|
if !metadata.hasHistoricalPath(path) {
|
|
t.Fatalf("projected metadata omitted shifted path %s: %#v", path, metadata.HistoricalPaths())
|
|
}
|
|
|
|
inspection := &Inspection{metadata: metadata, history: model, final: composed.root}
|
|
inspection.secrets = classifySecretPaths(nil, func(candidate Path) bool {
|
|
return reflect.DeepEqual(candidate, path)
|
|
}, metadata)
|
|
explanation, ok := inspection.Explain(path)
|
|
if !ok || !explanation.Effective.Redacted {
|
|
t.Fatalf("shifted explanation = %#v, %v", explanation, ok)
|
|
}
|
|
var foundSecondOrigin bool
|
|
for _, change := range explanation.History {
|
|
for _, location := range change.Origin.Locations {
|
|
foundSecondOrigin = foundSecondOrigin || location.SourceID == "second"
|
|
}
|
|
}
|
|
if !foundSecondOrigin {
|
|
t.Fatalf("shifted history lost second source attribution: %#v", explanation.History)
|
|
}
|
|
var rendered bytes.Buffer
|
|
if err := inspection.WriteExplanation(&rendered, path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(rendered.String(), `path: "/records/1/token"`) || strings.Contains(rendered.String(), "private") {
|
|
t.Fatalf("shifted explanation rendering is incomplete or unsafe: %s", rendered.String())
|
|
}
|
|
}
|
|
|
|
func TestReplacedProjectedSensitiveRecordRemainsRedacted(t *testing.T) {
|
|
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}})
|
|
composed, diagnostics := composeDocumentsWithPolicy(policy,
|
|
parseCompositionDocument(t, "first", "records: [{id: first, token: public}]"),
|
|
parseCompositionDocument(t, "second", "records: [{id: second, token: PRIVATE-RECORD-CANARY}]"),
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("compose diagnostics = %#v", diagnostics)
|
|
}
|
|
applications := append([]historyApplication(nil), composed.applications...)
|
|
applications = append(applications, historyApplication{
|
|
layer: LayerOverlay,
|
|
value: parseCompositionDocument(t, "replacement", "records: [{id: replacement, token: visible}]"),
|
|
})
|
|
final := applications[len(applications)-1].value
|
|
path := Keys("records").Index(1).Key("token")
|
|
baseline := &document.Node{Kind: document.Mapping}
|
|
model := sourceHistoryModelForInspection(baseline, final, nil, applications, nil)
|
|
metadata := Metadata{historyModel: model}
|
|
finalizeEffectiveMetadataPaths(&metadata, final)
|
|
inspection := &Inspection{metadata: metadata, history: model, final: final}
|
|
inspection.secrets = classifySecretPaths(nil, func(candidate Path) bool {
|
|
return reflect.DeepEqual(candidate, path)
|
|
}, metadata)
|
|
explanation, ok := inspection.Explain(path)
|
|
if !ok || explanation.Effective.Present {
|
|
t.Fatalf("replaced shifted record explanation = %#v, %v", explanation, ok)
|
|
}
|
|
var rendered bytes.Buffer
|
|
if err := inspection.WriteExplanation(&rendered, path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(rendered.String(), "PRIVATE-RECORD-CANARY") || !strings.Contains(rendered.String(), "[REDACTED]") {
|
|
t.Fatalf("replaced sensitive history leaked or was not redacted: %s", rendered.String())
|
|
}
|
|
}
|
|
|
|
func sourceHistoryModelForInspection(baseline, final *document.Node, origins map[string]Origin, applications []historyApplication, records []overlayRecord) *historyModel {
|
|
builder := newHistoryBuilder()
|
|
builder.observeBaseline(baseline)
|
|
builder.setScratch(baseline)
|
|
builder.observeSourceEvidence(final, origins, applications, records)
|
|
builder.model.final = final
|
|
return builder.freeze()
|
|
}
|
|
|
|
func TestMappedUpsertProjectsSensitiveReplacementEvidence(t *testing.T) {
|
|
lowerRoot := parseCompositionDocument(t, "lower", `records:
|
|
- id: first
|
|
- id: middle
|
|
- id: last
|
|
token: MAPPED-SECRET-CANARY
|
|
`)
|
|
higherRoot := parseCompositionDocument(t, "higher", `records:
|
|
- id: last
|
|
token: visible
|
|
- id: first
|
|
- id: added
|
|
`)
|
|
lower, _ := documentAtPath(lowerRoot, Keys("records"))
|
|
higher, _ := documentAtPath(higherRoot, Keys("records"))
|
|
combined, diagnostics := combineSequence(
|
|
lower, higher,
|
|
sequenceCombinationPolicy(sequenceModeUpsert, "id"),
|
|
sequenceCombinationContext{destination: Keys("records")},
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("combine diagnostics = %#v", diagnostics)
|
|
}
|
|
secretPath := Keys("records").Index(2).Key("token")
|
|
secretNode, present := documentAtPath(lowerRoot, Keys("records").Index(2).Key("token"))
|
|
if !present {
|
|
t.Fatal("sensitive source token is missing")
|
|
}
|
|
secretNode.Sensitive = true
|
|
application := historyApplication{
|
|
layer: LayerOverlay, value: higherRoot,
|
|
sequences: []sequenceHistoryContribution{{
|
|
path: Keys("records"), mode: sequenceHistoryMapped,
|
|
items: combined.higherItems, replacements: combined.replacements,
|
|
}},
|
|
}
|
|
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
|
|
applications := []historyApplication{{value: lowerRoot}, application}
|
|
replayed := &document.Node{Kind: document.Mapping}
|
|
for _, current := range applications {
|
|
applyHistoryApplication(&replayed, current, new(mappingIndex))
|
|
}
|
|
replayedRecords, _ := documentAtPath(replayed, Keys("records"))
|
|
if !reflect.DeepEqual(replayedRecords.Sequence, combined.value.Sequence) {
|
|
t.Fatalf("replayed value = %#v, want combination = %#v", replayedRecords.Sequence, combined.value.Sequence)
|
|
}
|
|
finalRoot := document.Clone(lowerRoot)
|
|
finalRecords, _ := documentAtPath(finalRoot, Keys("records"))
|
|
finalRecords.Sequence = document.Clone(combined.value).Sequence
|
|
|
|
baseline := &document.Node{Kind: document.Mapping}
|
|
model := sourceHistoryModelForInspection(baseline, finalRoot, nil, applications, nil)
|
|
metadata := Metadata{historyModel: model}
|
|
finalizeEffectiveMetadataPaths(&metadata, finalRoot)
|
|
inspection := &Inspection{metadata: metadata, history: model, final: finalRoot}
|
|
secretRule := func(candidate Path) bool { return reflect.DeepEqual(candidate, secretPath) }
|
|
inspection.secrets = classifySecretPaths(nil, secretRule, metadata)
|
|
explanation, ok := inspection.Explain(secretPath)
|
|
if !ok || !explanation.Effective.Redacted {
|
|
t.Fatalf("mapped replacement explanation = %#v, %v", explanation, ok)
|
|
}
|
|
var rendered bytes.Buffer
|
|
if err := inspection.WriteExplanation(&rendered, secretPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(rendered.String(), "MAPPED-SECRET-CANARY") || !strings.Contains(rendered.String(), "[REDACTED]") {
|
|
t.Fatalf("mapped replacement history leaked or was not redacted: %s", rendered.String())
|
|
}
|
|
}
|
|
|
|
func TestSequenceHistoryReplaysCombinationEvidence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
operation sequenceMode
|
|
lower string
|
|
higher string
|
|
}{
|
|
{
|
|
name: "append",
|
|
operation: sequenceModeAppend,
|
|
lower: "records: [{id: first}, {id: last}]\n",
|
|
higher: "records: [{id: added}]\n",
|
|
},
|
|
{
|
|
name: "upsert",
|
|
operation: sequenceModeUpsert,
|
|
lower: `records:
|
|
- id: first
|
|
label: same
|
|
nested: {removed: lower}
|
|
- id: middle
|
|
- id: last
|
|
nested: {removed: lower}
|
|
`,
|
|
higher: `records:
|
|
- id: last
|
|
nested: {kept: higher}
|
|
- id: first
|
|
label: same
|
|
- id: added
|
|
`,
|
|
},
|
|
{
|
|
name: "replace",
|
|
operation: sequenceModeReplace,
|
|
lower: "records: [{id: discarded}]\n",
|
|
higher: "records: [{id: replacement}]\n",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
lowerRoot := parseCompositionDocument(t, "lower", test.lower)
|
|
higherRoot := parseCompositionDocument(t, "higher", test.higher)
|
|
lower, _ := documentAtPath(lowerRoot, Keys("records"))
|
|
higher, _ := documentAtPath(higherRoot, Keys("records"))
|
|
fields := []string(nil)
|
|
if test.operation != sequenceModeReplace {
|
|
fields = []string{"id"}
|
|
}
|
|
combined, diagnostics := combineSequence(lower, higher, sequenceCombinationPolicy(test.operation, fields...), sequenceCombinationContext{destination: Keys("records")})
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("combine diagnostics = %#v", diagnostics)
|
|
}
|
|
|
|
application := historyApplication{
|
|
layer: LayerOverlay,
|
|
value: higherRoot,
|
|
sequences: []sequenceHistoryContribution{
|
|
sequenceHistoryContributionForCombination(Keys("records"), test.operation, higher, combined),
|
|
},
|
|
}
|
|
if len(combined.higherItems) != 0 {
|
|
combined.higherItems[0].outputIndex++
|
|
}
|
|
if len(combined.replacements) != 0 {
|
|
combined.replacements[0].outputIndex++
|
|
}
|
|
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
|
|
replayed := document.Clone(lowerRoot)
|
|
applyHistoryApplication(&replayed, application, new(mappingIndex))
|
|
got, present := documentAtPath(replayed, Keys("records"))
|
|
if !present || !reflect.DeepEqual(got.Sequence, combined.value.Sequence) {
|
|
t.Fatalf("replayed sequence = %#v, want combination = %#v", got, combined.value.Sequence)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMappedSequenceHistoryReplacesWholeRecordsAndKeepsDisplacedEvidence(t *testing.T) {
|
|
lowerRoot := parseCompositionDocument(t, "lower", `records:
|
|
- id: first
|
|
label: same
|
|
- id: middle
|
|
- id: last
|
|
nested: {removed: LOWER-CANARY}
|
|
`)
|
|
higherRoot := parseCompositionDocument(t, "higher", `records:
|
|
- id: last
|
|
nested: {kept: higher}
|
|
- id: first
|
|
label: same
|
|
- id: added
|
|
`)
|
|
lower, _ := documentAtPath(lowerRoot, Keys("records"))
|
|
higher, _ := documentAtPath(higherRoot, Keys("records"))
|
|
combined, diagnostics := combineSequence(
|
|
lower, higher,
|
|
sequenceCombinationPolicy(sequenceModeUpsert, "id"),
|
|
sequenceCombinationContext{destination: Keys("records")},
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
application := historyApplication{
|
|
layer: LayerOverlay, value: higherRoot,
|
|
sequences: []sequenceHistoryContribution{{
|
|
path: Keys("records"), mode: sequenceHistoryMapped,
|
|
items: combined.higherItems, replacements: combined.replacements,
|
|
}},
|
|
}
|
|
application.sequencesByPath = indexSequenceHistoryContributions(application.sequences)
|
|
clonedApplications := cloneHistoryApplications([]historyApplication{application})
|
|
clonedApplications[0].sequences[0].items[0].outputIndex = 99
|
|
clonedApplications[0].sequences[0].replacements[0].outputIndex = 99
|
|
if application.sequences[0].items[0].outputIndex == 99 || application.sequences[0].replacements[0].outputIndex == 99 {
|
|
t.Fatal("cloned mapped history aliases its source projections")
|
|
}
|
|
|
|
query := Keys("records").Index(2).Key("nested").Key("removed")
|
|
operationPath, relevant := applicationOperationPath(application, query, nil)
|
|
if !relevant || operationPath.String() != "/records/2" {
|
|
t.Fatalf("removed descendant operation = %s, %v", operationPath.String(), relevant)
|
|
}
|
|
localPath, ok := historyApplicationLocalPath(application, Keys("records").Index(2).Key("nested").Key("kept"))
|
|
if !ok || localPath.String() != "/records/0/nested/kept" {
|
|
t.Fatalf("mapped effective path localizes to %s, %v", localPath.String(), ok)
|
|
}
|
|
origin := historyApplicationOrigin(application, operationPath, nil)
|
|
if len(origin.Locations) != 1 || origin.Locations[0].SourceID != "higher" {
|
|
t.Fatalf("replacement operation origin = %#v", origin)
|
|
}
|
|
var walkedMappedPath bool
|
|
walkHistoryApplication(application, func(local, effective Path, _ *document.Node) {
|
|
if local.String() == "/records/0/nested/kept" && effective.String() == "/records/2/nested/kept" {
|
|
walkedMappedPath = true
|
|
}
|
|
})
|
|
if !walkedMappedPath {
|
|
t.Fatal("history walk did not project a noncontiguous incoming item")
|
|
}
|
|
|
|
replayed := document.Clone(lowerRoot)
|
|
applyHistoryApplication(&replayed, application, new(mappingIndex))
|
|
sequence, _ := documentAtPath(replayed, Keys("records"))
|
|
if _, present := documentAtPath(sequence.Sequence[2], Keys("nested", "removed")); present {
|
|
t.Fatal("whole-record replacement retained an omitted descendant")
|
|
}
|
|
if value, present := documentAtPath(sequence.Sequence[2], Keys("nested", "kept")); !present || value.Text != "higher" {
|
|
t.Fatalf("replacement descendant = %#v, %v", value, present)
|
|
}
|
|
if sequence.Sequence[1].Mapping[0].Value.Text != "middle" {
|
|
t.Fatal("unchanged neighboring record moved or changed")
|
|
}
|
|
if value, present := documentAtPath(lowerRoot, Keys("records").Index(2).Key("nested").Key("removed")); !present || value.Text != "LOWER-CANARY" {
|
|
t.Fatalf("displaced lower contribution was not retained as raw history: %#v, %v", value, present)
|
|
}
|
|
if got := application.sequences[0].replacements; !reflect.DeepEqual(got, combined.replacements) || len(got) != 2 {
|
|
t.Fatalf("replacement evidence lost equal-valued or noncontiguous events: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestHistorySourceSequenceApplicationsUseAssignedCoordinates(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
policy := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{
|
|
selector: sequenceIdentitySelector{literalSequenceSelector("collections"), anyMappingSequenceSelector(), literalSequenceSelector("records")},
|
|
identityFields: []string{"id"},
|
|
}})
|
|
first := parseCompositionDocument(t, "source-1", "collections: {north: {records: [{id: a, value: first}]}}\n")
|
|
second := parseCompositionDocument(t, "source-2", "collections: {north: {records: [{id: b, value: second}]}}\n")
|
|
composed, diagnostics := composeDocumentsWithPolicy(policy, first, second)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("compose diagnostics = %#v", diagnostics)
|
|
}
|
|
builder := newHistoryBuilder()
|
|
builder.observeSourceEvidence(composed.root, composed.origins, composed.applications, composed.overlays)
|
|
model := builder.freeze()
|
|
|
|
appendedPath := Keys("collections", "north", "records").Index(1).Key("value")
|
|
selections := model.selectHistory(appendedPath)
|
|
foundSecond := false
|
|
for _, selection := range selections {
|
|
if selection.role == historyRoleContribution && selection.attribution.origin.Locations[0].SourceID == "source-2" {
|
|
foundSecond = true
|
|
}
|
|
}
|
|
if !foundSecond {
|
|
t.Fatalf("appended source was not attributed at its assigned index: %#v", selections)
|
|
}
|
|
for _, selection := range model.selectHistory(Keys("collections", "north", "records").Index(0).Key("value")) {
|
|
if selection.role == historyRoleContribution && selection.attribution.origin.Locations[0].SourceID == "source-2" {
|
|
t.Fatalf("later source acquired an earlier append index: %#v", selection)
|
|
}
|
|
}
|
|
local, ok := historyApplicationLocalPath(composed.applications[1], appendedPath)
|
|
if !ok || local.String() != "/collections/north/records/0/value" {
|
|
t.Fatalf("assigned append path localizes to %s, %v", local.String(), ok)
|
|
}
|
|
}
|
|
|
|
func TestHistorySourceSequenceReplacementTracksMappedAndWholeSides(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
lowerRoot := parseCompositionDocument(t, "lower", `records:
|
|
- id: first
|
|
- id: middle
|
|
- id: last
|
|
nested: {removed: lower}
|
|
`)
|
|
higherRoot := parseCompositionDocument(t, "higher", `records:
|
|
- id: last
|
|
nested: {kept: higher}
|
|
- id: first
|
|
- id: added
|
|
`)
|
|
lower, _ := documentAtPath(lowerRoot, Keys("records"))
|
|
higher, _ := documentAtPath(higherRoot, Keys("records"))
|
|
combined, diagnostics := combineSequence(
|
|
lower, higher, sequenceCombinationPolicy(sequenceModeUpsert, "id"),
|
|
sequenceCombinationContext{destination: Keys("records")},
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
finalRoot := document.Clone(lowerRoot)
|
|
finalSequence, _ := documentAtPath(finalRoot, Keys("records"))
|
|
*finalSequence = *document.Clone(combined.value)
|
|
lowerOrigins := make(map[string]Origin)
|
|
collectOverlayOrigins(lowerOrigins, lowerRoot, Path{}, LayerMain)
|
|
higherOrigins := make(map[string]Origin)
|
|
collectOverlayOrigins(higherOrigins, higherRoot, Path{}, LayerOverlay)
|
|
assembly := newAssemblyAccumulator(&composedDocument{root: finalRoot, origins: lowerOrigins})
|
|
applyOverlaySequenceEvidence(
|
|
assembly, Keys("records"), lower, higher, combined,
|
|
collectionPolicyResolution{operation: sequenceModeUpsert}, higherOrigins, collectionPolicyScope{}, LayerOverlay,
|
|
)
|
|
higherApplication := historyApplication{
|
|
layer: LayerOverlay, value: higherRoot,
|
|
sequences: []sequenceHistoryContribution{{
|
|
path: Keys("records"), mode: sequenceHistoryMapped,
|
|
items: combined.higherItems, replacements: combined.replacements,
|
|
}},
|
|
}
|
|
higherApplication.sequencesByPath = indexSequenceHistoryContributions(higherApplication.sequences)
|
|
applications := []historyApplication{
|
|
{layer: LayerMain, value: lowerRoot}, higherApplication,
|
|
}
|
|
builder := newHistoryBuilder()
|
|
builder.observeSourceEvidence(finalRoot, assembly.current.origins, applications, assembly.current.overlays)
|
|
model := builder.freeze()
|
|
|
|
removedPath := Keys("records").Index(2).Key("nested").Key("removed")
|
|
removed := model.selectHistory(removedPath)
|
|
var lowerContribution, removal bool
|
|
for _, selection := range removed {
|
|
if selection.role == historyRoleContribution && selection.attribution.origin.Locations[0].SourceID == "lower" {
|
|
lowerContribution = true
|
|
}
|
|
if selection.role == historyRoleOperation {
|
|
removal = selection.before && !selection.after
|
|
}
|
|
}
|
|
if !lowerContribution || !removal {
|
|
t.Fatalf("omitted descendant evidence = %#v", removed)
|
|
}
|
|
|
|
introducedPath := Keys("records").Index(2).Key("nested").Key("kept")
|
|
introduced := model.selectHistory(introducedPath)
|
|
var higherContribution, introduction bool
|
|
for _, selection := range introduced {
|
|
if selection.role == historyRoleContribution && selection.attribution.origin.Locations[0].SourceID == "higher" {
|
|
higherContribution = true
|
|
}
|
|
if selection.role == historyRoleOperation {
|
|
introduction = !selection.before && selection.after
|
|
}
|
|
}
|
|
if !higherContribution || !introduction {
|
|
t.Fatalf("mapped introduced descendant evidence = %#v", introduced)
|
|
}
|
|
if local, ok := historyApplicationLocalPath(higherApplication, introducedPath); !ok || local.String() != "/records/0/nested/kept" {
|
|
t.Fatalf("mapped path localizes to %s, %v", local.String(), ok)
|
|
}
|
|
if selections := model.selectHistory(Keys("records").Index(2).Key("nested").Key("removed")); len(selections) > 0 {
|
|
for _, selection := range selections {
|
|
if selection.role == historyRoleContribution && selection.attribution.origin.Locations[0].SourceID == "higher" {
|
|
t.Fatalf("unaddressed source acquired removed-record attribution: %#v", selections)
|
|
}
|
|
}
|
|
}
|
|
|
|
wholeLower := parseCompositionDocument(t, "whole-lower", "records: [{id: old, nested: {removed: old}}]\n")
|
|
wholeHigher := parseCompositionDocument(t, "whole-higher", "records: [{id: new, nested: {added: new}}]\n")
|
|
wholeLowerSequence, _ := documentAtPath(wholeLower, Keys("records"))
|
|
wholeHigherSequence, _ := documentAtPath(wholeHigher, Keys("records"))
|
|
whole, diagnostics := combineSequence(
|
|
wholeLowerSequence, wholeHigherSequence, sequenceCombinationPolicy(sequenceModeReplace),
|
|
sequenceCombinationContext{destination: Keys("records")},
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
wholeFinal := document.Clone(wholeLower)
|
|
wholeFinalSequence, _ := documentAtPath(wholeFinal, Keys("records"))
|
|
*wholeFinalSequence = *document.Clone(whole.value)
|
|
wholeLowerOrigins := make(map[string]Origin)
|
|
collectOverlayOrigins(wholeLowerOrigins, wholeLower, Path{}, LayerMain)
|
|
wholeHigherOrigins := make(map[string]Origin)
|
|
collectOverlayOrigins(wholeHigherOrigins, wholeHigher, Path{}, LayerOverlay)
|
|
wholeAssembly := newAssemblyAccumulator(&composedDocument{root: wholeFinal, origins: wholeLowerOrigins})
|
|
applyOverlaySequenceEvidence(
|
|
wholeAssembly, Keys("records"), wholeLowerSequence, wholeHigherSequence, whole,
|
|
collectionPolicyResolution{operation: sequenceModeReplace}, wholeHigherOrigins, collectionPolicyScope{}, LayerOverlay,
|
|
)
|
|
wholeHigherApplication := historyApplication{
|
|
layer: LayerOverlay, value: wholeHigher,
|
|
sequences: []sequenceHistoryContribution{sequenceHistoryContributionForCombination(Keys("records"), sequenceModeReplace, wholeHigherSequence, whole)},
|
|
}
|
|
wholeHigherApplication.sequencesByPath = indexSequenceHistoryContributions(wholeHigherApplication.sequences)
|
|
wholeBuilder := newHistoryBuilder()
|
|
wholeBuilder.observeSourceEvidence(wholeFinal, wholeAssembly.current.origins, []historyApplication{
|
|
{layer: LayerMain, value: wholeLower}, wholeHigherApplication,
|
|
}, wholeAssembly.current.overlays)
|
|
wholeModel := wholeBuilder.freeze()
|
|
if len(wholeAssembly.current.overlays) != 1 {
|
|
t.Fatalf("whole-sequence replacement records = %#v", wholeAssembly.current.overlays)
|
|
}
|
|
wholeIntroductions := wholeAssembly.current.overlays[0].introduced
|
|
if len(wholeIntroductions) != 5 {
|
|
t.Fatalf("whole-sequence introduced paths = %#v", wholeIntroductions)
|
|
}
|
|
var introducedNested bool
|
|
for _, fact := range wholeIntroductions {
|
|
if sameConcreteHistoryPath(fact.path, Keys("records").Index(0).Key("nested").Key("added")) {
|
|
introducedNested = true
|
|
}
|
|
}
|
|
if !introducedNested {
|
|
t.Fatalf("whole-sequence nested introduction missing: %#v", wholeIntroductions)
|
|
}
|
|
oldWholePath := Keys("records").Index(0).Key("nested").Key("removed")
|
|
newWholePath := Keys("records").Index(0).Key("nested").Key("added")
|
|
oldWhole := wholeModel.selectHistory(oldWholePath)
|
|
newWhole := wholeModel.selectHistory(newWholePath)
|
|
if len(oldWhole) < 2 || oldWhole[len(oldWhole)-1].role != historyRoleOperation || !oldWhole[len(oldWhole)-1].before || oldWhole[len(oldWhole)-1].after {
|
|
t.Fatalf("whole-sequence removed descendant selections = %#v", oldWhole)
|
|
}
|
|
if len(newWhole) < 2 || newWhole[len(newWhole)-1].role != historyRoleOperation || newWhole[len(newWhole)-1].before || !newWhole[len(newWhole)-1].after {
|
|
t.Fatalf("whole-sequence introduced descendant selections = %#v", newWhole)
|
|
}
|
|
}
|
|
|
|
func mustParsePath(t *testing.T, value string) Path {
|
|
t.Helper()
|
|
path, err := ParsePath(value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|