fix(node): don't stamp InboundsAdoptedAt when the sync adopted nothing (#6284)

* fix(node): don't stamp InboundsAdoptedAt when the sync adopted nothing

Onboarding a node in selected mode with an empty tag list empties the
traffic snapshot via FilterNodeSnapshot before the merge sees it, so
the first clean sync adopts nothing — yet syncOne stamped
InboundsAdoptedAt regardless. The flag is documented as the first
clean sync that imported the node's pre-existing inbounds; stamping it
in this state arms the reconcile sweep (gated on the flag since
200ea091, the fix for #5898) to delete the node's pre-existing
inbounds on their next real sync: registering first and choosing tags
afterwards destroyed the node's inbounds.

Gate the stamp on the sync actually being able to adopt: in selected
mode, at least one selected tag or adopted alias must exist for the
snapshot filter to keep anything.

Fixes #6283

* restore atomicBool tests; trim comment to repo 2-line cap

The new test file unintentionally replaced the existing
node_traffic_sync_job_test.go, dropping its four atomicBool tests;
restore them and keep only an additive diff. Trim the syncCanAdopt
doc comment to the repository's 2-line comment cap.

* trim syncCanAdopt comment to the 2-line cap
This commit is contained in:
yzxcj797
2026-08-23 02:37:55 +08:00
committed by GitHub
parent af3e6c11b6
commit a255ab7c65
2 changed files with 54 additions and 1 deletions
@@ -3,6 +3,8 @@ package job
import (
"sync"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestAtomicBool_DefaultIsFalse(t *testing.T) {
@@ -67,3 +69,44 @@ func TestAtomicBool_ConcurrentSettersExactlyOneTakeWins(t *testing.T) {
t.Fatalf("expected exactly one reader to observe true, got %d", trueCount)
}
}
// Regression (#6283): a node onboarded in selected mode with an empty tag
// list empties its snapshot via FilterNodeSnapshot before the merge sees it,
// so that sync adopts nothing and must not stamp InboundsAdoptedAt.
func TestSyncCanAdoptInbounds(t *testing.T) {
cases := []struct {
name string
node *model.Node
aliases []string
expected bool
}{
{"all mode always adopts", &model.Node{InboundSyncMode: "all"}, nil, true},
{
"selected with tags adopts",
&model.Node{InboundSyncMode: "selected", InboundTags: []string{"in-443-tcp"}},
nil,
true,
},
{
"selected empty with adopted alias adopts",
&model.Node{InboundSyncMode: "selected"},
[]string{"in-443-tcp"},
true,
},
{
// The reported bug: registering in selected mode and choosing
// tags afterwards stamped adoption while adopting nothing.
"selected empty with no aliases adopts nothing",
&model.Node{InboundSyncMode: "selected"},
nil,
false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := syncCanAdoptInbounds(c.node, c.aliases); got != c.expected {
t.Fatalf("syncCanAdoptInbounds(%+v, %v) = %v, want %v", c.node, c.aliases, got, c.expected)
}
})
}
}