test(nodes): regionFallback.maxHops sets the fallback hop cap (#1879)

Review on #2190 asked for the 3-hop cap as a config value. Tests for the
accessor (unset or negative keeps 3, 0 is allowed, parsed from JSON) and
for the recompute honoring it.

Stub RegionFallbackConfig and an accessor that ignores it; fails on
assertions.
This commit is contained in:
Adam Nieto committed 2026-10-15 08:22:47 -05:00
1 parent 261178feb0
commit 862a4510c1
2 files changed
+55

No files matched your search

+14
View File
@@ -109,6 +109,10 @@ type Config struct {
// NormalizedRegionQuickPicks and config.example.json.
RegionQuickPicks []RegionQuickPick `json:"regionQuickPicks,omitempty"`
// RegionFallback tunes how the /api/nodes region filter places nodes
// never heard zero-hop (#1879). See RegionFallbackMaxHops.
RegionFallback *RegionFallbackConfig `json:"regionFallback,omitempty"`
Roles map[string]interface{} `json:"roles"`
HealthThresholds *HealthThresholds `json:"healthThresholds"`
PathTrust *PathTrustConfig `json:"pathTrust,omitempty"`
@@ -1284,3 +1288,13 @@ func (c *Config) configuredChannelNames() map[string]bool {
}
return names
}
// RegionFallbackConfig is the regionFallback block (#1879).
type RegionFallbackConfig struct {
MaxHops *int `json:"maxHops,omitempty"`
}
// RegionFallbackMaxHops is a stub.
func (c *Config) RegionFallbackMaxHops() int {
return 3
}
+41
View File
@@ -238,6 +238,47 @@ func TestRegionFallbackUsesFewestHops(t *testing.T) {
}
}
// regionFallback.maxHops sets the cap (#2190 review); unset or negative
// keeps the default of 3, and 0 places only nodes heard over an empty path.
func TestRegionFallbackMaxHopsConfig(t *testing.T) {
var parsed Config
if err := json.Unmarshal([]byte(`{"regionFallback":{"maxHops":4}}`), &parsed); err != nil {
t.Fatal(err)
}
cases := []struct {
name string
cfg *Config
want int
}{
{"nil config", nil, 3},
{"unset", &Config{}, 3},
{"empty block", &Config{RegionFallback: &RegionFallbackConfig{}}, 3},
{"negative", &Config{RegionFallback: &RegionFallbackConfig{MaxHops: intPtr(-1)}}, 3},
{"zero", &Config{RegionFallback: &RegionFallbackConfig{MaxHops: intPtr(0)}}, 0},
{"from JSON", &parsed, 4},
}
for _, c := range cases {
if got := c.cfg.RegionFallbackMaxHops(); got != c.want {
t.Errorf("%s: RegionFallbackMaxHops() = %d, want %d", c.name, got, c.want)
}
}
ps := newRegionTestStore(t, map[string]string{"o-aus": "AUS"})
ps.config = &Config{RegionFallback: &RegionFallbackConfig{MaxHops: intPtr(4)}}
addToStore(ps,
mkPathAdvert(1, "pk_four", map[string]string{"o-aus": `["aa","bb","cc","dd"]`}),
mkPathAdvert(2, "pk_five", map[string]string{"o-aus": `["aa","bb","cc","dd","ee"]`}),
)
ps.recomputeNodeHomeRegionSets()
want := map[string]NodeRegionPlacement{
"pk_four": {Basis: RegionBasisFewestHops, Regions: []string{"AUS"}, Hops: intPtr(4)},
"pk_five": {Basis: RegionBasisNone, Hops: intPtr(5)},
}
if got := ps.GetNodeRegionPlacements(); !reflect.DeepEqual(got, want) {
t.Errorf("maxHops 4: placements = %s, want %s", placementsString(got), placementsString(want))
}
}
// A node with no placement yet (its first advert arrived after the last
// recompute) is decided by the observer scan: in each region whose observers
// heard any of its adverts, however many hops out.