Files
meshcore-analyzer/internal/packetpath/trust_test.go
T
efitenandClaude Opus 5 176bb53335 fix(#1784): ship pathTrust default 1, not 2 (#1929)
Follows #1841. Moves the pathTrust default from 2 back to 1.

## Why

#1784's first acceptance criterion is **"Default behaviour remains
backward-compatible"**, and its example config shows
`minHashBytesForMapping: 1`. What shipped is 2.

The problem is not the value. It is that **there is no way to undo it
from the UI.** #1841 adds no control for the threshold: it is
`config.json` only, and changing it needs a restart. The customizer
gains a hint that says exactly that. So an instance that upgrades
without touching config switches to the stricter rule, and the only
visible symptom is that the neighbour graph and the resolved paths
quietly get smaller.

The existing "Hide 1-byte path hops" toggle (#1633) is a *display*
filter and does not change what counts as evidence, so it is not an
escape hatch either. The two are easy to confuse.

## How much this actually moves

Measured on a live instance via `/api/analytics/hash-sizes`, not
estimated:

| path-hop observations | count | share |
|---|---|---|
| 1-byte prefix | 116,923 | **56.0%** |
| 2-byte prefix | 86,031 | 41.2% |
| 3-byte prefix | 5,753 | 2.8% |

| repeaters by observed hash size | count |
|---|---|
| 1-byte | **645 (41%)** |
| 2-byte | 865 |
| 3-byte | 63 |

At threshold 2 the 1-byte column stops counting as mapping evidence.
`MeetsPathTrust` also drops the legacy bucket-0 observations with it
(pre-#1638 persisted neighbor edges that carry no per-mode breakdown),
so already-stored edges lose their evidence status on upgrade too.

## What this does not change

The knob works and is untouched. Operators who want the stricter
behaviour set `minHashBytesForMapping` to 2 or 3, which is the opt-in
#1784 describes. Only the default moves. Nothing about storage changes;
packets and paths were never affected either way.

## Also fixes an inconsistency inside #1841

Five frontend consumers already fall back to **1** when
`MC_getPathTrustThreshold()` is unavailable: `analytics.js`, `live.js`,
`map.js`, `nodes.js`, `route-view.js`. Two fell back to **2**:
`hop-filter.js` (the getter itself) and `customize-v2.js`. They now all
agree.

## Tests

- `internal/packetpath`: `TestMeetsPathTrust_ZeroValueOptIn` was
asserting the old default *through behaviour*, so it would need
rewriting on any future default change. It now asserts the property
instead: an absent JSON field resolves to
`DefaultMinHashBytesForMapping`, behaves identically to naming that
value outright, and an explicit stricter setting still wins. Package
tests pass.
- `test-issue-1633-hide-1byte-hops.js`: the case pinning the getter's
default is updated, with the reasoning in a comment so the next person
sees why it is 1. **37 passed, 0 failed** (master baseline: 37 passed, 0
failed).
- `cmd/server` config tests pass.

## One thing I want to flag rather than paper over

The test I changed was named `default is 2 (operator-confirmed)`. I am
overriding something that was confirmed with an operator, and I am not
claiming that confirmation was wrong. My reading is that it was about
the threshold being a *useful* value, which it is, rather than about it
being the default in a build with no UI to change it. If the intent
really was "2 out of the box for everyone", say so and I will close
this.

@Bjorkan as the issue author, @nullrouten0 and @Saarlandpower since you
have touched adjacent code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Wzwr3eXseyNM7Xj598djjE

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 10:27:35 +02:00

152 lines
5.1 KiB
Go

package packetpath
import "testing"
func TestMeetsPathTrust_DefaultThreshold(t *testing.T) {
// nil cfg → DefaultMinHashBytesForMapping (1): everything is trusted, which
// is the pre-#1784 behaviour. This test is the guard on that promise: if the
// default is ever raised, upgrading instances silently lose a large share of
// their mapping evidence with no UI control to opt back out.
cases := []struct {
prefixBytes int
want bool
}{
{0, true},
{1, true},
{2, true},
{3, true},
}
for _, c := range cases {
if got := MeetsPathTrust(c.prefixBytes, nil); got != c.want {
t.Errorf("MeetsPathTrust(%d, nil) = %v, want %v", c.prefixBytes, got, c.want)
}
}
}
func TestMeetsPathTrust_TrustAllOptIn(t *testing.T) {
cfg := &TrustConfig{MinHashBytesForMapping: 1}
for _, prefixBytes := range []int{0, 1, 2, 3} {
if !MeetsPathTrust(prefixBytes, cfg) {
t.Errorf("MeetsPathTrust(%d, minBytes=1) = false, want true", prefixBytes)
}
}
}
func TestMeetsPathTrust_Exclude1Byte(t *testing.T) {
// minHashBytesForMapping: 2 — recommended for denser meshes.
cfg := &TrustConfig{MinHashBytesForMapping: 2}
cases := []struct {
prefixBytes int
want bool
}{
{0, false},
{1, false},
{2, true},
{3, true},
}
for _, c := range cases {
if got := MeetsPathTrust(c.prefixBytes, cfg); got != c.want {
t.Errorf("MeetsPathTrust(%d, minBytes=2) = %v, want %v", c.prefixBytes, got, c.want)
}
}
}
func TestMeetsPathTrust_ZeroValueOptIn(t *testing.T) {
// A zero-value MinHashBytesForMapping (the JSON field absent) must resolve to
// DefaultMinHashBytesForMapping, not be read as an explicit opt-in to some
// other number. Asserted against the constant rather than against a literal
// so this keeps testing the property if the default is ever changed again.
unset := &TrustConfig{}
if got := unset.MinHashBytesOrDefault(); got != DefaultMinHashBytesForMapping {
t.Errorf("unset.MinHashBytesOrDefault() = %d, want %d", got, DefaultMinHashBytesForMapping)
}
// And it must behave identically to a config that names the default outright.
explicit := &TrustConfig{MinHashBytesForMapping: DefaultMinHashBytesForMapping}
for _, prefixBytes := range []int{0, 1, 2, 3} {
if MeetsPathTrust(prefixBytes, unset) != MeetsPathTrust(prefixBytes, explicit) {
t.Errorf("prefixBytes=%d: unset and explicit-default disagree", prefixBytes)
}
}
// An explicit stricter setting must still be honoured over the default.
strict := &TrustConfig{MinHashBytesForMapping: 2}
if MeetsPathTrust(1, strict) {
t.Error("MeetsPathTrust(1, minBytes=2) = true, want false — an explicit setting must win")
}
}
func TestMeetsPathTrust_ConservativeThreshold3(t *testing.T) {
cfg := &TrustConfig{MinHashBytesForMapping: 3}
cases := []struct {
prefixBytes int
want bool
}{
{0, false},
{1, false},
{2, false},
{3, true},
}
for _, c := range cases {
if got := MeetsPathTrust(c.prefixBytes, cfg); got != c.want {
t.Errorf("MeetsPathTrust(%d, minBytes=3) = %v, want %v", c.prefixBytes, got, c.want)
}
}
}
func TestMinHashBytesOrDefault(t *testing.T) {
var nilCfg *TrustConfig
if got := nilCfg.MinHashBytesOrDefault(); got != DefaultMinHashBytesForMapping {
t.Errorf("nil.MinHashBytesOrDefault() = %d, want %d", got, DefaultMinHashBytesForMapping)
}
unset := &TrustConfig{}
if got := unset.MinHashBytesOrDefault(); got != DefaultMinHashBytesForMapping {
t.Errorf("unset.MinHashBytesOrDefault() = %d, want %d", got, DefaultMinHashBytesForMapping)
}
explicit := &TrustConfig{MinHashBytesForMapping: 1}
if got := explicit.MinHashBytesOrDefault(); got != 1 {
t.Errorf("explicit(1).MinHashBytesOrDefault() = %d, want 1", got)
}
explicit2 := &TrustConfig{MinHashBytesForMapping: 2}
if got := explicit2.MinHashBytesOrDefault(); got != 2 {
t.Errorf("explicit(2).MinHashBytesOrDefault() = %d, want 2", got)
}
negative := &TrustConfig{MinHashBytesForMapping: -5}
if got := negative.MinHashBytesOrDefault(); got != DefaultMinHashBytesForMapping {
t.Errorf("negative.MinHashBytesOrDefault() = %d, want %d (defensive fallback)", got, DefaultMinHashBytesForMapping)
}
}
func TestMinHashBytesOrDefault_ClampsAboveMax(t *testing.T) {
cases := []int{4, 5, 99}
for _, v := range cases {
cfg := &TrustConfig{MinHashBytesForMapping: v}
if got := cfg.MinHashBytesOrDefault(); got != MaxHashBytes {
t.Errorf("MinHashBytesForMapping: %d -> MinHashBytesOrDefault() = %d, want %d (clamped)", v, got, MaxHashBytes)
}
}
}
func TestMinHashBytesOrDefault_ExactlyMax(t *testing.T) {
cfg := &TrustConfig{MinHashBytesForMapping: MaxHashBytes}
if got := cfg.MinHashBytesOrDefault(); got != MaxHashBytes {
t.Errorf("MinHashBytesOrDefault() = %d, want %d (exactly max, no clamp needed)", got, MaxHashBytes)
}
}
func TestMeetsPathTrust_ClampedThresholdOnlyTrustsMaxBytes(t *testing.T) {
cfg := &TrustConfig{MinHashBytesForMapping: 99}
cases := []struct {
prefixBytes int
want bool
}{
{0, false},
{1, false},
{2, false},
{3, true},
}
for _, c := range cases {
if got := MeetsPathTrust(c.prefixBytes, cfg); got != c.want {
t.Errorf("MeetsPathTrust(%d, minBytes=99->clamped) = %v, want %v", c.prefixBytes, got, c.want)
}
}
}