From 552e7e92ca83b91a4dbc23e9960c5615de7eeba5 Mon Sep 17 00:00:00 2001 From: steiler Date: Fri, 22 May 2026 13:51:56 +0200 Subject: [PATCH] fix(tree): align RemainsToExist with orphan-only-intended deletes RemainsToExist ignored DeleteOnlyIntended, so mandatory validation treated orphan-deleted leaves as removed and rejected transactions. Match CanDelete/ShouldDelete and add regression tests. Co-authored-by: Cursor --- pkg/tree/api/leaf_variants.go | 9 ++++-- pkg/tree/api/leaf_variants_test.go | 45 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) 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 {