package cluster import ( "bytes" "log" "os" "strings" "testing" ) // captureLog redirects the standard logger into a buffer for the duration of // fn and returns what was written. func captureLog(t *testing.T, fn func()) string { t.Helper() var buf bytes.Buffer orig := log.Writer() origFlags := log.Flags() log.SetOutput(&buf) log.SetFlags(0) defer func() { log.SetOutput(orig) log.SetFlags(origFlags) }() fn() return buf.String() } func TestDebugTokenSuppressedByDefault(t *testing.T) { os.Unsetenv(debugEnv) ReloadDebug() out := captureLog(t, func() { debugToken("ring[%s] OnToken cycle=%d", "node:7500", 42) debugToken("ring[%s] forward cycle=%d to %s", "node:7500", 42, "next:7500") debugToken("token-send %s: size=%dB elapsed=%v -> %d", "next:7500", 5605, "170ms", 200) }) if out != "" { t.Fatalf("steady-state token lines logged with W4F_DEBUG unset: %q", out) } } func TestDebugTokenEnabledByEnv(t *testing.T) { for _, v := range []string{"token", "TOKEN", "tokens", "ring", "1", "true", "yes", "all", "*", "yes-please"} { t.Run(v, func(t *testing.T) { t.Setenv(debugEnv, v) ReloadDebug() if !debugTokenOn.Load() { t.Fatalf("W4F_DEBUG=%q should enable the token heartbeat", v) } out := captureLog(t, func() { debugToken("ring[%s] OnToken cycle=%d", "node:7500", 7) }) if !strings.Contains(out, "OnToken cycle=7") { t.Fatalf("W4F_DEBUG=%q: expected the line to be logged, got %q", v, out) } }) } } // The whole point of the gate is to shrink the journal, so the failure paths // must stay loud without any env var — losing a send error to a quiet default // would be a bad trade. func TestFailurePathsStayLoud(t *testing.T) { os.Unsetenv(debugEnv) ReloadDebug() out := captureLog(t, func() { logf("token-send %s: size=%dB elapsed=%v err=%v", "down:7500", 10, "5ms", "connection refused") }) if !strings.Contains(out, "connection refused") { t.Fatalf("send errors must never be gated: got %q", out) } } func TestDebugAllOffForTokenOnly(t *testing.T) { t.Setenv(debugEnv, "token") ReloadDebug() if !debugTokenOn.Load() { t.Fatal("token level should enable debugToken") } if debugAllOn.Load() { t.Fatal("token level must not enable the catch-all debugAll") } out := captureLog(t, func() { debugAll("scratch diagnostic") }) if out != "" { t.Fatalf("debugAll should stay off at token level, got %q", out) } } func TestNormalizeDebugValue(t *testing.T) { cases := map[string]string{ "": "", "token": "token", "TOKEN": "token", "Ring": "token", "1": "all", "true": "all", "ALL": "all", "*": "all", "anything": "all", // unrecognised but deliberate → don't silence it "0": "all", // 0 is still a deliberate request for output } for in, want := range cases { if got := normalizeDebugValue(in); got != want { t.Errorf("normalizeDebugValue(%q) = %q, want %q", in, got, want) } } }