fix: normalize configured_scope to #-prefixed form like default_scope

cwichura flagged on PR #1867 that nodes.configured_scope (from the
/neighbors OTA report) was stored as the observer's raw scope string
("dk") while default_scope and every other scope display use the
#-prefixed hashRegion form ("#dk"). Apply the same regions.Normalize
already used for default_scope, with "*" passed through unprefixed
since it's a protocol wildcard, not a region name.
This commit is contained in:
dborup
2026-07-26 16:03:38 +02:00
parent bcb190bffa
commit a7f4a85009
2 changed files with 47 additions and 11 deletions
+36
View File
@@ -16,6 +16,7 @@ import (
"github.com/meshcore-analyzer/dbschema"
"github.com/meshcore-analyzer/packetpath"
"github.com/meshcore-analyzer/regions"
_ "modernc.org/sqlite"
)
@@ -1729,6 +1730,7 @@ func (s *Store) UpdateNodeConfiguredScope(pubkey, scope, reportedAt string) erro
if pubkey == "" {
return nil
}
scope = normalizeConfiguredScopeList(scope)
// Last-write-wins: skip if the stored confirmation is newer-or-equal.
if reportedAt != "" {
var curAt sql.NullString
@@ -1749,6 +1751,40 @@ func (s *Store) UpdateNodeConfiguredScope(pubkey, scope, reportedAt string) erro
return err
}
// normalizeConfiguredScopeList applies the same "#"-prefix normalization
// default_scope already gets (regions.Normalize, via matchScope) to a
// comma-separated /neighbors-report scope list, so both fields display
// consistently (flagged by cwichura on PR #1867: configured_scope was
// stored as the observer's raw OTA-query string -- "dk", not "#dk").
//
// "*" is passed through UNCHANGED: it's a MeshCore protocol wildcard
// ("responds to any scope"), not a real hashRegion name, so "#"-prefixing
// it would misleadingly present it as one. Empty input stays empty -- a
// responded-with-no-scopes report is a valid, meaningful "none configured"
// state (see UpdateNodeConfiguredScope's doc comment) and must not gain a
// stray "#".
func normalizeConfiguredScopeList(raw string) string {
if raw == "" {
return ""
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if p == "*" {
out = append(out, p)
continue
}
if name, ok := regions.Normalize(p); ok {
out = append(out, name)
}
}
return strings.Join(out, ",")
}
// RecordNaiveSkew is called when resolveRxTime() clamps a packet's envelope
// timestamp because the observer is emitting a zone-less local-time string
// off from UTC by more than 15 min (issue #1478). Stamps the observer's
+11 -11
View File
@@ -77,13 +77,13 @@ func TestHandleNeighborsReportWritesSelfAndResponded(t *testing.T) {
if sc, _ := configuredScope(t, store, originLower); !sc.Valid || sc.String != "*" {
t.Errorf("self configured_scope = %v, want '*'", sc)
}
// responded neighbor written, lowercased.
if sc, _ := configuredScope(t, store, respLower); !sc.Valid || sc.String != "de,eu" {
t.Errorf("responded configured_scope = %v, want 'de,eu'", sc)
// responded neighbor written, lowercased, and normalized to #-prefixed scope form.
if sc, _ := configuredScope(t, store, respLower); !sc.Valid || sc.String != "#de,#eu" {
t.Errorf("responded configured_scope = %v, want '#de,#eu'", sc)
}
// timeout neighbor untouched — prior confirmed value survives.
if sc, _ := configuredScope(t, store, timeoutLower); !sc.Valid || sc.String != "eu" {
t.Errorf("timeout node configured_scope = %v, want prior 'eu' (must not be cleared)", sc)
if sc, _ := configuredScope(t, store, timeoutLower); !sc.Valid || sc.String != "#eu" {
t.Errorf("timeout node configured_scope = %v, want prior '#eu' (must not be cleared)", sc)
}
}
@@ -139,22 +139,22 @@ func TestUpdateNodeConfiguredScopeLastWriteWins(t *testing.T) {
if err := store.UpdateNodeConfiguredScope(pk, "stale", "2026-07-24T00:00:00Z"); err != nil {
t.Fatal(err)
}
if sc, _ := configuredScope(t, store, pk); sc.String != "eu" {
t.Errorf("configured_scope = %q, want 'eu' (older report must not overwrite)", sc.String)
if sc, _ := configuredScope(t, store, pk); sc.String != "#eu" {
t.Errorf("configured_scope = %q, want '#eu' (older report must not overwrite)", sc.String)
}
// Newer report updates.
if err := store.UpdateNodeConfiguredScope(pk, "de", "2026-07-26T00:00:00Z"); err != nil {
t.Fatal(err)
}
if sc, _ := configuredScope(t, store, pk); sc.String != "de" {
t.Errorf("configured_scope = %q, want 'de' (newer report should update)", sc.String)
if sc, _ := configuredScope(t, store, pk); sc.String != "#de" {
t.Errorf("configured_scope = %q, want '#de' (newer report should update)", sc.String)
}
// inactive_nodes mirrored.
var inactive sql.NullString
if err := store.db.QueryRow(`SELECT configured_scope FROM inactive_nodes WHERE public_key = ?`, pk).Scan(&inactive); err != nil {
t.Fatal(err)
}
if inactive.String != "de" {
t.Errorf("inactive_nodes.configured_scope = %q, want 'de'", inactive.String)
if inactive.String != "#de" {
t.Errorf("inactive_nodes.configured_scope = %q, want '#de'", inactive.String)
}
}