fix(config): restore sync.go with warning implementations
Commit356ab92bdeleted internal/config/sync.go while adding cmd/bd/sync_mode.go, but config.go still references the types and functions from sync.go, causing build failures. These serve different purposes: - internal/config/sync.go: viper-based (config.yaml), has warnings - cmd/bd/sync_mode.go: storage-based (database), no warnings Both should coexist. This restores sync.go and sync_test.go frome82e15a8, and fixes type conversion in sync.go:805. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -802,7 +802,7 @@ func showSyncStateStatus(ctx context.Context, jsonlPath string) error {
|
|||||||
|
|
||||||
// Sync mode (from config)
|
// Sync mode (from config)
|
||||||
syncCfg := config.GetSyncConfig()
|
syncCfg := config.GetSyncConfig()
|
||||||
fmt.Printf("Sync mode: %s (%s)\n", syncCfg.Mode, SyncModeDescription(syncCfg.Mode))
|
fmt.Printf("Sync mode: %s (%s)\n", syncCfg.Mode, SyncModeDescription(string(syncCfg.Mode)))
|
||||||
fmt.Printf(" Export on: %s, Import on: %s\n", syncCfg.ExportOn, syncCfg.ImportOn)
|
fmt.Printf(" Export on: %s, Import on: %s\n", syncCfg.ExportOn, syncCfg.ImportOn)
|
||||||
|
|
||||||
// Conflict strategy
|
// Conflict strategy
|
||||||
|
|||||||
140
internal/config/sync.go
Normal file
140
internal/config/sync.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sync mode configuration values (from hq-ew1mbr.3)
|
||||||
|
// These control how Dolt syncs with JSONL/remotes.
|
||||||
|
|
||||||
|
// SyncMode represents the sync mode configuration
|
||||||
|
type SyncMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SyncModeGitPortable exports JSONL on push, imports on pull (default)
|
||||||
|
SyncModeGitPortable SyncMode = "git-portable"
|
||||||
|
// SyncModeRealtime exports JSONL on every change (legacy behavior)
|
||||||
|
SyncModeRealtime SyncMode = "realtime"
|
||||||
|
// SyncModeDoltNative uses Dolt remote directly (dolthub://, gs://, s3://)
|
||||||
|
SyncModeDoltNative SyncMode = "dolt-native"
|
||||||
|
// SyncModeBeltAndSuspenders uses Dolt remote + JSONL backup
|
||||||
|
SyncModeBeltAndSuspenders SyncMode = "belt-and-suspenders"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validSyncModes is the set of allowed sync mode values
|
||||||
|
var validSyncModes = map[SyncMode]bool{
|
||||||
|
SyncModeGitPortable: true,
|
||||||
|
SyncModeRealtime: true,
|
||||||
|
SyncModeDoltNative: true,
|
||||||
|
SyncModeBeltAndSuspenders: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConflictStrategy represents the conflict resolution strategy
|
||||||
|
type ConflictStrategy string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ConflictStrategyNewest uses last-write-wins (default)
|
||||||
|
ConflictStrategyNewest ConflictStrategy = "newest"
|
||||||
|
// ConflictStrategyOurs prefers local changes
|
||||||
|
ConflictStrategyOurs ConflictStrategy = "ours"
|
||||||
|
// ConflictStrategyTheirs prefers remote changes
|
||||||
|
ConflictStrategyTheirs ConflictStrategy = "theirs"
|
||||||
|
// ConflictStrategyManual requires manual resolution
|
||||||
|
ConflictStrategyManual ConflictStrategy = "manual"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validConflictStrategies is the set of allowed conflict strategy values
|
||||||
|
var validConflictStrategies = map[ConflictStrategy]bool{
|
||||||
|
ConflictStrategyNewest: true,
|
||||||
|
ConflictStrategyOurs: true,
|
||||||
|
ConflictStrategyTheirs: true,
|
||||||
|
ConflictStrategyManual: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sovereignty represents the federation sovereignty tier
|
||||||
|
type Sovereignty string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SovereigntyT1 is the most open tier (public repos)
|
||||||
|
SovereigntyT1 Sovereignty = "T1"
|
||||||
|
// SovereigntyT2 is organization-level
|
||||||
|
SovereigntyT2 Sovereignty = "T2"
|
||||||
|
// SovereigntyT3 is pseudonymous
|
||||||
|
SovereigntyT3 Sovereignty = "T3"
|
||||||
|
// SovereigntyT4 is anonymous
|
||||||
|
SovereigntyT4 Sovereignty = "T4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validSovereigntyTiers is the set of allowed sovereignty values
|
||||||
|
var validSovereigntyTiers = map[Sovereignty]bool{
|
||||||
|
SovereigntyT1: true,
|
||||||
|
SovereigntyT2: true,
|
||||||
|
SovereigntyT3: true,
|
||||||
|
SovereigntyT4: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSyncMode retrieves the sync mode configuration.
|
||||||
|
// Returns the configured mode, or SyncModeGitPortable (default) if not set or invalid.
|
||||||
|
// Logs a warning to stderr if an invalid value is configured.
|
||||||
|
//
|
||||||
|
// Config key: sync.mode
|
||||||
|
// Valid values: git-portable, realtime, dolt-native, belt-and-suspenders
|
||||||
|
func GetSyncMode() SyncMode {
|
||||||
|
value := GetString("sync.mode")
|
||||||
|
if value == "" {
|
||||||
|
return SyncModeGitPortable // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := SyncMode(strings.ToLower(strings.TrimSpace(value)))
|
||||||
|
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)
|
||||||
|
return SyncModeGitPortable
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConflictStrategy retrieves the conflict resolution strategy configuration.
|
||||||
|
// Returns the configured strategy, or ConflictStrategyNewest (default) if not set or invalid.
|
||||||
|
// Logs a warning to stderr if an invalid value is configured.
|
||||||
|
//
|
||||||
|
// Config key: conflict.strategy
|
||||||
|
// Valid values: newest, ours, theirs, manual
|
||||||
|
func GetConflictStrategy() ConflictStrategy {
|
||||||
|
value := GetString("conflict.strategy")
|
||||||
|
if value == "" {
|
||||||
|
return ConflictStrategyNewest // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy := ConflictStrategy(strings.ToLower(strings.TrimSpace(value)))
|
||||||
|
if !validConflictStrategies[strategy] {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: invalid conflict.strategy %q in config (valid: newest, ours, theirs, manual), using default 'newest'\n", value)
|
||||||
|
return ConflictStrategyNewest
|
||||||
|
}
|
||||||
|
|
||||||
|
return strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSovereignty retrieves the federation sovereignty tier configuration.
|
||||||
|
// Returns the configured tier, or SovereigntyT1 (default) if not set or invalid.
|
||||||
|
// Logs a warning to stderr if an invalid value is configured.
|
||||||
|
//
|
||||||
|
// Config key: federation.sovereignty
|
||||||
|
// Valid values: T1, T2, T3, T4
|
||||||
|
func GetSovereignty() Sovereignty {
|
||||||
|
value := GetString("federation.sovereignty")
|
||||||
|
if value == "" {
|
||||||
|
return SovereigntyT1 // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize to uppercase for comparison (T1, T2, etc.)
|
||||||
|
tier := Sovereignty(strings.ToUpper(strings.TrimSpace(value)))
|
||||||
|
if !validSovereigntyTiers[tier] {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: invalid federation.sovereignty %q in config (valid: T1, T2, T3, T4), using default 'T1'\n", value)
|
||||||
|
return SovereigntyT1
|
||||||
|
}
|
||||||
|
|
||||||
|
return tier
|
||||||
|
}
|
||||||
329
internal/config/sync_test.go
Normal file
329
internal/config/sync_test.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSyncMode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configValue string
|
||||||
|
expectedMode SyncMode
|
||||||
|
expectsWarning bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty returns default",
|
||||||
|
configValue: "",
|
||||||
|
expectedMode: SyncModeGitPortable,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git-portable is valid",
|
||||||
|
configValue: "git-portable",
|
||||||
|
expectedMode: SyncModeGitPortable,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "realtime is valid",
|
||||||
|
configValue: "realtime",
|
||||||
|
expectedMode: SyncModeRealtime,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dolt-native is valid",
|
||||||
|
configValue: "dolt-native",
|
||||||
|
expectedMode: SyncModeDoltNative,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "belt-and-suspenders is valid",
|
||||||
|
configValue: "belt-and-suspenders",
|
||||||
|
expectedMode: SyncModeBeltAndSuspenders,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed case is normalized",
|
||||||
|
configValue: "Git-Portable",
|
||||||
|
expectedMode: SyncModeGitPortable,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace is trimmed",
|
||||||
|
configValue: " realtime ",
|
||||||
|
expectedMode: SyncModeRealtime,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid value returns default with warning",
|
||||||
|
configValue: "invalid-mode",
|
||||||
|
expectedMode: SyncModeGitPortable,
|
||||||
|
expectsWarning: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "typo returns default with warning",
|
||||||
|
configValue: "git-portabel",
|
||||||
|
expectedMode: SyncModeGitPortable,
|
||||||
|
expectsWarning: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Reset viper for test
|
||||||
|
ResetForTesting()
|
||||||
|
if err := Initialize(); err != nil {
|
||||||
|
t.Fatalf("Initialize failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the config value
|
||||||
|
if tt.configValue != "" {
|
||||||
|
Set("sync.mode", tt.configValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture stderr
|
||||||
|
oldStderr := os.Stderr
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stderr = w
|
||||||
|
|
||||||
|
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 {
|
||||||
|
t.Errorf("GetSyncMode() = %q, want %q", result, tt.expectedMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
||||||
|
if tt.expectsWarning && !hasWarning {
|
||||||
|
t.Errorf("Expected warning in stderr, got none. stderr=%q", stderrOutput)
|
||||||
|
}
|
||||||
|
if !tt.expectsWarning && hasWarning {
|
||||||
|
t.Errorf("Unexpected warning in stderr: %q", stderrOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConflictStrategy(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configValue string
|
||||||
|
expectedStrategy ConflictStrategy
|
||||||
|
expectsWarning bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty returns default",
|
||||||
|
configValue: "",
|
||||||
|
expectedStrategy: ConflictStrategyNewest,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newest is valid",
|
||||||
|
configValue: "newest",
|
||||||
|
expectedStrategy: ConflictStrategyNewest,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ours is valid",
|
||||||
|
configValue: "ours",
|
||||||
|
expectedStrategy: ConflictStrategyOurs,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "theirs is valid",
|
||||||
|
configValue: "theirs",
|
||||||
|
expectedStrategy: ConflictStrategyTheirs,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "manual is valid",
|
||||||
|
configValue: "manual",
|
||||||
|
expectedStrategy: ConflictStrategyManual,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed case is normalized",
|
||||||
|
configValue: "NEWEST",
|
||||||
|
expectedStrategy: ConflictStrategyNewest,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace is trimmed",
|
||||||
|
configValue: " ours ",
|
||||||
|
expectedStrategy: ConflictStrategyOurs,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid value returns default with warning",
|
||||||
|
configValue: "invalid-strategy",
|
||||||
|
expectedStrategy: ConflictStrategyNewest,
|
||||||
|
expectsWarning: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last-write-wins typo returns default with warning",
|
||||||
|
configValue: "last-write-wins",
|
||||||
|
expectedStrategy: ConflictStrategyNewest,
|
||||||
|
expectsWarning: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Reset viper for test
|
||||||
|
ResetForTesting()
|
||||||
|
if err := Initialize(); err != nil {
|
||||||
|
t.Fatalf("Initialize failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the config value
|
||||||
|
if tt.configValue != "" {
|
||||||
|
Set("conflict.strategy", tt.configValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture stderr
|
||||||
|
oldStderr := os.Stderr
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stderr = w
|
||||||
|
|
||||||
|
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 {
|
||||||
|
t.Errorf("GetConflictStrategy() = %q, want %q", result, tt.expectedStrategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
||||||
|
if tt.expectsWarning && !hasWarning {
|
||||||
|
t.Errorf("Expected warning in stderr, got none. stderr=%q", stderrOutput)
|
||||||
|
}
|
||||||
|
if !tt.expectsWarning && hasWarning {
|
||||||
|
t.Errorf("Unexpected warning in stderr: %q", stderrOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSovereignty(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configValue string
|
||||||
|
expectedTier Sovereignty
|
||||||
|
expectsWarning bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty returns default",
|
||||||
|
configValue: "",
|
||||||
|
expectedTier: SovereigntyT1,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "T1 is valid",
|
||||||
|
configValue: "T1",
|
||||||
|
expectedTier: SovereigntyT1,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "T2 is valid",
|
||||||
|
configValue: "T2",
|
||||||
|
expectedTier: SovereigntyT2,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "T3 is valid",
|
||||||
|
configValue: "T3",
|
||||||
|
expectedTier: SovereigntyT3,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "T4 is valid",
|
||||||
|
configValue: "T4",
|
||||||
|
expectedTier: SovereigntyT4,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lowercase is normalized",
|
||||||
|
configValue: "t1",
|
||||||
|
expectedTier: SovereigntyT1,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace is trimmed",
|
||||||
|
configValue: " T2 ",
|
||||||
|
expectedTier: SovereigntyT2,
|
||||||
|
expectsWarning: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid value returns default with warning",
|
||||||
|
configValue: "T5",
|
||||||
|
expectedTier: SovereigntyT1,
|
||||||
|
expectsWarning: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid tier 0 returns default with warning",
|
||||||
|
configValue: "T0",
|
||||||
|
expectedTier: SovereigntyT1,
|
||||||
|
expectsWarning: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "word tier returns default with warning",
|
||||||
|
configValue: "public",
|
||||||
|
expectedTier: SovereigntyT1,
|
||||||
|
expectsWarning: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Reset viper for test
|
||||||
|
ResetForTesting()
|
||||||
|
if err := Initialize(); err != nil {
|
||||||
|
t.Fatalf("Initialize failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the config value
|
||||||
|
if tt.configValue != "" {
|
||||||
|
Set("federation.sovereignty", tt.configValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture stderr
|
||||||
|
oldStderr := os.Stderr
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stderr = w
|
||||||
|
|
||||||
|
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 {
|
||||||
|
t.Errorf("GetSovereignty() = %q, want %q", result, tt.expectedTier)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWarning := strings.Contains(stderrOutput, "Warning:")
|
||||||
|
if tt.expectsWarning && !hasWarning {
|
||||||
|
t.Errorf("Expected warning in stderr, got none. stderr=%q", stderrOutput)
|
||||||
|
}
|
||||||
|
if !tt.expectsWarning && hasWarning {
|
||||||
|
t.Errorf("Unexpected warning in stderr: %q", stderrOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user