diff --git a/pkg/tree/api/leaf_variants.go b/pkg/tree/api/leaf_variants.go index 0a3d84a5..8558e30b 100644 --- a/pkg/tree/api/leaf_variants.go +++ b/pkg/tree/api/leaf_variants.go @@ -233,9 +233,12 @@ func (lv *LeafVariants) RemainsToExist() bool { defaultOrRunningExists = true continue } - // if an entry exists that does not have the delete flag set, - // then a remaining LeafVariant exists. - if !l.GetDeleteFlag() { + // If an entry exists that does not have the delete flag set, OR is + // only being removed from the intended store (orphan delete: the + // device value stays unchanged), then a remaining LeafVariant exists. + // This keeps RemainsToExist() consistent with CanDelete()/ShouldDelete() + // which already treat DeleteOnlyIntended as "stays on device". + if !l.GetDeleteFlag() || l.GetDeleteOnlyIntendedFlag() { return true } deleteExists = true diff --git a/pkg/tree/api/leaf_variants_test.go b/pkg/tree/api/leaf_variants_test.go index 4e8b865d..574fc994 100644 --- a/pkg/tree/api/leaf_variants_test.go +++ b/pkg/tree/api/leaf_variants_test.go @@ -274,6 +274,51 @@ func TestLeafVariants_remainsToExist(t *testing.T) { }, expected: false, }, + { + // Orphan delete: the only owner is being removed from the intended + // store but the device value stays. Running stays. The entry must + // be reported as remaining so that mandatory/leafref validation + // does not falsely fire on paths that the device will still keep. + name: "Orphan delete with running", + setup: func() *LeafVariants { + lv := &LeafVariants{ + les: make(LeafVariantSlice, 0), + } + lerun := NewLeafEntry( + types.NewUpdate(&mockUpdateParent{}, &sdcpb.TypedValue{}, RunningValuesPrio, RunningIntentName, 0), + types.NewUpdateInsertFlags(), + nil, + ) + ledel := NewLeafEntry( + types.NewUpdate(&mockUpdateParent{}, &sdcpb.TypedValue{}, 10, "owner1", 0), + types.NewUpdateInsertFlags().SetDeleteFlag().SetDeleteOnlyUpdatedFlag(), + nil, + ) + lv.Add(lerun) + lv.Add(ledel) + return lv + }, + expected: true, + }, + { + // Same as above but no running. The intent is being orphan-deleted + // (only-intended), and there's no other variant to fall back to. + // The device still has the value, so the entry remains. + name: "Orphan delete without running", + setup: func() *LeafVariants { + lv := &LeafVariants{ + les: make(LeafVariantSlice, 0), + } + ledel := NewLeafEntry( + types.NewUpdate(&mockUpdateParent{}, &sdcpb.TypedValue{}, 10, "owner1", 0), + types.NewUpdateInsertFlags().SetDeleteFlag().SetDeleteOnlyUpdatedFlag(), + nil, + ) + lv.Add(ledel) + return lv + }, + expected: true, + }, } for _, tt := range tests {