refactor(config): improve sync config with warnings toggle and dedup

Code review improvements to internal/config/sync.go:

1. Warning suppression toggle
   - Add ConfigWarnings bool to enable/disable warnings
   - Add ConfigWarningWriter io.Writer for testable output

2. Consolidate sync mode constants
   - cmd/bd/sync_mode.go now imports from internal/config
   - Single source of truth for mode values
   - Uses shared IsValidSyncMode() for validation

3. Fix empty sovereignty semantics
   - Empty now returns SovereigntyNone (no restriction)
   - Only non-empty invalid values fall back to T1 with warning

4. Export validation helpers
   - IsValidSyncMode(), IsValidConflictStrategy(), IsValidSovereignty()
   - ValidSyncModes(), ValidConflictStrategies(), ValidSovereigntyTiers()
   - String() methods on all typed values

5. Logger interface
   - ConfigWarningWriter allows custom logging destinations
   - Tests can capture warnings without os.Stderr manipulation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
collins
2026-01-19 11:49:33 -08:00
committed by Steve Yegge
parent 80cd1f35c0
commit 521239cfdc
4 changed files with 308 additions and 72 deletions

View File

@@ -2,7 +2,6 @@ package config
import (
"bytes"
"os"
"strings"
"testing"
)
@@ -83,18 +82,14 @@ func TestGetSyncMode(t *testing.T) {
Set("sync.mode", tt.configValue)
}
// Capture stderr
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Capture warnings using ConfigWarningWriter
var buf bytes.Buffer
oldWriter := ConfigWarningWriter
ConfigWarningWriter = &buf
defer func() { ConfigWarningWriter = oldWriter }()
result := GetSyncMode()
// Restore stderr and get output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
buf.ReadFrom(r)
stderrOutput := buf.String()
if result != tt.expectedMode {
@@ -103,10 +98,10 @@ func TestGetSyncMode(t *testing.T) {
hasWarning := strings.Contains(stderrOutput, "Warning:")
if tt.expectsWarning && !hasWarning {
t.Errorf("Expected warning in stderr, got none. stderr=%q", stderrOutput)
t.Errorf("Expected warning in output, got none. output=%q", stderrOutput)
}
if !tt.expectsWarning && hasWarning {
t.Errorf("Unexpected warning in stderr: %q", stderrOutput)
t.Errorf("Unexpected warning in output: %q", stderrOutput)
}
})
}
@@ -188,18 +183,14 @@ func TestGetConflictStrategy(t *testing.T) {
Set("conflict.strategy", tt.configValue)
}
// Capture stderr
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Capture warnings using ConfigWarningWriter
var buf bytes.Buffer
oldWriter := ConfigWarningWriter
ConfigWarningWriter = &buf
defer func() { ConfigWarningWriter = oldWriter }()
result := GetConflictStrategy()
// Restore stderr and get output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
buf.ReadFrom(r)
stderrOutput := buf.String()
if result != tt.expectedStrategy {
@@ -208,10 +199,10 @@ func TestGetConflictStrategy(t *testing.T) {
hasWarning := strings.Contains(stderrOutput, "Warning:")
if tt.expectsWarning && !hasWarning {
t.Errorf("Expected warning in stderr, got none. stderr=%q", stderrOutput)
t.Errorf("Expected warning in output, got none. output=%q", stderrOutput)
}
if !tt.expectsWarning && hasWarning {
t.Errorf("Unexpected warning in stderr: %q", stderrOutput)
t.Errorf("Unexpected warning in output: %q", stderrOutput)
}
})
}
@@ -225,9 +216,9 @@ func TestGetSovereignty(t *testing.T) {
expectsWarning bool
}{
{
name: "empty returns default",
name: "empty returns no restriction",
configValue: "",
expectedTier: SovereigntyT1,
expectedTier: SovereigntyNone,
expectsWarning: false,
},
{
@@ -267,19 +258,19 @@ func TestGetSovereignty(t *testing.T) {
expectsWarning: false,
},
{
name: "invalid value returns default with warning",
name: "invalid value returns T1 with warning",
configValue: "T5",
expectedTier: SovereigntyT1,
expectsWarning: true,
},
{
name: "invalid tier 0 returns default with warning",
name: "invalid tier 0 returns T1 with warning",
configValue: "T0",
expectedTier: SovereigntyT1,
expectsWarning: true,
},
{
name: "word tier returns default with warning",
name: "word tier returns T1 with warning",
configValue: "public",
expectedTier: SovereigntyT1,
expectsWarning: true,
@@ -299,18 +290,14 @@ func TestGetSovereignty(t *testing.T) {
Set("federation.sovereignty", tt.configValue)
}
// Capture stderr
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Capture warnings using ConfigWarningWriter
var buf bytes.Buffer
oldWriter := ConfigWarningWriter
ConfigWarningWriter = &buf
defer func() { ConfigWarningWriter = oldWriter }()
result := GetSovereignty()
// Restore stderr and get output
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
buf.ReadFrom(r)
stderrOutput := buf.String()
if result != tt.expectedTier {
@@ -319,11 +306,175 @@ func TestGetSovereignty(t *testing.T) {
hasWarning := strings.Contains(stderrOutput, "Warning:")
if tt.expectsWarning && !hasWarning {
t.Errorf("Expected warning in stderr, got none. stderr=%q", stderrOutput)
t.Errorf("Expected warning in output, got none. output=%q", stderrOutput)
}
if !tt.expectsWarning && hasWarning {
t.Errorf("Unexpected warning in stderr: %q", stderrOutput)
t.Errorf("Unexpected warning in output: %q", stderrOutput)
}
})
}
}
func TestConfigWarningsToggle(t *testing.T) {
// Reset viper for test
ResetForTesting()
if err := Initialize(); err != nil {
t.Fatalf("Initialize failed: %v", err)
}
// Set an invalid value
Set("sync.mode", "invalid-mode")
// Capture warnings
var buf bytes.Buffer
oldWriter := ConfigWarningWriter
ConfigWarningWriter = &buf
// With warnings enabled (default)
ConfigWarnings = true
_ = GetSyncMode()
if !strings.Contains(buf.String(), "Warning:") {
t.Error("Expected warning with ConfigWarnings=true, got none")
}
// With warnings disabled
buf.Reset()
ConfigWarnings = false
_ = GetSyncMode()
if strings.Contains(buf.String(), "Warning:") {
t.Error("Expected no warning with ConfigWarnings=false, got one")
}
// Restore defaults
ConfigWarnings = true
ConfigWarningWriter = oldWriter
}
func TestIsValidSyncMode(t *testing.T) {
tests := []struct {
mode string
valid bool
}{
{"git-portable", true},
{"realtime", true},
{"dolt-native", true},
{"belt-and-suspenders", true},
{"Git-Portable", true}, // case insensitive
{" realtime ", true}, // whitespace trimmed
{"invalid", false},
{"", false},
}
for _, tt := range tests {
if got := IsValidSyncMode(tt.mode); got != tt.valid {
t.Errorf("IsValidSyncMode(%q) = %v, want %v", tt.mode, got, tt.valid)
}
}
}
func TestIsValidConflictStrategy(t *testing.T) {
tests := []struct {
strategy string
valid bool
}{
{"newest", true},
{"ours", true},
{"theirs", true},
{"manual", true},
{"NEWEST", true}, // case insensitive
{" ours ", true}, // whitespace trimmed
{"invalid", false},
{"lww", false},
{"", false},
}
for _, tt := range tests {
if got := IsValidConflictStrategy(tt.strategy); got != tt.valid {
t.Errorf("IsValidConflictStrategy(%q) = %v, want %v", tt.strategy, got, tt.valid)
}
}
}
func TestIsValidSovereignty(t *testing.T) {
tests := []struct {
sovereignty string
valid bool
}{
{"T1", true},
{"T2", true},
{"T3", true},
{"T4", true},
{"t1", true}, // case insensitive
{" T2 ", true}, // whitespace trimmed
{"", true}, // empty is valid (no restriction)
{"T0", false},
{"T5", false},
{"public", false},
}
for _, tt := range tests {
if got := IsValidSovereignty(tt.sovereignty); got != tt.valid {
t.Errorf("IsValidSovereignty(%q) = %v, want %v", tt.sovereignty, got, tt.valid)
}
}
}
func TestValidSyncModes(t *testing.T) {
modes := ValidSyncModes()
if len(modes) != 4 {
t.Errorf("ValidSyncModes() returned %d modes, want 4", len(modes))
}
expected := []string{"git-portable", "realtime", "dolt-native", "belt-and-suspenders"}
for i, m := range modes {
if m != expected[i] {
t.Errorf("ValidSyncModes()[%d] = %q, want %q", i, m, expected[i])
}
}
}
func TestValidConflictStrategies(t *testing.T) {
strategies := ValidConflictStrategies()
if len(strategies) != 4 {
t.Errorf("ValidConflictStrategies() returned %d strategies, want 4", len(strategies))
}
expected := []string{"newest", "ours", "theirs", "manual"}
for i, s := range strategies {
if s != expected[i] {
t.Errorf("ValidConflictStrategies()[%d] = %q, want %q", i, s, expected[i])
}
}
}
func TestValidSovereigntyTiers(t *testing.T) {
tiers := ValidSovereigntyTiers()
if len(tiers) != 4 {
t.Errorf("ValidSovereigntyTiers() returned %d tiers, want 4", len(tiers))
}
expected := []string{"T1", "T2", "T3", "T4"}
for i, tier := range tiers {
if tier != expected[i] {
t.Errorf("ValidSovereigntyTiers()[%d] = %q, want %q", i, tier, expected[i])
}
}
}
func TestSyncModeString(t *testing.T) {
if got := SyncModeGitPortable.String(); got != "git-portable" {
t.Errorf("SyncModeGitPortable.String() = %q, want %q", got, "git-portable")
}
}
func TestConflictStrategyString(t *testing.T) {
if got := ConflictStrategyNewest.String(); got != "newest" {
t.Errorf("ConflictStrategyNewest.String() = %q, want %q", got, "newest")
}
}
func TestSovereigntyString(t *testing.T) {
if got := SovereigntyT1.String(); got != "T1" {
t.Errorf("SovereigntyT1.String() = %q, want %q", got, "T1")
}
if got := SovereigntyNone.String(); got != "" {
t.Errorf("SovereigntyNone.String() = %q, want %q", got, "")
}
}