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:
@@ -4,26 +4,29 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/storage"
|
"github.com/steveyegge/beads/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sync mode constants define how beads synchronizes data with git.
|
// Sync mode constants - re-exported from internal/config for backward compatibility.
|
||||||
|
// These are used with storage.Storage (database) while config.SyncMode* are used
|
||||||
|
// with viper (config.yaml).
|
||||||
const (
|
const (
|
||||||
// SyncModeGitPortable exports to JSONL on push, imports on pull.
|
// SyncModeGitPortable exports to JSONL on push, imports on pull.
|
||||||
// This is the default mode - works with standard git workflows.
|
// This is the default mode - works with standard git workflows.
|
||||||
SyncModeGitPortable = "git-portable"
|
SyncModeGitPortable = string(config.SyncModeGitPortable)
|
||||||
|
|
||||||
// SyncModeRealtime exports to JSONL on every database mutation.
|
// SyncModeRealtime exports to JSONL on every database mutation.
|
||||||
// Provides immediate persistence but more git noise.
|
// Provides immediate persistence but more git noise.
|
||||||
SyncModeRealtime = "realtime"
|
SyncModeRealtime = string(config.SyncModeRealtime)
|
||||||
|
|
||||||
// SyncModeDoltNative uses Dolt remotes for sync, skipping JSONL.
|
// SyncModeDoltNative uses Dolt remotes for sync, skipping JSONL.
|
||||||
// Requires Dolt backend and configured Dolt remote.
|
// Requires Dolt backend and configured Dolt remote.
|
||||||
SyncModeDoltNative = "dolt-native"
|
SyncModeDoltNative = string(config.SyncModeDoltNative)
|
||||||
|
|
||||||
// SyncModeBeltAndSuspenders uses both Dolt remotes AND JSONL.
|
// SyncModeBeltAndSuspenders uses both Dolt remotes AND JSONL.
|
||||||
// Maximum redundancy - Dolt for versioning, JSONL for git portability.
|
// Maximum redundancy - Dolt for versioning, JSONL for git portability.
|
||||||
SyncModeBeltAndSuspenders = "belt-and-suspenders"
|
SyncModeBeltAndSuspenders = string(config.SyncModeBeltAndSuspenders)
|
||||||
|
|
||||||
// SyncModeConfigKey is the database config key for sync mode.
|
// SyncModeConfigKey is the database config key for sync mode.
|
||||||
SyncModeConfigKey = "sync.mode"
|
SyncModeConfigKey = "sync.mode"
|
||||||
@@ -45,32 +48,29 @@ const (
|
|||||||
TriggerChange = "change"
|
TriggerChange = "change"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSyncMode returns the configured sync mode, defaulting to git-portable.
|
// GetSyncMode returns the configured sync mode from the database, defaulting to git-portable.
|
||||||
|
// This reads from storage.Storage (database), not config.yaml.
|
||||||
|
// For config.yaml access, use config.GetSyncMode() instead.
|
||||||
func GetSyncMode(ctx context.Context, s storage.Storage) string {
|
func GetSyncMode(ctx context.Context, s storage.Storage) string {
|
||||||
mode, err := s.GetConfig(ctx, SyncModeConfigKey)
|
mode, err := s.GetConfig(ctx, SyncModeConfigKey)
|
||||||
if err != nil || mode == "" {
|
if err != nil || mode == "" {
|
||||||
return SyncModeGitPortable
|
return SyncModeGitPortable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate mode
|
// Validate mode using the shared validation
|
||||||
switch mode {
|
if !config.IsValidSyncMode(mode) {
|
||||||
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
|
|
||||||
return mode
|
|
||||||
default:
|
|
||||||
// Invalid mode, return default
|
|
||||||
return SyncModeGitPortable
|
return SyncModeGitPortable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSyncMode sets the sync mode configuration.
|
// SetSyncMode sets the sync mode configuration in the database.
|
||||||
func SetSyncMode(ctx context.Context, s storage.Storage, mode string) error {
|
func SetSyncMode(ctx context.Context, s storage.Storage, mode string) error {
|
||||||
// Validate mode
|
// Validate mode using the shared validation
|
||||||
switch mode {
|
if !config.IsValidSyncMode(mode) {
|
||||||
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
|
return fmt.Errorf("invalid sync mode: %s (valid: %s)",
|
||||||
// Valid
|
mode, fmt.Sprintf("%v", config.ValidSyncModes()))
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid sync mode: %s (valid: %s, %s, %s, %s)",
|
|
||||||
mode, SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.SetConfig(ctx, SyncModeConfigKey, mode)
|
return s.SetConfig(ctx, SyncModeConfigKey, mode)
|
||||||
|
|||||||
@@ -1124,9 +1124,9 @@ func TestFederationConfigDefaults(t *testing.T) {
|
|||||||
if cfg.Remote != "" {
|
if cfg.Remote != "" {
|
||||||
t.Errorf("GetFederationConfig().Remote = %q, want empty", cfg.Remote)
|
t.Errorf("GetFederationConfig().Remote = %q, want empty", cfg.Remote)
|
||||||
}
|
}
|
||||||
// Default sovereignty is T1 when not configured
|
// Default sovereignty is empty (no restriction) when not configured
|
||||||
if cfg.Sovereignty != SovereigntyT1 {
|
if cfg.Sovereignty != SovereigntyNone {
|
||||||
t.Errorf("GetFederationConfig().Sovereignty = %q, want %q (default)", cfg.Sovereignty, SovereigntyT1)
|
t.Errorf("GetFederationConfig().Sovereignty = %q, want %q (no restriction)", cfg.Sovereignty, SovereigntyNone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -9,6 +10,21 @@ import (
|
|||||||
// Sync mode configuration values (from hq-ew1mbr.3)
|
// Sync mode configuration values (from hq-ew1mbr.3)
|
||||||
// These control how Dolt syncs with JSONL/remotes.
|
// These control how Dolt syncs with JSONL/remotes.
|
||||||
|
|
||||||
|
// ConfigWarnings controls whether warnings are logged for invalid config values.
|
||||||
|
// Set to false to suppress warnings (useful for tests or scripts).
|
||||||
|
var ConfigWarnings = true
|
||||||
|
|
||||||
|
// ConfigWarningWriter is the destination for config warnings.
|
||||||
|
// Defaults to os.Stderr. Can be replaced for testing or custom logging.
|
||||||
|
var ConfigWarningWriter io.Writer = os.Stderr
|
||||||
|
|
||||||
|
// logConfigWarning logs a warning message if ConfigWarnings is enabled.
|
||||||
|
func logConfigWarning(format string, args ...interface{}) {
|
||||||
|
if ConfigWarnings && ConfigWarningWriter != nil {
|
||||||
|
fmt.Fprintf(ConfigWarningWriter, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SyncMode represents the sync mode configuration
|
// SyncMode represents the sync mode configuration
|
||||||
type SyncMode string
|
type SyncMode string
|
||||||
|
|
||||||
@@ -31,6 +47,21 @@ var validSyncModes = map[SyncMode]bool{
|
|||||||
SyncModeBeltAndSuspenders: true,
|
SyncModeBeltAndSuspenders: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidSyncModes returns the list of valid sync mode values.
|
||||||
|
func ValidSyncModes() []string {
|
||||||
|
return []string{
|
||||||
|
string(SyncModeGitPortable),
|
||||||
|
string(SyncModeRealtime),
|
||||||
|
string(SyncModeDoltNative),
|
||||||
|
string(SyncModeBeltAndSuspenders),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidSyncMode returns true if the given string is a valid sync mode.
|
||||||
|
func IsValidSyncMode(mode string) bool {
|
||||||
|
return validSyncModes[SyncMode(strings.ToLower(strings.TrimSpace(mode)))]
|
||||||
|
}
|
||||||
|
|
||||||
// ConflictStrategy represents the conflict resolution strategy
|
// ConflictStrategy represents the conflict resolution strategy
|
||||||
type ConflictStrategy string
|
type ConflictStrategy string
|
||||||
|
|
||||||
@@ -53,10 +84,27 @@ var validConflictStrategies = map[ConflictStrategy]bool{
|
|||||||
ConflictStrategyManual: true,
|
ConflictStrategyManual: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidConflictStrategies returns the list of valid conflict strategy values.
|
||||||
|
func ValidConflictStrategies() []string {
|
||||||
|
return []string{
|
||||||
|
string(ConflictStrategyNewest),
|
||||||
|
string(ConflictStrategyOurs),
|
||||||
|
string(ConflictStrategyTheirs),
|
||||||
|
string(ConflictStrategyManual),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidConflictStrategy returns true if the given string is a valid conflict strategy.
|
||||||
|
func IsValidConflictStrategy(strategy string) bool {
|
||||||
|
return validConflictStrategies[ConflictStrategy(strings.ToLower(strings.TrimSpace(strategy)))]
|
||||||
|
}
|
||||||
|
|
||||||
// Sovereignty represents the federation sovereignty tier
|
// Sovereignty represents the federation sovereignty tier
|
||||||
type Sovereignty string
|
type Sovereignty string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// SovereigntyNone means no sovereignty restriction (empty value)
|
||||||
|
SovereigntyNone Sovereignty = ""
|
||||||
// SovereigntyT1 is the most open tier (public repos)
|
// SovereigntyT1 is the most open tier (public repos)
|
||||||
SovereigntyT1 Sovereignty = "T1"
|
SovereigntyT1 Sovereignty = "T1"
|
||||||
// SovereigntyT2 is organization-level
|
// SovereigntyT2 is organization-level
|
||||||
@@ -67,7 +115,7 @@ const (
|
|||||||
SovereigntyT4 Sovereignty = "T4"
|
SovereigntyT4 Sovereignty = "T4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validSovereigntyTiers is the set of allowed sovereignty values
|
// validSovereigntyTiers is the set of allowed sovereignty values (excluding empty)
|
||||||
var validSovereigntyTiers = map[Sovereignty]bool{
|
var validSovereigntyTiers = map[Sovereignty]bool{
|
||||||
SovereigntyT1: true,
|
SovereigntyT1: true,
|
||||||
SovereigntyT2: true,
|
SovereigntyT2: true,
|
||||||
@@ -75,9 +123,28 @@ var validSovereigntyTiers = map[Sovereignty]bool{
|
|||||||
SovereigntyT4: true,
|
SovereigntyT4: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidSovereigntyTiers returns the list of valid sovereignty tier values.
|
||||||
|
func ValidSovereigntyTiers() []string {
|
||||||
|
return []string{
|
||||||
|
string(SovereigntyT1),
|
||||||
|
string(SovereigntyT2),
|
||||||
|
string(SovereigntyT3),
|
||||||
|
string(SovereigntyT4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidSovereignty returns true if the given string is a valid sovereignty tier.
|
||||||
|
// Empty string is valid (means no restriction).
|
||||||
|
func IsValidSovereignty(sovereignty string) bool {
|
||||||
|
if sovereignty == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return validSovereigntyTiers[Sovereignty(strings.ToUpper(strings.TrimSpace(sovereignty)))]
|
||||||
|
}
|
||||||
|
|
||||||
// GetSyncMode retrieves the sync mode configuration.
|
// GetSyncMode retrieves the sync mode configuration.
|
||||||
// Returns the configured mode, or SyncModeGitPortable (default) if not set or invalid.
|
// Returns the configured mode, or SyncModeGitPortable (default) if not set or invalid.
|
||||||
// Logs a warning to stderr if an invalid value is configured.
|
// Logs a warning if an invalid value is configured (unless ConfigWarnings is false).
|
||||||
//
|
//
|
||||||
// Config key: sync.mode
|
// Config key: sync.mode
|
||||||
// Valid values: git-portable, realtime, dolt-native, belt-and-suspenders
|
// Valid values: git-portable, realtime, dolt-native, belt-and-suspenders
|
||||||
@@ -89,7 +156,8 @@ func GetSyncMode() SyncMode {
|
|||||||
|
|
||||||
mode := SyncMode(strings.ToLower(strings.TrimSpace(value)))
|
mode := SyncMode(strings.ToLower(strings.TrimSpace(value)))
|
||||||
if !validSyncModes[mode] {
|
if !validSyncModes[mode] {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: invalid sync.mode %q in config (valid: git-portable, realtime, dolt-native, belt-and-suspenders), using default 'git-portable'\n", value)
|
logConfigWarning("Warning: invalid sync.mode %q in config (valid: %s), using default 'git-portable'\n",
|
||||||
|
value, strings.Join(ValidSyncModes(), ", "))
|
||||||
return SyncModeGitPortable
|
return SyncModeGitPortable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +166,7 @@ func GetSyncMode() SyncMode {
|
|||||||
|
|
||||||
// GetConflictStrategy retrieves the conflict resolution strategy configuration.
|
// GetConflictStrategy retrieves the conflict resolution strategy configuration.
|
||||||
// Returns the configured strategy, or ConflictStrategyNewest (default) if not set or invalid.
|
// Returns the configured strategy, or ConflictStrategyNewest (default) if not set or invalid.
|
||||||
// Logs a warning to stderr if an invalid value is configured.
|
// Logs a warning if an invalid value is configured (unless ConfigWarnings is false).
|
||||||
//
|
//
|
||||||
// Config key: conflict.strategy
|
// Config key: conflict.strategy
|
||||||
// Valid values: newest, ours, theirs, manual
|
// Valid values: newest, ours, theirs, manual
|
||||||
@@ -110,7 +178,8 @@ func GetConflictStrategy() ConflictStrategy {
|
|||||||
|
|
||||||
strategy := ConflictStrategy(strings.ToLower(strings.TrimSpace(value)))
|
strategy := ConflictStrategy(strings.ToLower(strings.TrimSpace(value)))
|
||||||
if !validConflictStrategies[strategy] {
|
if !validConflictStrategies[strategy] {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: invalid conflict.strategy %q in config (valid: newest, ours, theirs, manual), using default 'newest'\n", value)
|
logConfigWarning("Warning: invalid conflict.strategy %q in config (valid: %s), using default 'newest'\n",
|
||||||
|
value, strings.Join(ValidConflictStrategies(), ", "))
|
||||||
return ConflictStrategyNewest
|
return ConflictStrategyNewest
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,23 +187,39 @@ func GetConflictStrategy() ConflictStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetSovereignty retrieves the federation sovereignty tier configuration.
|
// GetSovereignty retrieves the federation sovereignty tier configuration.
|
||||||
// Returns the configured tier, or SovereigntyT1 (default) if not set or invalid.
|
// Returns the configured tier, or SovereigntyNone (empty, no restriction) if not set.
|
||||||
// Logs a warning to stderr if an invalid value is configured.
|
// Returns SovereigntyT1 and logs a warning if an invalid non-empty value is configured.
|
||||||
//
|
//
|
||||||
// Config key: federation.sovereignty
|
// Config key: federation.sovereignty
|
||||||
// Valid values: T1, T2, T3, T4
|
// Valid values: T1, T2, T3, T4 (empty means no restriction)
|
||||||
func GetSovereignty() Sovereignty {
|
func GetSovereignty() Sovereignty {
|
||||||
value := GetString("federation.sovereignty")
|
value := GetString("federation.sovereignty")
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return SovereigntyT1 // Default
|
return SovereigntyNone // No restriction
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize to uppercase for comparison (T1, T2, etc.)
|
// Normalize to uppercase for comparison (T1, T2, etc.)
|
||||||
tier := Sovereignty(strings.ToUpper(strings.TrimSpace(value)))
|
tier := Sovereignty(strings.ToUpper(strings.TrimSpace(value)))
|
||||||
if !validSovereigntyTiers[tier] {
|
if !validSovereigntyTiers[tier] {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: invalid federation.sovereignty %q in config (valid: T1, T2, T3, T4), using default 'T1'\n", value)
|
logConfigWarning("Warning: invalid federation.sovereignty %q in config (valid: %s, or empty for no restriction), using 'T1'\n",
|
||||||
|
value, strings.Join(ValidSovereigntyTiers(), ", "))
|
||||||
return SovereigntyT1
|
return SovereigntyT1
|
||||||
}
|
}
|
||||||
|
|
||||||
return tier
|
return tier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the SyncMode.
|
||||||
|
func (m SyncMode) String() string {
|
||||||
|
return string(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the ConflictStrategy.
|
||||||
|
func (s ConflictStrategy) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the Sovereignty.
|
||||||
|
func (s Sovereignty) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -83,18 +82,14 @@ func TestGetSyncMode(t *testing.T) {
|
|||||||
Set("sync.mode", tt.configValue)
|
Set("sync.mode", tt.configValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture stderr
|
// Capture warnings using ConfigWarningWriter
|
||||||
oldStderr := os.Stderr
|
var buf bytes.Buffer
|
||||||
r, w, _ := os.Pipe()
|
oldWriter := ConfigWarningWriter
|
||||||
os.Stderr = w
|
ConfigWarningWriter = &buf
|
||||||
|
defer func() { ConfigWarningWriter = oldWriter }()
|
||||||
|
|
||||||
result := GetSyncMode()
|
result := GetSyncMode()
|
||||||
|
|
||||||
// Restore stderr and get output
|
|
||||||
w.Close()
|
|
||||||
os.Stderr = oldStderr
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.ReadFrom(r)
|
|
||||||
stderrOutput := buf.String()
|
stderrOutput := buf.String()
|
||||||
|
|
||||||
if result != tt.expectedMode {
|
if result != tt.expectedMode {
|
||||||
@@ -103,10 +98,10 @@ func TestGetSyncMode(t *testing.T) {
|
|||||||
|
|
||||||
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
||||||
if tt.expectsWarning && !hasWarning {
|
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 {
|
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)
|
Set("conflict.strategy", tt.configValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture stderr
|
// Capture warnings using ConfigWarningWriter
|
||||||
oldStderr := os.Stderr
|
var buf bytes.Buffer
|
||||||
r, w, _ := os.Pipe()
|
oldWriter := ConfigWarningWriter
|
||||||
os.Stderr = w
|
ConfigWarningWriter = &buf
|
||||||
|
defer func() { ConfigWarningWriter = oldWriter }()
|
||||||
|
|
||||||
result := GetConflictStrategy()
|
result := GetConflictStrategy()
|
||||||
|
|
||||||
// Restore stderr and get output
|
|
||||||
w.Close()
|
|
||||||
os.Stderr = oldStderr
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.ReadFrom(r)
|
|
||||||
stderrOutput := buf.String()
|
stderrOutput := buf.String()
|
||||||
|
|
||||||
if result != tt.expectedStrategy {
|
if result != tt.expectedStrategy {
|
||||||
@@ -208,10 +199,10 @@ func TestGetConflictStrategy(t *testing.T) {
|
|||||||
|
|
||||||
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
||||||
if tt.expectsWarning && !hasWarning {
|
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 {
|
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
|
expectsWarning bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty returns default",
|
name: "empty returns no restriction",
|
||||||
configValue: "",
|
configValue: "",
|
||||||
expectedTier: SovereigntyT1,
|
expectedTier: SovereigntyNone,
|
||||||
expectsWarning: false,
|
expectsWarning: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -267,19 +258,19 @@ func TestGetSovereignty(t *testing.T) {
|
|||||||
expectsWarning: false,
|
expectsWarning: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid value returns default with warning",
|
name: "invalid value returns T1 with warning",
|
||||||
configValue: "T5",
|
configValue: "T5",
|
||||||
expectedTier: SovereigntyT1,
|
expectedTier: SovereigntyT1,
|
||||||
expectsWarning: true,
|
expectsWarning: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid tier 0 returns default with warning",
|
name: "invalid tier 0 returns T1 with warning",
|
||||||
configValue: "T0",
|
configValue: "T0",
|
||||||
expectedTier: SovereigntyT1,
|
expectedTier: SovereigntyT1,
|
||||||
expectsWarning: true,
|
expectsWarning: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "word tier returns default with warning",
|
name: "word tier returns T1 with warning",
|
||||||
configValue: "public",
|
configValue: "public",
|
||||||
expectedTier: SovereigntyT1,
|
expectedTier: SovereigntyT1,
|
||||||
expectsWarning: true,
|
expectsWarning: true,
|
||||||
@@ -299,18 +290,14 @@ func TestGetSovereignty(t *testing.T) {
|
|||||||
Set("federation.sovereignty", tt.configValue)
|
Set("federation.sovereignty", tt.configValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture stderr
|
// Capture warnings using ConfigWarningWriter
|
||||||
oldStderr := os.Stderr
|
var buf bytes.Buffer
|
||||||
r, w, _ := os.Pipe()
|
oldWriter := ConfigWarningWriter
|
||||||
os.Stderr = w
|
ConfigWarningWriter = &buf
|
||||||
|
defer func() { ConfigWarningWriter = oldWriter }()
|
||||||
|
|
||||||
result := GetSovereignty()
|
result := GetSovereignty()
|
||||||
|
|
||||||
// Restore stderr and get output
|
|
||||||
w.Close()
|
|
||||||
os.Stderr = oldStderr
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.ReadFrom(r)
|
|
||||||
stderrOutput := buf.String()
|
stderrOutput := buf.String()
|
||||||
|
|
||||||
if result != tt.expectedTier {
|
if result != tt.expectedTier {
|
||||||
@@ -319,11 +306,175 @@ func TestGetSovereignty(t *testing.T) {
|
|||||||
|
|
||||||
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
||||||
if tt.expectsWarning && !hasWarning {
|
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 {
|
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, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user