package engine import ( "reflect" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestResolutionSensitivityKeepsLogicalEdgesWithoutSubtreeDuplication(t *testing.T) { t.Parallel() before := parseDocumentForDecode(t, "settings: {a: old, b: old, c: old}\ncredentials: {private: p, public: q, child: r}\nunrelated: visible\n") after := parseDocumentForDecode(t, "settings: {a: new, b: new, c: new}\n") beforeSettings, _ := documentAtPath(before, Keys("settings")) afterSettings, _ := documentAtPath(after, Keys("settings")) beforeChild, _ := documentAtPath(before, Keys("settings", "a")) afterChild, _ := documentAtPath(after, Keys("settings", "a")) first := parseOverrideTestPath(t, "/credentials/private") second := parseOverrideTestPath(t, "/credentials/public") child := parseOverrideTestPath(t, "/credentials/child") builder := newHistoryBuilder() builder.observeBaseline(before) builder.setScratch(before) builder.observeResolutionEvidence([]resolutionSnapshot{ {change: Change{Path: Keys("settings"), Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}, Dependencies: []Path{first, second}}, before: beforeSettings, after: afterSettings}, {change: Change{Path: Keys("settings", "a"), Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}, Dependencies: []Path{child}}, before: beforeChild, after: afterChild}, }) model := builder.freeze() dependencies, inherited := model.sensitivityDependencies() if len(dependencies) != 3 || len(inherited) != 0 { t.Fatalf("got %d ordinary and %d inherited edges; want the three original dependencies", len(dependencies), len(inherited)) } for index, expected := range []struct{ target, dependency string }{ {"/settings", first.String()}, {"/settings", second.String()}, {"/settings/a", child.String()}, } { if dependencies[index].target.String() != expected.target || dependencies[index].dependency.String() != expected.dependency { t.Fatalf("edge %d lost its recorded target/dependency", index) } } classified := classifySecretPaths(nil, func(path Path) bool { return path.String() == child.String() }, Metadata{historyModel: model}) if !classified["/settings/a"] || classified["/settings"] || classified["/settings/b"] || classified["/unrelated"] { t.Fatalf("original child dependency omitted by replay grouping or propagated to unrelated values: %#v", classified) } } func TestHistoryResolutionRetainsOriginalShapeAttributionBehindReplayGroup(t *testing.T) { before := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{ Key: "0", Value: &document.Node{Kind: document.String, Text: "old"}, }}} after := &document.Node{Kind: document.Sequence, Sequence: []*document.Node{{ Kind: document.String, Text: "new", Sensitive: true, }}} baseline := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{ Key: "settings", Value: before, }}} parentDependency := Keys("parent-dependency") childDependency := Keys("child-dependency") snapshots := []resolutionSnapshot{ { change: Change{ Path: Keys("settings"), Origin: Origin{Layer: LayerResolution, Operation: ChangeDerivedDefault}, Reasons: []string{"parent normalization"}, Dependencies: []Path{parentDependency}, }, before: before, after: after, }, { change: Change{ Path: parseOverrideTestPath(t, "/settings/0"), Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}, Reasons: []string{"child normalization"}, Dependencies: []Path{childDependency}, }, before: before.Mapping[0].Value, after: after.Sequence[0], }, } builder := newHistoryBuilder() builder.observeBaseline(baseline) builder.setScratch(baseline) builder.observeResolutionEvidence(snapshots) model := builder.freeze() if got := model.resolutionPayload.replayGroups; len(got) != 1 || got[0].coveringRecordID != 0 || !reflect.DeepEqual(got[0].originalRecordIDs, []int{0, 1}) || !reflect.DeepEqual(got[0].change.Reasons, []string{"parent normalization", "child normalization"}) || !reflect.DeepEqual(got[0].change.Dependencies, []Path{parentDependency, childDependency}) || got[0].change.Origin.Operation != ChangeNormalization { t.Fatalf("replay group lost original references or merged annotations: %#v", got) } if len(model.resolutionPayload.snapshots) != 2 || model.resolutionPayload.snapshots[1].change.Reasons[0] != "child normalization" || model.resolutionPayload.snapshots[1].change.Dependencies[0].String() != childDependency.String() || model.resolutionPayload.snapshots[0].before != before || model.resolutionPayload.snapshots[1].after != after.Sequence[0] { t.Fatalf("original resolution records or their roots were not retained: %#v", model.resolutionPayload.snapshots) } keyPath := Keys("settings", "0") keySelections := model.selectHistory(keyPath) if len(keySelections) != 3 || !keySelections[0].after || keySelections[1].phase != historyPhaseResolution || !keySelections[1].before || keySelections[1].after || !keySelections[2].before || keySelections[2].after { t.Fatalf("mapping-key history did not retain its removed side: %#v", keySelections) } if got := keySelections[1].previous; len(got) != 1 || got[0].Layer != LayerDefault { t.Fatalf("removed key previous origin = %#v, want baseline origin", got) } indexPath := Keys("settings").Index(0) indexSelections := model.selectHistory(indexPath) if len(indexSelections) != 2 || indexSelections[0].phase != historyPhaseResolution || !indexSelections[0].after || indexSelections[0].before || !indexSelections[1].after || indexSelections[1].before { t.Fatalf("sequence-index history did not retain introduced sides: %#v", indexSelections) } if len(indexSelections[0].previous) != 0 { t.Fatalf("introduced index inherited a different key's previous origin: %#v", indexSelections[0].previous) } if got := indexSelections[1].attribution; got.origin.Operation != ChangeNormalization || !reflect.DeepEqual(got.reasons, []string{"child normalization"}) || !reflect.DeepEqual(got.dependencies, []Path{childDependency}) { t.Fatalf("covered original attribution = %#v", got) } foundSensitive := false for _, observation := range model.observations { if observation.phase == historyPhaseResolution && sameConcreteHistoryPath(observation.path, indexPath) && observation.sensitive { foundSensitive = true } } if !foundSensitive { t.Fatal("original resolution subtree sensitivity was omitted from observations") } } func TestMetadataFinalizationRegistersResolutionInventoryAndRoots(t *testing.T) { t.Parallel() beforeShape := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{ Key: "0", Value: &document.Node{Kind: document.String, Text: "old"}, }}} afterShape := &document.Node{Kind: document.Sequence, Sequence: []*document.Node{{ Kind: document.String, Text: "new", Sensitive: true, }}} before := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "shape", Value: beforeShape}}} final := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "shape", Value: afterShape}}} changePath := parseOverrideTestPath(t, "/shape/0") snapshot := resolutionSnapshot{ change: Change{ Path: changePath, Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}, Reasons: []string{"changed dynamic shape"}, Dependencies: []Path{Keys("source")}, }, before: beforeShape.Mapping[0].Value, after: afterShape.Sequence[0], } parentSnapshot := resolutionSnapshot{ change: Change{ Path: Keys("shape"), Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}, Reasons: []string{"replaced container"}, }, before: beforeShape, after: afterShape, } var metadata Metadata snapshots := []resolutionSnapshot{parentSnapshot, snapshot} builder := newHistoryBuilder() builder.setScratch(before) builder.observeDocument(before, historyPhaseBaseline) builder.observeDocument(final, historyPhasePostCLI) builder.prefixReady = true builder.observeResolutionEvidence(snapshots) metadata.historyBuilder = builder freezeMetadataHistory(&metadata, final) if got := metadata.HistoricalPaths(); len(got) != 3 || got[0].String() != "" || got[1].String() != "/shape" || got[2].String() != "/shape/0" { t.Fatalf("historical paths = %#v", got) } keyPath := Keys("shape", "0") indexPath := Keys("shape").Index(0) if !metadata.hasHistoricalPath(keyPath) || !metadata.hasHistoricalPath(indexPath) { t.Fatalf("resolution shape inventory omitted key or index: %#v", metadata.HistoricalPaths()) } if got := metadata.historyModel.selectHistory(keyPath); len(got) != 2 || !got[0].before || got[0].after || !got[1].before || got[1].after { t.Fatalf("removed mapping-key resolution selection = %#v", got) } if got := metadata.historyModel.selectHistory(indexPath); len(got) != 2 || got[0].before || !got[0].after || got[1].before || !got[1].after { t.Fatalf("introduced sequence-index resolution selection = %#v", got) } var retainedBefore, retainedAfter bool for _, root := range metadata.historyModel.documentRoots() { retainedBefore = retainedBefore || root == snapshot.before retainedAfter = retainedAfter || root == snapshot.after } if !retainedBefore || !retainedAfter { t.Fatalf("resolution roots not enumerated: before=%v after=%v", retainedBefore, retainedAfter) } }