package engine import ( "reflect" "slices" "testing" "go.maximumdirect.net/configkit/internal/document" ) func TestSensitivityCandidatesMergeRepeatedParsedPaths(t *testing.T) { t.Parallel() builder := newHistoryBuilder() key := Keys("shape", "0") index := Keys("shape").Index(0) parsed := parseOverrideTestPath(t, "/shape/0") dependency := parseOverrideTestPath(t, "/dependency") attribution := builder.addAttribution(Origin{Layer: LayerResolution}, nil, []Path{dependency}) payload := builder.nextPayload(historyPhaseResolution) for repeat := 0; repeat < 3; repeat++ { for _, path := range []Path{key, index} { builder.addObservation(historyObservation{ path: path, phase: historyPhaseResolution, payload: payload, reportedPath: Keys("shape"), role: historyRoleOperation, afterPresent: true, sensitive: repeat == 2, attributionRef: attribution, }) } } model := builder.freeze() _, inherited := model.sensitivityDependencies() candidates := model.sensitivityCandidates(inherited) for _, path := range []Path{key, index, parsed, dependency} { count := 0 for _, candidate := range candidates { if slices.Equal(candidate.path.segments, path.segments) { count++ if path.String() == "/shape/0" && !candidate.sensitive { t.Fatal("merged candidate lost a later sensitivity fact") } } } if count != 1 { t.Fatalf("semantic candidate %#v occurs %d times; want one", path, count) } } } func TestHistorySelectionSeparatesNumericShapesAndGroupsParsedSides(t *testing.T) { t.Parallel() builder := newHistoryBuilder() mapping := parseDocumentForDecode(t, "shape: {'0': old}\n") sequence := parseDocumentForDecode(t, "shape: [new]\n") shapeNode, _ := documentAtPath(mapping, Keys("shape")) shapeNode.Sensitive = true builder.observeDocument(mapping, historyPhaseSources) builder.observeDocument(mapping, historyPhaseBaseline) builder.observeDocument(sequence, historyPhaseResolution) baseOrigin := Origin{Layer: LayerMain} baseAttribution := builder.addAttribution(baseOrigin, nil, nil) keyPath := Keys("shape", "0") indexPath := Keys("shape").Index(0) definition := builder.nextPayload(historyPhaseSources) builder.addObservation(historyObservation{ path: keyPath, phase: historyPhaseSources, payload: definition, reportedPath: keyPath, role: historyRoleDefinition, afterPresent: true, attributionRef: baseAttribution, }) replacementAttribution := builder.addAttribution( Origin{Layer: LayerResolution, Operation: ChangeNormalization}, []string{"replace dynamic shape"}, []Path{Keys("shape")}, ) replacement := builder.nextPayload(historyPhaseResolution) for _, observation := range []historyObservation{ { path: keyPath, phase: historyPhaseResolution, payload: replacement, reportedPath: Keys("shape"), role: historyRoleOperation, beforePresent: true, attributionRef: replacementAttribution, }, { path: indexPath, phase: historyPhaseResolution, payload: replacement, reportedPath: Keys("shape"), role: historyRoleOperation, afterPresent: true, attributionRef: replacementAttribution, }, } { if got := builder.addObservation(observation); got < 0 { t.Fatal("failed to add concrete history observation") } } model := builder.freeze() shapes := model.concreteShapes("/shape/0") if len(shapes) != 2 || shapes[0].segments[1].kind != pathSegmentKey || shapes[1].segments[1].kind != pathSegmentIndex { t.Fatalf("concrete shapes = %#v", shapes) } if !reflect.DeepEqual(model.observedPaths()[:2], []Path{Path{}, Keys("shape")}) { t.Fatalf("first observed paths = %#v", model.observedPaths()[:2]) } historicalPaths := model.historicalPaths() var historicalPointers []string for _, path := range historicalPaths { historicalPointers = append(historicalPointers, path.String()) } if !reflect.DeepEqual(historicalPointers, []string{"", "/shape", "/shape/0"}) { t.Fatalf("historical paths = %#v", historicalPointers) } if len(model.concreteShapes("/shape/0")) != 2 { t.Fatalf("repeated inventory added a duplicate concrete shape: %#v", model.concreteShapes("/shape/0")) } foundSensitive := false for _, observation := range model.observations { if observation.role == historyRoleInventory && observation.path.String() == "/shape" && observation.sensitive { foundSensitive = true break } } if !foundSensitive { t.Fatal("sensitive document fact was not retained with inventory evidence") } keySelections := model.selectHistory(keyPath) if len(keySelections) != 2 || keySelections[0].role != historyRoleDefinition || keySelections[1].before != true || keySelections[1].after { t.Fatalf("mapping-key selections = %#v", keySelections) } if len(keySelections[1].previous) != 1 || keySelections[1].previous[0].Layer != LayerMain { t.Fatalf("mapping-key previous origins = %#v", keySelections[1].previous) } indexSelections := model.selectHistory(indexPath) if len(indexSelections) != 1 || indexSelections[0].before || !indexSelections[0].after || len(indexSelections[0].previous) != 0 { t.Fatalf("sequence-index selections = %#v", indexSelections) } parsed, err := ParsePath("/shape/0") if err != nil { t.Fatal(err) } parsedSelections := model.selectHistory(parsed) if len(parsedSelections) != 2 || !parsedSelections[1].before || !parsedSelections[1].after { t.Fatalf("parsed-path selections = %#v", parsedSelections) } if !reflect.DeepEqual(parsedSelections[1].attribution.reasons, []string{"replace dynamic shape"}) || len(parsedSelections[1].attribution.dependencies) != 1 || parsedSelections[1].attribution.dependencies[0].String() != "/shape" { t.Fatalf("resolved attribution = %#v", parsedSelections[1].attribution) } parsedSelections[1].attribution.reasons[0] = "mutated" parsedSelections[1].attribution.dependencies[0] = Path{} again := model.selectHistory(parsed) if again[1].attribution.reasons[0] != "replace dynamic shape" || again[1].attribution.dependencies[0].String() != "/shape" { t.Fatalf("selection mutation changed model attribution: %#v", again[1].attribution) } if selections := model.selectHistory(Keys("shape").Index(9)); len(selections) != 0 { t.Fatalf("never-observed index selections = %#v", selections) } } func TestHistorySelectionMatchesDeepSidesOfAncestorShapeChange(t *testing.T) { t.Parallel() builder := newHistoryBuilder() builder.observeDocument(parseDocumentForDecode(t, "shape: {'0': {leaf: old}}\n"), historyPhaseSources) builder.observeDocument(parseDocumentForDecode(t, "shape: [{leaf: new}]\n"), historyPhaseResolution) base := builder.addAttribution(Origin{Layer: LayerMain}, nil, nil) oldPath := Keys("shape", "0", "leaf") newPath := Keys("shape").Index(0).Key("leaf") definition := builder.nextPayload(historyPhaseSources) builder.addObservation(historyObservation{ path: oldPath, phase: historyPhaseSources, payload: definition, reportedPath: oldPath, role: historyRoleDefinition, afterPresent: true, attributionRef: base, }) operation := builder.nextPayload(historyPhaseResolution) operationAttribution := builder.addAttribution(Origin{Layer: LayerResolution, Operation: ChangeNormalization}, nil, nil) for _, observation := range []historyObservation{ {path: oldPath, phase: historyPhaseResolution, payload: operation, reportedPath: Keys("shape"), role: historyRoleOperation, beforePresent: true, attributionRef: operationAttribution}, {path: newPath, phase: historyPhaseResolution, payload: operation, reportedPath: Keys("shape"), role: historyRoleOperation, afterPresent: true, attributionRef: operationAttribution}, } { builder.addObservation(observation) } model := builder.freeze() newSelections := model.selectHistory(newPath) if len(newSelections) != 1 || newSelections[0].role != historyRoleOperation || newSelections[0].before || !newSelections[0].after || len(newSelections[0].previous) != 0 { t.Fatalf("new descendant selections = %#v", newSelections) } oldSelections := model.selectHistory(oldPath) if len(oldSelections) != 2 || !oldSelections[1].before || oldSelections[1].after || len(oldSelections[1].previous) != 1 || oldSelections[1].previous[0].Layer != LayerMain { t.Fatalf("removed descendant selections = %#v", oldSelections) } if selections := model.selectHistory(Keys("shape").Index(0).Key("missing")); len(selections) != 0 { t.Fatalf("never-observed descendant selections = %#v", selections) } } func TestHistoryModelFinalizesLocationAndSensitivityWithoutRetainingInventoryTrees(t *testing.T) { t.Parallel() root := parseDocumentForDecode(t, "shape: {'0': {value: old}}\n") value, _ := documentAtPath(root, Keys("shape", "0", "value")) value.Sensitive = true keyPath := Keys("shape", "0", "value") dependency := Keys("credentials", "source") builder := newHistoryBuilder() builder.observeDocument(root, historyPhaseSources) definitionAttribution := builder.addAttribution(Origin{ Layer: LayerMain, Locations: []Location{{SourceID: "base.yml", Line: 4, Column: 2}}, }, nil, nil) definition := builder.nextPayload(historyPhaseSources) builder.addObservation(historyObservation{ path: keyPath, phase: historyPhaseSources, payload: definition, reportedPath: keyPath, role: historyRoleDefinition, afterPresent: true, attributionRef: definitionAttribution, }) resolutionAttribution := builder.addAttribution( Origin{Layer: LayerResolution, Operation: ChangeNormalization}, []string{"normalized credential"}, []Path{dependency}, ) resolution := builder.nextPayload(historyPhaseResolution) builder.addObservation(historyObservation{ path: keyPath, phase: historyPhaseResolution, payload: resolution, reportedPath: Keys("shape"), role: historyRoleOperation, beforePresent: true, afterPresent: true, sensitive: true, attributionRef: resolutionAttribution, }) model := builder.freeze() if got := model.documentRoots(); len(got) != 0 { t.Fatalf("inventory-only document was retained: %#v", got) } if got, ok := model.documentAt(root, Keys("shape", "0")); ok || got != nil { t.Fatalf("lookup retained an inventory-only document = %#v, %v", got, ok) } if got, ok := model.latestHistoricalLocation(keyPath); !ok || got.SourceID != "base.yml" || got.Line != 4 || got.Column != 2 { t.Fatalf("latest historical location = %#v, %v", got, ok) } dependencies, inherited := model.sensitivityDependencies() candidates := model.sensitivityCandidates(inherited) var sawCandidate, sawDependency bool for _, candidate := range candidates { if sameConcreteHistoryPath(candidate.path, keyPath) && candidate.sensitive { sawCandidate = true } if slices.Equal(candidate.path.segments, dependency.segments) && candidate.classify { sawDependency = true } } if !sawCandidate || !sawDependency || len(dependencies) != 1 || dependencies[0].target.String() != "/shape" || dependencies[0].dependency.String() != dependency.String() { t.Fatal("sensitivity evidence omitted the concrete historical path or its recorded dependency") } } func TestHistoryBuilderScratchUsesOneDetachedWorkingDocument(t *testing.T) { t.Parallel() input := parseDocumentForDecode(t, "shape: {value: original}\n") builder := newHistoryBuilder() builder.setScratch(input) inputValue, _ := documentAtPath(input, Keys("shape", "value")) inputValue.Text = "producer mutation" scratchValue, ok := builder.scratchAt(Keys("shape", "value")) if !ok || scratchValue.Text != "original" { t.Fatalf("scratch value after producer mutation = %#v, %v", scratchValue, ok) } next := document.Clone(builder.scratch) nextValue, _ := documentAtPath(next, Keys("shape", "value")) nextValue.Text = "replayed" builder.advanceScratch(next) scratchValue, ok = builder.scratchAt(Keys("shape", "value")) if !ok || scratchValue.Text != "replayed" { t.Fatalf("advanced scratch value = %#v, %v", scratchValue, ok) } model := builder.freeze() if !model.frozen || builder.scratch != nil { t.Fatal("freeze retained mutable scratch state") } }